PageSourceSearch

https://history.house.gov/_openseadragon/openseadragon.js?nc=092320261821

js house.gov collected 2026-09-24 06:37:37 UTC 628,908 bytes, 16,579 lines download raw bytes

1//! OpenSeadragon 1.1.1
2//! Built on 2014-07-11
3//! Git commit: v1.1.1-18-ef8bd1a
4//! http://openseadragon.github.io
5//! License: http://openseadragon.github.io/license/
6
7/*
8 * OpenSeadragon
9 *
10 * Copyright (C) 2009 CodePlex Foundation
11 * Copyright (C) 2010-2013 OpenSeadragon contributors
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are
15 * met:
16 *
17 * - Redistributions of source code must retain the above copyright notice,
18 *   this list of conditions and the following disclaimer.
19 *
20 * - Redistributions in binary form must reproduce the above copyright
21 *   notice, this list of conditions and the following disclaimer in the
22 *   documentation and/or other materials provided with the distribution.
23 *
24 * - Neither the name of CodePlex Foundation nor the names of its
25 *   contributors may be used to endorse or promote products derived from
26 *   this software without specific prior written permission.
27 *
28 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
31 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
32 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
34 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
35 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
36 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
37 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
38 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39 */
40
41/*
42 * Portions of this source file taken from jQuery:
43 *
44 * Copyright 2011 John Resig
45 *
46 * Permission is hereby granted, free of charge, to any person obtaining
47 * a copy of this software and associated documentation files (the
48 * "Software"), to deal in the Software without restriction, including
49 * without limitation the rights to use, copy, modify, merge, publish,
50 * distribute, sublicense, and/or sell copies of the Software, and to
51 * permit persons to whom the Software is furnished to do so, subject to
52 * the following conditions:
53 *
54 * The above copyright notice and this permission notice shall be
55 * included in all copies or substantial portions of the Software.
56 *
57 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
58 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
59 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
60 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
61 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
62 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
63 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
64 */
65
66/*
67 * Portions of this source file taken from mattsnider.com:
68 *
69 * Copyright (c) 2006-2013 Matt Snider
70 *
71 * Permission is hereby granted, free of charge, to any person obtaining a
72 * copy of this software and associated documentation files (the "Software"),
73 * to deal in the Software without restriction, including without limitation
74 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
75 * and/or sell copies of the Software, and to permit persons to whom the
76 * Software is furnished to do so, subject to the following conditions:
77 *
78 * The above copyright notice and this permission notice shall be included
79 * in all copies or substantial portions of the Software.
80 *
81 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
82 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
83 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
84 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
85 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
86 * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
87 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
88 */
89
90
91/**
92 * @version  OpenSeadragon 1.1.1
93 *
94 * @file
95 * <h2><strong>OpenSeadragon - Javascript Deep Zooming</strong></h2>
96 * <p>
97 * OpenSeadragon provides an html interface for creating
98 * deep zoom user interfaces.  The simplest examples include deep
99 * zoom for large resolution images, and complex examples include
100 * zoomable map interfaces driven by SVG files.
101 * </p>
102 *
103 */
104
105/**
106 * @module OpenSeadragon
107 *
108 */
109
110/**
111 * @namespace OpenSeadragon
112 *
113 * @classdesc The root namespace for OpenSeadragon.  All utility methods
114 * and classes are defined on or below this namespace.
115 *
116 */
117
118
119// Typedefs
120
121 /**
122  * All required and optional settings for instantiating a new instance of an OpenSeadragon image viewer.
123  *
124  * @typedef {Object} Options
125  * @memberof OpenSeadragon
126  *
127  * @property {String} id
128  *     Id of the element to append the viewer's container element to. If not provided, the 'element' property must be provided.
129  *     If both the element and id properties are specified, the viewer is appended to the element provided in the element property.
130  *
131  * @property {Element} element
132  *     The element to append the viewer's container element to. If not provided, the 'id' property must be provided.
133  *     If both the element and id properties are specified, the viewer is appended to the element provided in the element property.
134  *
135  * @property {Array|String|Function|Object[]|Array[]|String[]|Function[]} [tileSources=null]
136  *     As an Array, the tileSource can hold either Objects or mixed
137  *     types of Arrays of Objects, Strings, or Functions. When a value is a String,
138  *     the tileSource is used to create a {@link OpenSeadragon.DziTileSource}.
139  *     When a value is a Function, the function is used to create a new
140  *     {@link OpenSeadragon.TileSource} whose abstract method
141  *     getUrl( level, x, y ) is implemented by the function. Finally, when it
142  *     is an Array of objects, it is used to create a
143  *     {@link OpenSeadragon.LegacyTileSource}.
144  *
145  * @property {Array} overlays Array of objects defining permanent overlays of
146  *     the viewer. The overlays added via this option and later removed with
147  *     {@link OpenSeadragon.Viewer#removeOverlay} will be added back when a new
148  *     image is opened.
149  *     To add overlays which can be definitively removed, one must use
150  *     {@link OpenSeadragon.Viewer#addOverlay}
151  *     If displaying a sequence of images, the overlays can be associated
152  *     with a specific page by passing the overlays array to the page's
153  *     tile source configuration.
154  *     Expected properties:
155  *     * x, y, (or px, py for pixel coordinates) to define the location.
156  *     * width, height in point if using x,y or in pixels if using px,py. If width
157  *       and height are specified, the overlay size is adjusted when zooming,
158  *       otherwise the size stays the size of the content (or the size defined by CSS).
159  *     * className to associate a class to the overlay
160  *     * id to set the overlay element. If an element with this id already exists,
161  *       it is reused, otherwise it is created. If not specified, a new element is
162  *       created.
163  *     * placement a string to define the relative position to the viewport.
164  *       Only used if no width and height are specified. Default: 'TOP_LEFT'.
165  *       See {@link OpenSeadragon.OverlayPlacement} for possible values.
166  *
167  * @property {String} [xmlPath=null]
168  *     <strong>DEPRECATED</strong>. A relative path to load a DZI file from the server.
169  *     Prefer the newer Options.tileSources.
170  *
171  * @property {String} [prefixUrl='/images/']
172  *     Prepends the prefixUrl to navImages paths, which is very useful
173  *     since the default paths are rarely useful for production
174  *     environments.
175  *
176  * @property {OpenSeadragon.NavImages} [navImages]
177  *     An object with a property for each button or other built-in navigation
178  *     control, eg the current 'zoomIn', 'zoomOut', 'home', and 'fullpage'.
179  *     Each of those in turn provides an image path for each state of the button
180  *     or navigation control, eg 'REST', 'GROUP', 'HOVER', 'PRESS'. Finally the
181  *     image paths, by default assume there is a folder on the servers root path
182  *     called '/images', eg '/images/zoomin_rest.png'.  If you need to adjust
183  *     these paths, prefer setting the option.prefixUrl rather than overriding
184  *     every image path directly through this setting.
185  *
186  * @property {Object} [tileHost=null]
187  *     TODO: Implement this. Currently not used.
188  *
189  * @property {Boolean} [debugMode=false]
190  *     TODO: provide an in-screen panel providing event detail feedback.
191  *
192  * @property {String} [debugGridColor='#437AB2']
193  *
194  * @property {Number} [blendTime=0]
195  *     Specifies the duration of animation as higher or lower level tiles are
196  *     replacing the existing tile.
197  *
198  * @property {Boolean} [alwaysBlend=false]
199  *     Forces the tile to always blend.  By default the tiles skip blending
200  *     when the blendTime is surpassed and the current animation frame would
201  *     not complete the blend.
202  *
203  * @property {Boolean} [autoHideControls=true]
204  *     If the user stops interacting with the viewport, fade the navigation
205  *     controls.  Useful for presentation since the controls are by default
206  *     floated on top of the image the user is viewing.
207  *
208  * @property {Boolean} [immediateRender=false]
209  *     Render the best closest level first, ignoring the lowering levels which
210  *     provide the effect of very blurry to sharp. It is recommended to change
211  *     setting to true for mobile devices.
212  *
213  * @property {Number} [defaultZoomLevel=0]
214  *     Zoom level to use when image is first opened or the home button is clicked.
215  *     If 0, adjusts to fit viewer.
216  *
217  * @property {Number} [opacity=1]
218  *     Opacity of the drawer (1=opaque, 0=transparent)
219  *
220  * @property {Number} [layersAspectRatioEpsilon=0.0001]
221  *     Maximum aspectRatio mismatch between 2 layers.
222  *
223  * @property {Number} [degrees=0]
224  *     Initial rotation.
225  *
226  * @property {Number} [minZoomLevel=null]
227  *
228  * @property {Number} [maxZoomLevel=null]
229  *
230  * @property {Boolean} [panHorizontal=true]
231  *     Allow horizontal pan.
232  *
233  * @property {Boolean} [panVertical=true]
234  *     Allow vertical pan.
235  *
236  * @property {Boolean} [constrainDuringPan=false]
237  *
238  * @property {Boolean} [wrapHorizontal=false]
239  *     Set to true to force the image to wrap horizontally within the viewport.
240  *     Useful for maps or images representing the surface of a sphere or cylinder.
241  *
242  * @property {Boolean} [wrapVertical=false]
243  *     Set to true to force the image to wrap vertically within the viewport.
244  *     Useful for maps or images representing the surface of a sphere or cylinder.
245  *
246  * @property {Number} [minZoomImageRatio=0.9]
247  *     The minimum percentage ( expressed as a number between 0 and 1 ) of
248  *     the viewport height or width at which the zoom out will be constrained.
249  *     Setting it to 0, for example will allow you to zoom out infinitly.
250  *
251  * @property {Number} [maxZoomPixelRatio=1.1]
252  *     The maximum ratio to allow a zoom-in to affect the highest level pixel
253  *     ratio. This can be set to Infinity to allow 'infinite' zooming into the
254  *     image though it is less effective visually if the HTML5 Canvas is not
255  *     availble on the viewing device.
256  *
257  * @property {Boolean} [autoResize=true]
258  *     Set to false to prevent polling for viewer size changes. Useful for providing custom resize behavior.
259  *
260  * @property {Number} [pixelsPerWheelLine=40]
261  *     For pixel-resolution scrolling devices, the number of pixels equal to one scroll line.
262  *
263  * @property {Number} [visibilityRatio=0.5]
264  *     The percentage ( as a number from 0 to 1 ) of the source image which
265  *     must be kept within the viewport.  If the image is dragged beyond that
266  *     limit, it will 'bounce' back until the minimum visibility ration is
267  *     achieved.  Setting this to 0 and wrapHorizontal ( or wrapVertical ) to
268  *     true will provide the effect of an infinitely scrolling viewport.
269  *
270  * @property {Number} [imageLoaderLimit=0]
271  *     The maximum number of image requests to make concurrently.  By default
272  *     it is set to 0 allowing the browser to make the maximum number of
273  *     image requests in parallel as allowed by the browsers policy.
274  *
275  * @property {Number} [clickTimeThreshold=300]
276  *      The number of milliseconds within which a pointer down-up event combination
277  *      will be treated as a click gesture.
278  *
279  * @property {Number} [clickDistThreshold=5]
280  *      The maximum distance allowed between a pointer down event and a pointer up event
281  *      to be treated as a click gesture.
282  *
283  * @property {Number} [dblClickTimeThreshold=300]
284  *      The number of milliseconds within which two pointer down-up event combinations
285  *      will be treated as a double-click gesture.
286  *
287  * @property {Number} [dblClickDistThreshold=20]
288  *      The maximum distance allowed between two pointer click events
289  *      to be treated as a double-click gesture.
290  *
291  * @property {Number} [springStiffness=6.5]
292  *
293  * @property {Number} [animationTime=1.2]
294  *     Specifies the animation duration per each {@link OpenSeadragon.Spring}
295  *     which occur when the image is dragged or zoomed.
296  *
297  * @property {OpenSeadragon.GestureSettings} [gestureSettingsMouse]
298  *     Settings for gestures generated by a mouse pointer device. (See {@link OpenSeadragon.GestureSettings})
299  * @property {Boolean} [gestureSettingsMouse.scrollToZoom=true] - Zoom on scroll gesture
300  * @property {Boolean} [gestureSettingsMouse.clickToZoom=true] - Zoom on click gesture
301  * @property {Boolean} [gestureSettingsMouse.dblClickToZoom=false] - Zoom on double-click gesture. Note: If set to true
302  *     then clickToZoom should be set to false to prevent multiple zooms.
303  * @property {Boolean} [gestureSettingsMouse.pinchToZoom=false] - Zoom on pinch gesture
304  * @property {Boolean} [gestureSettingsMouse.flickEnabled=false] - Enable flick gesture
305  * @property {Number} [gestureSettingsMouse.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
306  * @property {Number} [gestureSettingsMouse.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
307  *
308  * @property {OpenSeadragon.GestureSettings} [gestureSettingsTouch]
309  *     Settings for gestures generated by a touch pointer device. (See {@link OpenSeadragon.GestureSettings})
310  * @property {Boolean} [gestureSettingsTouch.scrollToZoom=false] - Zoom on scroll gesture
311  * @property {Boolean} [gestureSettingsTouch.clickToZoom=false] - Zoom on click gesture
312  * @property {Boolean} [gestureSettingsTouch.dblClickToZoom=true] - Zoom on double-click gesture. Note: If set to true
313  *     then clickToZoom should be set to false to prevent multiple zooms.
314  * @property {Boolean} [gestureSettingsTouch.pinchToZoom=true] - Zoom on pinch gesture
315  * @property {Boolean} [gestureSettingsTouch.flickEnabled=true] - Enable flick gesture
316  * @property {Number} [gestureSettingsTouch.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
317  * @property {Number} [gestureSettingsTouch.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
318  *
319  * @property {OpenSeadragon.GestureSettings} [gestureSettingsPen]
320  *     Settings for gestures generated by a pen pointer device. (See {@link OpenSeadragon.GestureSettings})
321  * @property {Boolean} [gestureSettingsPen.scrollToZoom=false] - Zoom on scroll gesture
322  * @property {Boolean} [gestureSettingsPen.clickToZoom=true] - Zoom on click gesture
323  * @property {Boolean} [gestureSettingsPen.dblClickToZoom=false] - Zoom on double-click gesture. Note: If set to true
324  *     then clickToZoom should be set to false to prevent multiple zooms.
325  * @property {Boolean} [gestureSettingsPen.pinchToZoom=false] - Zoom on pinch gesture
326  * @property {Boolean} [gestureSettingsPen.flickEnabled=false] - Enable flick gesture
327  * @property {Number} [gestureSettingsPen.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
328  * @property {Number} [gestureSettingsPen.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
329  *
330  * @property {OpenSeadragon.GestureSettings} [gestureSettingsUnknown]
331  *     Settings for gestures generated by unknown pointer devices. (See {@link OpenSeadragon.GestureSettings})
332  * @property {Boolean} [gestureSettingsUnknown.scrollToZoom=true] - Zoom on scroll gesture
333  * @property {Boolean} [gestureSettingsUnknown.clickToZoom=false] - Zoom on click gesture
334  * @property {Boolean} [gestureSettingsUnknown.dblClickToZoom=true] - Zoom on double-click gesture. Note: If set to true
335  *     then clickToZoom should be set to false to prevent multiple zooms.
336  * @property {Boolean} [gestureSettingsUnknown.pinchToZoom=true] - Zoom on pinch gesture
337  * @property {Boolean} [gestureSettingsUnknown.flickEnabled=true] - Enable flick gesture
338  * @property {Number} [gestureSettingsUnknown.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
339  * @property {Number} [gestureSettingsUnknown.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
340  *
341  * @property {Number} [zoomPerClick=2.0]
342  *     The "zoom distance" per mouse click or touch tap. <em><strong>Note:</strong> Setting this to 1.0 effectively disables the click-to-zoom feature (also see gestureSettings[Mouse|Touch|Pen].clickToZoom/dblClickToZoom).</em>
343  *
344  * @property {Number} [zoomPerScroll=1.2]
345  *     The "zoom distance" per mouse scroll or touch pinch. <em><strong>Note:</strong> Setting this to 1.0 effectively disables the mouse-wheel zoom feature (also see gestureSettings[Mouse|Touch|Pen].scrollToZoom}).</em>
346  *
347  * @property {Number} [zoomPerSecond=1.0]
348  *     The number of seconds to animate a single zoom event over.
349  *
350  * @property {Boolean} [showNavigator=false]
351  *     Set to true to make the navigator minimap appear.
352  *
353  * @property {Boolean} [navigatorId=navigator-GENERATED DATE]
354  *     The ID of a div to hold the navigator minimap.
355  *     If an ID is specified, the navigatorPosition, navigatorSizeRatio, navigatorMaintainSizeRatio, and navigatorTop|Left|Height|Width options will be ignored.
356  *     If an ID is not specified, a div element will be generated and placed on top of the main image.
357  *
358  * @property {String} [navigatorPosition='TOP_RIGHT']
359  *     Valid values are 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT', or 'ABSOLUTE'.<br>
360  *     If 'ABSOLUTE' is specified, then navigatorTop|Left|Height|Width determines the size and position of the navigator minimap in the viewer, and navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.<br>
361  *     For 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', and 'BOTTOM_RIGHT', the navigatorSizeRatio or navigatorHeight|Width values determine the size of the navigator minimap.
362  *
363  * @property {Number} [navigatorSizeRatio=0.2]
364  *     Ratio of navigator size to viewer size. Ignored if navigatorHeight|Width are specified.
365  *
366  * @property {Boolean} [navigatorMaintainSizeRatio=false]
367  *     If true, the navigator minimap is resized (using navigatorSizeRatio) when the viewer size changes.
368  *
369  * @property {Number|String} [navigatorTop=null]
370  *     Specifies the location of the navigator minimap (see navigatorPosition).
371  *
372  * @property {Number|String} [navigatorLeft=null]
373  *     Specifies the location of the navigator minimap (see navigatorPosition).
374  *
375  * @property {Number|String} [navigatorHeight=null]
376  *     Specifies the size of the navigator minimap (see navigatorPosition).
377  *     If specified, navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.
378  *
379  * @property {Number|String} [navigatorWidth=null]
380  *     Specifies the size of the navigator minimap (see navigatorPosition).
381  *     If specified, navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.
382  *
383  * @property {Boolean} [navigatorAutoResize=true]
384  *     Set to false to prevent polling for navigator size changes. Useful for providing custom resize behavior.
385  *     Setting to false can also improve performance when the navigator is configured to a fixed size.
386  *
387  * @property {Number} [controlsFadeDelay=2000]
388  *     The number of milliseconds to wait once the user has stopped interacting
389  *     with the interface before begining to fade the controls. Assumes
390  *     showNavigationControl and autoHideControls are both true.
391  *
392  * @property {Number} [controlsFadeLength=1500]
393  *     The number of milliseconds to animate the controls fading out.
394  *
395  * @property {Number} [maxImageCacheCount=200]
396  *     The max number of images we should keep in memory (per drawer).
397  *
398  * @property {Number} [timeout=30000]
399  *
400  * @property {Boolean} [useCanvas=true]
401  *     Set to false to not use an HTML canvas element for image rendering even if canvas is supported.
402  *
403  * @property {Number} [minPixelRatio=0.5]
404  *     The higher the minPixelRatio, the lower the quality of the image that
405  *     is considered sufficient to stop rendering a given zoom level.  For
406  *     example, if you are targeting mobile devices with less bandwith you may
407  *     try setting this to 1.5 or higher.
408  *
409  * @property {Boolean} [mouseNavEnabled=true]
410  *     Is the user able to interact with the image via mouse or touch. Default
411  *     interactions include draging the image in a plane, and zooming in toward
412  *     and away from the image.
413  *
414  * @property {Boolean}
414 [showNavigationControl=true]
415  *     Set to false to prevent the appearance of the default navigation controls.<br>
416  *     Note that if set to false, the customs buttons set by the options
417  *     zoomInButton, zoomOutButton etc, are rendered inactive.
418  *
419  * @property {OpenSeadragon.ControlAnchor} [navigationControlAnchor=TOP_LEFT]
420  *     Placement of the default navigation controls.
421  *     To set the placement of the sequence controls, see the
422  *     sequenceControlAnchor option.
423  *
424  * @property {Boolean} [showZoomControl=true]
425  *     If true then + and - buttons to zoom in and out are displayed.<br>
426  *     Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
427  *     this setting when set to false.
428  *
429  * @property {Boolean} [showHomeControl=true]
430  *     If true then the 'Go home' button is displayed to go back to the original
431  *     zoom and pan.<br>
432  *     Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
433  *     this setting when set to false.
434  *
435  * @property {Boolean} [showFullPageControl=true]
436  *     If true then the 'Toggle full page' button is displayed to switch
437  *     between full page and normal mode.<br>
438  *     Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
439  *     this setting when set to false.
440  *
441  * @property {Boolean} [showRotationControl=false]
442  *     If true then the rotate left/right controls will be displayed as part of the
443  *     standard controls. This is also subject to the browser support for rotate
444  *     (e.g. viewer.drawer.canRotate()).<br>
445  *     Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
446  *     this setting when set to false.
447  *
448  * @property {Boolean} [showSequenceControl=true]
449  *     If the viewer has been configured with a sequence of tile sources, then
450  *     provide buttons for navigating forward and backward through the images.
451  *
452  * @property {OpenSeadragon.ControlAnchor} [sequenceControlAnchor=TOP_LEFT]
453  *     Placement of the default sequence controls.
454  *
455  * @property {Boolean} [navPrevNextWrap=false]
456  *     If true then the 'previous' button will wrap to the last image when
457  *     viewing the first image and the 'next' button will wrap to the first
458  *     image when viewing the last image.
459  *
460  * @property {String} zoomInButton
461  *     Set the id of the custom 'Zoom in' button to use.
462  *     This is useful to have a custom button anywhere in the web page.<br>
463  *     To only change the button images, consider using
464  *     {@link OpenSeadragon.Options.navImages}
465  *
466  * @property {String} zoomOutButton
467  *     Set the id of the custom 'Zoom out' button to use.
468  *     This is useful to have a custom button anywhere in the web page.<br>
469  *     To only change the button images, consider using
470  *     {@link OpenSeadragon.Options.navImages}
471  *
472  * @property {String} homeButton
473  *     Set the id of the custom 'Go home' button to use.
474  *     This is useful to have a custom button anywhere in the web page.<br>
475  *     To only change the button images, consider using
476  *     {@link OpenSeadragon.Options.navImages}
477  *
478  * @property {String} fullPageButton
479  *     Set the id of the custom 'Toggle full page' button to use.
480  *     This is useful to have a custom button anywhere in the web page.<br>
481  *     To only change the button images, consider using
482  *     {@link OpenSeadragon.Options.navImages}
483  *
484  * @property {String} rotateLeftButton
485  *     Set the id of the custom 'Rotate left' button to use.
486  *     This is useful to have a custom button anywhere in the web page.<br>
487  *     To only change the button images, consider using
488  *     {@link OpenSeadragon.Options.navImages}
489  *
490  * @property {String} rotateRightButton
491  *     Set the id of the custom 'Rotate right' button to use.
492  *     This is useful to have a custom button anywhere in the web page.<br>
493  *     To only change the button images, consider using
494  *     {@link OpenSeadragon.Options.navImages}
495  *
496  * @property {String} previousButton
497  *     Set the id of the custom 'Previous page' button to use.
498  *     This is useful to have a custom button anywhere in the web page.<br>
499  *     To only change the button images, consider using
500  *     {@link OpenSeadragon.Options.navImages}
501  *
502  * @property {String} nextButton
503  *     Set the id of the custom 'Next page' button to use.
504  *     This is useful to have a custom button anywhere in the web page.<br>
505  *     To only change the button images, consider using
506  *     {@link OpenSeadragon.Options.navImages}
507  *
508  * @property {Number} [initialPage=0]
509  *     If the viewer has been configured with a sequence of tile sources, display this page initially.
510  *
511  * @property {Boolean} [preserveViewport=false]
512  *     If the viewer has been configured with a sequence of tile sources, then
513  *     normally navigating to through each image resets the viewport to 'home'
514  *     position.  If preserveViewport is set to true, then the viewport position
515  *     is preserved when navigating between images in the sequence.
516  *
517  * @property {Boolean} [showReferenceStrip=false]
518  *     If the viewer has been configured with a sequence of tile sources, then
519  *     display a scrolling strip of image thumbnails for navigating through the images.
520  *
521  * @property {String} [referenceStripScroll='horizontal']
522  *
523  * @property {Element} [referenceStripElement=null]
524  *
525  * @property {Number} [referenceStripHeight=null]
526  *
527  * @property {Number} [referenceStripWidth=null]
528  *
529  * @property {String} [referenceStripPosition='BOTTOM_LEFT']
530  *
531  * @property {Number} [referenceStripSizeRatio=0.2]
532  *
533  * @property {Boolean} [collectionMode=false]
534  *
535  * @property {Number} [collectionRows=3]
536  *
537  * @property {String} [collectionLayout='horizontal']
538  *
539  * @property {Number}
539 [collectionTileSize=800]
540  *
541  * @property {String|Boolean} [crossOriginPolicy=false]
542  *      Valid values are 'Anonymous', 'use-credentials', and false. If false, canvas requests will
543  *      not use CORS, and the canvas will be tainted.
544  *
545  */
546
547 /**
548  * Settings for gestures generated by a pointer device.
549  *
550  * @typedef {Object} GestureSettings
551  * @memberof OpenSeadragon
552  *
553  * @property {Boolean} scrollToZoom
554  *     Set to false to disable zooming on scroll gestures.
555  *
556  * @property {Boolean} clickToZoom
557  *     Set to false to disable zooming on click gestures.
558  *
559  * @property {Boolean} dblClickToZoom
560  *     Set to false to disable zooming on double-click gestures. Note: If set to true
561  *     then clickToZoom should be set to false to prevent multiple zooms.
562  *
563  * @property {Boolean} pinchToZoom
564  *     Set to false to disable zooming on pinch gestures.
565  *
566  * @property {Boolean} flickEnabled
567  *     Set to false to disable the kinetic panning effect (flick) at the end of a drag gesture.
568  *
569  * @property {Number} flickMinSpeed
570  *     If flickEnabled is true, the minimum speed (in pixels-per-second) required to cause the kinetic panning effect (flick) at the end of a drag gesture.
571  *
572  * @property {Number} flickMomentum
573  *     If flickEnabled is true, a constant multiplied by the velocity to determine the distance of the kinetic panning effect (flick) at the end of a drag gesture.
574  *     A larger value will make the flick feel "lighter", while a smaller value will make the flick feel "heavier".
575  *     Note: springStiffness and animationTime also affect the "spring" used to stop the flick animation.
576  *
577  */
578
579/**
580  * The names for the image resources used for the image navigation buttons.
581  *
582  * @typedef {Object} NavImages
583  * @memberof OpenSeadragon
584  *
585  * @property {Object} zoomIn - Images for the zoom-in button.
586  * @property {String} zoomIn.REST
587  * @property {String} zoomIn.GROUP
588  * @property {String} zoomIn.HOVER
589  * @property {String} zoomIn.DOWN
590  *
591  * @property {Object} zoomOut - Images for the zoom-out button.
592  * @property {String} zoomOut.REST
593  * @property {String} zoomOut.GROUP
594  * @property {String} zoomOut.HOVER
595  * @property {String} zoomOut.DOWN
596  *
597  * @property {Object} home - Images for the home button.
598  * @property {String} home.REST
599  * @property {String} home.GROUP
600  * @property {String} home.HOVER
601  * @property {String} home.DOWN
602  *
603  * @property {Object} fullpage - Images for the full-page button.
604  * @property {String} fullpage.REST
605  * @property {String} fullpage.GROUP
606  * @property {String} fullpage.HOVER
607  * @property {String} fullpage.DOWN
608  *
609  * @property {Object} rotateleft - Images for the rotate left button.
610  * @property {String} rotateleft.REST
611  * @property {String} rotateleft.GROUP
612  * @property {String} rotateleft.HOVER
613  * @property {String} rotateleft.DOWN
614  *
615  * @property {Object} rotateright - Images for the rotate right button.
616  * @property {String} rotateright.REST
617  * @property {String} rotateright.GROUP
618  * @property {String} rotateright.HOVER
619  * @property {String} rotateright.DOWN
620  *
621  * @property {Object} previous - Images for the previous button.
622  * @property {String} previous.REST
623  * @property {String} previous.GROUP
624  * @property {String} previous.HOVER
625  * @property {String} previous.DOWN
626  *
627  * @property {Object} next - Images for the next button.
628  * @property {String} next.REST
629  * @property {String} next.GROUP
630  * @property {String} next.HOVER
631  * @property {String} next.DOWN
632  *
633  */
634
635
636 /**
637  * This function serves as a single point of instantiation for an {@link OpenSeadragon.Viewer}, including all
638  * combinations of out-of-the-box configurable features.
639  *
640  * @function OpenSeadragon
641  * @memberof module:OpenSeadragon
642  * @param {OpenSeadragon.Options} options - Viewer options.
643  * @returns {OpenSeadragon.Viewer}
644  */
645window.OpenSeadragon = window.OpenSeadragon || function( options ){
646
647    return new OpenSeadragon.Viewer( options );
648
649};
650
651
652(function( $ ){
653
654
655    /**
656     * The OpenSeadragon version.
657     *
658     * @member {Object} OpenSeadragon.version
659     * @property {String} versionStr - The version number as a string ('major.minor.revision').
660     * @property {Number} major - The major version number.
661     * @property {Number} minor - The minor version number.
662     * @property {Number} revision - The revision number.
663     * @since 1.0.0
664     */
665    $.version = {
666        versionStr: '1.1.1',
667        major: parseInt('1', 10),
668        minor: parseInt('1', 10),
669        revision: parseInt('1', 10)
670    };
671
672
673    /**
674     * Taken from jquery 1.6.1
675     * [[Class]] -> type pairs
676     * @private
677     */
678    var class2type = {
679            '[object Boolean]':     'boolean',
680            '[object Number]':      'number',
681            '[object String]':      'string',
682            '[object Function]':    'function',
683            '[object Array]':       'array',
684            '[object Date]':        'date',
685            '[object RegExp]':      'regexp',
686            '[object Object]':      'object'
687        },
688        // Save a reference to some core methods
689        toString    = Object.prototype.toString,
690        hasOwn      = Object.prototype.hasOwnProperty;
691
692    /**
693     * Taken from jQuery 1.6.1
694     * @function isFunction
695     * @memberof OpenSeadragon
696     * @see {@link http://www.jquery.com/ jQuery}
697     */
698    $.isFunction = function( obj ) {
699        return $.type(obj) === "function";
700    };
701
702
703    /**
704     * Taken from jQuery 1.6.1
705     * @function isArray
706     * @memberof OpenSeadragon
707     * @see {@link http://www.jquery.com/ jQuery}
708     */
709    $.isArray = Array.isArray || function( obj ) {
710        return $.type(obj) === "array";
711    };
712
713
714    /**
715     * A crude way of determining if an object is a window.
716     * Taken from jQuery 1.6.1
717     * @function isWindow
718     * @memberof OpenSeadragon
719     * @see {@link http://www.jquery.com/ jQuery}
720     */
721    $.isWindow = function( obj ) {
722        return obj && typeof obj === "object" && "setInterval" in obj;
723    };
724
725
726    /**
727     * Taken from jQuery 1.6.1
728     * @function type
729     * @memberof OpenSeadragon
730     * @see {@link http://www.jquery.com/ jQuery}
731     */
732    $.type = function( obj ) {
733        return ( obj === null ) || ( obj === undefined ) ?
734            String( obj ) :
735            class2type[ toString.call(obj) ] || "object";
736    };
737
738
739    /**
740     * Taken from jQuery 1.6.1
741     * @function isPlainObject
742     * @memberof OpenSeadragon
743     * @see {@link http://www.jquery.com/ jQuery}
744     */
745    $.isPlainObject = function( obj ) {
746        // Must be an Object.
747        // Because of IE, we also have to check the presence of the constructor property.
748        // Make sure that DOM nodes and window objects don't pass through, as well
749        if ( !obj || OpenSeadragon.type(obj) !== "object" || obj.nodeType || $.isWindow( obj ) ) {
750            return false;
751        }
752
753        // Not own constructor property must be Object
754        if ( obj.constructor &&
755            !hasOwn.call(obj, "constructor") &&
756            !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
757            return false;
758        }
759
760        // Own properties are enumerated firstly, so to speed up,
761        // if last one is own, then all properties are own.
762
763        var key;
764        for ( key in obj ) {}
765
766        return key === undefined || hasOwn.call( obj, key );
767    };
768
769
770    /**
771     * Taken from jQuery 1.6.1
772     * @function isEmptyObject
773     * @memberof OpenSeadragon
774     * @see {@link http://www.jquery.com/ jQuery}
775     */
776    $.isEmptyObject = function( obj ) {
777        for ( var name in obj ) {
778            return false;
779        }
780        return true;
781    };
782
783
784    /**
785     * True if the browser supports the HTML5 canvas element
786     * @member {Boolean} supportsCanvas
787     * @memberof OpenSeadragon
788     */
789    $.supportsCanvas = (function () {
790        var canvasElement = document.createElement( 'canvas' );
791        return !!( $.isFunction( canvasElement.getContext ) &&
792                    canvasElement.getContext( '2d' ) );
793    }());
794
795
796}( OpenSeadragon ));
797
798/**
799 *  This closure defines all static methods available to the OpenSeadragon
800 *  namespace.  Many, if not most, are taked directly from jQuery for use
801 *  to simplify and reduce common programming patterns.  More static methods
802 *  from jQuery may eventually make their way into this though we are
803 *  attempting to avoid an explicit dependency on jQuery only because
804 *  OpenSeadragon is a broadly useful code base and would be made less broad
805 *  by requiring jQuery fully.
806 *
807 *  Some static methods have also been refactored from the original OpenSeadragon
808 *  project.
809 */
810(function( $ ){
811
812    /**
813     * Taken from jQuery 1.6.1
814     * @function extend
815     * @memberof OpenSeadragon
816     * @see {@link http://www.jquery.com/ jQuery}
817     */
818    $.extend = function() {
819        var options,
820            name,
821            src,
822            copy,
823            copyIsArray,
824            clone,
825            target  = arguments[ 0 ] || {},
826            length  = arguments.length,
827            deep    = false,
828            i       = 1;
829
830        // Handle a deep copy situation
831        if ( typeof target === "boolean" ) {
832            deep    = target;
833            target  = arguments[ 1 ] || {};
834            // skip the boolean and the target
835            i = 2;
836        }
837
838        // Handle case when target is a string or something (possible in deep copy)
839        if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) {
840            target = {};
841        }
842
843        // extend jQuery itself if only one argument is passed
844        if ( length === i ) {
845            target = this;
846            --i;
847        }
848
849        for ( ; i < length; i++ ) {
850            // Only deal with non-null/undefined values
851            options = arguments[ i ];
852            if ( options !== null || options !== undefined ) {
853                // Extend the base object
854                for ( name in options ) {
855                    src = target[ name ];
856                    copy = options[ name ];
857
858                    // Prevent never-ending loop
859                    if ( target === copy ) {
860                        continue;
861                    }
862
863                    // Recurse if we're merging plain objects or arrays
864                    if ( deep && copy && ( OpenSeadragon.isPlainObject( copy ) || ( copyIsArray = OpenSeadragon.isArray( copy ) ) ) ) {
865                        if ( copyIsArray ) {
866                            copyIsArray = false;
867                            clone = src && OpenSeadragon.isArray( src ) ? src : [];
868
869                        } else {
870                            clone = src && OpenSeadragon.isPlainObject( src ) ? src : {};
871                        }
872
873                        // Never move original objects, clone them
874                        target[ name ] = OpenSeadragon.extend( deep, clone, copy );
875
876                    // Don't bring in undefined values
877                    } else if ( copy !== undefined ) {
878                        target[ name ] = copy;
879                    }
880                }
881            }
882        }
883
884        // Return the modified object
885        return target;
886    };
887
888
889    $.extend( $, /** @lends OpenSeadragon */{
890        /**
891         * The default values for the optional settings documented at {@link OpenSeadragon.Options}.
892         * @static
893         * @type {Object}
894         */
895        DEFAULT_SETTINGS: {
896            //DATA SOURCE DETAILS
897            xmlPath:                null,
898            tileSources:            null,
899            tileHost:               null,
900            initialPage:            0,
901            crossOriginPolicy:      false,
902
903            //PAN AND ZOOM SETTINGS AND CONSTRAINTS
904            panHorizontal:          true,
905            panVertical:            true,
906            constrainDuringPan:     false,
907            wrapHorizontal:         false,
908            wrapVertical:           false,
909            visibilityRatio:        0.5, //-> how much of the viewer can be negative space
910            minPixelRatio:          0.5, //->closer to 0 draws tiles meant for a higher zoom at this zoom
911            defaultZoomLevel:       0,
912            minZoomLevel:           null,
913            maxZoomLevel:           null,
914
915            //UI RESPONSIVENESS AND FEEL
916            clickTimeThreshold:     300,
917            clickDistThreshold:     5,
918            dblClickTimeThreshold:  300,
919            dblClickDistThreshold:  20,
920            springStiffness:        6.5,
921            animationTime:          1.2,
922            gestureSettingsMouse:   { scrollToZoom: true,  clickToZoom: true,  dblClickToZoom: false, pinchToZoom: false, flickEnabled: false, flickMinSpeed: 120, flickMomentum: 0.25 },
923            gestureSettingsTouch:   { scrollToZoom: false, clickToZoom: false, dblClickToZoom: true,  pinchToZoom: true,  flickEnabled: true,  flickMinSpeed: 120, flickMomentum: 0.25 },
924            gestureSettingsPen:     { scrollToZoom: false, clickToZoom: true,  dblClickToZoom: false, pinchToZoom: false, flickEnabled: false, flickMinSpeed: 120, flickMomentum: 0.25 },
925            gestureSettingsUnknown: { scrollToZoom: false, clickToZoom: false, dblClickToZoom: true,  pinchToZoom: true,  flickEnabled: true,  flickMinSpeed: 120, flickMomentum: 0.25 },
926            zoomPerClick:           2,
927            zoomPerScroll:          1.2,
928            zoomPerSecond:          1.0,
929            blendTime:              0,
930            alwaysBlend:            false,
931            autoHideControls:       true,
932            immediateRender:        false,
933            minZoomImageRatio:      0.9, //-> closer to 0 allows zoom out to infinity
934            maxZoomPixelRatio:      1.1, //-> higher allows 'over zoom' into pixels
935            pixelsPerWheelLine:     40,
936            autoResize:             true,
937
938            //DEFAULT CONTROL SETTINGS
939            showSequenceControl:     true,  //SEQUENCE
940            sequenceControlAnchor:   null,  //SEQUENCE
941            preserveViewport:        false, //SEQUENCE
942            navPrevNextWrap:         false, //SEQUENCE
943            showNavigationControl:   true,  //ZOOM/HOME/FULL/ROTATION
944            navigationControlAnchor: null,  //ZOOM/HOME/FULL/ROTATION
945            showZoomControl:         true,  //ZOOM
946            showHomeControl:         true,  //HOME
947            showFullPageControl:     true,  //FULL
948            showRotationControl:     false, //ROTATION
949            controlsFadeDelay:       2000,  //ZOOM/HOME/FULL/SEQUENCE
950            controlsFadeLength:      1500,  //ZOOM/HOME/FULL/SEQUENCE
951            mouseNavEnabled:         true,  //GENERAL MOUSE INTERACTIVITY
952
953            //VIEWPORT NAVIGATOR SETTINGS
954            showNavigator:              false,
955            navigatorId:                null,
956            navigatorPosition:          null,
957            navigatorSizeRatio:         0.2,
958            navigatorMaintainSizeRatio: false,
959            navigatorTop:               null,
960            navigatorLeft:              null,
961            navigatorHeight:            null,
962            navigatorWidth:             null,
963            navigatorAutoResize:        true,
964
965            // INITIAL ROTATION
966            degrees:                0,
967
968            // APPEARANCE
969            opacity:                1,
970
971            // LAYERS SETTINGS
972            layersAspectRatioEpsilon:   0.0001,
973
974            //REFERENCE STRIP SETTINGS
975            showReferenceStrip:          false,
976            referenceStripScroll:       'horizontal',
977            referenceStripElement:       null,
978            referenceStripHeight:        null,
979            referenceStripWidth:         null,
980            referenceStripPosition:      'BOTTOM_LEFT',
981            referenceStripSizeRatio:     0.2,
982
983            //COLLECTION VISUALIZATION SETTINGS
984            collectionRows:         3, //or columns depending on layout
985            collectionLayout:       'horizontal', //vertical
986            collectionMode:         false,
987            collectionTileSize:     800,
988
989            //PERFORMANCE SETTINGS
990            imageLoaderLimit:       0,
991            maxImageCacheCount:     200,
992            timeout:                30000,
993            useCanvas:              true,  // Use canvas element for drawing if available
994
995            //INTERFACE RESOURCE SETTINGS
996            prefixUrl:              "/images/",
997            navImages: {
998                zoomIn: {
999                    REST:   'zoomin_rest.png',
1000                    GROUP:  'zoomin_grouphover.png',
1001                    HOVER:  'zoomin_hover.png',
1002                    DOWN:   'zoomin_pressed.png'
1003                },
1004                zoomOut: {
1005                    REST:   'zoomout_rest.png',
1006                    GROUP:  'zoomout_grouphover.png',
1007                    HOVER:  'zoomout_hover.png',
1008                    DOWN:   'zoomout_pressed.png'
1009                },
1010                home: {
1011                    REST:   'home_rest.png',
1012                    GROUP:  'home_grouphover.png',
1013                    HOVER:  'home_hover.png',
1014                    DOWN:   'home_pressed.png'
1015                },
1016                fullpage: {
1017                    REST:   'fullpage_rest.png',
1018                    GROUP:  'fullpage_grouphover.png',
1019                    HOVER:  'fullpage_hover.png',
1020                    DOWN:   'fullpage_pressed.png'
1021                },
1022                rotateleft: {
1023                    REST:   'rotateleft_rest.png',
1024                    GROUP:  'rotateleft_grouphover.png',
1025                    HOVER:  'rotateleft_hover.png',
1026                    DOWN:   'rotateleft_pressed.png'
1027                },
1028                rotateright: {
1029                    REST:   'rotateright_rest.png',
1030                    GROUP:  'rotateright_grouphover.png',
1031                    HOVER:  'rotateright_hover.png',
1032                    DOWN:   'rotateright_pressed.png'
1033                },
1034                previous: {
1035                    REST:   'previous_rest.png',
1036                    GROUP:  'previous_grouphover.png',
1037                    HOVER:  'previous_hover.png',
1038                    DOWN:   'previous_pressed.png'
1039                },
1040                next: {
1041                    REST:   'next_rest.png',
1042                    GROUP:  'next_grouphover.png',
1043                    HOVER:  'next_hover.png',
1044                    DOWN:   'next_pressed.png'
1045                }
1046            },
1047
1048            //DEVELOPER SETTINGS
1049            debugMode:              false,
1050            debugGridColor:         '#437AB2'
1051        },
1052
1053
1054        /**
1055         * TODO: get rid of this.  I can't see how it's required at all.  Looks
1056         *       like an early legacy code artifact.
1057         * @static
1058         * @ignore
1059         */
1060        SIGNAL: "----seadragon----",
1061
1062
1063        /**
1064         * Returns a function which invokes the method as if it were a method belonging to the object.
1065         * @function
1066         * @param {Object} object
1067         * @param {Function} method
1068         * @returns {Function}
1069         */
1070        delegate: function( object, method ) {
1071            return function(){
1072                var args = arguments;
1073                if ( args === undefined ){
1074                    args = [];
1075                }
1076                return method.apply( object, args );
1077            };
1078        },
1079
1080
1081        /**
1082         * An enumeration of Browser vendors.
1083         * @static
1084         * @type {Object}
1085         * @property {Number} UNKNOWN
1086         * @property {Number} IE
1087         * @property {Number} FIREFOX
1088         * @property {Number} SAFARI
1089         * @property {Number} CHROME
1090         * @property {Number} OPERA
1091         */
1092        BROWSERS: {
1093            UNKNOWN:    0,
1094            IE:         1,
1095            FIREFOX:    2,
1096            SAFARI:     3,
1097            CHROME:     4,
1098            OPERA:      5
1099        },
1100
1101
1102        /**
1103         * Returns a DOM Element for the given id or element.
1104         * @function
1105         * @param {String|Element} element Accepts an id or element.
1106         * @returns {Element} The element with the given id, null, or the element itself.
1107         */
1108        getElement: function( element ) {
1109            if ( typeof ( element ) == "string" ) {
1110                element = document.getElementById( element );
1111            }
1112            return element;
1113        },
1114
1115
1116        /**
1117         * Determines the position of the upper-left corner of the element.
1118         * @function
1119         * @param {Element|String} element - the elemenet we want the position for.
1120         * @returns {OpenSeadragon.Point} - the position of the upper left corner of the element.
1121         */
1122        getElementPosition: function( element ) {
1123            var result = new $.Point(),
1124                isFixed,
1125                offsetParent;
1126
1127            element      = $.getElement( element );
1128            isFixed      = $.getElementStyle( element ).position == "fixed";
1129            offsetParent = getOffsetParent( element, isFixed );
1130
1131            while ( offsetParent ) {
1132
1133                result.x += element.offsetLeft;
1134                result.y += element.offsetTop;
1135
1136                if ( isFixed ) {
1137                    result = result.plus( $.getPageScroll() );
1138                }
1139
1140                element = offsetParent;
1141                isFixed = $.getElementStyle( element ).position == "fixed";
1142                offsetParent = getOffsetParent( element, isFixed );
1143            }
1144
1145            return result;
1146        },
1147
1148
1149        /**
1150         * Determines the position of the upper-left corner of the element adju
1150sted for current page and/or element scroll.
1151         * @function
1152         * @param {Element|String} element - the element we want the position for.
1153         * @returns {OpenSeadragon.Point} - the position of the upper left corner of the element adjusted for current page and/or element scroll.
1154         */
1155        getElementOffset: function( element ) {
1156            element = $.getElement( element );
1157
1158            var doc = element && element.ownerDocument,
1159                docElement,
1160                win,
1161                boundingRect = { top: 0, left: 0 };
1162
1163            if ( !doc ) {
1164                return new $.Point();
1165            }
1166
1167            docElement = doc.documentElement;
1168
1169            if ( typeof element.getBoundingClientRect !== typeof undefined ) {
1170                boundingRect = element.getBoundingClientRect();
1171            }
1172
1173            win = ( doc == doc.window ) ?
1174                doc :
1175                ( doc.nodeType === 9 ) ?
1176                    doc.defaultView || doc.parentWindow :
1177                    false;
1178
1179            return new $.Point(
1180                boundingRect.left + ( win.pageXOffset || docElement.scrollLeft ) - ( docElement.clientLeft || 0 ),
1181                boundingRect.top + ( win.pageYOffset || docElement.scrollTop ) - ( docElement.clientTop || 0 )
1182            );
1183        },
1184
1185
1186        /**
1187         * Determines the height and width of the given element.
1188         * @function
1189         * @param {Element|String} element
1190         * @returns {OpenSeadragon.Point}
1191         */
1192        getElementSize: function( element ) {
1193            element = $.getElement( element );
1194
1195            return new $.Point(
1196                element.clientWidth,
1197                element.clientHeight
1198            );
1199        },
1200
1201
1202        /**
1203         * Returns the CSSStyle object for the given element.
1204         * @function
1205         * @param {Element|String} element
1206         * @returns {CSSStyle}
1207         */
1208        getElementStyle:
1209            document.documentElement.currentStyle ?
1210            function( element ) {
1211                element = $.getElement( element );
1212                return element.currentStyle;
1213            } :
1214            function( element ) {
1215                element = $.getElement( element );
1216                return window.getComputedStyle( element, "" );
1217            },
1218
1219
1220        /**
1221         * Determines if a point is within the bounding rectangle of the given element (hit-test).
1222         * @function
1223         * @param {Element|String} element
1224         * @param {OpenSeadragon.Point} point
1225         * @returns {Boolean}
1226         */
1227        pointInElement: function( element, point ) {
1228            element = $.getElement( element );
1229            var offset = $.getElementOffset( element ),
1230                size = $.getElementSize( element );
1231            return point.x >= offset.x && point.x < offset.x + size.x && point.y < offset.y + size.y && point.y >= offset.y;
1232        },
1233
1234
1235        /**
1236         * Gets the latest event, really only useful internally since its
1237         * specific to IE behavior.
1238         * @function
1239         * @param {Event} [event]
1240         * @returns {Event}
1241         * @deprecated For internal use only
1242         * @private
1243         */
1244        getEvent: function( event ) {
1245            if( event ){
1246                $.getEvent = function( event ) {
1247                    return event;
1248                };
1249            } else {
1250                $.getEvent = function() {
1251                    return window.event;
1252                };
1253            }
1254            return $.getEvent( event );
1255        },
1256
1257
1258        /**
1259         * Gets the position of the mouse on the screen for a given event.
1260         * @function
1261         * @param {Event} [event]
1262         * @returns {OpenSeadragon.Point}
1263         */
1264        getMousePosition: function( event ) {
1265
1266            if ( typeof( event.pageX ) == "number" ) {
1267                $.getMousePosition = function( event ){
1268                    var result = new $.Point();
1269
1270                    event = $.getEvent( event );
1271                    result.x = event.pageX;
1272                    result.y = event.pageY;
1273
1274                    return result;
1275                };
1276            } else if ( typeof( event.clientX ) == "number" ) {
1277                $.getMousePosition = function( event ){
1278                    var result = new $.Point();
1279
1280                    event = $.getEvent( event );
1281                    result.x =
1282                        event.clientX +
1283                        document.body.scrollLeft +
1284                        document.documentElement.scrollLeft;
1285                    result.y =
1286                        event.clientY +
1287                        document.body.scrollTop +
1288                        document.documentElement.scrollTop;
1289
1290                    return result;
1291                };
1292            } else {
1293                throw new Error(
1294                    "Unknown event mouse position, no known technique."
1295                );
1296            }
1297
1298            return $.getMousePosition( event );
1299        },
1300
1301
1302        /**
1303         * Determines the page's current scroll position.
1304         * @function
1305         * @returns {OpenSeadragon.Point}
1306         */
1307        getPageScroll: function() {
1308            var docElement  = document.documentElement || {},
1309                body        = document.body || {};
1310
1311            if ( typeof( window.pageXOffset ) == "number" ) {
1312                $.getPageScroll = function(){
1313                    return new $.Point(
1314                        window.pageXOffset,
1315                        window.pageYOffset
1316                    );
1317                };
1318            } else if ( body.scrollLeft || body.scrollTop ) {
1319                $.getPageScroll = function(){
1320                    return new $.Point(
1321                        document.body.scrollLeft,
1322                        document.body.scrollTop
1323                    );
1324                };
1325            } else if ( docElement.scrollLeft || docElement.scrollTop ) {
1326                $.getPageScroll = function(){
1327                    return new $.Point(
1328                        document.documentElement.scrollLeft,
1329                        document.documentElement.scrollTop
1330                    );
1331                };
1332            } else {
1333                // We can't reassign the function yet, as there was no scroll.
1334                return new $.Point(0,0);
1335            }
1336
1337            return $.getPageScroll();
1338        },
1339
1340        /**
1341         * Set the page scroll position.
1342         * @function
1343         * @returns {OpenSeadragon.Point}
1344         */
1345        setPageScroll: function( scroll ) {
1346            if ( typeof ( window.scrollTo ) !== "undefined" ) {
1347                $.setPageScroll = function( scroll ) {
1348                    window.scrollTo( scroll.x, scroll.y );
1349                };
1350            } else {
1351                var originalScroll = $.getPageScroll();
1352                if ( originalScroll.x === scroll.x &&
1353                    originalScroll.y === scroll.y ) {
1354                    // We are already correctly positioned and there
1355                    // is no way to detect the correct method.
1356                    return;
1357                }
1358
1359                document.body.scrollLeft = scroll.x;
1360                document.body.scrollTop = scroll.y;
1361                var currentScroll = $.getPageScroll();
1362                if ( currentScroll.x !== originalScroll.x &&
1363                    currentScroll.y !== originalScroll.y ) {
1364                    $.setPageScroll = function( scroll ) {
1365                        document.body.scrollLeft = scroll.x;
1366                        document.body.scrollTop = scroll.y;
1367                    };
1368                    return;
1369                }
1370
1371                document.documentElement.scrollLeft = scroll.x;
1372                document.documentElement.scrollTop = scroll.y;
1373                currentScroll = $.getPageScroll();
1374                if ( currentScroll.x !== originalScroll.x &&
1375                    currentScroll.y !== originalScroll.y ) {
1376                    $.setPageScroll = function( scroll ) {
1377                        document.documentElement.scrollLeft = scroll.x;
1378                        document.documentElement.scrollTop = scroll.y;
1379                    };
1380                    return;
1381                }
1382
1383                // We can't find anything working, so we do nothing.
1384                $.setPageScroll = function( scroll ) {
1385                };
1386            }
1387
1388            return $.setPageScroll( scroll );
1389        },
1390
1391        /**
1392         * Determines the size of the browsers window.
1393         * @function
1394         * @returns {OpenSeadragon.Point}
1395         */
1396        getWindowSize: function() {
1397            var docElement = document.documentElement || {},
1398                body    = document.body || {};
1399
1400            if ( typeof( window.innerWidth ) == 'number' ) {
1401                $.getWindowSize = function(){
1402                    return new $.Point(
1403                        window.innerWidth,
1404                        window.innerHeight
1405                    );
1406                };
1407            } else if ( docElement.clientWidth || docElement.clientHeight ) {
1408                $.getWindowSize = function(){
1409                    return new $.Point(
1410                        document.documentElement.clientWidth,
1411                        document.documentElement.clientHeight
1412                    );
1413                };
1414            } else if ( body.clientWidth || body.clientHeight ) {
1415                $.getWindowSize = function(){
1416                    return new $.Point(
1417                        document.body.clientWidth,
1418                        document.body.clientHeight
1419                    );
1420                };
1421            } else {
1422                throw new Error("Unknown window size, no known technique.");
1423            }
1424
1425            return $.getWindowSize();
1426        },
1427
1428
1429        /**
1430         * Wraps the given element in a nest of divs so that the element can
1431         * be easily centered using CSS tables
1432         * @function
1433         * @param {Element|String} element
1434         * @returns {Element} outermost wrapper element
1435         */
1436        makeCenteredNode: function( element ) {
1437            // Convert a possible ID to an actual HTMLElement
1438            element = $.getElement( element );
1439
1440            /*
1441                CSS tables require you to have a display:table/row/cell hierarchy so we need to create
1442                three nested wrapper divs:
1443             */
1444
1445            var wrappers = [
1446                $.makeNeutralElement( 'div' ),
1447                $.makeNeutralElement( 'div' ),
1448                $.makeNeutralElement( 'div' )
1449            ];
1450
1451            // It feels like we should be able to pass style dicts to makeNeutralElement:
1452            $.extend(wrappers[0].style, {
1453                display: "table",
1454                height: "100%",
1455                width: "100%"
1456            });
1457
1458            $.extend(wrappers[1].style, {
1459                display: "table-row"
1460            });
1461
1462            $.extend(wrappers[2].style, {
1463                display: "table-cell",
1464                verticalAlign: "middle",
1465                textAlign: "center"
1466            });
1467
1468            wrappers[0].appendChild(wrappers[1]);
1469            wrappers[1].appendChild(wrappers[2]);
1470            wrappers[2].appendChild(element);
1471
1472            return wrappers[0];
1473        },
1474
1475
1476        /**
1477         * Creates an easily positionable element of the given type that therefor
1478         * serves as an excellent container element.
1479         * @function
1480         * @param {String} tagName
1481         * @returns {Element}
1482         */
1483        makeNeutralElement: function( tagName ) {
1484            var element = document.createElement( tagName ),
1485                style   = element.style;
1486
1487            style.background = "transparent none";
1488            style.border     = "none";
1489            style.margin     = "0px";
1490            style.padding    = "0px";
1491            style.position   = "static";
1492
1493            return element;
1494        },
1495
1496
1497        /**
1498         * Returns the current milliseconds, using Date.now() if available
1499         * @function
1500         */
1501        now: function( ) {
1502          if (Date.now) {
1503            $.now = Date.now;
1504          } else {
1505            $.now = function() { return new Date().getTime(); };
1506          }
1507
1508          return $.now();
1509        },
1510
1511
1512        /**
1513         * Ensures an image is loaded correctly to support alpha transparency.
1514         * Generally only IE has issues doing this correctly for formats like
1515         * png.
1516         * @function
1517         * @param {String} src
1518         * @returns {Element}
1519         */
1520        makeTransparentImage: function( src ) {
1521
1522            $.makeTransparentImage = function( src ){
1523                var img = $.makeNeutralElement( "img" );
1524
1525                img.src = src;
1526
1527                return img;
1528            };
1529
1530            if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 7 ) {
1531
1532                $.makeTransparentImage = function( src ){
1533                    var img     = $.makeNeutralElement( "img" ),
1534                        element = null;
1535
1536                    element = $.makeNeutralElement("span");
1537                    element.style.display = "inline-block";
1538
1539                    img.onload = function() {
1540                        element.style.width  = element.style.width || img.width + "px";
1541                        element.style.height = element.style.height || img.height + "px";
1542
1543                        img.onload = null;
1544                        img = null;     // to prevent memory leaks in IE
1545                    };
1546
1547                    img.src = src;
1548                    element.style.filter =
1549                        "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" +
1550                        src +
1551                        "', sizingMethod='scale')";
1552
1553                    return element;
1554                };
1555
1556            }
1557
1558            return $.makeTransparentImage( src );
1559        },
1560
1561
1562        /**
1563         * Sets the opacity of the specified element.
1564         * @function
1565         * @param {Element|String} element
1566         * @param {Number} opacity
1567         * @param {Boolean} [usesAlpha]
1568         */
1569        setElementOpacity: function( element, opacity, usesAlpha ) {
1570
1571            var ieOpacity,
1572                ieFilter;
1573
1574            element = $.getElement( element );
1575
1576            if ( usesAlpha && !$.Browser.alpha ) {
1577                opacity = Math.round( opacity );
1578            }
1579
1580            if ( $.Browser.opacity ) {
1581                element.style.opacity = opacity < 1 ? opacity : "";
1582            } else {
1583                if ( opacity < 1 ) {
1584                    ieOpacity = Math.round( 100 * opacity );
1585                    ieFilter  = "alpha(opacity=" + ieOpacity + ")";
1586                    element.style.filter = ieFilter;
1587                } else {
1588                    element.style.filter = "";
1589                }
1590            }
1591        },
1592
1593
1594        /**
1595         * Add the specified CSS class to the element if not present.
1596         * @function
1597         * @param {Element|String} element
1598         * @param {String} className
1599         */
1600        addClass: function( element, className ) {
1601            element = $.getElement( element );
1602
1603            if ( ! element.className ) {
1604                element.className = className;
1605            } else if ( ( ' ' + element.className + ' ' ).
1606                indexOf( ' ' + className + ' ' ) === -1 ) {
1607                element.className += ' ' + className;
1608            }
1609        },
1610
1611        /**
1612         * Find the first index at which an element is found in an array or -1
1613         * if not present.
1614         *
1615         * Code taken and adapted from
1616         * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Compatibility
1617         *
1618         * @function
1619         * @param {Array} array The array from which to find the element
1620         * @param {Object} searchElement The element to 
1620find
1621         * @param {Number} [fromIndex=0] Index to start research.
1622         * @returns {Number} The index of the element in the array.
1623         */
1624        indexOf: function( array, searchElement, fromIndex ) {
1625            if ( Array.prototype.indexOf ) {
1626                this.indexOf = function( array, searchElement, fromIndex ) {
1627                    return array.indexOf( searchElement, fromIndex );
1628                };
1629            } else {
1630                this.indexOf = function( array, searchElement, fromIndex ) {
1631                    var i,
1632                        pivot = ( fromIndex ) ? fromIndex : 0,
1633                        length;
1634                    if ( !array ) {
1635                        throw new TypeError( );
1636                    }
1637
1638                    length = array.length;
1639                    if ( length === 0 || pivot >= length ) {
1640                        return -1;
1641                    }
1642
1643                    if ( pivot < 0 ) {
1644                        pivot = length - Math.abs( pivot );
1645                    }
1646
1647                    for ( i = pivot; i < length; i++ ) {
1648                        if ( array[i] === searchElement ) {
1649                            return i;
1650                        }
1651                    }
1652                    return -1;
1653                };
1654            }
1655            return this.indexOf( array, searchElement, fromIndex );
1656        },
1657
1658        /**
1659         * Remove the specified CSS class from the element.
1660         * @function
1661         * @param {Element|String} element
1662         * @param {String} className
1663         */
1664        removeClass: function( element, className ) {
1665            var oldClasses,
1666                newClasses = [],
1667                i;
1668
1669            element = $.getElement( element );
1670            oldClasses = element.className.split( /\s+/ );
1671            for ( i = 0; i < oldClasses.length; i++ ) {
1672                if ( oldClasses[ i ] && oldClasses[ i ] !== className ) {
1673                    newClasses.push( oldClasses[ i ] );
1674                }
1675            }
1676            element.className = newClasses.join(' ');
1677        },
1678
1679
1680        /**
1681         * Adds an event listener for the given element, eventName and handler.
1682         * @function
1683         * @param {Element|String} element
1684         * @param {String} eventName
1685         * @param {Function} handler
1686         * @param {Boolean} [useCapture]
1687         */
1688        addEvent: (function () {
1689            if ( window.addEventListener ) {
1690                return function ( element, eventName, handler, useCapture ) {
1691                    element = $.getElement( element );
1692                    element.addEventListener( eventName, handler, useCapture );
1693                };
1694            } else if ( window.attachEvent ) {
1695                return function ( element, eventName, handler, useCapture ) {
1696                    element = $.getElement( element );
1697                    element.attachEvent( 'on' + eventName, handler );
1698                };
1699            } else {
1700                throw new Error( "No known event model." );
1701            }
1702        }()),
1703
1704
1705        /**
1706         * Remove a given event listener for the given element, event type and
1707         * handler.
1708         * @function
1709         * @param {Element|String} element
1710         * @param {String} eventName
1711         * @param {Function} handler
1712         * @param {Boolean} [useCapture]
1713         */
1714        removeEvent: (function () {
1715            if ( window.removeEventListener ) {
1716                return function ( element, eventName, handler, useCapture ) {
1717                    element = $.getElement( element );
1718                    element.removeEventListener( eventName, handler, useCapture );
1719                };
1720            } else if ( window.detachEvent ) {
1721                return function( element, eventName, handler, useCapture ) {
1722                    element = $.getElement( element );
1723                    element.detachEvent( 'on' + eventName, handler );
1724                };
1725            } else {
1726                throw new Error( "No known event model." );
1727            }
1728        }()),
1729
1730
1731        /**
1732         * Cancels the default browser behavior had the event propagated all
1733         * the way up the DOM to the window object.
1734         * @function
1735         * @param {Event} [event]
1736         */
1737        cancelEvent: function( event ) {
1738            event = $.getEvent( event );
1739
1740            if ( event.preventDefault ) {
1741                $.cancelEvent = function( event ){
1742                    // W3C for preventing default
1743                    event.preventDefault();
1744                };
1745            } else {
1746                $.cancelEvent = function( event ){
1747                    event = $.getEvent( event );
1748                    // legacy for preventing default
1749                    event.cancel = true;
1750                    // IE for preventing default
1751                    event.returnValue = false;
1752                };
1753            }
1754            $.cancelEvent( event );
1755        },
1756
1757
1758        /**
1759         * Stops the propagation of the event up the DOM.
1760         * @function
1761         * @param {Event} [event]
1762         */
1763        stopEvent: function( event ) {
1764            event = $.getEvent( event );
1765
1766            if ( event.stopPropagation ) {
1767                // W3C for stopping propagation
1768                $.stopEvent = function( event ){
1769                    event.stopPropagation();
1770                };
1771            } else {
1772                // IE for stopping propagation
1773                $.stopEvent = function( event ){
1774                    event = $.getEvent( event );
1775                    event.cancelBubble = true;
1776                };
1777
1778            }
1779
1780            $.stopEvent( event );
1781        },
1782
1783
1784        /**
1785         * Similar to OpenSeadragon.delegate, but it does not immediately call
1786         * the method on the object, returning a function which can be called
1787         * repeatedly to delegate the method. It also allows additonal arguments
1788         * to be passed during construction which will be added during each
1789         * invocation, and each invocation can add additional arguments as well.
1790         *
1791         * @function
1792         * @param {Object} object
1793         * @param {Function} method
1794         * @param [args] any additional arguments are passed as arguments to the
1795         *  created callback
1796         * @returns {Function}
1797         */
1798        createCallback: function( object, method ) {
1799            //TODO: This pattern is painful to use and debug.  It's much cleaner
1800            //      to use pinning plus anonymous functions.  Get rid of this
1801            //      pattern!
1802            var initialArgs = [],
1803                i;
1804            for ( i = 2; i < arguments.length; i++ ) {
1805                initialArgs.push( arguments[ i ] );
1806            }
1807
1808            return function() {
1809                var args = initialArgs.concat( [] ),
1810                    i;
1811                for ( i = 0; i < arguments.length; i++ ) {
1812                    args.push( arguments[ i ] );
1813                }
1814
1815                return method.apply( object, args );
1816            };
1817        },
1818
1819
1820        /**
1821         * Retreives the value of a url parameter from the window.location string.
1822         * @function
1823         * @param {String} key
1824         * @returns {String} The value of the url parameter or null if no param matches.
1825         */
1826        getUrlParameter: function( key ) {
1827            var value = URLPARAMS[ key ];
1828            return value ? value : null;
1829        },
1830
1831        /**
1832         * Retrieves the protocol used by the url. The url can either be absolute
1833         * or relative.
1834         * @function
1835         * @private
1836         * @param {String} url The url to retrieve the protocol from.
1837         * @return {String} The protocol (http:, https:, file:, ftp: ...)
1838         */
1839        getUrlProtocol: function( url ) {
1840            var match = url.match(/^([a-z]+:)\/\//i);
1841            if ( match === null ) {
1842                // Relative URL, retrive the protocol from window.location
1843                return window.location.protocol;
1844            }
1845            return match[1].toLowerCase();
1846        },
1847
1848        /**
1849         * Create an XHR object
1850         * @private
1851         * @param {type} [local] If set to true, the XHR will be file: protocol
1852         * compatible if possible (but may raise a warning in the browser).
1853         * @returns {XMLHttpRequest}
1854         */
1855        createAjaxRequest: function( local ) {
1856            // IE11 does not support window.ActiveXObject so we just try to
1857            // create one to see if it is supported.
1858            // See: http://msdn.microsoft.com/en-us/library/ie/dn423948%28v=vs.85%29.aspx
1859            var supportActiveX;
1860            try {
1861                /* global ActiveXObject:true */
1862                supportActiveX = !!new ActiveXObject( "Microsoft.XMLHTTP" );
1863            } catch( e ) {
1864                supportActiveX = false;
1865            }
1866
1867            if ( supportActiveX ) {
1868                if ( window.XMLHttpRequest ) {
1869                    $.createAjaxRequest = function( local ) {
1870                        if ( local ) {
1871                            return new ActiveXObject( "Microsoft.XMLHTTP" );
1872                        }
1873                        return new XMLHttpRequest();
1874                    };
1875                } else {
1876                    $.createAjaxRequest = function() {
1877                        return new ActiveXObject( "Microsoft.XMLHTTP" );
1878                    };
1879                }
1880            } else if ( window.XMLHttpRequest ) {
1881                $.createAjaxRequest = function() {
1882                    return new XMLHttpRequest();
1883                };
1884            } else {
1885                throw new Error( "Browser doesn't support XMLHttpRequest." );
1886            }
1887            return $.createAjaxRequest( local );
1888        },
1889
1890        /**
1891         * Makes an AJAX request.
1892         * @function
1893         * @param {String} url - the url to request
1894         * @param {Function} onSuccess - a function to call on a successful response
1895         * @param {Function} onError - a function to call on when an error occurs
1896         * @throws {Error}
1897         */
1898        makeAjaxRequest: function( url, onSuccess, onError ) {
1899            var protocol = $.getUrlProtocol( url );
1900            var request = $.createAjaxRequest( protocol === "file:" );
1901
1902            if ( !$.isFunction( onSuccess ) ) {
1903                throw new Error( "makeAjaxRequest requires a success callback" );
1904            }
1905
1906            request.onreadystatechange = function() {
1907                // 4 = DONE (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#Properties)
1908                if ( request.readyState == 4 ) {
1909                    request.onreadystatechange = function(){};
1910
1911                    var successStatus =
1912                        protocol === "http:" || protocol === "https:" ? 200 : 0;
1913                    if ( request.status === successStatus ) {
1914                        onSuccess( request );
1915                    } else {
1916                        $.console.log( "AJAX request returned %d: %s", request.status, url );
1917
1918                        if ( $.isFunction( onError ) ) {
1919                            onError( request );
1920                        }
1921                    }
1922                }
1923            };
1924
1925            try {
1926                request.open( "GET", url, true );
1927                request.send( null );
1928            } catch (e) {
1929                var msg = e.message;
1930
1931                /*
1932                    IE < 10 does not support CORS and an XHR request to a different origin will fail as soon
1933                    as send() is called. This is particularly easy to miss during development and appear in
1934                    production if you use a CDN or domain sharding and the security policy is likely to break
1935                    exception handlers since any attempt to access a property of the request object will
1936                    raise an access denied TypeError inside the catch block.
1937
1938                    To be friendlier, we'll check for this specific error and add a documentation pointer
1939                    to point developers in the right direction. We test the exception number because IE's
1940                    error messages are localized.
1941                */
1942                var oldIE = $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 10;
1943                if ( oldIE && typeof( e.number ) != "undefined" && e.number == -2147024891 ) {
1944                    msg += "\nSee http://msdn.microsoft.com/en-us/library/ms537505(v=vs.85).aspx#xdomain";
1945                }
1946
1947                $.console.log( "%s while making AJAX request: %s", e.name, msg );
1948
1949                request.onreadystatechange = function(){};
1950
1951                if ( $.isFunction( onError ) ) {
1952                    onError( request, e );
1953                }
1954            }
1955        },
1956
1957        /**
1958         * Taken from jQuery 1.6.1
1959         * @function
1960         * @param {Object} options
1961         * @param {String} options.url
1962         * @param {Function} options.callback
1963         * @param {String} [options.param='callback'] The name of the url parameter
1964         *      to request the jsonp provider with.
1965         * @param {String} [options.callbackName=] The name of the callback to
1966         *      request the jsonp provider with.
1967         */
1968        jsonp: function( options ){
1969            var script,
1970                url     = options.url,
1971                head    = document.head ||
1972                    document.getElementsByTagName( "head" )[ 0 ] ||
1973                    document.documentElement,
1974                jsonpCallback = options.callbackName || 'openseadragon' + $.now(),
1975                previous      = window[ jsonpCallback ],
1976                replace       = "$1" + jsonpCallback + "$2",
1977                callbackParam = options.param || 'callback',
1978                callback      = options.callback;
1979
1980            url = url.replace( /(\=)\?(&|$)|\?\?/i, replace );
1981            // Add callback manually
1982            url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback;
1983
1984            // Install callback
1985            window[ jsonpCallback ] = function( response ) {
1986                if ( !previous ){
1987                    try{
1988                        delete window[ jsonpCallback ];
1989                    }catch(e){
1990                        //swallow
1991                    }
1992                } else {
1993                    window[ jsonpCallback ] = previous;
1994                }
1995                if( callback && $.isFunction( callback ) ){
1996                    callback( response );
1997                }
1998            };
1999
2000            script = document.createElement( "script" );
2001
2002            //TODO: having an issue with async info requests
2003            if( undefined !== options.async || false !== options.async ){
2004                script.async = "async";
2005            }
2006
2007            if ( options.scriptCharset ) {
2008                script.charset = options.scriptCharset;
2009            }
2010
2011            script.src = url;
2012
2013            // Attach handlers for all browsers
2014            script.onload = script.onreadystatechange = function( _, isAbort ) {
2015
2016                if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
2017
2018                    // Handle memory leak in IE
2019                    script.onload = script.onreadystatechange = null;
2020
2021                    // Remove the script
2022                    if ( head && script.parentNode ) {
2023                        head.removeChild( script );
2024                    }
2025
2026                    // Dereference the script
2027                    script = undefined;
2028                }
2029            };
2030            // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
2031            // This arises when a base node is used (#2709 and #4378).
2032            head.insertBefore( script, head.firstChild );
2033
2034        },
2035
2036
2037        /**
2038         * Fully deprecated. Will throw an error.
2039         * @function
2040         * @deprecated use {@link OpenSeadragon.Viewer#open}
2041         */
2042        createFromDZI: function() {
2043            throw "OpenSeadragon.createFromDZI is deprecated, use Viewer.open.";
2044        },
2045
2046        /**
2047         * Parses an XML string into a DOM Document.
2048         * @function
2049         * @param {String} string
2050         * @returns {Document}
2051         */
2052        parseXml: function( string ) {
2053            if ( window.DOMParser ) {
2054
2055                $.parseXml = function( string ) {
2056                    var xmlDoc = null,
2057                        parser;
2058
2059                    parser = new DOMParser();
2060                    xmlDoc = parser.parseFromString( string, "text/xml" );
2061                    return xmlDoc;
2062                };
2063
2064            } else if ( window.ActiveXObject ) {
2065
2066                $.parseXml = function( string ) {
2067                    var xmlDoc = null;
2068
2069                    xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" );
2070                    xmlDoc.async = false;
2071                    xmlDoc.loadXML( string );
2072                    return xmlDoc;
2073                };
2074
2075            } else {
2076                throw new Error( "Browser doesn't support XML DOM." );
2077            }
2078
2079            return $.parseXml( string );
2080        },
2081
2082
2083        /**
2084         * Reports whether the image format is supported for tiling in this
2085         * version.
2086         * @function
2087         * @param {String} [extension]
2088         * @returns {Boolean}
2089         */
2090        imageFormatSupported: function( extension ) {
2091            extension = extension ? extension : "";
2092            return !!FILEFORMATS[ extension.toLowerCase() ];
2093        }
2094
2095    });
2096
2097
2098    /**
2099     * The current browser vendor, version, and related information regarding detected features.
2100     * @member {Object} Browser
2101     * @memberof OpenSeadragon
2102     * @static
2103     * @type {Object}
2104     * @property {OpenSeadragon.BROWSERS} vendor - One of the {@link OpenSeadragon.BROWSERS} enumeration values.
2105     * @property {Number} version
2106     * @property {Boolean} alpha - Does the browser support image alpha transparency.
2107     */
2108    $.Browser = {
2109        vendor:     $.BROWSERS.UNKNOWN,
2110        version:    0,
2111        alpha:      true
2112    };
2113
2114
2115    var FILEFORMATS = {
2116            "bmp":  false,
2117            "jpeg": true,
2118            "jpg":  true,
2119            "png":  true,
2120            "tif":  false,
2121            "wdp":  false
2122        },
2123        URLPARAMS = {};
2124
2125    (function() {
2126        //A small auto-executing routine to determine the browser vendor,
2127        //version and supporting feature sets.
2128        var app = navigator.appName,
2129            ver = navigator.appVersion,
2130            ua  = navigator.userAgent,
2131            regex;
2132
2133        //console.error( 'appName: ' + navigator.appName );
2134        //console.error( 'appVersion: ' + navigator.appVersion );
2135        //console.error( 'userAgent: ' + navigator.userAgent );
2136
2137        switch( navigator.appName ){
2138            case "Microsoft Internet Explorer":
2139                if( !!window.attachEvent &&
2140                    !!window.ActiveXObject ) {
2141
2142                    $.Browser.vendor = $.BROWSERS.IE;
2143                    $.Browser.version = parseFloat(
2144                        ua.substring(
2145                            ua.indexOf( "MSIE" ) + 5,
2146                            ua.indexOf( ";", ua.indexOf( "MSIE" ) ) )
2147                        );
2148                }
2149                break;
2150            case "Netscape":
2151                if( !!window.addEventListener ){
2152                    if ( ua.indexOf( "Firefox" ) >= 0 ) {
2153                        $.Browser.vendor = $.BROWSERS.FIREFOX;
2154                        $.Browser.version = parseFloat(
2155                            ua.substring( ua.indexOf( "Firefox" ) + 8 )
2156                        );
2157                    } else if ( ua.indexOf( "Safari" ) >= 0 ) {
2158                        $.Browser.vendor = ua.indexOf( "Chrome" ) >= 0 ?
2159                            $.BROWSERS.CHROME :
2160                            $.BROWSERS.SAFARI;
2161                        $.Browser.version = parseFloat(
2162                            ua.substring(
2163                                ua.substring( 0, ua.indexOf( "Safari" ) ).lastIndexOf( "/" ) + 1,
2164                                ua.indexOf( "Safari" )
2165                            )
2166                        );
2167                    } else {
2168                        regex = new RegExp( "Trident/.*rv:([0-9]{1,}[.0-9]{0,}) ");
2169                        if ( regex.exec( ua ) !== null ) {
2170                            $.Browser.vendor = $.BROWSERS.IE;
2171                            $.Browser.version = parseFloat( RegExp.$1 );
2172                        }
2173                    }
2174                }
2175                break;
2176            case "Opera":
2177                $.Browser.vendor = $.BROWSERS.OPERA;
2178                $.Browser.version = parseFloat( ver );
2179                break;
2180        }
2181
2182            // ignore '?' portion of query string
2183        var query = window.location.search.substring( 1 ),
2184            parts = query.split('&'),
2185            part,
2186            sep,
2187            i;
2188
2189        for ( i = 0; i < parts.length; i++ ) {
2190            part = parts[ i ];
2191            sep  = part.indexOf( '=' );
2192
2193            if ( sep > 0 ) {
2194                URLPARAMS[ part.substring( 0, sep ) ] =
2195                    decodeURIComponent( part.substring( sep + 1 ) );
2196            }
2197        }
2198
2199        //determine if this browser supports image alpha transparency
2200        $.Browser.alpha = !(
2201            (
2202                $.Browser.vendor == $.BROWSERS.IE &&
2203                $.Browser.version < 9
2204            ) || (
2205                $.Browser.vendor == $.BROWSERS.CHROME &&
2206                $.Browser.version < 2
2207            )
2208        );
2209
2210        //determine if this browser supports element.style.opacity
2211        $.Browser.opacity = !(
2212            $.Browser.vendor == $.BROWSERS.IE &&
2213            $.Browser.version < 9
2214        );
2215
2216    })();
2217
2218
2219    //TODO: $.console is often used inside a try/catch block which generally
2220    //      prevents allowings errors to occur with detection until a debugger
2221    //      is attached.  Although I've been guilty of the same anti-p
2221attern
2222    //      I eventually was convinced that errors should naturally propogate in
2223    //      all but the most special cases.
2224    /**
2225     * A convenient alias for console when available, and a simple null
2226     * function when console is unavailable.
2227     * @static
2228     * @private
2229     */
2230    var nullfunction = function( msg ){
2231            //document.location.hash = msg;
2232        };
2233
2234    $.console = window.console || {
2235        log:    nullfunction,
2236        debug:  nullfunction,
2237        info:   nullfunction,
2238        warn:   nullfunction,
2239        error:  nullfunction
2240    };
2241
2242
2243    // Adding support for HTML5's requestAnimationFrame as suggested by acdha.
2244    // Implementation taken from matt synder's post here:
2245    // http://mattsnider.com/cross-browser-and-legacy-supported-requestframeanimation/
2246    (function( w ) {
2247
2248        // most browsers have an implementation
2249        var requestAnimationFrame = w.requestAnimationFrame ||
2250            w.mozRequestAnimationFrame ||
2251            w.webkitRequestAnimationFrame ||
2252            w.msRequestAnimationFrame;
2253
2254        var cancelAnimationFrame = w.cancelAnimationFrame ||
2255            w.mozCancelAnimationFrame ||
2256            w.webkitCancelAnimationFrame ||
2257            w.msCancelAnimationFrame;
2258
2259        // polyfill, when necessary
2260        if ( requestAnimationFrame && cancelAnimationFrame ) {
2261            // We can't assign these window methods directly to $ because they
2262            // expect their "this" to be "window", so we call them in wrappers.
2263            $.requestAnimationFrame = function(){
2264                return requestAnimationFrame.apply( w, arguments );
2265            };
2266            $.cancelAnimationFrame = function(){
2267                return cancelAnimationFrame.apply( w, arguments );
2268            };
2269        } else {
2270            var aAnimQueue = [],
2271                processing = [],
2272                iRequestId = 0,
2273                iIntervalId;
2274
2275            // create a mock requestAnimationFrame function
2276            $.requestAnimationFrame = function( callback ) {
2277                aAnimQueue.push( [ ++iRequestId, callback ] );
2278
2279                if ( !iIntervalId ) {
2280                    iIntervalId = setInterval( function() {
2281                        if ( aAnimQueue.length ) {
2282                            var time = $.now();
2283                            // Process all of the currently outstanding frame
2284                            // requests, but none that get added during the
2285                            // processing.
2286                            // Swap the arrays so we don't have to create a new
2287                            // array every frame.
2288                            var temp = processing;
2289                            processing = aAnimQueue;
2290                            aAnimQueue = temp;
2291                            while ( processing.length ) {
2292                                processing.shift()[ 1 ]( time );
2293                            }
2294                        } else {
2295                            // don't continue the interval, if unnecessary
2296                            clearInterval( iIntervalId );
2297                            iIntervalId = undefined;
2298                        }
2299                    }, 1000 / 50);  // estimating support for 50 frames per second
2300                }
2301
2302                return iRequestId;
2303            };
2304
2305            // create a mock cancelAnimationFrame function
2306            $.cancelAnimationFrame = function( requestId ) {
2307                // find the request ID and remove it
2308                var i, j;
2309                for ( i = 0, j = aAnimQueue.length; i < j; i += 1 ) {
2310                    if ( aAnimQueue[ i ][ 0 ] === requestId ) {
2311                        aAnimQueue.splice( i, 1 );
2312                        return;
2313                    }
2314                }
2315
2316                // If it's not in the queue, it may be in the set we're currently
2317                // processing (if cancelAnimationFrame is called from within a
2318                // requestAnimationFrame callback).
2319                for ( i = 0, j = processing.length; i < j; i += 1 ) {
2320                    if ( processing[ i ][ 0 ] === requestId ) {
2321                        processing.splice( i, 1 );
2322                        return;
2323                    }
2324                }
2325            };
2326        }
2327    })( window );
2328
2329    /**
2330     * @private
2331     * @inner
2332     * @function
2333     * @param {Element} element
2334     * @param {Boolean} [isFixed]
2335     * @returns {Element}
2336     */
2337    function getOffsetParent( element, isFixed ) {
2338        if ( isFixed && element != document.body ) {
2339            return document.body;
2340        } else {
2341            return element.offsetParent;
2342        }
2343    }
2344
2345    /**
2346     * @private
2347     * @inner
2348     * @function
2349     * @param {XMLHttpRequest} xhr
2350     * @param {String} tilesUrl
2351     * @deprecated
2352     */
2353    function processDZIResponse( xhr, tilesUrl ) {
2354        var status,
2355            statusText,
2356            doc = null;
2357
2358        if ( !xhr ) {
2359            throw new Error( $.getString( "Errors.Security" ) );
2360        } else if ( xhr.status !== 200 && xhr.status !== 0 ) {
2361            status     = xhr.status;
2362            statusText = ( status == 404 ) ?
2363                "Not Found" :
2364                xhr.statusText;
2365            throw new Error( $.getString( "Errors.Status", status, statusText ) );
2366        }
2367
2368        if ( xhr.responseXML && xhr.responseXML.documentElement ) {
2369            doc = xhr.responseXML;
2370        } else if ( xhr.responseText ) {
2371            doc = $.parseXml( xhr.responseText );
2372        }
2373
2374        return processDZIXml( doc, tilesUrl );
2375    }
2376
2377    /**
2378     * @private
2379     * @inner
2380     * @function
2381     * @param {Document} xmlDoc
2382     * @param {String} tilesUrl
2383     * @deprecated
2384     */
2385    function processDZIXml( xmlDoc, tilesUrl ) {
2386
2387        if ( !xmlDoc || !xmlDoc.documentElement ) {
2388            throw new Error( $.getString( "Errors.Xml" ) );
2389        }
2390
2391        var root     = xmlDoc.documentElement,
2392            rootName = root.tagName;
2393
2394        if ( rootName == "Image" ) {
2395            try {
2396                return processDZI( root, tilesUrl );
2397            } catch ( e ) {
2398                throw (e instanceof Error) ?
2399                    e :
2400                    new Error( $.getString("Errors.Dzi") );
2401            }
2402        } else if ( rootName == "Collection" ) {
2403            throw new Error( $.getString( "Errors.Dzc" ) );
2404        } else if ( rootName == "Error" ) {
2405            return $._processDZIError( root );
2406        }
2407
2408        throw new Error( $.getString( "Errors.Dzi" ) );
2409    }
2410
2411    /**
2412     * @private
2413     * @inner
2414     * @function
2415     * @param {Element} imageNode
2416     * @param {String} tilesUrl
2417     * @deprecated
2418     */
2419    function processDZI( imageNode, tilesUrl ) {
2420        var fileFormat    = imageNode.getAttribute( "Format" ),
2421            sizeNode      = imageNode.getElementsByTagName( "Size" )[ 0 ],
2422            dispRectNodes = imageNode.getElementsByTagName( "Dis
2422playRect" ),
2423            width         = parseInt( sizeNode.getAttribute( "Width" ), 10 ),
2424            height        = parseInt( sizeNode.getAttribute( "Height" ), 10 ),
2425            tileSize      = parseInt( imageNode.getAttribute( "TileSize" ), 10 ),
2426            tileOverlap   = parseInt( imageNode.getAttribute( "Overlap" ), 10 ),
2427            dispRects     = [],
2428            dispRectNode,
2429            rectNode,
2430            i;
2431
2432        if ( !$.imageFormatSupported( fileFormat ) ) {
2433            throw new Error(
2434                $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() )
2435            );
2436        }
2437
2438        for ( i = 0; i < dispRectNodes.length; i++ ) {
2439            dispRectNode = dispRectNodes[ i ];
2440            rectNode     = dispRectNode.getElementsByTagName( "Rect" )[ 0 ];
2441
2442            dispRects.push( new $.DisplayRect(
2443                parseInt( rectNode.getAttribute( "X" ), 10 ),
2444                parseInt( rectNode.getAttribute( "Y" ), 10 ),
2445                parseInt( rectNode.getAttribute( "Width" ), 10 ),
2446                parseInt( rectNode.getAttribute( "Height" ), 10 ),
2447                0,  // ignore MinLevel attribute, bug in Deep Zoom Composer
2448                parseInt( dispRectNode.getAttribute( "MaxLevel" ), 10 )
2449            ));
2450        }
2451        return new $.DziTileSource(
2452            width,
2453            height,
2454            tileSize,
2455            tileOverlap,
2456            tilesUrl,
2457            fileFormat,
2458            dispRects
2459        );
2460    }
2461
2462    /**
2463     * @private
2464     * @inner
2465     * @function
2466     * @param {Element} imageNode
2467     * @param {String} tilesUrl
2468     * @deprecated
2469     */
2470    function processDZIJSON( imageData, tilesUrl ) {
2471        var fileFormat    = imageData.Format,
2472            sizeData      = imageData.Size,
2473            dispRectData  = imageData.DisplayRect || [],
2474            width         = parseInt( sizeData.Width, 10 ),
2475            height        = parseInt( sizeData.Height, 10 ),
2476            tileSize      = parseInt( imageData.TileSize, 10 ),
2477            tileOverlap   = parseInt( imageData.Overlap, 10 ),
2478            dispRects     = [],
2479            rectData,
2480            i;
2481
2482        if ( !$.imageFormatSupported( fileFormat ) ) {
2483            throw new Error(
2484                $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() )
2485            );
2486        }
2487
2488        for ( i = 0; i < dispRectData.length; i++ ) {
2489            rectData     = dispRectData[ i ].Rect;
2490
2491            dispRects.push( new $.DisplayRect(
2492                parseInt( rectData.X, 10 ),
2493                parseInt( rectData.Y, 10 ),
2494                parseInt( rectData.Width, 10 ),
2495                parseInt( rectData.Height, 10 ),
2496                0,  // ignore MinLevel attribute, bug in Deep Zoom Composer
2497                parseInt( rectData.MaxLevel, 10 )
2498            ));
2499        }
2500        return new $.DziTileSource(
2501            width,
2502            height,
2503            tileSize,
2504            tileOverlap,
2505            tilesUrl,
2506            fileFormat,
2507            dispRects
2508        );
2509    }
2510
2511    /**
2512     * @private
2513     * @inner
2514     * @function
2515     * @param {Document} errorNode
2516     * @throws {Error}
2517     * @deprecated
2518     */
2519    $._processDZIError = function ( errorNode ) {
2520        var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ],
2521            message     = messageNode.firstChild.nodeValue;
2522
2523        throw new Error(message);
2524    };
2525
2526}( OpenSeadragon ));
2527
2528/*
2529 * OpenSeadragon - full-screen support functions
2530 *
2531 * Copyright (C) 2009 CodePlex Foundation
2532 * Copyright (C) 2010-2013 OpenSeadragon contributors
2533 *
2534 * Redistribution and use in source and binary forms, with or without
2535 * modification, are permitted provided that the following conditions are
2536 * met:
2537 *
2538 * - Redistributions of source code must retain the above copyright notice,
2539 *   this list of conditions and the following disclaimer.
2540 *
2541 * - Redistributions in binary form must reproduce the above copyright
2542 *   notice, this list of conditions and the following disclaimer in the
2543 *   documentation and/or other materials provided with the distribution.
2544 *
2545 * - Neither the name of CodePlex Foundation nor the names of its
2546 *   contributors may be used to endorse or promote products derived from
2547 *   this software without specific prior written permission.
2548 *
2549 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
2550 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
2551 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
2552 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
2553 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
2554 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
2555 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
2556 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
2557 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
2558 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2559 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2560 */
2561
2562(function( $ ) {
2563    /**
2564     * Determine native full screen support we can get from the browser.
2565     * @member fullScreenApi
2566     * @memberof OpenSeadragon
2567     * @type {object}
2568     * @property {Boolean} supportsFullScreen Return true if full screen API is supported.
2569     * @property {Function} isFullScreen Return true if currently in full screen mode.
2570     * @property {Function} getFullScreenElement Return the element currently in full screen mode.
2571     * @property {Function} requestFullScreen Make a request to go in full screen mode.
2572     * @property {Function} exitFullScreen Make a request to exit full screen mode.
2573     * @property {Function} cancelFullScreen Deprecated, use exitFullScreen instead.
2574     * @property {String} fullScreenEventName Event fired when the full screen mode change.
2575     * @property {String} fullScreenErrorEventName Event fired when a request to go
2576     * in full screen mode failed.
2577     */
2578    var fullScreenApi = {
2579        supportsFullScreen: false,
2580        isFullScreen: function() { return false; },
2581        getFullScreenElement: function() { return null; },
2582        requestFullScreen: function() {},
2583        exitFullScreen: function() {},
2584        cancelFullScreen: function() {},
2585        fullScreenEventName: '',
2586        fullScreenErrorEventName: ''
2587    };
2588
2589    // check for native support
2590    if ( document.exitFullscreen ) {
2591        // W3C standard
2592        fullScreenApi.supportsFullScreen = true;
2593        fullScreenApi.getFullScreenElement = function() {
2594            return document.fullscreenElement;
2595        };
2596        fullScreenApi.requestFullScreen = function( element ) {
2597            return element.requestFullscreen();
2598        };
2599        fullScreenApi.exitFullScreen = function() {
2600            document.exitFullscreen();
2601        };
2602        fullScreenApi.fullScreenEventName = "fullscreenchange";
2603        fullScreenApi.fullScreenErrorEventName = "fullscreenerror";
2604    } else if ( document.msExitFullscreen ) {
2605        // IE 11
2606        fullScreenApi.supportsFullScreen = true;
2607        fullScreenApi.getFullScreenElement = function() {
2608            return document.msFullscreenElement;
2609        };
2610        fullScreenApi.requestFullScreen = function( element ) {
2611            return element.msRequestFullscreen();
2612        };
2613        fullScreenApi.exitFullScreen = function() {
2614            document.msExitFullscreen();
2615        };
2616        fullScreenApi.fullScreenEventName = "MSFullscreenChange";
2617        fullScreenApi.fullScreenErrorEventName = "MSFullscreenError";
2618    } else if ( document.webkitExitFullscreen ) {
2619        // Recent webkit
2620        fullScreenApi.supportsFullScreen = true;
2621        fullScreenApi.getFullScreenElement = function() {
2622            return document.webkitFullscreenElement;
2623        };
2624        fullScreenApi.requestFullScreen = function( element ) {
2625            return element.webkitRequestFullscreen();
2626        };
2627        fullScreenApi.exitFullScreen = function() {
2628            document.webkitExitFullscreen();
2629        };
2630        fullScreenApi.fullScreenEventName = "webkitfullscreenchange";
2631        fullScreenApi.fullScreenErrorEventName = "webkitfullscreenerror";
2632    } else if ( document.webkitCancelFullScreen ) {
2633        // Old webkit
2634        fullScreenApi.supportsFullScreen = true;
2635        fullScreenApi.getFullScreenElement = function() {
2636            return document.webkitCurrentFullScreenElement;
2637        };
2638        fullScreenApi.requestFullScreen = function( element ) {
2639            return element.webkitRequestFullScreen();
2640        };
2641        fullScreenApi.exitFullScreen = function() {
2642            document.webkitCancelFullScreen();
2643        };
2644        fullScreenApi.fullScreenEventName = "webkitfullscreenchange";
2645        fullScreenApi.fullScreenErrorEventName = "webkitfullscreenerror";
2646    } else if ( document.mozCancelFullScreen ) {
2647        // Firefox
2648        fullScreenApi.supportsFullScreen = true;
2649        fullScreenApi.getFullScreenElement = function() {
2650            return document.mozFullScreenElement;
2651        };
2652        fullScreenApi.requestFullScreen = function( element ) {
2653            return element.mozRequestFullScreen();
2654        };
2655        fullScreenApi.exitFullScreen = function() {
2656            document.mozCancelFullScreen();
2657        };
2658        fullScreenApi.fullScreenEventName = "mozfullscreenchange";
2659        fullScreenApi.fullScreenErrorEventName = "mozfullscreenerror";
2660    }
2661    fullScreenApi.isFullScreen = function() {
2662        return fullScreenApi.getFullScreenElement() !== null;
2663    };
2664    fullScreenApi.cancelFullScreen = function() {
2665        $.console.error("cancelFullScreen is deprecated. Use exitFullScreen instead.");
2666        fullScreenApi.exitFullScreen();
2667    };
2668
2669    // export api
2670    $.extend( $, fullScreenApi );
2671
2672})( OpenSeadragon );
2673
2674/*
2675 * OpenSeadragon - EventSource
2676 *
2677 * Copyright (C) 2009 CodePlex Foundation
2678 * Copyright (C) 2010-2013 OpenSeadragon contributors
2679 *
2680 * Redistribution and use in source and binary forms, with or without
2681 * modification, are permitted provided that the following conditions are
2682 * met:
2683 *
2684 * - Redistributions of source code must retain the above copyright notice,
2685 *   this list of conditions and the following disclaimer.
2686 *
2687 * - Redistributions in binary form must reproduce the above copyright
2688 *   notice, this list of conditions and the following disclaimer in the
2689 *   documentation and/or other materials provided with the distribution.
2690 *
2691 * - Neither the name of CodePlex Foundation nor the names of its
2692 *   contributors may be used to endorse or promote products derived from
2693 *   this software without specific prior written permission.
2694 *
2695 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
2696 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
2697 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
2698 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
2699 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
2700 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
2701 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
2702 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
2703 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
2704 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2705 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2706 */
2707
2708(function($){
2709
2710/**
2711 * Event handler method signature used by all OpenSeadragon events.
2712 *
2713 * @callback EventHandler
2714 * @memberof OpenSeadragon
2715 * @param {Object} event - See individual events for event-specific properties.
2716 */
2717
2718
2719/**
2720 * @class EventSource
2721 * @classdesc For use by classes which want to support custom, non-browser events.
2722 *
2723 * @memberof OpenSeadragon
2724 */
2725$.EventSource = function() {
2726    this.events = {};
2727};
2728
2729$.EventSource.prototype = /** @lends OpenSeadragon.EventSource.prototype */{
2730
2731    // TODO: Add a method 'one' which automatically unbinds a listener after the first triggered event that matches.
2732
2733    /**
2734     * Add an event handler for a given event.
2735     * @function
2736     * @param {String} eventName - Name of event to register.
2737     * @param {OpenSeadragon.EventHandler} handler - Function to call when event is triggered.
2738     * @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler.
2739     */
2740    addHandler: function ( eventName, handler, userData ) {
2741        var events = this.events[ eventName ];
2742        if ( !events ) {
2743            this.events[ eventName ] = events = [];
2744        }
2745        if ( handler && $.isFunction( handler ) ) {
2746            events[ events.length ] = { handler: handler, userData: userData || null };
2747        }
2748    },
2749
2750    /**
2751     * Remove a specific event handler for a given event.
2752     * @function
2753     * @param {String} eventName - Name of event for which the handler is to be removed.
2754     * @param {OpenSeadragon.EventHandler} handler - Function to be removed.
2755     */
2756    removeHandler: function ( eventName, handler ) {
2757        var events = this.events[ eventName ],
2758            handlers = [],
2759            i;
2760        if ( !events ) {
2761            return;
2762        }
2763        if ( $.isArray( events ) ) {
2764            for ( i = 0; i < events.length; i++ ) {
2765                if ( events[i].handler !== handler ) {
2766                    handlers.push( events[ i ] );
2767                }
2768            }
2769            this.events[ eventName ] = handlers;
2770        }
2771    },
2772
2773
2774    /**
2775     * Remove all event handlers for a given event type. If no type is given all
2776     * event handlers for every event type are removed.
2777     * @function
2778     * @param {String} eventName - Name of event for which all handlers are to be removed.
2779     */
2780    removeAllHandlers: function( eventName ) {
2781        if ( eventName ){
2782            this.events[ eventName ] = [];
2783        } else{
2784            for ( var eventType in this.events ) {
2785                this.events[ eventType ] = [];
2786            }
2787        }
2788    },
2789
2790    /**
2791     * Get a function which iterates the list of all handlers registered for a given event, calling the handler for each.
2792     * @function
2793     * @param {String} eventName - Name of event to get handlers for.
2794     */
2795    getHandler: function ( eventName ) {
2796        var events = this.events[ eventName ];
2797        if ( !events || !events.length ) {
2798            return null;
2799        }
2800        events = events.length === 1 ?
2801            [ events[ 0 ] ] :
2802            Array.apply( null, events );
2803        return function ( source, args ) {
2804            var i,
2805                length = events.length;
2806            for ( i = 0; i < length; i++ ) {
2807                if ( events[ i ] ) {
2808                    args.eventSource = source;
2809                    args.userData = events[ i ].userData;
2810                    events[ i ].handler( args );
2811                }
2812            }
2813        };
2814    },
2815
2816    /**
2817     * Trigger an event, optionally passing additional information.
2818     * @function
2819     * @param {String} eventName - Name of event to register.
2820     * @param {Object} eventArgs - Event-specific data.
2821     */
2822    raiseEvent: function( eventName, eventArgs ) {
2823        //uncomment if you want to get a log of all events
2824        //$.console.log( eventName );
2825        var handler = this.getHandler( eventName );
2826
2827        if ( handler ) {
2828            if ( !eventArgs ) {
2829                eventArgs = {};
2830            }
2831
2832            handler( this, eventArgs );
2833        }
2834    }
2835};
2836
2837}( OpenSeadragon ));
2838
2839/*
2840 * OpenSeadragon - MouseTracker
2841 *
2842 * Copyright (C) 2009 CodePlex Foundation
2843 * Copyright (C) 2010-2013 OpenSeadragon contributors
2844 *
2845 * Redistribution and use in source and binary forms, with or without
2846 * modification, are permitted provided that the following conditions are
2847 * met:
2848 *
2849 * - Redistributions of source code must retain the above copyright notice,
2850 *   this list of conditions and the following disclaimer.
2851 *
2852 * - Redistributions in binary form must reproduce the above copyright
2853 *   notice, this list of conditions and the following disclaimer in the
2854 *   documentation and/or other materials provided with the distribution.
2855 *
2856 * - Neither the name of CodePlex Foundation nor the names of its
2857 *   contributors may be used to endorse or promote products derived from
2858 *   this software without specific prior written permission.
2859 *
2860 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
2861 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
2862 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
2863 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
2864 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
2865 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
2866 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
2867 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
2868 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
2869 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2870 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2871 */
2872
2873(function ( $ ) {
2874
2875        // dictionary from hash to private properties
2876    var THIS           = {};
2877
2878
2879    /**
2880     * @class MouseTracker
2881     * @classdesc Provides simplified handling of common pointer device (mouse, touch, pen, etc.) gestures
2882     *            and keyboard events on a specified element.
2883     * @memberof OpenSeadragon
2884     * @param {Object} options
2885     *      Allows configurable properties to be entirely specified by passing
2886     *      an options object to the constructor.  The constructor also supports
2887     *      the original positional arguments 'element', 'clickTimeThreshold',
2888     *      and 'clickDistThreshold' in that order.
2889     * @param {Element|String} options.element
2890     *      A reference to an element or an element id for which the pointer/key
2891     *      events will be monitored.
2892     * @param {Number} options.clickTimeThreshold
2893     *      The number of milliseconds within which a pointer down-up event combination
2894     *      will be treated as a click gesture.
2895     * @param {Number} options.clickDistThreshold
2896     *      The maximum distance allowed between a pointer down event and a pointer up event
2897     *      to be treated as a click gesture.
2898     * @param {Number} options.dblClickTimeThreshold
2899     *      The number of milliseconds within which two pointer down-up event combinations
2900     *      will be treated as a double-click gesture.
2901     * @param {Number} options.dblClickDistThreshold
2902     *      The maximum distance allowed between two pointer click events
2903     *      to be treated as a click gesture.
2904     * @param {Number} [options.stopDelay=50]
2905     *      The number of milliseconds without pointer move before the stop
2906     *      event is fired.
2907     * @param {OpenSeadragon.EventHandler} [options.enterHandler=null]
2908     *      An optional handler for pointer enter.
2909     * @param {OpenSeadragon.EventHandler} [options.exitHandler=null]
2910     *      An optional handler for pointer exit.
2911     * @param {OpenSeadragon.EventHandler} [options.pressHandler=null]
2912     *      An optional handler for pointer press.
2913     * @param {OpenSeadragon.EventHandler} [options.releaseHandler=null]
2914     *      An optional handler for pointer release.
2915     * @param {OpenSeadragon.EventHandler} [options.moveHandler=null]
2916     *      An optional handler for pointer move.
2917     * @param {OpenSeadragon.EventHandler} [options.scrollHandler=null]
2918     *      An optional handler for mouse wheel scroll.
2919     * @param {OpenSeadragon.EventHandler} [options.clickHandler=null]
2920     *      An optional handler for pointer click.
2921     * @param {OpenSeadragon.EventHandler} [options.dblClickHandler=null]
2922     *      An optional handler for pointer double-click.
2923     * @param {OpenSeadragon.EventHandler} [options.dragHandler=null]
2924     *      An optional handler for the drag gesture.
2925     * @param {OpenSeadragon.EventHandler} [options.dragEndHandler=null]
2926     *      An optional handler for after a drag gesture.
2927     * @param {OpenSeadragon.EventHandler} [options.pinchHandler=null]
2928     *      An optional handler for the pinch gesture.
2929     * @param {OpenSeadragon.EventHandler} [options.keyHandler=null]
2930     *      An optional handler for keypress.
2931     * @param {OpenSeadragon.EventHandler} [options.focusHandler=null]
2932     *      An optional handler for focus.
2933     * @param {OpenSeadragon.EventHandler} [options.blurHandler=null]
2934     *      An optional handler for blur.
2935     * @param {Object} [options.userData=null]
2936     *      Arbitrary object to be passed unchanged to any attached handler methods.
2937     */
2938    $.MouseTracker = function ( options ) {
2939
2940        var args = arguments;
2941
2942        if ( !$.isPlainObject( options ) ) {
2943            options = {
2944                element:            args[ 0 ],
2945                clickTimeThreshold: args[ 1 ],
2946                clickDistThreshold: args[ 2 ]
2947            };
2948        }
2949
2950        this.hash               = Math.random(); // An unique hash for this tracker.
2951        /**
2952         * The element for which pointer events are being monitored.
2953         * @member {Element} element
2954         * @memberof OpenSeadragon.MouseTracker#
2955         */
2956        this.element            = $.getElement( options.element );
2957        /**
2958         * The number of milliseconds within which a pointer down-up event combination 
2959         * will be treated as a click gesture.
2960         * @member {Number} clickTimeThreshold
2961         * @memberof OpenSeadragon.MouseTracker#
2962         */
2963        this.clickTimeThreshold = options.clickTimeThreshold;
2964        /**
2965         * The maximum distance allowed between a pointer down event and a pointer up event
2966         * to be treated as a click gesture.
2967         * @member {Number} clickDistThreshold
2968         * @memberof OpenSeadragon.MouseTracker#
2969         */
2970        this.clickDistThreshold = options.clickDistThreshold;
2971        /**
2972         * The number of milliseconds within which two pointer down-up event combinations
2973         * will be treated as a double-click gesture.
2974         * @member {Number} dblClickTimeThreshold
2975         * @memberof OpenSeadragon.MouseTracker#
2976         */
2977        this.dblClickTimeThreshold = options.dblClickTimeThreshold;
2978        /**
2979         * The maximum distance allowed between two pointer click events
2980         * to be treated as a click gesture.
2981         * @member {Number} clickDistThreshold
2982         * @memberof OpenSeadragon.MouseTracker#
2983         */
2984        this.dblClickDistThreshold = options.dblClickDistThreshold;
2985        this.userData           = options.userData        || null;
2986        this.stopDelay          = options.stopDelay       || 50;
2987
2988        this.enterHandler       = options.enterHandler    || null;
2989        this.exitHandler        = options.exitHandler     || null;
2990        this.pressHandler       = options.pressHandler    || null;
2991        this.releaseHandler     = options.releaseHandler  || null;
2992        this.moveHandler        = options.moveHandler     || null;
2993        this.scrollHandler      = options.scrollHandler   || null;
2994        this.clickHandler       = options.clickHandler    || null;
2995        this.dblClickHandler    = options.dblClickHandler || null;
2996        this.dragHandler        = options.dragHandler     || null;
2997        this.dragEndHandler     = options.dragEndHandler  || null;
2998        this.pinchHandler       = options.pinchHandler    || null;
2999        this.stopHandler        = options.stopHandler     || null;
3000        this.keyHandler         = options.keyHandler      || null;
3001        this.focusHandler       = options.focusHandler    || null;
3002        this.blurHandler        = options.blurHandler     || null;
3003
3004        //Store private properties in a scope sealed hash map
3005        var _this = this;
3006
3007        /**
3008         * @private
3009         * @property {Boolean} tracking
3010         *      Are we currently tracking pointer events for this element.
3011         * @property {Boolean} capturing
3012         *      Are we curruently capturing mouse events (legacy mouse events only).
3013         */
3014        THIS[ this.hash ] = {
3015            click:                 function ( event ) { onClick( _this, event ); },
3016            dblclick:              function ( event ) { onDblClick( _this, event ); },
3017            keypress:              function ( event ) { onKeyPress( _this, event ); },
3018            focus:                 function ( event ) { onFocus( _this, event ); },
3019            blur:                  function ( event ) { onBlur( _this, event ); },
3020
3021            wheel:                 function ( event ) { onWheel( _this, event ); },
3022            mousewheel:            function ( event ) { onMouseWheel( _this, event ); },
3023            DOMMouseScroll:        function ( event ) { onMouseWheel( _this, event ); },
3024            MozMousePixelScroll:   function ( event ) { onMouseWheel( _this, event ); },
3025
3026            mouseover:             function ( event ) { onMouseOver( _this, event ); },
3027            mouseout:              function ( event ) { onMouseOut( _this, event ); },
3028            mouseenter:            function ( event ) { onMouseEnter( _this, event ); },
3029            mouseleave:            function ( event ) { onMouseLeave( _this, event ); },
3030            mousedown:             function ( event ) { onMouseDown( _this, event ); },
3031            mouseup:               function ( event ) { onMouseUp( _this, event ); },
3032            mouseupcaptured:       function ( event ) { onMouseUpCaptured( _this, event ); },
3033            mousemove:             function ( event ) { onMouseMove( _this, event ); },
3034            mousemovecaptured:     function ( event ) { onMouseMoveCaptured( _this, event ); },
3035
3036            touchenter:            function ( event ) { onTouchEnter( _this, event ); },
3037            touchleave:            function ( event ) { onTouchLeave( _this, event ); },
3038            touchstart:            function ( event ) { onTouchStart( _this, event ); },
3039            touchend:              function ( event ) { onTouchEnd( _this, event ); },
3040            touchmove:             function ( event ) { onTouchMove( _this, event ); },
3041            touchcancel:           function ( event ) { onTouchCancel( _this, event ); },
3042
3043            gesturestart:          function ( event ) { onGestureStart( _this, event ); },
3044            gesturechange:         function ( event ) { onGestureChange( _this, event ); },
3045
3046            pointerenter:          function ( event ) { onPointerEnter( _this, event ); },
3047            MSPointerEnter:        function ( event ) { onPointerEnter( _this, event ); },
3048            pointerleave:          function ( event ) { onPointerLeave( _this, event ); },
3049            MSPointerLeave:        function ( event ) { onPointerLeave( _this, event ); },
3050            pointerdown:           function ( event ) { onPointerDown( _this, event ); },
3051            MSPointerDown:         function ( event ) { onPointerDown( _this, event ); },
3052            pointerup:             function ( event ) { onPointerUp( _this, event ); },
3053            MSPointerUp:           function ( event ) { onPointerUp( _this, event ); },
3054            pointermove:           function ( event ) { onPointerMove( _this, event ); },
3055            MSPointerMove:         function ( event ) { onPointerMove( _this, event ); },
3056            pointercancel:         function ( event ) { onPointerCancel( _this, event ); },
3057            MSPointerCancel:       function ( event ) { onPointerCancel( _this, event ); },
3058
3059            tracking:              false,
3060
3061            // Active pointers lists. Array of GesturePointList objects, one for each pointer device type.
3062            // GesturePointList objects are added each time a pointer is tracked by a new pointer device type (see getActivePointersListByType()).
3063            // Active pointers are any pointer being tracked for this element which are in the hit-test area 
3064            //     of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
3065            activePointersLists:   [],
3066
3067            // Legacy mouse event tracking
3068            capturing:             false,
3069
3070            // Tracking for double-click gesture
3071            lastClickPos:          null,
3072            dblClickTimeOut:       null,
3073
3074            // Tracking for pinch gesture
3075            pinchGPoints:          [],
3076            lastPinchDist:         0,
3077            currentPinchDist:      0,
3078            lastPinchCenter:       null,
3079            currentPinchCenter:    null
3080        };
3081
3082    };
3083
3084    $.MouseTracker.prototype = /** @lends OpenSeadragon.MouseTracker.prototype */{
3085
3086        /**
3087         * Clean up any events or objects created by the tracker.
3088         * @function
3089         */
3090        destroy: function () {
3091            stopTracking( this );
3092            this.element = null;
3093
3094            THIS[ this.hash ] = null;
3095            delete THIS[ this.hash ];
3096        },
3097
3098        /**
3099         * Are we currently tracking events on this element.
3100         * @deprecated Just use this.tracking
3101         * @function
3102         * @returns {Boolean} Are we currently tracking events on this element.
3103         */
3104        isTracking: function () {
3105            return THIS[ this.hash ].tracking;
3106        },
3107
3108        /**
3109         * Enable or disable whether or not we are tracking events on this element.
3110         * @function
3111         * @param {Boolean} track True to start tracking, false to stop tracking.
3112         * @returns {OpenSeadragon.MouseTracker} Chainable.
3113         */
3114        setTracking: function ( track ) {
3115            if ( track ) {
3116                startTracking( this );
3117            } else {
3118                stopTracking( this );
3119            }
3120            //chain
3121            return this;
3122        },
3123
3124        /**
3125         * Returns the {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} for the given pointer device type,
3126         * creating and caching a new {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} if one doesn't already exist for the type.
3127         * @function
3128         * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc.
3129         * @returns {OpenSeadragon.MouseTracker.GesturePointList}
3130         */
3131        getActivePointersListByType: function ( type ) {
3132            var delegate = THIS[ this.hash ],
3133                i,
3134                len = delegate.activePointersLists.length,
3135                list;
3136
3137            for ( i = 0; i < len; i++ ) {
3138                if ( delegate.activePointersLists[ i ].type === type ) {
3139                    return delegate.activePointersLists[ i ];
3140                }
3141            }
3142
3143            list = new $.MouseTracker.GesturePointList( type );
3144            delegate.activePointersLists.push( list );
3145            return list;
3146        },
3147
3148        /**
3149         * Implement or assign implementation to these handlers during or after
3150         * calling the constructor.
3151         * @function
3152         * @param {Object} event
3153         * @param {OpenSeadragon.MouseTracker} event.eventSource
3154         *      A reference to the tracker instance.
3155         * @param {String} event.pointerType
3156         *     "mouse", "touch", "pen", etc.
3157         * @param {OpenSeadragon.Point} event.position
3158         *      The position of the event relative to the tracked element.
3159         * @param {Number} event.buttons
3160         *      Current buttons pressed.
3161         *      Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3162         * @param {Boolean} event.insideElementPressed
3163         *      True if the left mouse button is currently being pressed and was
3164         *      initiated inside the tracked element, otherwise false.
3165         * @param {Boolean} event.buttonDownAny
3166         *      Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
3167         * @param {Boolean} event.isTouchEvent
3168         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3169         * @param {Object} event.originalEvent
3170         *      The original event object.
3171         * @param {Boolean} event.preventDefaultAction
3172         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3173         * @param {Object} event.userData
3174         *      Arbitrary user-defined object.
3175         */
3176        enterHandler: function () { },
3177
3178        /**
3179         * Implement or assign implementation to these 
3179handlers during or after
3180         * calling the constructor.
3181         * @function
3182         * @param {Object} event
3183         * @param {OpenSeadragon.MouseTracker} event.eventSource
3184         *      A reference to the tracker instance.
3185         * @param {String} event.pointerType
3186         *     "mouse", "touch", "pen", etc.
3187         * @param {OpenSeadragon.Point} event.position
3188         *      The position of the event relative to the tracked element.
3189         * @param {Number} event.buttons
3190         *      Current buttons pressed.
3191         *      Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3192         * @param {Boolean} event.insideElementPressed
3193         *      True if the left mouse button is currently being pressed and was
3194         *      initiated inside the tracked element, otherwise false.
3195         * @param {Boolean} event.buttonDownAny
3196         *      Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
3197         * @param {Boolean} event.isTouchEvent
3198         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3199         * @param {Object} event.originalEvent
3200         *      The original event object.
3201         * @param {Boolean} event.preventDefaultAction
3202         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3203         * @param {Object} event.userData
3204         *      Arbitrary user-defined object.
3205         */
3206        exitHandler: function () { },
3207
3208        /**
3209         * Implement or assign implementation to these handlers during or after
3210         * calling the constructor.
3211         * @function
3212         * @param {Object} event
3213         * @param {OpenSeadragon.MouseTracker} event.eventSource
3214         *      A reference to the tracker instance.
3215         * @param {String} event.pointerType
3216         *     "mouse", "touch", "pen", etc.
3217         * @param {OpenSeadragon.Point} event.position
3218         *      The position of the event relative to the tracked element.
3219         * @param {Number} event.buttons
3220         *      Current buttons pressed.
3221         *      Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3222         * @param {Boolean} event.isTouchEvent
3223         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3224         * @param {Object} event.originalEvent
3225         *      The original event object.
3226         * @param {Boolean} event.preventDefaultAction
3227         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3228         * @param {Object} event.userData
3229         *      Arbitrary user-defined object.
3230         */
3231        pressHandler: function () { },
3232
3233        /**
3234         * Implement or assign implementation to these handlers during or after
3235         * calling the constructor.
3236         * @function
3237         * @param {Object} event
3238         * @param {OpenSeadragon.MouseTracker} event.eventSource
3239         *      A reference to the tracker instance.
3240         * @param {String} event.pointerType
3241         *     "mouse", "touch", "pen", etc.
3242         * @param {OpenSeadragon.Point} event.position
3243         *      The position of the event relative to the tracked element.
3244         * @param {Number} event.buttons
3245         *      Current buttons pressed.
3246         *      Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3247         * @param {Boolean} event.insideElementPressed
3248         *      True if the left mouse button is currently being pressed and was
3249         *      initiated inside the tracked element, otherwise false.
3250         * @param {Boolean} event.insideElementReleased
3251         *      True if the cursor inside the tracked element when the button was released.
3252         * @param {Boolean} event.isTouchEvent
3253         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3254         * @param {Object} event.originalEvent
3255         *      The original event object.
3256         * @param {Boolean} event.preventDefaultAction
3257         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3258         * @param {Object} event.userData
3259         *      Arbitrary user-defined object.
3260         */
3261        releaseHandler: function () { },
3262
3263        /**
3264         * Implement or assign implementation to these handlers during or after
3265         * calling the constructor.
3266         * @function
3267         * @param {Object} event
3268         * @param {OpenSeadragon.MouseTracker} event.eventSource
3269         *      A reference to the tracker instance.
3270         * @param {String} event.pointerType
3271         *     "mouse", "touch", "pen", etc.
3272         * @param {OpenSeadragon.Point} event.position
3273         *      The position of the event relative to the tracked element.
3274         * @param {Number} event.buttons
3275         *      Current buttons pressed.
3276         *      Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3277         * @param {Boolean} event.isTouchEvent
3278         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3279         * @param {Object} event.originalEvent
3280         *      The original event object.
3281         * @param {Boolean} event.preventDefaultAction
3282         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3283         * @param {Object} event.userData
3284         *      Arbitrary user-defined object.
3285         */
3286        moveHandler: function () { },
3287
3288        /**
3289         * Implement or assign implementation to these handlers during or after
3290         * calling the constructor.
3291         * @function
3292         * @param {Object} event
3293         * @param {OpenSeadragon.MouseTracker} event.eventSource
3294         *      A reference to the tracker instance.
3295         * @param {String} event.pointerType
3296         *     "mouse", "touch", "pen", etc.
3297         * @param {OpenSeadragon.Point} event.position
3298         *      The position of the event relative to the tracked element.
3299         * @param {Number} event.scroll
3300         *      The scroll delta for the event.
3301         * @param {Boolean} event.shift
3302         *      True if the shift key was pressed during this event.
3303         * @param {Boolean} event.isTouchEvent
3304         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead. Touch devices no longer generate scroll event.</span>
3305         * @param {Object} event.originalEvent
3306         *      The original event object.
3307         * @param {Boolean} event.preventDefaultAction
3308         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3309         * @param {Object} event.userData
3310         *      Arbitrary user-defined object.
3311         */
3312        scrollHandler: function () { },
3313
3314        /**
3315         * Implement or assign implementation to these handlers during or after
3316         * calling the constructor.
3317         * @function
3318         * @param {Object} event
3319         * @param {OpenSeadragon.MouseTracker} event.eventSource
3320         *      A reference to the tracker instance.
3321         * @param {String} event.pointerType
3322         *     "mouse", "touch", "pen", etc.
3323         * @param {OpenSeadragon.Point} event.position
3324         *      The position of the event relative to the tracked element.
3325         * @param {Boolean} event.quick
3326         *      True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for ignoring drag events.
3327         * @param {Boolean} event.shift
3328         *      True if the shift key was pressed during this event.
3329         * @param {Boolean} event.isTouchEvent
3330         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3331         * @param {Object} event.originalEvent
3332         *      The original event object.
3333         * @param {Boolean} event.preventDefaultAction
3334         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3335         * @param {Object} event.userData
3336         *      Arbitrary user-defined object.
3337         */
3338        clickHandler: function () { },
3339
3340        /**
3341         * Implement or assign implementation to these handlers during or after
3342         * calling the constructor.
3343         * @function
3344         * @param {Object} event
3345         * @param {OpenSeadragon.MouseTracker} event.eventSource
3346         *      A reference to the tracker instance.
3347         * @param {String} event.pointerType
3348         *     "mouse", "touch", "pen", etc.
3349         * @param {OpenSeadragon.Point} event.position
3350         *      The position of the event relative to the tracked element.
3351         * @param {Boolean} event.shift
3352         *      True if the shift key was pressed during this event.
3353         * @param {Boolean} event.isTouchEvent
3354         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3355         * @param {Object} event.originalEvent
3356         *      The original event object.
3357         * @param {Boolean} event.preventDefaultAction
3358         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3359         * @param {Object} event.userData
3360         *      Arbitrary user-defined object.
3361         */
3362        dblClickHandler: function () { },
3363
3364        /**
3365         * Implement or assign implementation to these handlers during or after
3366         * calling the constructor.
3367         * @function
3368         * @param {Object} event
3369         * @param {OpenSeadragon.MouseTracker} event.eventSource
3370         *      A reference to the tracker instance.
3371         * @param {String} event.pointerType
3372         *     "mouse", "touch", "pen", etc.
3373         * @param {OpenSeadragon.Point} event.position
3374         *      The position of the event relative to the tracked element.
3375         * @param {Number} event.buttons
3376         *      Current buttons pressed.
3377         *      Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3378         * @param {OpenSeadragon.Point} event.delta
3379         *      The x,y components of the difference between the current position and the last drag event position.  Useful for ignoring or weighting the events.
3380         * @param {Number} event.speed
3381         *     Current computed speed, in pixels per second.
3382         * @param {Number} event.direction
3383         *     Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
3384         * @param {Boolean} event.shift
3385         *      True if the shift key was pressed during this event.
3386         * @param {Boolean} event.isTouchEvent
3387         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3388         * @param {Object} event.originalEvent
3389         *      The original event object.
3390         * @param {Boolean} event.preventDefaultAction
3391         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3392         * @param {Object} event.userData
3393         *      Arbitrary user-defined object.
3394         */
3395        dragHandler: function () { },
3396
3397        /**
3398         * Implement or assign implementation to these handlers during or after
3399         * calling the constructor.
3400         * @function
3401         * @param {Object} event
3402         * @param {OpenSeadragon.MouseTracker} event.eventSource
3403         *      A reference to the tracker instance.
3404         * @param {String} event.pointerType
3405         *     "mouse", "touch", "pen", etc.
3406         * @param {OpenSeadragon.Point} event.position
3407         *      The position of the event relative to the tracked element.
3408         * @param {Number} event.speed
3409         *     Speed at the end of a drag gesture, in pixels per second.
3410         * @param {Number} event.direction
3411         *     Direction at the end of a drag gesture, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
3412         * @param {Boolean} event.shift
3413         *      True if the shift key was pressed during this event.
3414         * @param {Boolean} event.isTouchEvent
3415         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3416         * @param {Object} event.originalEvent
3417         *      The original event object.
3418         * @param {Boolean} event.preventDefaultAction
3419         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3420         * @param {Object} event.userData
3421         *      Arbitrary user-defined object.
3422         */
3423        dragEndHandler: function () { },
3424
3425        /**
3426         * Implement or assign implementation to these handlers during or after
3427         * calling the constructor.
3428         * @function
3429         * @param {Object} event
3430         * @param {OpenSeadragon.MouseTracker} event.eventSource
3431         *      A reference to the tracker instance.
3432         * @param {String} event.pointerType
3433         *     "mouse", "touch", "pen", etc.
3434         * @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} event.gesturePoints
3435         *      Gesture points associated with the gesture. Velocity data can be found here.
3436         * @param {OpenSeadragon.Point} event.lastCenter
3437         *      The previous center point of the two pinch contact points relative to the tracked element.
3438         * @param {OpenSeadragon.Point} event.center
3439         *      The center point of the two pinch contact points relative to the tracked element.
3440         * @param {Number} event.lastDistance
3441         *      The previous distance between the two pinch contact points in CSS pixels.
3442         * @param {Number} event.distance
3443         *      The distance between the two pinch contact points in CSS pixels.
3444         * @param {Boolean} event.shift
3445         *      True if the shift key was pressed during this event.
3446         * @param {Object} event.originalEvent
3447         *      The original event object.
3448         * @param {Boolean} event.preventDefaultAction
3449         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3450         * @param {Object} event.userData
3451         *      Arbitrary user-defined object.
3452         */
3453        pinchHandler: function () { },
3454
3455        /**
3456         * Implement or assign implementation to these handlers during or after
3457         * calling the constructor.
3458         * @function
3459         * @param {
3459Object} event
3460         * @param {OpenSeadragon.MouseTracker} event.eventSource
3461         *      A reference to the tracker instance.
3462         * @param {String} event.pointerType
3463         *     "mouse", "touch", "pen", etc.
3464         * @param {OpenSeadragon.Point} event.position
3465         *      The position of the event relative to the tracked element.
3466         * @param {Number} event.buttons
3467         *      Current buttons pressed.
3468         *      Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3469         * @param {Boolean} event.isTouchEvent
3470         *      True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
3471         * @param {Object} event.originalEvent
3472         *      The original event object.
3473         * @param {Boolean} event.preventDefaultAction
3474         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3475         * @param {Object} event.userData
3476         *      Arbitrary user-defined object.
3477         */
3478        stopHandler: function () { },
3479
3480        /**
3481         * Implement or assign implementation to these handlers during or after
3482         * calling the constructor.
3483         * @function
3484         * @param {Object} event
3485         * @param {OpenSeadragon.MouseTracker} event.eventSource
3486         *      A reference to the tracker instance.
3487         * @param {Number} event.keyCode
3488         *      The key code that was pressed.
3489         * @param {Boolean} event.shift
3490         *      True if the shift key was pressed during this event.
3491         * @param {Object} event.originalEvent
3492         *      The original event object.
3493         * @param {Boolean} event.preventDefaultAction
3494         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3495         * @param {Object} event.userData
3496         *      Arbitrary user-defined object.
3497         */
3498        keyHandler: function () { },
3499
3500        /**
3501         * Implement or assign implementation to these handlers during or after
3502         * calling the constructor.
3503         * @function
3504         * @param {Object} event
3505         * @param {OpenSeadragon.MouseTracker} event.eventSource
3506         *      A reference to the tracker instance.
3507         * @param {Object} event.originalEvent
3508         *      The original event object.
3509         * @param {Boolean} event.preventDefaultAction
3510         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3511         * @param {Object} event.userData
3512         *      Arbitrary user-defined object.
3513         */
3514        focusHandler: function () { },
3515
3516        /**
3517         * Implement or assign implementation to these handlers during or after
3518         * calling the constructor.
3519         * @function
3520         * @param {Object} event
3521         * @param {OpenSeadragon.MouseTracker} event.eventSource
3522         *      A reference to the tracker instance.
3523         * @param {Object} event.originalEvent
3524         *      The original event object.
3525         * @param {Boolean} event.preventDefaultAction
3526         *      Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false.
3527         * @param {Object} event.userData
3528         *      Arbitrary user-defined object.
3529         */
3530        blurHandler: function () { }
3531    };
3532
3533
3534    /**
3535     * Provides continuous computation of velocity (speed and direction) of active pointers.
3536     * This is a singleton, used by all MouseTracker instances, as it is unlikely there will ever be more than
3537     * two active gesture pointers at a time.
3538     *
3539     * @private
3540     * @member gesturePointVelocityTracker
3541     * @memberof OpenSeadragon.MouseTracker
3542     */
3543    $.MouseTracker.gesturePointVelocityTracker = (function () {
3544        var trackerPoints = [],
3545            intervalId = 0,
3546            lastTime = 0;
3547
3548        // Generates a unique identifier for a tracked gesture point
3549        var _generateGuid = function ( tracker, gPoint ) {
3550            return tracker.hash.toString() + gPoint.type + gPoint.id.toString();
3551        };
3552
3553        // Interval timer callback. Computes velocity for all tracked gesture points.
3554        var _doTracking = function () {
3555            var i,
3556                len = trackerPoints.length,
3557                trackPoint,
3558                gPoint,
3559                now = $.now(),
3560                elapsedTime,
3561                distance,
3562                speed;
3563
3564            elapsedTime = now - lastTime;
3565            lastTime = now;
3566
3567            for ( i = 0; i < len; i++ ) {
3568                trackPoint = trackerPoints[ i ];
3569                gPoint = trackPoint.gPoint;
3570                // Math.atan2 gives us just what we need for a velocity vector, as we can simply
3571                //   use cos()/sin() to extract the x/y velocity components.
3572                gPoint.direction = Math.atan2( gPoint.currentPos.y - trackPoint.lastPos.y, gPoint.currentPos.x - trackPoint.lastPos.x );
3573                // speed = distance / elapsed time
3574                distance = trackPoint.lastPos.distanceTo( gPoint.currentPos );
3575                trackPoint.lastPos = gPoint.currentPos;
3576                speed = 1000 * distance / ( elapsedTime + 1 );
3577                // Simple biased average, favors the most recent speed computation. Smooths out erratic gestures a bit.
3578                gPoint.speed = 0.75 * speed + 0.25 * gPoint.speed;
3579            }
3580        };
3581
3582        // Public. Add a gesture point to be tracked
3583        var addPoint = function ( tracker, gPoint ) {
3584            var guid = _generateGuid( tracker, gPoint );
3585
3586            trackerPoints.push(
3587                {
3588                    guid: guid,
3589                    gPoint: gPoint,
3590                    lastPos: gPoint.currentPos
3591                } );
3592
3593            // Only fire up the interval timer when there's gesture pointers to track
3594            if ( trackerPoints.length === 1 ) {
3595                lastTime = $.now();
3596                intervalId = window.setInterval( _doTracking, 50 );
3597            }
3598        };
3599
3600        // Public. Stop tracking a gesture point
3601        var removePoint = function ( tracker, gPoint ) {
3602            var guid = _generateGuid( tracker, gPoint ),
3603                i,
3604                len = trackerPoints.length;
3605            for ( i = 0; i < len; i++ ) {
3606                if ( trackerPoints[ i ].guid === guid ) {
3607                    trackerPoints.splice( i, 1 );
3608                    // Only run the interval timer if theres gesture pointers to track
3609                    len--;
3610                    if ( len === 0 ) {
3611                        window.clearInterval( intervalId );
3612                    }
3613                    break;
3614                }
3615            }
3616        };
3617
3618        return {
3619            addPoint:    addPoint,
3620            removePoint: removePoint
3621        };
3622    } )();
3623
3624
3625///////////////////////////////////////////////////////////////////////////////
3626// Pointer event model and feature detection
3627///////////////////////////////////////////////////////////////////////////////
3628
3629    /**
3630     * Detect available mouse wheel event name.
3631     */
3632    $.MouseTracker.wheelEventName = ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version > 8 ) ||
3633                                                ( 'onwheel' in document.createElement( 'div' ) ) ? 'wheel' : // Modern browsers support 'wheel'
3634                                    document.onmousewheel !== undefined ? 'mousewheel' :                     // Webkit and IE support at least 'mousewheel'
3635                                    'DOMMouseScroll';                                                        // Assume old Firefox
3636
3637    /**
3638     * Detect legacy mouse capture support.
3639     */
3640    $.MouseTracker.supportsMouseCapture = (function () {
3641        var divElement = document.createElement( 'div' );
3642        return $.isFunction( divElement.setCapture ) && $.isFunction( divElement.releaseCapture );
3643    }());
3644
3645    /**
3646     * Detect browser pointer device event model(s) and build appropriate list of events to subscribe to.
3647     */
3648    $.MouseTracker.subscribeEvents = [ "click", "dblclick", "keypress", "focus", "blur", $.MouseTracker.wheelEventName ];
3649
3650    if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) {
3651        // Older Firefox
3652        $.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" );
3653    }
3654
3655    if ( window.PointerEvent ) {
3656        // IE11 and other W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents)
3657        $.MouseTracker.subscribeEvents.push( "pointerenter", "pointerleave", "pointerdown", "pointerup", "pointermove", "pointercancel" );
3658        $.MouseTracker.unprefixedPointerEvents = true;
3659        if( navigator.maxTouchPoints ) {
3660            $.MouseTracker.maxTouchPoints = navigator.maxTouchPoints;
3661        } else {
3662            $.MouseTracker.maxTouchPoints = 0;
3663        }
3664        $.MouseTracker.haveTouchEnter = true;
3665        $.MouseTracker.haveMouseEnter = true;
3666    } else if ( window.MSPointerEvent ) {
3667        // IE10
3668        $.MouseTracker.subscribeEvents.push( "MSPointerEnter", "MSPointerLeave", "MSPointerDown", "MSPointerUp", "MSPointerMove", "MSPointerCancel" );
3669        $.MouseTracker.unprefixedPointerEvents = false;
3670        if( navigator.msMaxTouchPoints ) {
3671            $.MouseTracker.maxTouchPoints = navigator.msMaxTouchPoints;
3672        } else {
3673            $.MouseTracker.maxTouchPoints = 0;
3674        }
3675        $.MouseTracker.haveTouchEnter = true;
3676        $.MouseTracker.haveMouseEnter = true;
3677    } else {
3678        // Legacy W3C mouse events
3679        // TODO: Favor mouseenter/mouseleave over mouseover/mouseout when Webkit browser support is better
3680        $.MouseTracker.subscribeEvents.push( "mouseover", "mouseout", "mousedown", "mouseup", "mousemove" );
3681        $.MouseTracker.haveMouseEnter = false;
3682        if ( 'ontouchstart' in window ) {
3683            // iOS, Android, and other W3c Touch Event implementations (see http://www.w3.org/TR/2011/WD-touch-events-20110505
3683)
3684            $.MouseTracker.subscribeEvents.push( "touchstart", "touchend", "touchmove", "touchcancel" );
3685            if ( 'ontouchenter' in window ) {
3686                $.MouseTracker.subscribeEvents.push( "touchenter", "touchleave" );
3687                $.MouseTracker.haveTouchEnter = true;
3688            } else {
3689                $.MouseTracker.haveTouchEnter = false;
3690            }
3691        } else {
3692            $.MouseTracker.haveTouchEnter = false;
3693        }
3694        if ( 'ongesturestart' in window ) {
3695            // iOS (see https://developer.apple.com/library/safari/documentation/UserExperience/Reference/GestureEventClassReference/GestureEvent/GestureEvent.html)
3696            //   Subscribe to these to prevent default gesture handling
3697            $.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" );
3698        }
3699        $.MouseTracker.mousePointerId = "legacy-mouse";
3700        $.MouseTracker.maxTouchPoints = 10;
3701    }
3702    
3703
3704///////////////////////////////////////////////////////////////////////////////
3705// Classes and typedefs
3706///////////////////////////////////////////////////////////////////////////////
3707
3708    /**
3709     * Represents a point of contact on the screen made by a mouse cursor, pen, touch, or other pointer device.
3710     *
3711     * @typedef {Object} GesturePoint
3712     * @memberof OpenSeadragon.MouseTracker
3713     *
3714     * @property {Number} id
3715     *     Identifier unique from all other active GesturePoints for a given pointer device.
3716     * @property {String} type
3717     *     The pointer device type: "mouse", "touch", "pen", etc.
3718     * @property {Boolean} captured
3719     *     True if events for the gesture point are captured to the tracked element.
3720     * @property {Boolean} isPrimary
3721     *     True if the gesture point is a master pointer amongst the set of active pointers for each pointer type. True for mouse and primary (first) touch/pen pointers.
3722     * @property {Boolean} insideElementPressed
3723     *     True if button pressed or contact point initiated inside the screen area of the tracked element.
3724     * @property {Boolean} insideElement
3725     *     True if pointer or contact point is currently inside the bounds of the tracked element.
3726     * @property {Number} speed
3727     *     Current computed speed, in pixels per second.
3728     * @property {Number} direction
3729     *     Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
3730     * @property {OpenSeadragon.Point} contactPos
3731     *     The initial pointer contact position, relative to the page including any scrolling. Only valid if the pointer has contact (pressed, touch contact, pen contact).
3732     * @property {Number} contactTime
3733     *     The initial pointer contact time, in milliseconds. Only valid if the pointer has contact (pressed, touch contact, pen contact).
3734     * @property {OpenSeadragon.Point} lastPos
3735     *     The last pointer position, relative to the page including any scrolling.
3736     * @property {Number} lastTime
3737     *     The last pointer contact time, in milliseconds.
3738     * @property {OpenSeadragon.Point} currentPos
3739     *     The current pointer position, relative to the page including any scrolling.
3740     * @property {Number} currentTime
3741     *     The current pointer contact time, in milliseconds.
3742     */
3743
3744
3745    /**
3746     * @class GesturePointList
3747     * @classdesc Provides an abstraction for a set of active {@link OpenSeadragon.MouseTracker.GesturePoint|GesturePoint} objects for a given pointer device type.
3748     *            Active pointers are any pointer being tracked for this element which are in the hit-test area 
3749     *            of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
3750     * @memberof OpenSeadragon.MouseTracker
3751     * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc.
3752     */
3753    $.MouseTracker.GesturePointList = function ( type ) {
3754        this._gPoints = [];
3755        /**
3756         * The pointer device type: "mouse", "touch", "pen", etc.
3757         * @member {String} type
3758         * @memberof OpenSeadragon.MouseTracker.GesturePointList#
3759         */
3760        this.type = type;
3761        /**
3762         * Current buttons pressed for the device.
3763         * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
3764         * @member {Number} buttons
3765         * @memberof OpenSeadragon.MouseTracker.GesturePointList#
3766         */
3767        this.buttons = 0;
3768        /**
3769         * Current number of contact points (touch points, mouse down, etc.) for the device.
3770         * @member {Number} contacts
3771         * @memberof OpenSeadragon.MouseTracker.GesturePointList#
3772         */
3773        this.contacts = 0;
3774        /**
3775         * Current number of clicks for the device. Used for multiple click gesture tracking.
3776         * @member {Number} clicks
3777         * @memberof OpenSeadragon.MouseTracker.GesturePointList#
3778         */
3779        this.clicks = 0;
3780    };
3781    $.MouseTracker.GesturePointList.prototype = /** @lends OpenSeadragon.MouseTracker.GesturePointList.prototype */{
3782        /**
3783         * @function
3784         * @returns {Number} Number of gesture points in the list.
3785         */
3786        getLength: function () {
3787            return this._gPoints.length;
3788        },
3789        /**
3790         * @function
3791         * @returns {Array.<OpenSeadragon.MouseTracker.GesturePoint>} The list of gesture points in the list as an array (read-only).
3792         */
3793        asArray: function () {
3794            return this._gPoints;
3795        },
3796        /**
3797         * @function
3798         * @param {OpenSeadragon.MouseTracker.GesturePoint} gesturePoint - A gesture point to add to the list.
3799         * @returns {Number} Number of gesture points in the list.
3800         */
3801        add: function ( gp ) {
3802            return this._gPoints.push( gp );
3803        },
3804        /**
3805         * @function
3806         * @param {Number} id - The id of the gesture point to remove from the list.
3807         * @returns {Number} Number of gesture points in the list.
3808         */
3809        removeById: function ( id ) {
3810            var i,
3811                len = this._gPoints.length;
3812            for ( i = 0; i < len; i++ ) {
3813                if ( this._gPoints[ i ].id === id ) {
3814                    this._gPoints.splice( i, 1 );
3815                    break;
3816                }
3817            }
3818            return this._gPoints.length;
3819        },
3820        /**
3821         * @function
3822         * @param {Number} index - The index of the gesture point to retrieve from the list.
3823         * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point at the given index, or null if not found.
3824         */
3825        getByIndex: function ( index ) {
3826            if ( index < this._gPoints.length) {
3827                return this._gPoints[ index ];
3828            }
3829
3830            return null;
3831        },
3832        /**
3833         * @function
3834         * @param {Number} id - The id of the gesture point to retrieve from the list.
3835         * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point with the given id, or null if not found.
3836         */
3837        getById: function ( id ) {
3838            var i,
3839                len = this._gPoints.length;
3840            for ( i = 0; i < len; i++ ) {
3841                if ( this._gPoints[ i ].id === id ) {
3842                    return this._gPoints[ i ];
3843                }
3844            }
3845            return null;
3846        },
3847        /**
3848         * @function
3849         * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The primary gesture point in the list, or null if not found.
3850         */
3851        getPrimary: function ( id ) {
3852            var i,
3853                len = this._gPoints.length;
3854            for ( i = 0; i < len; i++ ) {
3855                if ( this._gPoints[ i ].isPrimary ) {
3856                    return this._gPoints[ i ];
3857                }
3858            }
3859            return null;
3860        }
3861    };
3862    
3863
3864///////////////////////////////////////////////////////////////////////////////
3865// Utility functions
3866///////////////////////////////////////////////////////////////////////////////
3867
3868    /**
3869     * Starts tracking pointer events on the tracked element.
3870     * @private
3871     * @inner
3872     */
3873    function startTracking( tracker ) {
3874        var delegate = THIS[ tracker.hash ],
3875            event,
3876            i;
3877
3878        if ( !delegate.tracking ) {
3879            for ( i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) {
3880                event = $.MouseTracker.subscribeEvents[ i ];
3881                $.addEvent(
3882                    tracker.element,
3883                    event,
3884                    delegate[ event ],
3885                    false
3886                );
3887            }
3888            delegate.tracking = true;
3889        }
3890    }
3891
3892    /**
3893     * Stops tracking pointer events on the tracked element.
3894     * @private
3895     * @inner
3896     */
3897    function stopTracking( tracker ) {
3898        var delegate = THIS[ tracker.hash ],
3899            event,
3900            i;
3901
3902        if ( delegate.tracking ) {
3903            for ( i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) {
3904                event = $.MouseTracker.subscribeEvents[ i ];
3905                $.removeEvent(
3906                    tracker.element,
3907                    event,
3908                    delegate[ event ],
3909                    false
3910                );
3911            }
3912
3913            releaseMouse( tracker );
3914            delegate.tracking = false;
3915        }
3916    }
3917
3918    /**
3919     * Begin capturing mouse events to the tracked element (legacy mouse events only).
3920     * @private
3921     * @inner
3922     */
3923    function captureMouse( tracker ) {
3924        var delegate = THIS[ tracker.hash ];
3925
3926        if ( !delegate.capturing ) {
3927            if ( $.MouseTracker.supportsMouseCapture ) {
3928                // IE<10, Firefox, other browsers with setCapture()/releaseCapture()
3929                tracker.element.setCapture( true );
3930            } else {
3931                // For browsers without setCapture()/releaseCapture(), we emulate mouse capture by hanging listeners on the window object.
3932                //    (Note we listen on the capture phase so the captured handlers will get called first)
3933                $.addEvent(
3934                    window,
3935                    "mouseup",
3936                    delegate.mouseupcaptured,
3937                    true
3938                );
3939                $.addEvent(
3940                    window,
3941                    "mousemove",
3942                    delegate.mousemovecaptured,
3943                    true
3944                );
3945            }
3946            delegate.capturing = true;
3947        }
3948    }
3949
3950
3951    /**
3952     * Stop capturing mouse events to the tracked element (legacy mouse events only).
3953     * @private
3954     * @inner
3955     */
3956    function releaseMouse( tracker ) {
3957        var delegate = THIS[ tracker.hash ];
3958
3959        if ( delegate.capturing ) {
3960            if ( $.MouseTracker.supportsMouseCapture ) {
3961                // IE<10, Firefox, other browsers with setCapture()/releaseCapture()
3962                tracker.element.releaseCapture();
3963            } else {
3964                // For browsers without setCapture()/releaseCapture(), we emulate mouse capture by hanging listeners on the window object.
3965                //    (Note we listen on the capture phase so the captured handlers will get called first)
3966                $.removeEvent(
3967                    window,
3968                    "mousemove",
3969                    delegate.mousemovecaptured,
3970                    true
3971                );
3972                $.removeEvent(
3973                    window,
3974                    "mouseup",
3975                    delegate.mouseupcaptured,
3976                    true
3977                );
3978            }
3979            delegate.capturing = false;
3980        }
3981    }
3982
3983
3984    /**
3985     * Gets a W3C Pointer Events model compatible pointer type string from a DOM pointer event.
3986     * IE10 used a long integer value, but the W3C specification (and IE11+) use a string "mouse", "touch", "pen", etc.
3987     * @private
3988     * @inner
3989     */
3990    function getPointerType( event ) {
3991        var pointerTypeStr;
3992        if ( $.MouseTracker.unprefixedPointerEvents ) {
3993            pointerTypeStr = event.pointerType;
3994        } else {
3995            // IE10
3996            //  MSPOINTER_TYPE_TOUCH: 0x00000002
3997            //  MSPOINTER_TYPE_PEN:   0x00000003
3998            //  MSPOINTER_TYPE_MOUSE: 0x00000004
3999            switch( event.pointerType )
4000            {
4001                case 0x00000002:
4002                    pointerTypeStr = 'touch';
4003                    break;
4004                case 0x00000003:
4005                    pointerTypeStr = 'pen';
4006                    break;
4007                case 0x00000004:
4008                    pointerTypeStr = 'mouse';
4009                    break;
4010                default:
4011                    pointerTypeStr = '';
4012            }
4013        }
4014        return pointerTypeStr;
4015    }
4016
4017
4018    /**
4019     * @private
4020     * @inner
4021     */
4022    function getMouseAbsolute( event ) {
4023        return $.getMousePosition( event );
4024    }
4025
4026    /**
4027     * @private
4028     * @inner
4029     */
4030    function getMouseRelative( event, element ) {
4031        return getPointRelativeToAbsolute( getMouseAbsolute( event ), element );
4032    }
4033
4034    /**
4035     * @private
4036     * @inner
4037     */
4038    function getPointRelativeToAbsolute( point, element ) {
4039        var offset = $.getElementOffset( element );
4040        return point.minus( offset );
4041    }
4042
4043    /**
4044     * @private
4045     * @inner
4046     */
4047    function getCenterPoint( point1, point2 ) {
4048        return new $.Point( ( point1.x + point2.x ) / 2, ( point1.y + point2.y ) / 2 );
4049    }
4050
4051
4052///////////////////////////////////////////////////////////////////////////////
4053// Device-specific DOM event handlers
4054///////////////////////////////////////////////////////////////////////////////
4055
4056    /**
4057     * @private
4058     * @inner
4059     */
4060    function onClick( tracker, event ) {
4061        if ( tracker.clickHandler ) {
4062            $.cancelEvent( event );
4063        }
4064    }
4065
4066
4067    /**
4068     * @private
4069     * @inner
4070     */
4071    function onDblClick( tracker, event ) {
4072        if ( tracker.dblClickHandler ) {
4073            $.cancelEvent( event );
4074        }
4075    }
4076
4077
4078    /**
4079     * @private
4080     * @inner
4081     */
4082    function onKeyPress( tracker, event ) {
4083        //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
4084        var propagate;
4085        if ( tracker.keyHandler ) {
4086            event = $.getEvent( event );
4087            propagate = tracker.keyHandler(
4088                {
4089                    eventSource:          tracker,
4090                    position:             getMouseRelative( event, tracker.element ),
4091                    keyCode:              event.keyCode ? event.keyCode : event.charCode,
4092                    shift:                event.shiftKey,
4093                    originalEvent:        event,
4094                    preventDefaultAction: false,
4095                    userData:             tracker.userData
4096                }
4097            );
4098            if ( !propagate ) {
4099                $.cancelEvent( event );
4100            }
4101        }
4102    }
4103
4104
4105    /**
4106     * @private
4107     * @inner
4108     */
4109    function onFocus( tracker, event ) {
4110        //console.log( "focus %s", event );
4111        var propagate;
4112        if ( tracker.focusHandler ) {
4113            event = $.getEvent( event );
4114            propagate = tracker.focusHandler(
4115                {
4116                    eventSource:          tracker,
4117                    originalEvent:        event,
4118                    preventDefaultAction: false,
4119                    userData:             tracker.userData
4120                }
4121            );
4122            if ( propagate === false ) {
4123                $.cancelEvent( event );
4124            }
4125        }
4126    }
4127
4128
4129    /**
4130     * @private
4131     * @inner
4132     */
4133    function onBlur( tracker, event ) {
4134        //console.log( "blur %s", event );
4135        var propagate;
4136        if ( tracker.blurHandler ) {
4137            event = $.getEvent( event );
4138            propagate = tracker.blurHandler(
4139                {
4140                    eventSource:          tracker,
4141                    originalEvent:        event,
4142                    preventDefaultAction: false,
4143                    userData:             tracker.userData
4144                }
4145            );
4146            if ( propagate === false ) {
4147                $.cancelEvent( event );
4148            }
4149        }
4150    }
4151
4152
4153    /**
4154     * Handler for 'wheel' events
4155     *
4156     * @private
4157     * @inner
4158     */
4159    function onWheel( tracker, event ) {
4160        handleWheelEvent( tracker, event, event );
4161    }
4162
4163
4164    /**
4165     * Handler for 'mousewheel', 'DOMMouseScroll', and 'MozMousePixelScroll' events
4166     *
4167     * @private
4168     * @inner
4169     */
4170    function onMouseWheel( tracker, event ) {
4171        event = $.getEvent( event );
4172
4173        // Simulate a 'wheel' event
4174        var simulatedEvent = {
4175            target:     event.target || event.srcElement,
4176            type:       "wheel",
4177            shiftKey:   event.shiftKey || false,
4178            clientX:    event.clientX,
4179            clientY:    event.clientY,
4180            pageX:      event.pageX ? event.pageX : event.clientX,
4181            pageY:      event.pageY ? event.pageY : event.clientY,
4182            deltaMode:  event.type == "MozMousePixelScroll" ? 0 : 1, // 0=pixel, 1=line, 2=page
4183            deltaX:     0,
4184            deltaZ:     0
4185        };
4186
4187        // Calculate deltaY
4188        if ( $.MouseTracker.wheelEventName == "mousewheel" ) {
4189            simulatedEvent.deltaY = - 1 / $.DEFAULT_SETTINGS.pixelsPerWheelLine * event.wheelDelta;
4190        } else {
4191            simulatedEvent.deltaY = event.detail;
4192        }
4193
4194        handleWheelEvent( tracker, simulatedEvent, event );
4195    }
4196
4197
4198    /**
4199     * Handles 'wheel' events. 
4200     * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()).
4201     *
4202     * @private
4203     * @inner
4204     */
4205    function handleWheelEvent( tracker, event, originalEvent ) {
4206        var nDelta = 0,
4207            propagate;
4208
4209        // The nDelta variable is gated to provide smooth z-index scrolling
4210        //   since the mouse wheel allows for substantial deltas meant for rapid
4211        //   y-index scrolling.
4212        // event.deltaMode: 0=pixel, 1=line, 2=page
4213        // TODO: Deltas in pixel mode should be accumulated then a scroll value computed after $.DEFAULT_SETTINGS.pixelsPerWheelLine threshold reached
4214        nDelta = event.deltaY < 0 ? 1 : -1;
4215
4216        if ( tracker.scrollHandler ) {
4217            propagate = tracker.scrollHandler(
4218                {
4219                    eventSource:          tracker,
4220                    pointerType:          'mouse',
4221                    position:             getMouseRelative( event, tracker.element ),
4222                    scroll:               nDelta,
4223                    shift:                event.shiftKey,
4224                    isTouchEvent:         false,
4225                    originalEvent:        originalEvent,
4226                    preventDefaultAction: false,
4227                    userData:             tracker.userData
4228                }
4229            );
4230            if ( propagate === false ) {
4231                $.cancelEvent( originalEvent );
4232            }
4233        }
4234    }
4235
4236
4237    /**
4238     * @private
4239     * @inner
4240     */
4241    function isParentChild( parent, child )
4242    {
4243       if ( parent === child ) {
4244           return false;
4245       }
4246       while ( child && child !== parent ) {
4247           child = child.parentNode;
4248       }
4249       return child === parent;
4250    }
4251
4252
4253    /**
4254     * @private
4255     * @inner
4256     */
4257    function onMouseOver( tracker, event ) {
4258        var gPoint;
4259
4260        event = $.getEvent( event );
4261
4262        if ( this === event.relatedTarget || isParentChild( this, event.relatedTarget ) ) {
4263            return;
4264        }
4265
4266        gPoint = {
4267            id: $.MouseTracker.mousePointerId,
4268            type: 'mouse',
4269            isPrimary: true,
4270            currentPos: getMouseAbsolute( event ),
4271            currentTime: $.now()
4272        };
4273
4274        updatePointersEnter( tracker, event, [ gPoint ] );
4275    }
4276
4277
4278    /**
4279     * @private
4280     * @inner
4281     */
4282    function onMouseOut( tracker, event ) {
4283        var gPoint;
4284
4285        event = $.getEvent( event );
4286
4287        if ( this === event.relatedTarget || isParentChild( this, event.relatedTarget ) ) {
4288            return;
4289        }
4290
4291        gPoint = {
4292            id: $.MouseTracker.mousePointerId,
4293            type: 'mouse',
4294            isPrimary: true,
4295            currentPos: getMouseAbsolute( event ),
4296            currentTime: $.now()
4297        };
4298
4299        updatePointersExit( tracker, event, [ gPoint ] );
4300    }
4301
4302
4303    /**
4304     * @private
4305     * @inner
4306     */
4307    function onMouseEnter( tracker, event ) {
4308        var gPoint;
4309
4310        event = $.getEvent( event );
4311
4312        gPoint = {
4313            id: $.MouseTracker.mousePointerId,
4314            type: 'mouse',
4315            isPrimary: true,
4316            currentPos: getMouseAbsolute( event ),
4317            currentTime: $.now()
4318        };
4319
4320        updatePointersEnter( tracker, event, [ gPoint ] );
4321    }
4322
4323
4324    /**
4325     * @private
4326     * @inner
4327     */
4328    function onMouseLeave( tracker, event ) {
4329        var gPoint;
4330
4331        event = $.getEvent( event );
4332
4333        gPoint = {
4334            id: $.MouseTracker.mousePointerId,
4335            type: 'mouse',
4336            isPrimary: true,
4337            currentPos: getMouseAbsolute( event ),
4338            currentTime: $.now()
4339        };
4340
4341        updatePointersExit( tracker, event, [ gPoint ] );
4342    }
4343
4344
4345    /**
4346     * @private
4347     * @inner
4348     */
4349    function onMouseDown( tracker, event ) {
4350        var gPoint;
4351
4352        event = $.getEvent( event );
4353
4354        gPoint = {
4355            id: $.MouseTracker.mousePointerId,
4356            type: 'mouse',
4357            isPrimary: true,
4358            currentPos: getMouseAbsolute( event ),
4359            currentTime: $.now()
4360        };
4361
4362        if ( updatePointersDown( tracker, event, [ gPoint ], event.button ) ) {
4363            $.stopEvent( event );
4364            captureMouse( tracker );
4365        }
4366
4367        if ( tracker.clickHandler || tracker.dblClickHandler || tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler ) {
4368            $.cancelEvent( event );
4369        }
4370    }
4371
4372
4373    /**
4374     * @private
4375     * @inner
4376     */
4377    function onMouseUp( tracker, event ) {
4378        handleMouseUp( tracker, event );
4379    }
4380
4381    /**
4382     * This handler is attached to the window object (on the capture phase) to emulate mouse capture.
4383     * Only triggered in W3C browsers that don't have setCapture/releaseCapture 
4384     * methods or don't support the new pointer events model.
4385     * onMouseUp is still attached to the tracked element, so stop propagation to avoid processing twice.
4386     *
4387     * @private
4388     * @inner
4389     */
4390    function onMouseUpCaptured( tracker, event ) {
4391        handleMouseUp( tracker, event );
4392        $.stopEvent( event );
4393    }
4394
4395
4396    /**
4397     * @private
4398     * @inner
4399     */
4400    function handleMouseUp( tracker, event ) {
4401        var gPoint;
4402
4403        event = $.getEvent( event );
4404
4405        gPoint = {
4406            id: $.MouseTracker.mousePointerId,
4407            type: 'mouse',
4408            isPrimary: true,
4409            currentPos: getMouseAbsolute( event ),
4410            currentTime: $.now()
4411        };
4412
4413        if ( updatePointersUp( tracker, event, [ gPoint ], event.button ) ) {
4414            releaseMouse( tracker );
4415        }
4416    }
4417
4418
4419    /**
4420     * @private
4421     * @inner
4422     */
4423    function onMouseMove( tracker, event ) {
4424        handleMouseMove( tracker, event );
4425   }
4426
4427    
4428    /**
4429     * This handler is attached to the window object (on the capture phase) to emulate mouse capture.
4430     * Only triggered in W3C browsers that don't have setCapture/releaseCapture 
4431     * methods or don't support the new pointer events model.
4432     * onMouseMove is still attached to the tracked element, so stop propagation to avoid processing twice.
4433     *
4434     * @private
4435     * @inner
4436     */
4437    function onMouseMoveCaptured( tracker, event ) {
4438        handleMouseMove( tracker, event );
4439        $.stopEvent( event );
4440    }
4441
4442
4443    /**
4444     * @private
4445     * @inner
4446     */
4447    function handleMouseMove( tracker, event ) {
4448        var gPoint;
4449
4450        event = $.getEvent( event );
4451
4452        gPoint = {
4453            id: $.MouseTracker.mousePointerId,
4454            type: 'mouse',
4455            isPrimary: true,
4456            currentPos: getMouseAbsolute( event ),
4457            currentTime: $.now()
4458        };
4459
4460        updatePointersMove( tracker, event, [ gPoint ] );
4461    }
4462
4463
4464    /**
4465     * @private
4466     * @inner
4467     */
4468    function onTouchEnter( tracker, event ) {
4469        var i,
4470            touchCount = event.changedTouches.length,
4471            gPoints = [];
4472
4473        for ( i = 0; i < touchCount; i++ ) {
4474            gPoints.push( {
4475                id: event.changedTouches[ i ].identifier,
4476                type: 'touch',
4477                // isPrimary not set - let the updatePointers functions determine it
4478                currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
4479                currentTime: $.now()
4480            } );
4481        }
4482
4483        updatePointersEnter( tracker, event, gPoints );
4484    }
4485
4486
4487    /**
4488     * @private
4489     * @inner
4490     */
4491    function onTouchLeave( tracker, event ) {
4492        var i,
4493            touchCount = event.changedTouches.length,
4494            gPoints = [];
4495
4496        for ( i = 0; i < touchCount; i++ ) {
4497            gPoints.push( {
4498                id: event.changedTouches[ i ].identifier,
4499                type: 'touch',
4500                // isPrimary not set - let the updatePointers functions determine it
4501                currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
4502                currentTime: $.now()
4503            } );
4504        }
4505
4506        updatePointersExit( tracker, event, gPoints );
4507    }
4508
4509
4510    /**
4511     * @private
4512     * @inner
4513     */
4514    function onTouchStart( tracker, event ) {
4515        var time,
4516            i,
4517            touchCount = event.changedTouches.length,
4518            gPoints = [];
4519
4520        time = $.now();
4521
4522        for ( i = 0; i < touchCount; i++ ) {
4523            gPoints.push( {
4524                id: event.changedTouches[ i ].identifier,
4525                type: 'touch',
4526                // isPrimary not set - let the updatePointers functions determine it
4527                currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
4528                currentTime: time
4529            } );
4530        }
4531
4532        // simulate touchenter if not natively available
4533        if ( !$.MouseTracker.haveTouchEnter ) {
4534            updatePointersEnter( tracker, event, gPoints );
4535        }
4536
4537        if ( updatePointersDown( tracker, event, gPoints, 0 ) ) { // 0 means primary button press/release or touch contact
4538            // Touch event model start, end, and move events are always captured so we don't need to capture explicitly
4539        }
4540
4541        $.cancelEvent( event );
4542    }
4543
4544
4545    /**
4546     * @private
4547     * @inner
4548     */
4549    function onTouchEnd( tracker, event ) {
4550        var time,
4551            i,
4552            touchCount = event.changedTouches.length,
4553            gPoints = [];
4554
4555        time = $.now();
4556
4557        for ( i = 0; i < touchCount; i++ ) {
4558            gPoints.push( {
4559                id: event.changedTouches[ i ].identifier,
4560                type: 'touch',
4561                // isPrimary not set - let the updatePointers functions determine it
4562                currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
4563                currentTime: time
4564            } );
4565        }
4566
4567        // Touch event model start, end, and move events are always captured so we don't need to release capture.
4568        // We'll ignore the should-release-capture return value here
4569        updatePointersUp( tracker, event, gPoints, 0 ); // 0 means primary button press/release or touch contact
4570
4571        // simulate touchleave if not natively available
4572        if ( !$.MouseTracker.haveTouchEnter && touchCount > 0 ) {
4573            updatePointersExit( tracker, event, gPoints );
4574        }
4575
4576        $.cancelEvent( event );
4577    }
4578
4579
4580    /**
4581     * @private
4582     * @inner
4583     */
4584    function onTouchMove( tracker, event ) {
4585        var i,
4586            touchCount = event.changedTouches.length,
4587            gPoints = [];
4588
4589        for ( i = 0; i < touchCount; i++ ) {
4590            gPoints.push( {
4591                id: event.changedTouches[ i ].identifier,
4592                type: 'touch',
4593                // isPrimary not set - let the updatePointers functions determine it
4594                currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
4595                currentTime: $.now()
4596            } );
4597        }
4598
4599        updatePointersMove( tracker, event, gPoints );
4600
4601        $.cancelEvent( event );
4602    }
4603
4604
4605    /**
4606     * @private
4607     * @inner
4608     */
4609    function onTouchCancel( tracker, event ) {
4610        var i,
4611            touchCount = event.changedTouches.length,
4612            gPoints = [];
4613        
4614        for ( i = 0; i < touchCount; i++ ) {
4615            gPoints.push( {
4616                id: event.changedTouches[ i ].identifier,
4617                type: 'touch'
4618            } );
4619        }
4620
4621        updatePointersCancel( tracker, event, gPoints );
4622    }
4623
4624
4625    /**
4626     * @private
4627     * @inner
4628     */
4629    function onGestureStart( tracker, event ) {
4630        event.stopPropagation();
4631        event.preventDefault();
4632        return false;
4633    }
4634
4635
4636    /**
4637     * @private
4638     * @inner
4639     */
4640    function onGestureChange( tracker, event ) {
4641        event.stopPropagation();
4642        event.preventDefault();
4643        return false;
4644    }
4645
4646
4647    /**
4648     * @private
4649     * @inner
4650     */
4651    function onPointerEnter( tracker, event ) {
4652        var gPoint;
4653
4654        gPoint = {
4655            id: event.pointerId,
4656            type: getPointerType( event ),
4657            isPrimary: event.isPrimary,
4658            currentPos: getMouseAbsolute( event ),
4659            currentTime: $.now()
4660        };
4661
4662        updatePointersEnter( tracker, event, [ gPoint ] );
4663    }
4664
4665
4666    /**
4667     * @private
4668     * @inner
4669     */
4670    function onPointerLeave( tracker, event ) {
4671        var gPoint;
4672
4673        gPoint = {
4674            id: event.pointerId,
4675            type: getPointerType( event ),
4676            isPrimary: event.isPrimary,
4677            currentPos: getMouseAbsolute( event ),
4678            currentTime: $.now()
4679        };
4680
4681        updatePointersExit( tracker, event, [ gPoint ] );
4682    }
4683
4684
4685    /**
4686     * @private
4687     * @inner
4688     */
4689    function onPointerDown( tracker, event ) {
4690        var gPoint;
4691
4692        gPoint = {
4693            id: event.pointerId,
4694            type: getPointerType( event ),
4695            isPrimary: event.isPrimary,
4696            currentPos: getMouseAbsolute( event ),
4697            currentTime: $.now()
4698        };
4699
4700        if ( updatePointersDown( tracker, event, [ gPoint ], event.button ) ) {
4701            if ( $.MouseTracker.unprefixedPointerEvents ) {
4702                event.currentTarget.setPointerCapture( event.pointerId );
4703            } else {
4704                event.currentTarget.msSetPointerCapture( event.pointerId );
4705            }
4706            $.stopEvent( event );
4707        }
4708
4709        if ( tracker.clickHandler || tracker.dblClickHandler || tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) {
4710            $.cancelEvent( event );
4711        }
4712    }
4713
4714
4715    /**
4716     * @private
4717     * @inner
4718     */
4719    function onPointerUp( tracker, event ) {
4720        var gPoint;
4721
4722        gPoint = {
4723            id: event.pointerId,
4724            type: getPointerType( event ),
4725            isPrimary: event.isPrimary,
4726            currentPos: getMouseAbsolute( event ),
4727            currentTime: $.now()
4728        };
4729
4730        if ( updatePointersUp( tracker, event, [ gPoint ], event.button ) ) {
4731            if ( $.MouseTracker.unprefixedPointerEvents ) {
4732                event.currentTarget.releasePointerCapture( event.pointerId );
4733            } else {
4734                event.currentTarget.msReleasePointerCapture( event.pointerId );
4735            }
4736        }
4737    }
4738
4739
4740    /**
4741     * @private
4742     * @inner
4743     */
4744    function onPointerMove( tracker, event ) {
4745        // Pointer changed coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height)
4746        var gPoint;
4747
4748        gPoint = {
4749            id: event.pointerId,
4750            type: getPointerType( event ),
4751            isPrimary: event.isPrimary,
4752            currentPos: getMouseAbsolute( event ),
4753            currentTime: $.now()
4754        };
4755
4756        updatePointersMove( tracker, event, [ gPoint ] );
4757    }
4758
4759
4760    /**
4761     * @private
4762     * @inner
4763     */
4764    function onPointerCancel( tracker, event ) {
4765        var gPoint;
4766
4767        gPoint = {
4768            id: event.pointerId,
4769            type: getPointerType( event )
4770        };
4771
4772        updatePointersCancel( tracker, event, [ gPoint ] );
4773    }
4774
4775
4776///////////////////////////////////////////////////////////////////////////////
4777// Device-agnostic DOM event handlers
4778///////////////////////////////////////////////////////////////////////////////
4779
4780    /**
4781     * @function
4782     * @private
4783     * @inner
4784     * @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList
4785     *     The GesturePointList to track the pointer in.
4786     * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
4787     *      Gesture point to track.
4788     * @returns {Number} Number of gesture points in pointsList.
4789     */
4790    function startTrackingPointer( pointsList, gPoint ) {
4791
4792        // If isPrimary is not known for the pointer then set it according to our rules: 
4793        //    true if the first pointer in the gesture, otherwise false
4794        if ( !gPoint.hasOwnProperty( 'isPrimary' ) ) {
4795            if ( pointsList.getLength() === 0 ) {
4796                gPoint.isPrimary = true;
4797            } else {
4798                gPoint.isPrimary = false;
4799            }
4800        }
4801        gPoint.speed = 0;
4802        gPoint.direction = 0;
4803        gPoint.contactPos = gPoint.currentPos;
4804        gPoint.contactTime = gPoint.currentTime;
4805        gPoint.lastPos = gPoint.currentPos;
4806        gPoint.lastTime = gPoint.currentTime;
4807
4808        return pointsList.add( gPoint );
4809    }
4810
4811
4812    /**
4813     * @function
4814     * @private
4815     * @inner
4816     * @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList
4817     *     The GesturePointList to stop tracking the pointer on.
4818     * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
4819     *      Gesture point to stop tracking.
4820     * @returns {Number} Number of gesture points in pointsList.
4821     */
4822    function stopTrackingPointer( pointsList, gPoint ) {
4823        var listLength,
4824            primaryPoint;
4825
4826        if ( pointsList.getById( gPoint.id ) ) {
4827            listLength = pointsList.removeById( gPoint.id );
4828
4829            // If isPrimary is not known for the pointer and we just removed the primary pointer from the list then we need to set another pointer as primary
4830            if ( !gPoint.hasOwnProperty( 'isPrimary' ) ) {
4831                primaryPoint = pointsList.getPrimary();
4832                if ( !primaryPoint ) {
4833                    primaryPoint = pointsList.getByIndex( 0 );
4834                    if ( primaryPoint ) {
4835                        primaryPoint.isPrimary = true;
4836                    }
4837                }
4838            }
4839        } else {
4840            listLength = pointsList.getLength();
4841        }
4842
4843        return listLength;
4844    }
4845
4846
4847    /**
4848     * @function
4849     * @private
4850     * @inner
4851     * @param {OpenSeadragon.MouseTracker} tracker
4852     *     A reference to the MouseTracker instance.
4853     * @param {Object} event
4854     *     A reference to the originating DOM event.
4855     * @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} gPoints
4856     *      Gesture points associated with the event.
4857     */
4858    function updatePointersEnter( tracker, event, gPoints ) {
4859        var pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ),
4860            i,
4861            gPointCount = gPoints.length,
4862            curGPoint,
4863            updateGPoint,
4864            propagate;
4865
4866        for ( i = 0; i < gPointCount; i++ ) {
4867            curGPoint = gPoints[ i ];
4868            updateGPoint = pointsList.getById( curGPoint.id );
4869
4870            if ( updateGPoint ) {
4871                // Already tracking the pointer...update it
4872                updateGPoint.insideElement = true;
4873                updateGPoint.lastPos = updateGPoint.currentPos;
4874                updateGPoint.lastTime = updateGPoint.currentTime;
4875                updateGPoint.currentPos = curGPoint.currentPos;
4876                updateGPoint.currentTime = curGPoint.currentTime;
4877
4878                curGPoint = updateGPoint;
4879            } else {
4880                // Initialize for tracking and add to the tracking list
4881                curGPoint.captured = false;
4882                curGPoint.insideElementPressed = false;
4883                curGPoint.insideElement = true;
4884                startTrackingPointer( pointsList, curGPoint );
4885            }
4886
4887            // Enter
4888            if ( tracker.enterHandler ) {
4889                propagate = tracker.enterHandler(
4890                    {
4891                        eventSource:          tracker,
4892                        pointerType:          curGPoint.type,
4893                        position:             getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ),
4894                        buttons:              pointsList.buttons,
4895                        insideElementPressed: curGPoint.insideElementPressed,
4896                        buttonDownAny:        pointsList.buttons !== 0,
4897                        isTouchEvent:         curGPoint.type === 'touch',
4898                        originalEvent:        event,
4899                        preventDefaultAction: false,
4900                        userData:             tracker.userData
4901                    }
4902                );
4903                if ( propagate === false ) {
4904                    $.cancelEvent( event );
4905                }
4906            }
4907        }
4908    }
4909
4910
4911    /**
4912     * @function
4913     * @private
4914     * @inner
4915     * @param {OpenSeadragon.MouseTracker} tracker
4916     *     A reference to the MouseTracker instance.
4917     * @param {Object} event
4918     *     A reference to the originating DOM event.
4919     * @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} gPoints
4920     *      Gesture points associated with the event.
4921     */
4922    function updatePointersExit( tracker, event, gPoints ) {
4923        var delegate = THIS[ tracker.hash ],
4924            pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ),
4925            i,
4926            gPointCount = gPoints.length,
4927            curGPoint,
4928            updateGPoint,
4929            propagate;
4930
4931        for ( i = 0; i < gPointCount; i++ ) {
4932            curGPoint = gPoints[ i ];
4933            updateGPoint = pointsList.getById( curGPoint.id );
4934
4935            if ( updateGPoint ) {
4936                // Already tracking the pointer. If captured then update it, else stop tracking it
4937                if ( updateGPoint.captured ) {
4938                    updateGPoint.insideElement = false;
4939                    updateGPoint.lastPos = updateGPoint.currentPos;
4940                    updateGPoint.lastTime = updateGPoint.currentTime;
4941                    updateGPoint.currentPos = curGPoint.currentPos;
4942                    updateGPoint.currentTime = curGPoint.currentTime;
4943                } else {
4944                    stopTrackingPointer( pointsList, updateGPoint );
4945                }
4946
4947                curGPoint = updateGPoint;
4948            }
4949
4950            // Exit
4951            if ( tracker.exitHandler ) {
4952                propagate = tracker.exitHandler(
4953                    {
4954                        eventSource:          tracker,
4955                        pointerType:          curGPoint.type,
4956                        position:             getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ),
4957                        buttons:              pointsList.buttons,
4958                        insideElementPressed: updateGPoint ? updateGPoint.insideElementPressed : false,
4959                        buttonDownAny:        pointsList.buttons !== 0,
4960                        isTouchEvent:         curGPoint.type === 'touch',
4961                        originalEvent:        event,
4962                        preventDefaultAction: false,
4963                        userData:             tracker.userData
4964                    }
4965                );
4966
4967                if ( propagate === false ) {
4968                    $.cancelEvent( event );
4969                }
4970            }
4971        }
4972    }
4973
4974
4975    /**
4976     * @function
4977     * @private
4978     * @inner
4979     * @param {OpenSeadragon.MouseTracker} tracker
4980     *     A reference to the MouseTracker instance.
4981     * @param {Object} event
4982     *     A reference to the originating DOM event.
4983     * @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} gPoints
4984     *      Gesture points associated with the event.
4985     * @param {Number} buttonChanged
4986     *      The button involved in the event: -1: none, 0: primary, 1: aux, 2: secondary, 3: X1, 4: X2, 5: pen eraser.
4987     *      Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, 
4988     *      only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
4989     *
4990     * @returns {Boolean} True if pointers should be captured to the tracked element, otherwise false.
4991     */
4992    function updatePointersDown( tracker, event, gPoints, buttonChanged ) {
4993        var delegate = THIS[ tracker.hash ],
4994            propagate,
4995            pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ),
4996            i,
4997            gPointCount = gPoints.length,
4998            curGPoint,
4999            updateGPoint;
5000
5001        if ( typeof event.buttons !== 'undefined' ) {
5002            pointsList.buttons = event.buttons;
5003        } else {
5004            if ( buttonChanged === 0 ) {
5005                // Primary
5006                pointsList.buttons |= 1;
5007            } else if ( buttonChanged === 1 ) {
5008                // Aux
5009                pointsList.buttons |= 4;
5010            } else if ( buttonChanged === 2 ) {
5011                // Secondary
5012                pointsList.buttons |= 2;
5013            } else if ( buttonChanged === 3 ) {
5014                // X1 (Back)
5015                pointsList.buttons |= 8;
5016            } else if ( buttonChanged === 4 ) {
5017                // X2 (Forward)
5018                pointsList.buttons |= 16;
5019            } else if ( buttonChanged === 5 ) {
5020                // Pen Eraser
5021                pointsList.buttons |= 32;
5022            }
5023        }
5024
5025        // Only capture and track primary button, pen, and touch contacts
5026        //if ( buttonChanged !== 0 ) {
5027        if ( buttonChanged !== 0 && buttonChanged !== 1 ) { //TODO Remove this IE8 compatibility and use the commented line above
5028            return false;
5029        }
5030
5031        for ( i = 0; i < gPointCount; i++ ) {
5032            curGPoint = gPoints[ i ];
5033            updateGPoint = pointsList.getById( curGPoint.id );
5034
5035            if ( updateGPoint ) {
5036                // Already tracking the pointer...update it
5037                updateGPoint.captured = true;
5038                updateGPoint.insideElementPressed = true;
5039                updateGPoint.insideElement = true;
5040                updateGPoint.contactPos = curGPoint.currentPos;
5041                updateGPoint.contactTime = curGPoint.currentTime;
5042                updateGPoint.lastPos = updateGPoint.currentPos;
5043                updateGPoint.lastTime = updateGPoint.currentTime;
5044                updateGPoint.currentPos = curGPoint.currentPos;
5045                updateGPoint.currentTime = curGPoint.currentTime;
5046
5047                curGPoint = updateGPoint;
5048            } else {
5049                // Initialize for tracking and add to the tracking list (no pointerover or pointermove event occurred before this)
5050                curGPoint.captured = true;
5051                curGPoint.insideElementPressed = true;
5052                curGPoint.insideElement = true;
5053                startTrackingPointer( pointsList, curGPoint );
5054            }
5055
5056            pointsList.contacts++;
5057
5058            if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) {
5059                $.MouseTracker.gesturePointVelocityTracker.addPoint( tracker, curGPoint );
5060            }
5061
5062            if ( pointsList.contacts === 1 ) {
5063                // Press
5064                if ( tracker.pressHandler ) {
5065                    propagate = tracker.pressHandler(
5066                        {
5067                            eventSource:          tracker,
5068                            pointerType:          curGPoint.type,
5069                            position:             getPointRelativeToAbsolute( curGPoint.contactPos, tracker.element ),
5070                            buttons:              pointsList.buttons,
5071                            isTouchEvent:         curGPoint.type === 'touch',
5072                            originalEvent:        event,
5073                            preventDefaultAction: false,
5074                            userData:             tracker.userData
5075                        }
5076                    );
5077                    if ( propagate === false ) {
5078                        $.cancelEvent( event );
5079                    }
5080                }
5081            } else if ( pointsList.contacts === 2 ) {
5082                if ( tracker.pinchHandler && curGPoint.type === 'touch' ) {
5083                    // Initialize for pinch
5084                    delegate.pinchGPoints = pointsList.asArray();
5085                    delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
5086                    delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
5087                }
5088            }
5089        }
5090
5091        return true;
5092    }
5093
5094
5095    /**
5096     * @function
5097     * @private
5098     * @inner
5099     * @param {OpenSeadragon.MouseTracker} tracker
5100     *     A reference to the MouseTracker instance.
5101     * @param {Object} event
5102     *     A reference to the originating DOM event.
5103     * @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} gPoints
5104     *      Gesture points associated with the event.
5105     * @param {Number} buttonChanged
5106     *      The button involved in the event: -1: none, 0: primary, 1: aux, 2: secondary, 3: X1, 4: X2, 5: pen eraser.
5107     *      Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, 
5108     *      only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
5109     *
5110     * @returns {Boolean} True if pointer capture should be released from the tracked element, otherwise false.
5111     */
5112    function updatePointersUp( tracker, event, gPoints, buttonChanged ) {
5113        var delegate = THIS[ tracker.hash ],
5114            pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ),
5115            propagate,
5116            insideElementReleased,
5117            releasePoint,
5118            releaseTime,
5119            i,
5120            gPointCount = gPoints.length,
5121            curGPoint,
5122            updateGPoint,
5123            releaseCapture = false,
5124            wasCaptured = false,
5125            quick;
5126
5127        if ( typeof event.buttons !== 'undefined' ) {
5128            pointsList.buttons = event.buttons;
5129        } else {
5130            if ( buttonChanged === 0 ) {
5131                // Primary
5132                pointsList.buttons ^= ~1;
5133            } else if ( buttonChanged === 1 ) {
5134                // Aux
5135                pointsList.buttons ^= ~4;
5136            } else if ( buttonChanged === 2 ) {
5137                // Secondary
5138                pointsList.buttons ^= ~2;
5139            } else if ( buttonChanged === 3 ) {
5140                // X1 (Back)
5141                pointsList.buttons ^= ~8;
5142            } else if ( buttonChanged === 4 ) {
5143                // X2 (Forward)
5144                pointsList.buttons ^= ~16;
5145            } else if ( buttonChanged === 5 ) {
5146                // Pen Eraser
5147                pointsList.buttons ^= ~32;
5148            }
5149        }
5150
5151        // Only capture and track primary button, pen, and touch contacts
5152        //if ( buttonChanged !== 0 ) {
5153        if ( buttonChanged !== 0 && buttonChanged !== 1 ) { //TODO Remove this IE8 compatibility and use the commented line above
5154            return false;
5155        }
5156
5157        for ( i = 0; i < gPointCount; i++ ) {
5158            curGPoint = gPoints[ i ];
5159            updateGPoint = pointsList.getById( curGPoint.id );
5160
5161            if ( updateGPoint ) {
5162                // Update the pointer, stop tracking it if not still in this element
5163                if ( updateGPoint.captured ) {
5164                    updateGPoint.captured = false;
5165                    releaseCapture = true;
5166                    wasCaptured = true;
5167                }
5168                updateGPoint.lastPos = updateGPoint.currentPos;
5169                updateGPoint.lastTime = updateGPoint.currentTime;
5170                updateGPoint.currentPos = curGPoint.currentPos;
5171                updateGPoint.currentTime = curGPoint.currentTime;
5172                if ( !updateGPoint.insideElement ) {
5173                    stopTrackingPointer( pointsList, updateGPoint );
5174                }
5175
5176                releasePoint = updateGPoint.currentPos;
5177                releaseTime = updateGPoint.currentTime;
5178
5179                if ( wasCaptured ) {
5180                    // Pointer was activated in our element but could have been removed in any element since events are captured to our element
5181
5182                    pointsList.contacts--;
5183
5184                    if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) {
5185                        $.MouseTracker.gesturePointVelocityTracker.removePoint( tracker, updateGPoint );
5186                    }
5187
5188                    if ( pointsList.contacts === 0 ) {
5189
5190                        // Release (pressed in our element)
5191                        if ( tracker.releaseHandler ) {
5192                            propagate = tracker.releaseHandler(
5193                                {
5194                                    eventSource:           tracker,
5195                                    pointerType:           updateGPoint.type,
5196                                    position:              getPointRelativeToAbsolute( releasePoint, tracker.element ),
5197                                    buttons:               pointsList.buttons,
5198                                    insideElementPressed:  updateGPoint.insideElementPressed,
5199                                    insideElementReleased: updateGPoint.insideElement,
5200                                    isTouchEvent:          updateGPoint.type === 'touch',
5201                                    originalEvent:         event,
5202                                    preventDefaultAction:  false,
5203                                    userData:              tracker.userData
5204                                }
5205                            );
5206                            if ( propagate === false ) {
5207                                $.cancelEvent( event );
5208                            }
5209                        }
5210
5211                        // Drag End
5212                        if ( tracker.dragEndHandler && !updateGPoint.currentPos.equals( updateGPoint.contactPos ) ) {
5213                            propagate = tracker.dragEndHandler(
5214                                {
5215                                    eventSource:          tracker,
5216                                    pointerType:          updateGPoint.type,
5217                                    position:             getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
5218                                    speed:                updateGPoint.speed,
5219                                    direction:            updateGPoint.direction,
5220                                    shift:                event.shiftKey,
5221                                    isTouchEvent:         updateGPoint.type === 'touch',
5222                                    originalEvent:        event,
5223                                    preventDefaultAction: false,
5224                                    userData:             tracker.userData
5225                                }
5226                            );
5227                            if ( propagate === false ) {
5228                                $.cancelEvent( event );
5229                            }
5230                        }
5231
5232                        // Click / Double-Click
5233                        if ( ( tracker.clickHandler || tracker.dblClickHandler ) && updateGPoint.insideElement ) {
5234                            quick = releaseTime - updateGPoint.contactTime <= tracker.clickTimeThreshold &&
5235                                            updateGPoint.contactPos.distanceTo( releasePoint ) <= tracker.clickDistThreshold;
5236
5237                            // Click
5238                            if ( tracker.clickHandler ) {
5239                                propagate = tracker.clickHandler(
5240                                    {
5241                                        eventSource:          tracker,
5242                                        pointerType:          updateGPoint.type,
5243                                        position:             getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
5244                                        quick:                quick,
5245                                        shift:                event.shiftKey,
5246                                        isTouchEvent:         updateGPoint.type === 'touch',
5247                                        originalEvent:        event,
5248                                        preventDefaultAction: false,
5249                                        userData:             tracker.userData
5250                                    }
5251                                );
5252                                if ( propagate === false ) {
5253                                    $.cancelEvent( event );
5254                                }
5255                            }
5256
5257                            // Double-Click
5258                            if ( tracker.dblClickHandler && quick ) {
5259                                pointsList.clicks++;
5260                                if ( pointsList.clicks === 1 ) {
5261                                    delegate.lastClickPos = releasePoint;
5262                                    /*jshint loopfunc:true*/
5263                                    delegate.dblClickTimeOut = setTimeout( function() {
5264                                        pointsList.clicks = 0;
5265                                    }, tracker.dblClickTimeThreshold );
5266                                    /*jshint loopfunc:false*/
5267                                } else if ( pointsList.clicks === 2 ) {
5268                                    clearTimeout( delegate.dblClickTimeOut );
5269                                    pointsList.clicks = 0;
5270                                    if ( delegate.lastClickPos.distanceTo( releasePoint ) <= tracker.dblClickDistThreshold ) {
5271                                        propagate = tracker.dblClickHandler(
5272                                            {
5273                                                eventSource:          tracker,
5274                                                pointerType:          updateGPoint.type,
5275                                                position:             getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
5276                                                shift:                event.shiftKey,
5277                                                isTouchEvent:         updateGPoint.type === 'touch',
5278                                                originalEvent:        event,
5279                                                preventDefaultAction: false,
5280                                                userData:             tracker.userData
5281                                            }
5282                                        );
5283                                        if ( propagate === false ) {
5284                                            $.cancelEvent( event );
5285                                        }
5286                                    }
5287                                    delegate.lastClickPos = null;
5288                                }
5289                            }
5290                        }
5291                    } else if ( pointsList.contacts === 2 ) {
5292                        if ( tracker.pinchHandler && updateGPoint.type === 'touch' ) {
5293                            // Reset for pinch
5294                            delegate.pinchGPoints = pointsList.asArray();
5295                            delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
5296                            delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
5297                        }
5298                    }
5299                } else {
5300                    // Pointer was activated in another element but removed in our element
5301
5302                    // Release (pressed in another element)
5303                    if ( tracker.releaseHandler ) {
5304                        propagate = tracker.releaseHandler(
5305                            {
5306                                eventSource:           tracker,
5307                                pointerType:           updateGPoint.type,
5308                                position:              getPointRelativeToAbsolute( releasePoint, tracker.element ),
5309                                buttons:               pointsList.buttons,
5310                                insideElementPressed:  updateGPoint.insideElementPressed,
5311                                insideElementReleased: updateGPoint.insideElement,
5312                                isTouchEvent:          updateGPoint.type === 'touch',
5313                                originalEvent:         event,
5314                                preventDefaultAction:  false,
5315                                userData:              tracker.userData
5316                            }
5317                        );
5318                        if ( propagate === false ) {
5319                            $.cancelEvent( event );
5320                        }
5321                    }
5322                }
5323            }
5324        }
5325
5326        return releaseCapture;
5327    }
5328
5329
5330    /**
5331     * Call when pointer(s) change coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height)
5332     *
5333     * @function
5334     * @private
5335     * @inner
5336     * @param {OpenSeadragon.MouseTracker} tracker
5337     *     A reference to the MouseTracker instance.
5338     * @param {Object} event
5339     *     A reference to the originating DOM event.
5340     * @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} gPoints
5341     *      Gesture points associated with the event.
5342     */
5343    function updatePointersMove( tracker, event, gPoints ) {
5344        var delegate = THIS[ tracker.hash ],
5345            pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ),
5346            i,
5347            gPointCount = gPoints.length,
5348            curGPoint,
5349            updateGPoint,
5350            gPointArray,
5351            delta,
5352            propagate;
5353
5354        if ( typeof event.buttons !== 'undefined' ) {
5355            pointsList.buttons = event.buttons;
5356        }
5357
5358        for ( i = 0; i < gPointCount; i++ ) {
5359            curGPoint = gPoints[ i ];
5360            updateGPoint = pointsList.getById( curGPoint.id );
5361
5362            if ( updateGPoint ) {
5363                // Already tracking the pointer...update it
5364                if ( curGPoint.hasOwnProperty( 'isPrimary' ) ) {
5365                    updateGPoint.isPrimary = curGPoint.isPrimary;
5366                }
5367                updateGPoint.lastPos = updateGPoint.currentPos;
5368                updateGPoint.lastTime = updateGPoint.currentTime;
5369                updateGPoint.currentPos = curGPoint.currentPos;
5370                updateGPoint.currentTime = curGPoint.currentTime;
5371            } else {
5372                // Initialize for tracking and add to the tracking list (no pointerover or pointerdown event occurred before this)
5373                curGPoint.captured = false;
5374                curGPoint.insideElementPressed = false;
5375                curGPoint.insideElement = true;
5376                startTrackingPointer( pointsList, curGPoint );
5377            }
5378        }
5379
5380        // Stop (mouse only)
5381        if ( tracker.stopHandler && gPoints[ 0 ].type === 'mouse' ) {
5382            clearTimeout( tracker.stopTimeOut );
5383            tracker.stopTimeOut = setTimeout( function() {
5384                handlePointerStop( tracker, event, gPoints[ 0 ].type );
5385            }, tracker.stopDelay );
5386        }
5387
5388        if ( pointsList.contacts === 0 ) {
5389            // Move (no contacts: hovering mouse or other hover-capable device)
5390            if ( tracker.moveHandler ) {
5391                propagate = tracker.moveHandler(
5392                    {
5393                        eventSource:          tracker,
5394                        pointerType:          gPoints[ 0 ].type,
5395                        position:             getPointRelativeToAbsolute( gPoints[ 0 ].currentPos, tracker.element ),
5396                        buttons:              pointsList.buttons,
5397                        isTouchEvent:         gPoints[ 0 ].type === 'touch',
5398                        originalEvent:        event,
5399                        preventDefaultAction: false,
5400                        userData:             tracker.userData
5401                    }
5402                );
5403                if ( propagate === false ) {
5404                    $.cancelEvent( event );
5405                }
5406            }
5407        } else if ( pointsList.contacts === 1 ) {
5408            // Move (1 contact)
5409            if ( tracker.moveHandler ) {
5410                updateGPoint = pointsList.asArray()[ 0 ];
5411                propagate = tracker.moveHandler(
5412                    {
5413                        eventSource:          tracker,
5414                        pointerType:          updateGPoint.type,
5415                        position:             getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
5416                        buttons:              pointsList.buttons,
5417                        isTouchEvent:         updateGPoint.type === 'touch',
5418                        originalEvent:        event,
5419                        preventDefaultAction: false,
5420                        userData:             tracker.userData
5421                    }
5422                );
5423                if ( propagate === false ) {
5424                    $.cancelEvent( event );
5425                }
5426            }
5427
5428            // Drag
5429            if ( tracker.dragHandler ) {
5430                updateGPoint = pointsList.asArray()[ 0 ];
5431                delta = updateGPoint.currentPos.minus( updateGPoint.lastPos );
5432                propagate = tracker.dragHandler(
5433                    {
5434                        eventSource:          tracker,
5435                        pointerType:          updateGPoint.type,
5436                        position:             getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
5437                        buttons:              pointsList.buttons,
5438                        delta:                delta,
5439                        speed:                updateGPoint.speed,
5440                        direction:            updateGPoint.direction,
5441                        shift:                event.shiftKey,
5442                        isTouchEvent:         updateGPoint.type === 'touch',
5443                        originalEvent:        event,
5444                        preventDefaultAction: false,
5445                        userData:             tracker.userData
5446                    }
5447                );
5448                if ( propagate === false ) {
5449                    $.cancelEvent( event );
5450                }
5451            }
5452        } else if ( pointsList.contacts === 2 ) {
5453            // Move (2 contacts, use center)
5454            if ( tracker.moveHandler ) {
5455                gPointArray = pointsList.asArray();
5456                propagate = tracker.moveHandler(
5457                    {
5458                        eventSource:          tracker,
5459                        pointerType:          gPointArray[ 0 ].type,
5460                        position:             getPointRelativeToAbsolute( getCenterPoint( gPointArray[ 0 ].currentPos, gPointArray[ 1 ].currentPos ), tracker.element ),
5461                        buttons:              pointsList.buttons,
5462                        isTouchEvent:         gPointArray[ 0 ].type === 'touch',
5463                        originalEvent:        event,
5464                        preventDefaultAction: false,
5465                        userData:             tracker.userData
5466                    }
5467                );
5468                if ( propagate === false ) {
5469                    $.cancelEvent( event );
5470                }
5471            }
5472
5473            // Pinch
5474            if ( tracker.pinchHandler && gPoints[ 0 ].type === 'touch' ) {
5475                delta = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
5476                if ( delta != delegate.currentPinchDist ) {
5477                    delegate.lastPinchDist = delegate.currentPinchDist;
5478                    delegate.currentPinchDist = delta;
5479                    delegate.lastPinchCenter = delegate.currentPinchCenter;
5480                    delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
5481                    propagate = tracker.pinchHandler(
5482                        {
5483                            eventSource:          tracker,
5484                            pointerType:          'touch',
5485                            gesturePoints:        delegate.pinchGPoints,
5486                            lastCenter:           getPointRelativeToAbsolute( delegate.lastPinchCenter, tracker.element ),
5487                            center:               getPointRelativeToAbsolute( delegate.currentPinchCenter, tracker.element ),
5488                            lastDistance:         delegate.lastPinchDist,
5489                            distance:             delegate.currentPinchDist,
5490                            shift:                event.shiftKey,
5491                            originalEvent:        event,
5492                            preventDefaultAction: false,
5493                            userData:             tracker.userData
5494                        }
5495                    );
5496                    if ( propagate === false ) {
5497                        $.cancelEvent( event );
5498                    }
5499                }
5500            }
5501        }
5502    }
5503
5504
5505    /**
5506     * @function
5507     * @private
5508     * @inner
5509     * @param {OpenSeadragon.MouseTracker} tracker
5510     *     A reference to the MouseTracker instance.
5511     * @param {Object} event
5512     *     A reference to the originating DOM event.
5513     * @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} gPoints
5514     *      Gesture points associated with the event.
5515     */
5516    function updatePointersCancel( tracker, event, gPoints ) {
5517        updatePointersUp( tracker, event, gPoints, 0 );
5518        updatePointersExit( tracker, event, gPoints );
5519    }
5520
5521
5522    /**
5523     * @private
5524     * @inner
5525     */
5526    function handlePointerStop( tracker, originalMoveEvent, pointerType ) {
5527        if ( tracker.stopHandler ) {
5528            tracker.stopHandler( {
5529                eventSource:          tracker,
5530                pointerType:          pointerType,
5531                position:             getMouseRelative( originalMoveEvent, tracker.element ),
5532                buttons:              tracker.getActivePointersListByType( pointerType ).buttons,
5533                isTouchEvent:         pointerType === 'touch',
5534                originalEvent:        originalMoveEvent,
5535                preventDefaultAction: false,
5536                userData:             tracker.userData
5537            } );
5538        }
5539    }
5540
5541} ( OpenSeadragon ) );
5542
5543/*
5544 * OpenSeadragon - Control
5545 *
5546 * Copyright (C) 2009 CodePlex Foundation
5547 * Copyright (C) 2010-2013 OpenSeadragon contributors
5548 *
5549 * Redistribution and use in source and binary forms, with or without
5550 * modification, are permitted provided that the following conditions are
5551 * met:
5552 *
5553 * - Redistributions of source code must retain the above copyright notice,
5554 *   this list of conditions and the following disclaimer.
5555 *
5556 * - Redistributions in binary form must reproduce the above copyright
5557 *   notice, this list of conditions and the following disclaimer in the
5558 *   documentation and/or other materials provided with the distribution.
5559 *
5560 * - Neither the name of CodePlex Foundation nor the names of its
5561 *   contributors may be used to endorse or promote products derived from
5562 *   this software without specific prior written permission.
5563 *
5564 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5565 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5566 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
5567 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
5568 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
5569 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
5570 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
5571 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
5572 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
5573 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
5574 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
5575 */
5576
5577(function( $ ){
5578
5579/**
5580 * An enumeration of supported locations where controls can be anchored.
5581 * The anchoring is always relative to the container.
5582 * @member ControlAnchor
5583 * @memberof OpenSeadragon
5584 * @static
5585 * @type {Object}
5586 * @property {Number} NONE
5587 * @property {Number} TOP_LEFT
5588 * @property {Number} TOP_RIGHT
5589 * @property {Number} BOTTOM_LEFT
5590 * @property {Number} BOTTOM_RIGHT
5591 * @property {Number} ABSOLUTE
5592 */
5593$.ControlAnchor = {
5594    NONE: 0,
5595    TOP_LEFT: 1,
5596    TOP_RIGHT: 2,
5597    BOTTOM_RIGHT: 3,
5598    BOTTOM_LEFT: 4,
5599    ABSOLUTE: 5
5600};
5601
5602/**
5603 * @class Control
5604 * @classdesc A Control represents any interface element which is meant to allow the user
5605 * to interact with the zoomable interface. Any control can be anchored to any
5606 * element.
5607 *
5608 * @memberof OpenSeadragon
5609 * @param {Element} element - the control element to be anchored in the container.
5610 * @param {Object } options - All required and optional settings for configuring a control element.
5611 * @param {OpenSeadragon.ControlAnchor} [options.anchor=OpenSeadragon.ControlAnchor.NONE] - the position of the control
5612 *  relative to the container.
5613 * @param {Boolean} [options.attachToViewer=true] - Whether the control should be added directly to the viewer, or
5614 *  directly to the container
5615 * @param {Boolean} [options.autoFade=true] - Whether the control should have the autofade behavior
5616 * @param {Element} container - the element to control will be anchored too.
5617 */
5618$.Control = function ( element, options, container ) {
5619    var parent = element.parentNode;
5620    if (typeof options === 'number')
5621    {
5622        $.console.error("Passing an anchor directly into the OpenSeadragon.Control constructor is deprecated; " +
5623                        "please use an options object instead.  " +
5624                        "Support for this deprecated variant is scheduled for removal in December 2013");
5625         options = {anchor: options};
5626    }
5627    options.attachToViewer = (typeof options.attachToViewer === 'undefined') ? true : options.attachToViewer;
5628    /**
5629     * True if the control should have autofade behavior.
5630     * @member {Boolean} autoFade
5631     * @memberof OpenSeadragon.Control#
5632     */
5633    this.autoFade = (typeof options.autoFade === 'undefined') ? true : options.autoFade;
5634    /**
5635     * The element providing the user interface with some type of control (e.g. a zoom-in button).
5636     * @member {Element} element
5637     * @memberof OpenSeadragon.Control#
5638     */
5639    this.element    = element;
5640    /**
5641     * The position of the Control relative to its container.
5642     * @member {OpenSeadragon.ControlAnchor} anchor
5643     * @memberof OpenSeadragon.Control#
5644     */
5645    this.anchor     = options.anchor;
5646    /**
5647     * The Control's containing element.
5648     * @member {Element} container
5649     * @memberof OpenSeadragon.Control#
5650     */
5651    this.container  = container;
5652    /**
5653     * A neutral element surrounding the control element.
5654     * @member {Element} wrapper
5655     * @memberof OpenSeadragon.Control#
5656     */
5657    if ( this.anchor == $.ControlAnchor.ABSOLUTE ) {
5658        this.wrapper    = $.makeNeutralElement( "div" );
5659        this.wrapper.style.position = "absolute";
5660        this.wrapper.style.top = typeof ( options.top )  == "number" ? ( options.top + 'px' ) : options.top;
5661        this.wrapper.style.left  = typeof ( options.left )  == "number" ?  (options.left + 'px' ) : options.left;
5662        this.wrapper.style.height = typeof ( options.height )  == "number" ? ( options.height + 'px' ) : options.height;
5663        this.wrapper.style.width  = typeof ( options.width )  == "number" ? ( options.width + 'px' ) : options.width;
5664        this.wrapper.style.margin = "0px";
5665        this.wrapper.style.padding = "0px";
5666
5667        this.element.style.position = "relative";
5668        this.element.style.top = "0px";
5669        this.element.style.left = "0px";
5670        this.element.style.height = "100%";
5671        this.element.style.width = "100%";
5672    } else {
5673        this.wrapper    = $.makeNeutralElement( "div" );
5674        this.wrapper.style.display = "inline-block";
5675        if ( this.anchor == $.ControlAnchor.NONE ) {
5676            // IE6 fix
5677            this.wrapper.style.width = this.wrapper.style.height = "100%";
5678        }
5679    }
5680    this.wrapper.appendChild( this.element );
5681
5682    if (options.attachToViewer ) {
5683        if ( this.anchor == $.ControlAnchor.TOP_RIGHT ||
5684             this.anchor == $.ControlAnchor.BOTTOM_RIGHT ) {
5685            this.container.insertBefore(
5686                this.wrapper,
5687                this.container.firstChild
5688            );
5689        } else {
5690            this.container.appendChild( this.wrapper );
5691        }
5692    } else {
5693        parent.appendChild( this.wrapper );
5694    }
5695};
5696
5697$.Control.prototype = /** @lends OpenSeadragon.Control.prototype */{
5698
5699    /**
5700     * Removes the control from the container.
5701     * @function
5702     */
5703    destroy: function() {
5704        this.wrapper.removeChild( this.element );
5705        this.container.removeChild( this.wrapper );
5706    },
5707
5708    /**
5709     * Determines if the control is currently visible.
5710     * @function
5711     * @return {Boolean} true if currenly visible, false otherwise.
5712     */
5713    isVisible: function() {
5714        return this.wrapper.style.display != "none";
5715    },
5716
5717    /**
5718     * Toggles the visibility of the control.
5719     * @function
5720     * @param {Boolean} visible - true to make visible, false to hide.
5721     */
5722    setVisible: function( visible ) {
5723        this.wrapper.style.display = visible ?
5724            ( this.anchor == $.ControlAnchor.ABSOLUTE ? 'block' : 'inline-block' ) :
5725            "none";
5726    },
5727
5728    /**
5729     * Sets the opacity level for the control.
5730     * @function
5731     * @param {Number} opactiy - a value between 1 and 0 inclusively.
5732     */
5733    setOpacity: function( opacity ) {
5734        if ( this.element[ $.SIGNAL ] && $.Browser.vendor == $.BROWSERS.IE ) {
5735            $.setElementOpacity( this.element, opacity, true );
5736        } else {
5737            $.setElementOpacity( this.wrapper, opacity, true );
5738        }
5739    }
5740};
5741
5742}( OpenSeadragon ));
5743
5744/*
5745 * OpenSeadragon - ControlDock
5746 *
5747 * Copyright (C) 2009 CodePlex Foundation
5748 * Copyright (C) 2010-2013 OpenSeadragon contributors
5749 *
5750 * Redistribution and use in source and binary forms, with or without
5751 * modification, are permitted provided that the following conditions are
5752 * met:
5753 *
5754 * - Redistributions of source code must retain the above copyright notice,
5755 *   this list of conditions and the following disclaimer.
5756 *
5757 * - Redistributions in binary form must reproduce the above copyright
5758 *   notice, this list of conditions and the following disclaimer in the
5759 *   documentation and/or other materials provided with the distribution.
5760 *
5761 * - Neither the name of CodePlex Foundation nor the names of its
5762 *   contributors may be used to endorse or promote products derived from
5763 *   this software without specific prior written permission.
5764 *
5765 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5766 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5767 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
5768 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
5769 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
5770 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
5771 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
5772 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
5773 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
5774 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
5775 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
5776 */
5777
5778(function( $ ){
5779    /**
5780     * @class ControlDock
5781     * @classdesc Provides a container element (a &lt;form&gt; element) with support for the layout of control elements.
5782     *
5783     * @memberof OpenSeadragon
5784     */
5785    $.ControlDock = function( options ){
5786        var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'],
5787            layout,
5788            i;
5789
5790        $.extend( true, this, {
5791            id: 'controldock-'+$.now()+'-'+Math.floor(Math.random()*1000000),
5792            container: $.makeNeutralElement( 'div' ),
5793            controls: []
5794        }, options );
5795
5796        // Disable the form's submit; otherwise button clicks and return keys
5797        // can trigger it.
5798        this.container.onsubmit = function() {
5799            return false;
5800        };
5801
5802        if( this.element ){
5803            this.element = $.getElement( this.element );
5804            this.element.appendChild( this.container );
5805            this.element.style.position = 'relative';
5806            this.container.style.width = '100%';
5807            this.container.style.height = '100%';
5808        }
5809
5810        for( i = 0; i < layouts.length; i++ ){
5811            layout = layouts[ i ];
5812            this.controls[ layout ] = $.makeNeutralElement( "div" );
5813            this.controls[ layout ].style.position = 'absolute';
5814            if ( layout.match( 'left' ) ){
5815                this.controls[ layout ].style.left = '0px';
5816            }
5817            if ( layout.match( 'right' ) ){
5818                this.controls[ layout ].style.right = '0px';
5819            }
5820            if ( layout.match( 'top' ) ){
5821                this.controls[ layout ].style.top = '0px';
5822            }
5823            if ( layout.match( 'bottom' ) ){
5824                this.controls[ layout ].style.bottom = '0px';
5825            }
5826        }
5827
5828        this.container.appendChild( this.controls.topleft );
5829        this.container.appendChild( this.controls.topright );
5830        this.container.appendChild( this.controls.bottomright );
5831        this.container.appendChild( this.controls.bottomleft );
5832    };
5833
5834    $.ControlDock.prototype = /** @lends OpenSeadragon.ControlDock.prototype */{
5835
5836        /**
5837         * @function
5838         */
5839        addControl: function ( element, controlOptions ) {
5840            element = $.getElement( element );
5841            var div = null;
5842
5843            if ( getControlIndex( this, element ) >= 0 ) {
5844                return;     // they're trying to add a duplicate control
5845            }
5846
5847            switch ( controlOptions.anchor ) {
5848                case $.ControlAnchor.TOP_RIGHT:
5849                    div = this.controls.topright;
5850                    element.style.position = "relative";
5851                    element.style.paddingRight = "0px";
5852                    element.style.paddingTop = "0px";
5853                    break;
5854                case $.ControlAnchor.BOTTOM_RIGHT:
5855                    div = this.controls.bottomright;
5856                    element.style.position = "relative";
5857                    element.style.paddingRight = "0px";
5858                    element.style.paddingBottom = "0px";
5859                    break;
5860                case $.ControlAnchor.BOTTOM_LEFT:
5861                    div = this.controls.bottomleft;
5862                    element.style.position = "relative";
5863                    element.style.paddingLeft = "0px";
5864                    element.style.paddingBottom = "0px";
5865                    break;
5866                case $.ControlAnchor.TOP_LEFT:
5867                    div = this.controls.topleft;
5868                    element.style.position = "relative";
5869                    element.style.paddingLeft = "0px";
5870                    element.style.paddingTop = "0px";
5871                    break;
5872                case $.ControlAnchor.ABSOLUTE:
5873                    div = this.container;
5874                    element.style.margin = "0px";
5875                    element.style.padding = "0px";
5876                    break;
5877                default:
5878                case $.ControlAnchor.NONE:
5879                    div = this.container;
5880                    element.style.margin = "0px";
5881                    element.style.padding = "0px";
5882                    break;
5883            }
5884
5885            this.controls.push(
5886                new $.Control( element, controlOptions, div )
5887            );
5888            element.style.display = "inline-block";
5889        },
5890
5891
5892        /**
5893         * @function
5894         * @return {OpenSeadragon.ControlDock} Chainable.
5895         */
5896        removeControl: function ( element ) {
5897            element = $.getElement( element );
5898            var i = getControlIndex( this, element );
5899
5900            if ( i >= 0 ) {
5901                this.controls[ i ].destroy();
5902                this.controls.splice( i, 1 );
5903            }
5904
5905            return this;
5906        },
5907
5908        /**
5909         * @function
5910         * @return {OpenSeadragon.ControlDock} Chainable.
5911         */
5912        clearControls: function () {
5913            while ( this.controls.length > 0 ) {
5914                this.controls.pop().destroy();
5915            }
5916
5917            return this;
5918        },
5919
5920
5921        /**
5922         * @function
5923         * @return {Boolean}
5924         */
5925        areControlsEnabled: function () {
5926            var i;
5927
5928            for ( i = this.controls.length - 1; i >= 0; i-- ) {
5929                if ( this.controls[ i ].isVisible() ) {
5930                    return true;
5931                }
5932            }
5933
5934            return false;
5935        },
5936
5937
5938        /**
5939         * @function
5940         * @return {OpenSeadragon.ControlDock} Chainable.
5941         */
5942        setControlsEnabled: function( enabled ) {
5943            var i;
5944
5945            for ( i = this.controls.length - 1; i >= 0; i-- ) {
5946                this.controls[ i ].setVisible( enabled );
5947            }
5948
5949            return this;
5950        }
5951
5952    };
5953
5954
5955    ///////////////////////////////////////////////////////////////////////////////
5956    // Utility methods
5957    ///////////////////////////////////////////////////////////////////////////////
5958    function getControlIndex( dock, element ) {
5959        var controls = dock.controls,
5960            i;
5961
5962        for ( i = controls.length - 1; i >= 0; i-- ) {
5963            if ( controls[ i ].element == element ) {
5964                return i;
5965            }
5966        }
5967
5968        return -1;
5969    }
5970
5971}( OpenSeadragon ));
5972
5973/*
5974 * OpenSeadragon - Viewer
5975 *
5976 * Copyright (C) 2009 CodePlex Foundation
5977 * Copyright (C) 2010-2013 OpenSeadragon contributors
5978 *
5979 * Redistribution and use in source and binary forms, with or without
5980 * modification, are permitted provided that the following conditions are
5981 * met:
5982 *
5983 * - Redistributions of source code must retain the above copyright notice,
5984 *   this list of conditions and the following disclaimer.
5985 *
5986 * - Redistributions in binary form must reproduce the above copyright
5987 *   notice, this list of conditions and the following disclaimer in the
5988 *   documentation and/or other materials provided with the distribution.
5989 *
5990 * - Neither the name of CodePlex Foundation nor the names of its
5991 *   contributors may be used to endorse or promote products derived from
5992 *   this software without specific prior written permission.
5993 *
5994 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5995 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5996 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
5997 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
5998 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
5999 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
6000 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
6001 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
6002 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
6003 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
6004 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6005 */
6006
6007(function( $ ){
6008
6009// dictionary from hash to private properties
6010var THIS = {},
6011// We keep a list of viewers so we can 'wake-up' each viewer on
6012// a page after toggling between fullpage modes
6013    VIEWERS = {};
6014
6015/**
6016 *
6017 * The main point of entry into creating a zoomable image on the page.
6018 *
6019 * We have provided an idiomatic javascript constructor which takes
6020 * a single object, but still support the legacy positional arguments.
6021 *
6022 * The options below are given in order that they appeared in the constructor
6023 * as arguments and we translate a positional call into an idiomatic call.
6024 *
6025 * @class Viewer
6026 * @classdesc The main OpenSeadragon viewer class.
6027 *
6028 * @memberof OpenSeadragon
6029 * @extends OpenSeadragon.EventSource
6030 * @extends OpenSeadragon.ControlDock
6031 * @param {OpenSeadragon.Options} options - Viewer options.
6032 *
6033 **/
6034$.Viewer = function( options ) {
6035
6036    var args  = arguments,
6037        _this = this,
6038        i;
6039
6040
6041    //backward compatibility for positional args while prefering more
6042    //idiomatic javascript options object as the only argument
6043    if( !$.isPlainObject( options ) ){
6044        options = {
6045            id:                 args[ 0 ],
6046            xmlPath:            args.length > 1 ? args[ 1 ] : undefined,
6047            prefixUrl:          args.length > 2 ? args[ 2 ] : undefined,
6048            controls:           args.length > 3 ? args[ 3 ] : undefined,
6049            overlays:           args.length > 4 ? args[ 4 ] : undefined
6050        };
6051    }
6052
6053    //options.config and the general config argument are deprecated
6054    //in favor of the more direct specification of optional settings
6055    //being pass directly on the options object
6056    if ( options.config ){
6057        $.extend( true, options, options.config );
6058        delete options.config;
6059    }
6060
6061    //Public properties
6062    //Allow the options object to override global defaults
6063    $.extend( true, this, {
6064
6065        //internal state and dom identifiers
6066        id:             options.id,
6067        hash:           options.hash || options.id,
6068
6069        //dom nodes
6070        /**
6071         * The parent element of this Viewer instance, passed in when the Viewer was created.
6072         * @member {Element} element
6073         * @memberof OpenSeadragon.Viewer#
6074         */
6075        element:        null,
6076        /**
6077         * A &lt;div&gt; element (provided by {@link OpenSeadragon.ControlDock}), the base element of this Viewer instance.<br><br>
6078         * Child element of {@link OpenSeadragon.Viewer#element}.
6079         * @member {Element} container
6080         * @memberof OpenSeadragon.Viewer#
6081         */
6082        container:      null,
6083        /**
6084         * A &lt;textarea&gt; element, the element where keyboard events are handled.<br><br>
6085         * Child element of {@link OpenSeadragon.Viewer#container},
6086         * positioned below {@link OpenSeadragon.Viewer#canvas}. 
6087         * @member {Element} keyboardCommandArea
6088         * @memberof OpenSeadragon.Viewer#
6089         */
6090        keyboardCommandArea: null,
6091        /**
6092         * A &lt;div&gt; element, the element where user-input events are handled for panning and zooming.<br><br>
6093         * Child element of {@link OpenSeadragon.Viewer#container},
6094         * positioned on top of {@link OpenSeadragon.Viewer#keyboardCommandArea}.<br><br>
6095         * The parent of {@link OpenSeadragon.Drawer#canvas} instances. 
6096         * @member {Element} canvas
6097         * @memberof OpenSeadragon.Viewer#
6098         */
6099        canvas:         null,
6100
6101        // Overlays list. An overlay allows to add html on top of the viewer.
6102        overlays:           [],
6103        // Container inside the canvas where overlays are drawn.
6104        overlaysContainer:  null,
6105
6106        //private state properties
6107        previousBody:   [],
6108
6109        //This was originally initialized in the constructor and so could never
6110        //have anything in it.  now it can because we allow it to be specified
6111        //in the options and is only empty by default if not specified. Also
6112        //this array was returned from get_controls which I find confusing
6113        //since this object has a controls property which is treated in other
6114        //functions like clearControls.  I'm removing the accessors.
6115        customControls: [],
6116
6117        //These are originally not part options but declared as members
6118        //in initialize.  It's still considered idiomatic to put them here
6119        source:         null,
6120        /**
6121         * Handles rendering of tiles in the viewer. Created for each TileSource opened.
6122         * @member {OpenSeadragon.Drawer} drawer
6123         * @memberof OpenSeadragon.Viewer#
6124         */
6125        drawer:             null,
6126        drawers:            [],
6127        // Container inside the canvas where drawers (layers) are drawn.
6128        drawersContainer:   null,
6129        /**
6130         * Handles coordinate-related functionality - zoom, pan, rotation, etc. Created for each TileSource opened.
6131         * @member {OpenSeadragon.Viewport} viewport
6132         * @memberof OpenSeadragon.Viewer#
6133         */
6134        viewport:       null,
6135        /**
6136         * @member {OpenSeadragon.Navigator} navigator
6137         * @memberof OpenSeadragon.Viewer#
6138         */
6139        navigator:      null,
6140
6141        //A collection viewport is a separate viewport used to provide
6142        //simultaneous rendering of sets of tiles
6143        collectionViewport:     null,
6144        collectionDrawer:       null,
6145
6146        //UI image resources
6147        //TODO: rename navImages to uiImages
6148        navImages:      null,
6149
6150        //interface button controls
6151        buttons:        null,
6152
6153        //TODO: this is defunct so safely remove it
6154        profiler:       null
6155
6156    }, $.DEFAULT_SETTINGS, options );
6157
6158    if ( typeof( this.hash) === "undefined" ) {
6159        throw new Error("A hash must be defined, either by specifying options.id or options.hash.");
6160    }
6161    if ( typeof( THIS[ this.hash ] ) !== "undefined" ) {
6162        // We don't want to throw an error here, as the user might have discarded
6163        // the previous viewer with the same hash and now want to re
6163create it.
6164        $.console.warn("Hash " + this.hash + " has already been used.");
6165    }
6166
6167    //Private state properties
6168    THIS[ this.hash ] = {
6169        "fsBoundsDelta":     new $.Point( 1, 1 ),
6170        "prevContainerSize": null,
6171        "animating":         false,
6172        "forceRedraw":       false,
6173        "mouseInside":       false,
6174        "group":             null,
6175        // whether we should be continuously zooming
6176        "zooming":           false,
6177        // how much we should be continuously zooming by
6178        "zoomFactor":        null,
6179        "lastZoomTime":      null,
6180        // did we decide this viewer has a sequence of tile sources
6181        "sequenced":         false,
6182        "sequence":          0,
6183        "fullPage":          false,
6184        "onfullscreenchange": null
6185    };
6186
6187    this._updateRequestId = null;
6188    this.currentOverlays = [];
6189
6190    //Inherit some behaviors and properties
6191    $.EventSource.call( this );
6192
6193    this.addHandler( 'open-failed', function ( event ) {
6194        var msg = $.getString( "Errors.OpenFailed", event.eventSource, event.message);
6195        _this._showMessage( msg );
6196    });
6197
6198    $.ControlDock.call( this, options );
6199
6200    //Deal with tile sources
6201    var initialTileSource;
6202
6203    if ( this.xmlPath  ){
6204        //Deprecated option.  Now it is preferred to use the tileSources option
6205        this.tileSources = [ this.xmlPath ];
6206    }
6207
6208    if ( this.tileSources  ){
6209        // tileSources is a complex option...
6210        //
6211        // It can be a string, object, or an array of any of strings and objects.
6212        // At this point we only care about if it is an Array or not.
6213        //
6214        if( $.isArray( this.tileSources ) ){
6215
6216            //must be a sequence of tileSource since the first item
6217            //is a legacy tile source
6218            if( this.tileSources.length > 1 ){
6219                THIS[ this.hash ].sequenced = true;
6220            }
6221            
6222            //Keeps the initial page within bounds
6223            if ( this.initialPage > this.tileSources.length - 1 ){
6224                this.initialPage = this.tileSources.length - 1;
6225            }
6226            
6227            initialTileSource = this.tileSources[ this.initialPage ];
6228            
6229            //Update the sequence (aka currrent page) property
6230            THIS[ this.hash ].sequence = this.initialPage;
6231        } else {
6232            initialTileSource = this.tileSources;
6233        }
6234    }
6235
6236    this.element              = this.element || document.getElementById( this.id );
6237    this.canvas               = $.makeNeutralElement( "div" );
6238    this.keyboardCommandArea  = $.makeNeutralElement( "textarea" );
6239    this.drawersContainer     = $.makeNeutralElement( "div" );
6240    this.overlaysContainer    = $.makeNeutralElement( "div" );
6241
6242    this.canvas.className = "openseadragon-canvas";
6243    (function( style ){
6244        style.width    = "100%";
6245        style.height   = "100%";
6246        style.overflow = "hidden";
6247        style.position = "absolute";
6248        style.top      = "0px";
6249        style.left     = "0px";
6250        // Disable browser default touch handling
6251        if (style["touch-action"] !== undefined) {
6252            style["touch-action"] = "none";
6253        } else if (style["-ms-touch-action"] !== undefined) {
6254            style["-ms-touch-action"] = "none";
6255        }
6256    }(this.canvas.style));
6257
6258    //the container is created through applying the ControlDock constructor above
6259    this.container.className = "openseadragon-container";
6260    (function( style ){
6261        style.width     = "100%";
6262        style.height    = "100%";
6263        style.position  = "relative";
6264        style.overflow  = "hidden";
6265        style.left      = "0px";
6266        style.top       = "0px";
6267        style.textAlign = "left";  // needed to protect against
6268    }( this.container.style ));
6269
6270    this.keyboardCommandArea.className = "keyboard-command-area";
6271    (function( style ){
6272        style.width    = "100%";
6273        style.height   = "100%";
6274        style.overflow = "hidden";
6275        style.position = "absolute";
6276        style.top      = "0px";
6277        style.left     = "0px";
6278        style.resize   = "none";
6279    }(  this.keyboardCommandArea.style ));
6280
6281    this.container.insertBefore( this.canvas, this.container.firstChild );
6282    this.container.insertBefore( this.keyboardCommandArea, this.container.firstChild );
6283    this.element.appendChild( this.container );
6284    this.canvas.appendChild( this.drawersContainer );
6285    this.canvas.appendChild( this.overlaysContainer );
6286
6287    //Used for toggling between fullscreen and default container size
6288    //TODO: these can be closure private and shared across Viewer
6289    //      instances.
6290    this.bodyWidth      = document.body.style.width;
6291    this.bodyHeight     = document.body.style.height;
6292    this.bodyOverflow   = document.body.style.overflow;
6293    this.docOverflow    = document.documentElement.style.overflow;
6294
6295    this.keyboardCommandArea.innerTracker = new $.MouseTracker({
6296            _this : this,
6297            element:            this.keyboardCommandArea,
6298            focusHandler:       function( event ){
6299                if ( !event.preventDefaultAction ) {
6300                    var point    = $.getElementPosition( this.element );
6301                    window.scrollTo( 0, point.y );
6302                }
6303            },
6304
6305            keyHandler:         function( event ){
6306                if ( !event.preventDefaultAction ) {
6307                    switch( event.keyCode ){
6308                        case 61://=|+
6309                            _this.viewport.zoomBy(1.1);
6310                            _this.viewport.applyConstraints();
6311                            return false;
6312                        case 45://-|_
6313                            _this.viewport.zoomBy(0.9);
6314                            _this.viewport.applyConstraints();
6315                            return false;
6316                        case 48://0|)
6317                            _this.viewport.goHome();
6318                            _this.viewport.applyConstraints();
6319                            return false;
6320                        case 119://w
6321                        case 87://W
6322                        case 38://up arrow
6323                            if ( event.shift ) {
6324                                _this.viewport.zoomBy(1.1);
6325                            } else {
6326                                _this.viewport.panBy(new $.Point(0, -0.05));
6327                            }
6328                            _this.viewport.applyConstraints();
6329                            return false;
6330                        case 115://s
6331                        case 83://S
6332                        case 40://down arrow
6333                            if ( event.shift ) {
6334                                _this.viewport.zoomBy(0.9);
6335                            } else {
6336                                _this.viewport.panBy(new $.Point(0, 0.05));
6337                            }
6338                            _this.viewport.applyConstraints();
6339                            return false;
6340                        case 97://a
6341                        case 37://left arrow
6342                            _this.viewport.panBy(new $.Point(-0.05, 0));
6343                            _this.viewport.applyConstraints();
6344                            return false;
6345                        case 100://d
6346                        case 39://right arrow
6347                            _this.viewport.panBy(new $.Point(0.05, 0));
6348                            _this.viewport.applyConstraints();
6349                            return false;
6350                        default:
6351                            //console.log( 'navigator keycode %s', event.keyCode );
6352                            return true;
6353                    }
6354                }
6355            }
6356        }).setTracking( true ); // default state
6357
6358
6359    this.innerTracker = new $.MouseTracker({
6360        element:               this.canvas,
6361        clickTimeThreshold:    this.clickTimeThreshold,
6362        clickDistThreshold:    this.clickDistThreshold,
6363        dblClickTimeThreshold: this.dblClickTimeThreshold,
6364        dblClickDistThreshold: this.dblClickDistThreshold,
6365        clickHandler:          $.delegate( this, onCanvasClick ),
6366        dblClickHandler:       $.delegate( this, onCanvasDblClick ),
6367        dragHandler:           $.delegate( this, onCanvasDrag ),
6368        dragEndHandler:        $.delegate( this, onCanvasDragEnd ),
6369        releaseHandler:        $.delegate( this, onCanvasRelease ),
6370        scrollHandler:         $.delegate( this, onCanvasScroll ),
6371        pinchHandler:          $.delegate( this, onCanvasPinch )
6372    }).setTracking( this.mouseNavEnabled ? true : false ); // default state
6373
6374    this.outerTracker = new $.MouseTracker({
6375        element:               this.container,
6376        clickTimeThreshold:    this.clickTimeThreshold,
6377        clickDistThreshold:    this.clickDistThreshold,
6378        dblClickTimeThreshold: this.dblClickTimeThreshold,
6379        dblClickDistThreshold: this.dblClickDistThreshold,
6380        enterHandler:          $.delegate( this, onContainerEnter ),
6381        exitHandler:           $.delegate( this, onContainerExit ),
6382        pressHandler:          $.delegate( this, onContainerPress ),
6383        releaseHandler:        $.delegate( this, onContainerRelease )
6384    }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking
6385
6386    if( this.toolbar ){
6387        this.toolbar = new $.ControlDock({ element: this.toolbar });
6388    }
6389
6390    this.bindStandardControls();
6391    this.bindSequenceControls();
6392
6393    if ( initialTileSource ) {
6394        this.open( initialTileSource );
6395
6396        if ( this.tileSources.length > 1 ) {
6397            this._updateSequenceButtons( this.initialPage );
6398        }
6399    }
6400
6401    for ( i = 0; i < this.customControls.length; i++ ) {
6402        this.addControl(
6403            this.customControls[ i ].id,
6404            {anchor: this.customControls[ i ].anchor}
6405        );
6406    }
6407
6408    $.requestAnimationFrame( function(){
6409        beginControlsAutoHide( _this );
6410    } );    // initial fade out
6411
6412};
6413
6414$.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, /** @lends OpenSeadragon.Viewer.prototype */{
6415
6416
6417    /**
6418     * @function
6419     * @return {Boolean}
6420     */
6421    isOpen: function () {
6422        return !!this.source;
6423    },
6424
6425    /**
6426     * A deprecated function, renamed to 'open' to match event name and
6427     * match current 'close' method.
6428     * @function
6429     * @param {String} dzi xml string or the url to a DZI xml document.
6430     * @return {OpenSeadragon.Viewer} Chainable.
6431     *
6432     * @deprecated - use {@link OpenSeadragon.Viewer#open} instead.
6433     */
6434    openDzi: function ( dzi ) {
6435        return this.open( dzi );
6436    },
6437
6438    /**
6439     * A deprecated function, renamed to 'open' to match event name and
6440     * match current 'close' method.
6441     * @function
6442     * @param {String|Object|Function} See OpenSeadragon.Viewer.prototype.open
6443     * @return {OpenSeadragon.Viewer} Chainable.
6444     *
6445     * @deprecated - use {@link OpenSeadragon.Viewer#open} instead.
6446     */
6447    openTileSource: function ( tileSource ) {
6448        return this.open( tileSource );
6449    },
6450
6451    /**
6452     * Open a TileSource object into the viewer.
6453     *
6454     * tileSources is a complex option...
6455     *
6456     * It can be a string, object, function, or an array of any of these:
6457     *
6458     * - A String implies a url used to determine the tileSource implementation
6459     *      based on the file extension of url. JSONP is implied by *.js,
6460     *      otherwise the url is retrieved as text and the resulting text is
6461     *      introspected to determine if its json, xml, or text and parsed.
6462     * - An Object implies an inline configuration which has a single
6463     *      property sufficient for being able to determine tileSource
6464     *      implementation. If the object has a property which is a function
6465     *      named 'getTileUrl', it is treated as a custom TileSource.
6466     * @function
6467     * @param {String|Object|Function}
6468     * @return {OpenSeadragon.Viewer} Chainable.
6469     * @fires OpenSeadragon.Viewer.event:open
6470     * @fires OpenSeadragon.Viewer.event:open-failed
6471     */
6472    open: function ( tileSource ) {
6473        var _this = this;
6474
6475        _this._hideMessage();
6476
6477        getTileSourceImplementation( _this, tileSource, function( tileSource ) {
6478            openTileSource( _this, tileSource );
6479        }, function( event ) {
6480            /**
6481             * Raised when an error occurs loading a TileSource.
6482             *
6483             * @event open-failed
6484             * @memberof OpenSeadragon.Viewer
6485             * @type {object}
6486             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6487             * @property {String} message
6488             * @property {String} source
6489             * @property {?Object} userData - Arbitrary subscriber-defined object.
6490             */
6491            _this.raiseEvent( 'open-failed', event );
6492        });
6493
6494        return this;
6495    },
6496
6497
6498    /**
6499     * @function
6500     * @return {OpenSeadragon.Viewer} Chainable.
6501     * @fires OpenSeadragon.Viewer.event:close
6502     */
6503    close: function ( ) {
6504        
6505        if ( !THIS[ this.hash ] ) {
6506            //this viewer has already been destroyed: returning immediately
6507            return this;
6508        }
6509        
6510        if ( this._updateRequestId !== null ) {
6511            $.cancelAnimationFrame( this._updateRequestId );
6512            this._updateRequestId = null;
6513        }
6514
6515        if ( this.navigator ) {
6516            this.navigator.close();
6517        }
6518
6519        this.clearOverlays();
6520        this.drawersContainer.innerHTML = "";
6521        this.overlaysContainer.innerHTML = "";
6522
6523        if ( this.drawer ) {
6524            this.drawer.destroy();
6525        }
6526
6527        this.source     = null;
6528        this.drawer     = null;
6529        this.drawers    = [];
6530
6531        this.viewport   = this.preserveViewport ? this.viewport : null;
6532
6533
6534        VIEWERS[ this.hash ] = null;
6535        delete VIEWERS[ this.hash ];
6536
6537        /**
6538         * Raised when the viewer is closed (see {@link OpenSeadragon.Viewer#close}).
6539         *
6540         * @event close
6541         * @memberof OpenSeadragon.Viewer
6542         * @type {object}
6543         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6544         * @property {?Object} userData - Arbitrary subscriber-defined object.
6545         */
6546        this.raiseEvent( 'close' );
6547
6548        return this;
6549    },
6550
6551
6552    /**
6553     * Function to destroy the viewer and clean up everything created by OpenSeadragon.
6554     * 
6555     * Example:
6556     * var viewer = OpenSeadragon({
6557     *   [...]
6558     * });
6559     *
6560     * //when you are done with the viewer:
6561     * viewer.destroy();
6562     * viewer = null; //important
6563     *
6564     * @function
6565     */
6566    destroy: function( ) {
6567        this.close();
6568
6569        //TODO: implement this...
6570        //this.unbindSequenceControls()
6571        //this.unbindStandardControls()        
6572        
6573        this.removeAllHandlers();
6574
6575        // Go through top element (passed to us) and remove all children
6576        // Use removeChild to make sure it handles SVG or any non-html
6577        // also it performs better - http://jsperf.com/innerhtml-vs-removechild/15
6578        if (this.element){
6579            while (this.element.firstChild) {
6580                this.element.removeChild(this.element.firstChild);
6581            }
6582        }
6583
6584        // destroy the mouse trackers
6585        if (this.keyboardCommandArea){
6586            this.keyboardCommandArea.innerTracker.destroy();
6587        }
6588        if (this.innerTracker){
6589            this.innerTracker.destroy();
6590        }
6591        if (this.outerTracker){
6592            this.outerTracker.destroy();
6593        }
6594
6595        THIS[ this.hash ] = null;
6596        delete THIS[ this.hash ];
6597
6598        // clear all our references to dom objects
6599        this.canvas = null;
6600        this.keyboardCommandArea = null;
6601        this.container = null;
6602
6603        // clear our reference to the main element - they will need to pass it in again, creating a new viewer
6604        this.element = null;
6605    },
6606
6607
6608    /**
6609     * @function
6610     * @return {Boolean}
6611     */
6612    isMouseNavEnabled: function () {
6613        return this.innerTracker.isTracking();
6614    },
6615
6616    /**
6617     * @function
6618     * @param {Boolean} enabled - true to enable, false to disable
6619     * @return {OpenSeadragon.Viewer} Chainable.
6620     * @fires OpenSeadragon.Viewer.event:mouse-enabled
6621     */
6622    setMouseNavEnabled: function( enabled ){
6623        this.innerTracker.setTracking( enabled );
6624        /**
6625         * Raised when mouse/touch navigation is enabled or disabled (see {@link OpenSeadragon.Viewer#setMouseNavEnabled}).
6626         *
6627         * @event mouse-enabled
6628         * @memberof OpenSeadragon.Viewer
6629         * @type {object}
6630         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6631         * @property {Boolean} enabled
6632         * @property {?Object} userData - Arbitrary subscriber-defined object.
6633         */
6634        this.raiseEvent( 'mouse-enabled', { enabled: enabled } );
6635        return this;
6636    },
6637
6638
6639    /**
6640     * @function
6641     * @return {Boolean}
6642     */
6643    areControlsEnabled: function () {
6644        var enabled = this.controls.length,
6645            i;
6646        for( i = 0; i < this.controls.length; i++ ){
6647            enabled = enabled && this.controls[ i ].isVisibile();
6648        }
6649        return enabled;
6650    },
6651
6652
6653    /**
6654     * Shows or hides the controls (e.g. the default navigation buttons).
6655     *
6656     * @function
6657     * @param {Boolean} true to show, false to hide.
6658     * @return {OpenSeadragon.Viewer} Chainable.
6659     * @fires OpenSeadragon.Viewer.event:controls-enabled
6660     */
6661    setControlsEnabled: function( enabled ) {
6662        if( enabled ){
6663            abortControlsAutoHide( this );
6664        } else {
6665            beginControlsAutoHide( this );
6666        }
6667        /**
6668         * Raised when the navigation controls are shown or hidden (see {@link OpenSeadragon.Viewer#setControlsEnabled}).
6669         *
6670         * @event controls-enabled
6671         * @memberof OpenSeadragon.Viewer
6672         * @type {object}
6673         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6674         * @property {Boolean} enabled
6675         * @property {?Object} userData - Arbitrary subscriber-defined object.
6676         */
6677        this.raiseEvent( 'controls-enabled', { enabled: enabled } );
6678        return this;
6679    },
6680
6681
6682    /**
6683     * @function
6684     * @return {Boolean}
6685     */
6686    isFullPage: function () {
6687        return THIS[ this.hash ].fullPage;
6688    },
6689
6690
6691    /**
6692     * Toggle full page mode.
6693     * @function
6694     * @param {Boolean} fullPage
6695     *      If true, enter full page mode.  If false, exit full page mode.
6696     * @return {OpenSeadragon.Viewer} Chainable.
6697     * @fires OpenSeadragon.Viewer.event:pre-full-page
6698     * @fires OpenSeadragon.Viewer.event:full-page
6699     */
6700    setFullPage: function( fullPage ) {
6701
6702        var body = document.body,
6703            bodyStyle = body.style,
6704            docStyle = document.documentElement.style,
6705            _this = this,
6706            hash,
6707            nodes,
6708            i;
6709
6710        //dont bother modifying the DOM if we are already in full page mode.
6711        if ( fullPage == this.isFullPage() ) {
6712            return this;
6713        }
6714
6715        var fullPageEventArgs = {
6716            fullPage: fullPage,
6717            preventDefaultAction: false
6718        };
6719        /**
6720         * Raised when the viewer is about to change to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
6721         *
6722         * @event pre-full-page
6723         * @memberof OpenSeadragon.Viewer
6724         * @type {object}
6725         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6726         * @property {Boolean} fullPage - True if entering full-page mode, false if exiting full-page mode.
6727         * @property {Boolean} preventDefaultAction - Set to true to prevent full-page mode change. Default: false.
6728         * @property {?Object} userData - Arbitrary subscriber-defined object.
6729         */
6730        this.raiseEvent( 'pre-full-page', fullPageEventArgs );
6731        if ( fullPageEventArgs.preventDefaultAction ) {
6732            return this;
6733        }
6734
6735        if ( fullPage ) {
6736
6737            this.elementSize = $.getElementSize( this.element );
6738            this.pageScroll = $.getPageScroll();
6739
6740            this.elementMargin = this.element.style.margin;
6741            this.element.style.margin = "0";
6742            this.elementPadding = this.element.style.padding;
6743            this.element.style.padding = "0";
6744
6745            this.bodyMargin = bodyStyle.margin;
6746            this.docMargin = docStyle.margin;
6747            bodyStyle.margin = "0";
6748            docStyle.margin = "0";
6749
6750            this.bodyPadding = bodyStyle.padding;
6751            this.docPadding = docStyle.padding;
6752            bodyStyle.padding = "0";
6753            docStyle.padding = "0";
6754
6755            this.bodyWidth = bodyStyle.width;
6756            this.bodyHeight = bodyStyle.height;
6757            bodyStyle.width = "100%";
6758            bodyStyle.height = "100%";
6759
6760            //when entering full screen on the ipad it wasnt sufficient to leave
6761            //the body intact as only only the top half of the screen would
6762            //respond to touch events on the canvas, while the bottom half treated
6763            //them as touch events on the document body.  Thus we remove and store
6764            //the bodies elements and replace them when we leave full screen.
6765            this.previousBody = [];
6766            THIS[ this.hash ].prevElementParent = this.element.parentNode;
6767            THIS[ this.hash ].prevNextSibling = this.element.nextSibling;
6768            THIS[ this.hash ].prevElementWidth = this.element.style.width;
6769            THIS[ this.hash ].prevElementHeight = this.element.style.height;
6770            nodes = body.childNodes.length;
6771            for ( i = 0; i < nodes; i++ ) {
6772                this.previousBody.push( body.childNodes[ 0 ] );
6773                body.removeChild( body.childNodes[ 0 ] );
6774            }
6775
6776            //If we've got a toolbar, we need to enable the user to use css to
6777            //preserve it in fullpage mode
6778            if ( this.toolbar && this.toolbar.element ) {
6779                //save a reference to the parent so we can put it back
6780                //in the long run we need a better strategy
6781                this.toolbar.parentNode = this.toolbar.element.parentNode;
6782                this.toolbar.nextSibling = this.toolbar.element.nextSibling;
6783                body.appendChild( this.toolbar.element );
6784
6785                //Make sure the user has some ability to style the toolbar based
6786                //on the mode
6787                $.addClass( this.toolbar.element, 'fullpage' );
6788            }
6789
6790            $.addClass( this.element, 'fullpage' );
6791            body.appendChild( this.element );
6792
6793            this.element.style.height = $.getWindowSize().y + 'px';
6794            this.element.style.width = $.getWindowSize().x + 'px';
6795
6796            if ( this.toolbar && this.toolbar.element ) {
6797                this.element.style.height = (
6798                    $.getElementSize( this.element ).y - $.getElementSize( this.toolbar.element ).y
6799                ) + 'px';
6800            }
6801
6802            THIS[ this.hash ].fullPage = true;
6803
6804            // mouse will be inside container now
6805            $.delegate( this, onContainerEnter )( {} );
6806
6807        } else {
6808
6809            this.element.style.margin = this.elementMargin;
6810            this.element.style.padding = this.elementPadding;
6811
6812            bodyStyle.margin = this.bodyMargin;
6813            docStyle.margin = this.docMargin;
6814
6815            bodyStyle.padding = this.bodyPadding;
6816            docStyle.padding = this.docPadding;
6817
6818            bodyStyle.width = this.bodyWidth;
6819            bodyStyle.height = this.bodyHeight;
6820
6821            body.removeChild( this.element );
6822            nodes = this.previousBody.length;
6823            for ( i = 0; i < nodes; i++ ) {
6824                body.appendChild( this.previousBody.shift() );
6825            }
6826
6827            $.removeClass( this.element, 'fullpage' );
6828            THIS[ this.hash ].prevElementParent.insertBefore(
6829                this.element,
6830                THIS[ this.hash ].prevNextSibling
6831            );
6832
6833            //If we've got a toolbar, we need to enable the user to use css to
6834            //reset it to its original state
6835            if ( this.toolbar && this.toolbar.element ) {
6836                body.removeChild( this.toolbar.element );
6837
6838                //Make sure the user has some ability to style the toolbar based
6839                //on the mode
6840                $.removeClass( this.toolbar.element, 'fullpage' );
6841
6842                this.toolbar.parentNode.insertBefore(
6843                    this.toolbar.element,
6844                    this.toolbar.nextSibling
6845                );
6846                delete this.toolbar.parentNode;
6847                delete this.toolbar.nextSibling;
6848            }
6849
6850            this.element.style.width = THIS[ this.hash ].prevElementWidth;
6851            this.element.style.height = THIS[ this.hash ].prevElementHeight;
6852
6853            // After exiting fullPage or fullScreen, it can take some time
6854            // before the browser can actually set the scroll.
6855            var restoreScrollCounter = 0;
6856            var restoreScroll = function() {
6857                $.setPageScroll( _this.pageScroll );
6858                var pageScroll = $.getPageScroll();
6859                restoreScrollCounter++;
6860                if ( restoreScrollCounter < 10 &&
6861                    pageScroll.x !== _this.pageScroll.x ||
6862                    pageScroll.y !== _this.pageScroll.y ) {
6863                    $.requestAnimationFrame( restoreScroll );
6864                }
6865            };
6866            $.requestAnimationFrame( restoreScroll );
6867
6868            THIS[ this.hash ].fullPage = false;
6869
6870            // mouse will likely be outside now
6871            $.delegate( this, onContainerExit )( { } );
6872
6873        }
6874
6875        if ( this.navigator && this.viewport ) {
6876            this.navigator.update( this.viewport );
6877        }
6878
6879        /**
6880         * Raised when the viewer has changed to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
6881         *
6882         * @event full-page
6883         * @memberof OpenSeadragon.Viewer
6884         * @type {object}
6885         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6886         * @property {Boolean} fullPage - True if changed to full-page mode, false if exited full-page mode.
6887         * @property {?Object} userData - Arbitrary subscriber-defined object.
6888         */
6889        this.raiseEvent( 'full-page', { fullPage: fullPage } );
6890
6891        return this;
6892    },
6893
6894    /**
6895     * Toggle full screen mode if supported. Toggle full page mode otherwise.
6896     * @function
6897     * @param {Boolean} fullScreen
6898     *      If true, enter full screen mode.  If false, exit full screen mode.
6899     * @return {OpenSeadragon.Viewer} Chainable.
6900     * @fires OpenSeadragon.Viewer.event:pre-full-screen
6901     * @fires OpenSeadragon.Viewer.event:full-screen
6902     */
6903    setFullScreen: function( fullScreen ) {
6904        var _this = this;
6905
6906        if ( !$.supportsFullScreen ) {
6907            return this.setFullPage( fullScreen );
6908        }
6909
6910        if ( $.isFullScreen() === fullScreen ) {
6911            return this;
6912        }
6913
6914        var fullScreeEventArgs = {
6915            fullScreen: fullScreen,
6916            preventDefaultAction: false
6917        };
6918        /**
6919         * Raised when the viewer is about to change to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
6920         *
6921         * @event pre-full-screen
6922         * @memberof OpenSeadragon.Viewer
6923         * @type {object}
6924         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6925         * @property {Boolean} fullScreen - True if entering full-screen mode, false if exiting full-screen mode.
6926         * @property {Boolean} preventDefaultAction - Set to true to prevent full-screen mode change. Default: false.
6927         * @property {?Object} userData - Arbitrary subscriber-defined object.
6928         */
6929        this.raiseEvent( 'pre-full-screen', fullScreeEventArgs );
6930        if ( fullScreeEventArgs.preventDefaultAction ) {
6931            return this;
6932        }
6933
6934        if ( fullScreen ) {
6935
6936            this.setFullPage( true );
6937            // If the full page mode is not actually entered, we need to prevent
6938            // the full screen mode.
6939            if ( !this.isFullPage() ) {
6940                return this;
6941            }
6942
6943            this.fullPageStyleWidth = this.element.style.width;
6944            this.fullPageStyleHeight = this.element.style.height;
6945            this.element.style.width = '100%';
6946            this.element.style.height = '100%';
6947
6948            var onFullScreenChange = function() {
6949                var isFullScreen = $.isFullScreen();
6950                if ( !isFullScreen ) {
6951                    $.removeEvent( document, $.fullScreenEventName, onFullScreenChange );
6952                    $.removeEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
6953
6954                    _this.setFullPage( false );
6955                    if ( _this.isFullPage() ) {
6956                        _this.element.style.width = _this.fullPageStyleWidth;
6957                        _this.element.style.height = _this.fullPageStyleHeight;
6958                    }
6959                }
6960                if ( _this.navigator && _this.viewport ) {
6961                    _this.navigator.update( _this.viewport );
6962                }
6963                /**
6964                 * Raised when the viewer has changed to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
6965                 *
6966                 * @event full-screen
6967                 * @memberof OpenSeadragon.Viewer
6968                 * @type {object}
6969                 * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
6970                 * @property {Boolean} fullScreen - True if changed to full-screen mode, false if exited full-screen mode.
6971                 * @property {?Object} userData - Arbitrary subscriber-defined object.
6972                 */
6973                _this.raiseEvent( 'full-screen', { fullScreen: isFullScreen } );
6974            };
6975            $.addEvent( document, $.fullScreenEventName, onFullScreenChange );
6976            $.addEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
6977
6978            $.requestFullScreen( document.body );
6979
6980        } else {
6981            $.exitFullScreen();
6982        }
6983        return this;
6984    },
6985
6986    /**
6987     * @function
6988     * @return {Boolean}
6989     */
6990    isVisible: function () {
6991        return this.container.style.visibility != "hidden";
6992    },
6993
6994
6995    /**
6996     * @function
6997     * @param {Boolean} visible
6998     * @return {OpenSeadragon.Viewer} Chainable.
6999     * @fires OpenSeadragon.Viewer.event:visible
7000     */
7001    setVisible: function( visible ){
7002        this.container.style.visibility = visible ? "" : "hidden";
7003        /**
7004         * Raised when the viewer is shown or hidden (see {@link OpenSeadragon.Viewer#setVisible}).
7005         *
7006         * @event visible
7007         * @memberof OpenSeadragon.Viewer
7008         * @type {object}
7009         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7010         * @property {Boolean} visible
7011         * @property {?Object} userData - Arbitrary subscriber-defined object.
7012         */
7013        this.raiseEvent( 'visible', { visible: visible } );
7014        return this;
7015    },
7016
7017    /**
7018     * Add a layer.
7019     * options.tileSource can be anything that {@link OpenSeadragon.Viewer#open}
7020     *  supports except arrays of images as layers cannot be sequences.
7021     * @function
7022     * @param {Object} options
7023     * @param {String|Object|Function} options.tileSource The TileSource of the layer.
7024     * @param {Number} [options.opacity=1] The opacity of the layer.
7025     * @param {Number} [options.level] The level of the layer. Added on top of
7026     * all other layers if not specified.
7027     * @returns {OpenSeadragon.Viewer} Chainable.
7028     * @fires OpenSeadragon.Viewer.event:add-layer
7029     * @fires OpenSeadragon.Viewer.event:add-layer-failed
7030     */
7031    addLayer: function( options ) {
7032        var _this = this,
7033            tileSource = options.tileSource;
7034
7035        if ( !this.isOpen() ) {
7036            throw new Error( "An image must be loaded before adding layers." );
7037        }
7038        if ( !tileSource ) {
7039            throw new Error( "No tile source provided as new layer." );
7040        }
7041        if ( this.collectionMode ) {
7042            throw new Error( "Layers not supported in collection mode." );
7043        }
7044
7045        function raiseAddLayerFailed( event ) {
7046             /**
7047             * Raised when an error occurs while adding a layer.
7048             * @event add-layer-failed
7049             * @memberOf OpenSeadragon.Viewer
7050             * @type {object}
7051             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7052             * @property {String} message
7053             * @property {String} source
7054             * @property {Object} options The options passed to the addLayer method.
7055             * @property {?Object} userData - Arbitrary subscriber-defined object.
7056             */
7057            _this.raiseEvent( 'add-layer-failed', event );
7058        }
7059
7060        getTileSourceImplementation( this, tileSource, function( tileSource ) {
7061
7062            if ( tileSource instanceof Array ) {
7063                raiseAddLayerFailed({
7064                    message: "Sequences can not be added as layers.",
7065                    source: tileSource,
7066                    options: options
7067                });
7068                return;
7069            }
7070
7071            for ( var i = 0; i < _this.drawers.length; i++ ) {
7072                var otherAspectRatio = _this.drawers[ i ].source.aspectRatio;
7073                var diff = otherAspectRatio - tileSource.aspectRatio;
7074                if ( Math.abs( diff ) > _this.layersAspectRatioEpsilon ) {
7075                    raiseAddLayerFailed({
7076                        message: "Aspect ratio mismatch with layer " + i + ".",
7077                        source: tileSource,
7078                        options: options
7079                    });
7080                    return;
7081                }
7082            }
7083
7084            var drawer = new $.Drawer({
7085                viewer: _this,
7086                source: tileSource,
7087                viewport: _this.viewport,
7088                element: _this.drawersContainer,
7089                opacity: options.opacity !== undefined ?
7090                    options.opacity : _this.opacity,
7091                maxImageCacheCount: _this.maxImageCacheCount,
7092                imageLoaderLimit: _this.imageLoaderLimit,
7093                minZoomImageRatio: _this.minZoomImageRatio,
7094                wrapHorizontal: _this.wrapHorizontal,
7095                wrapVertical: _this.wrapVertical,
7096                immediateRender: _this.immediateRender,
7097                blendTime: _this.blendTime,
7098                alwaysBlend: _this.alwaysBlend,
7099                minPixelRatio: _this.minPixelRatio,
7100                timeout: _this.timeout,
7101                debugMode: _this.debugMode,
7102                debugGridColor: _this.debugGridColor
7103            });
7104            _this.drawers.push( drawer );
7105            if ( options.level !== undefined ) {
7106                _this.setLayerLevel( drawer, options.level );
7107            }
7108            THIS[ _this.hash ].forceRedraw = true;
7109            /**
7110             * Raised when a layer is successfully added.
7111             * @event add-layer
7112             * @memberOf OpenSeadragon.Viewer
7113             * @type {object}
7114             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7115             * @property {Object} options The options passed to the addLayer method.
7116             * @property {OpenSeadragon.Drawer} drawer The layer's underlying drawer.
7117             * @property {?Object} userData - Arbitrary subscriber-defined object.
7118             */
7119            _this.raiseEvent( 'add-layer', {
7120                options: options,
7121                drawer: drawer
7122            });
7123        }, function( event ) {
7124            event.options = options;
7125            raiseAddLayerFailed(event);
7126        } );
7127
7128        return this;
7129    },
7130
7131    /**
7132     * Get the layer at the specified level.
7133     * @param {Number} level The layer to retrieve level.
7134     * @returns {OpenSeadragon.Drawer} The layer at the specified level.
7135     */
7136    getLayerAtLevel: function( level ) {
7137        if ( level >= this.drawers.length ) {
7138            throw new Error( "Level bigger than number of layers." );
7139        }
7140        return this.drawers[ level ];
7141    },
7142
7143    /**
7144     * Get the level of the layer associated with the given drawer or -1 if not
7145     * present.
7146     * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the layer.
7147     * @returns {Number} The level of the layer or -1 if not present.
7148     */
7149    getLevelOfLayer: function( drawer ) {
7150        return $.indexOf( this.drawers, drawer );
7151    },
7152
7153    /**
7154     * Get the number of layers used.
7155     * @returns {Number} The number of layers used.
7156     */
7157    getLayersCount: function() {
7158        return this.drawers.length;
7159    },
7160
7161    /**
7162     * Change the level of a layer so that it appears over or under others.
7163     * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the changing
7164     * level layer.
7165     * @param {Number} level The new level
7166     * @returns {OpenSeadragon.Viewer} Chainable.
7167     * @fires OpenSeadragon.Viewer.event:layer-level-changed
7168     */
7169    setLayerLevel: function( drawer, level ) {
7170        var oldLevel = this.getLevelOfLayer( drawer );
7171
7172        if ( level >= this.drawers.length ) {
7173            throw new Error( "Level bigger than number of layers." );
7174        }
7175        if ( level === oldLevel || oldLevel === -1 ) {
7176            return this;
7177        }
7178        if ( level === 0 || oldLevel === 0 ) {
7179            if ( THIS[ this.hash ].sequenced ) {
7180                throw new Error( "Cannot reassign base level when in sequence mode." );
7181            }
7182            // We need to re-assign the base drawer and the source
7183            this.drawer = level === 0 ? drawer : this.getLayerAtLevel( level );
7184            this.source = this.drawer.source;
7185        }
7186        this.drawers.splice( oldLevel, 1 );
7187        this.drawers.splice( level, 0, drawer );
7188        this.drawersContainer.removeChild( drawer.canvas );
7189        if ( level === 0 ) {
7190            var nextLevelCanvas = this.drawers[ 1 ].canvas;
7191            nextLevelCanvas.parentNode.insertBefore( drawer.canvas,
7192                nextLevelCanvas );
7193        } else {
7194            // Insert right after layer at level - 1
7195            var prevLevelCanvas = this.drawers[level - 1].canvas;
7196            prevLevelCanvas.parentNode.insertBefore( drawer.canvas,
7197                prevLevelCanvas.nextSibling );
7198        }
7199
7200        /**
7201         * Raised when the order of the layers has been changed.
7202         * @event layer-level-changed
7203         * @memberOf OpenSeadragon.Viewer
7204         * @type {object}
7205         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7206         * @property {OpenSeadragon.Drawer} drawer - The drawer which level has
7207         * been changed
7208         * @property {Number} previousLevel - The previous level of the drawer
7209         * @property {Number} newLevel - The new level of the drawer
7210         * @property {?Object} userData - Arbitrary subscriber-defined object.
7211         */
7212        this.raiseEvent( 'layer-level-changed', {
7213            drawer: drawer,
7214            previousLevel: oldLevel,
7215            newLevel: level
7216        } );
7217
7218        return this;
7219    },
7220
7221    /**
7222     * Remove a layer. If there is only one layer, close the viewer.
7223     * @function
7224     * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the layer 
7225     * to remove
7226     * @returns {OpenSeadragon.Viewer} Chainable.
7227     * @fires OpenSeadragon.Viewer.event:remove-layer
7228     */
7229    removeLayer: function( drawer ) {
7230        var index = this.drawers.indexOf( drawer );
7231        if ( index === -1 ) {
7232            return this;
7233        }
7234        if ( index === 0 ) {
7235            if ( THIS[ this.hash ].sequenced ) {
7236                throw new Error( "Cannot remove base layer when in sequence mode." );
7237            }
7238            if ( this.drawers.length === 1 ) {
7239                this.close();
7240                return this;
7241            }
7242            this.drawer = this.drawers[ 1 ];
7243        }
7244
7245        this.drawers.splice( index, 1 );
7246        this.drawersContainer.removeChild( drawer.canvas );
7247        /**
7248         * Raised when a layer is removed.
7249         * @event remove-layer
7250         * @memberOf OpenSeadragon.Viewer
7251         * @type {object}
7252         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7253         * @property {OpenSeadragon.Drawer} drawer The layer's underlying drawer.
7254         * @property {?Object} userData - Arbitrary subscriber-defined object.
7255         */
7256        this.raiseEvent( 'remove-layer', { drawer: drawer } );
7257        return this;
7258    },
7259
7260    /**
7261     * Force the viewer to redraw its drawers.
7262     * @returns {OpenSeadragon.Viewer} Chainable.
7263     */
7264    forceRedraw: function() {
7265        THIS[ this.hash ].forceRedraw = true;
7266        return this;
7267    },
7268
7269    /**
7270     * @function
7271     * @return {OpenSeadragon.Viewer} Chainable.
7272     */
7273    bindSequenceControls: function(){
7274
7275        //////////////////////////////////////////////////////////////////////////
7276        // Image Sequence Controls
7277        //////////////////////////////////////////////////////////////////////////
7278        var onFocusHandler          = $.delegate( this, onFocus ),
7279            onBlurHandler           = $.delegate( this, onBlur ),
7280            onNextHandler           = $.delegate( this, onNext ),
7281            onPreviousHandler       = $.delegate( this, onPrevious ),
7282            navImages               = this.navImages,
7283            useGroup                = true ;
7284
7285        if( this.showSequenceControl && THIS[ this.hash ].sequenced ){
7286
7287            if( this.previousButton || this.nextButton ){
7288                //if we are binding to custom buttons then layout and
7289                //grouping is the responsibility of the page author
7290                useGroup = false;
7291            }
7292
7293            this.previousButton = new $.Button({
7294                element:    this.previousButton ? $.getElement( this.previousButton ) : null,
7295                clickTimeThreshold: this.clickTimeThreshold,
7296                clickDistThreshold: this.clickDistThreshold,
7297                tooltip:    $.getString( "Tooltips.PreviousPage" ),
7298                srcRest:    resolveUrl( this.prefixUrl, navImages.previous.REST ),
7299                srcGroup:   resolveUrl( this.prefixUrl, navImages.previous.GROUP ),
7300                srcHover:   resolveUrl( this.prefixUrl, navImages.previous.HOVER ),
7301                srcDown:    resolveUrl( this.prefixUrl, navImages.previous.DOWN ),
7302                onRelease:  onPreviousHandler,
7303                onFocus:    onFocusHandler,
7304                onBlur:     onBlurHandler
7305            });
7306
7307            this.nextButton = new $.Button({
7308                element:    this.nextButton ? $.getElement( this.nextButton ) : null,
7309                clickTimeThreshold: this.clickTimeThreshold,
7310                clickDistThreshold: this.clickDistThreshold,
7311                tooltip:    $.getString( "Tooltips.NextPage" ),
7312                srcRest:    resolveUrl( this.prefixUrl, navImages.next.REST ),
7313                srcGroup:   resolveUrl( this.prefixUrl, navImages.next.GROUP ),
7314                srcHover:   resolveUrl( this.prefixUrl, navImages.next.HOVER ),
7315                srcDown:    resolveUrl( this.prefixUrl, navImages.next.DOWN ),
7316                onRelease:  onNextHandler,
7317                onFocus:    onFocusHandler,
7318                onBlur:     onBlurHandler
7319            });
7320
7321            if( !this.navPrevNextWrap ){
7322                this.previousButton.disable();
7323            }
7324
7325            if( useGroup ){
7326                this.paging = new $.ButtonGroup({
7327                    buttons: [
7328                        this.previousButton,
7329                        this.nextButton
7330                    ],
7331                    clickTimeThreshold: this.clickTimeThreshold,
7332                    clickDistThreshold: this.clickDistThreshold
7333                });
7334
7335                this.pagingControl = this.paging.element;
7336
7337                if( this.toolbar ){
7338                    this.toolbar.addControl(
7339                        this.pagingControl,
7340                        {anchor: $.ControlAnchor.BOTTOM_RIGHT}
7341                    );
7342                }else{
7343                    this.addControl(
7344                        this.pagingControl,
7345                        {anchor: this.sequenceControlAnchor || $.ControlAnchor.TOP_LEFT}
7346                    );
7347                }
7348            }
7349        }
7350        return this;
7351    },
7352
7353
7354    /**
7355     * @function
7356     * @return {OpenSeadragon.Viewer} Chainable.
7357     */
7358    bindStandardControls: function(){
7359        //////////////////////////////////////////////////////////////////////////
7360        // Navigation Controls
7361        //////////////////////////////////////////////////////////////////////////
7362        var beginZoomingInHandler   = $.delegate( this, beginZoomingIn ),
7363            endZoomingHandler       = $.delegate( this, endZooming ),
7364            doSingleZoomInHandler   = $.delegate( this, doSingleZoomIn ),
7365            beginZoomingOutHandler  = $.delegate( this, beginZoomingOut ),
7366            doSingleZoomOutHandler  = $.delegate( this, doSingleZoomOut ),
7367            onHomeHandler           = $.delegate( this, onHome ),
7368            onFullScreenHandler     = $.delegate( this, onFullScreen ),
7369            onRotateLeftHandler     = $.delegate( this, onRotateLeft ),
7370            onRotateRightHandler    = $.delegate( this, onRotateRight ),
7371            onFocusHandler          = $.delegate( this, onFocus ),
7372            onBlurHandler           = $.delegate( this, onBlur ),
7373            navImages               = this.navImages,
7374            buttons                 = [],
7375            useGroup                = true ;
7376
7377
7378        if ( this.showNavigationControl ) {
7379
7380            if( this.zoomInButton || this.zoomOutButton ||
7381                this.homeButton || this.fullPageButton ||
7382                this.rotateLeftButton || this.rotateRightButton ) {
7383                //if we are binding to custom buttons then layout and
7384                //grouping is the responsibility of the page author
7385                useGroup = false;
7386            }
7387
7388            if ( this.showZoomControl ) {
7389                buttons.push( this.zoomInButton = new $.Button({
7390                    element:    this.zoomInButton ? $.getElement( this.zoomInButton ) : null,
7391                    clickTimeThreshold: this.clickTimeThreshold,
7392                    clickDistThreshold: this.clickDistThreshold,
7393                    tooltip:    $.getString( "Tooltips.ZoomIn" ),
7394                    srcRest:    resolveUrl( this.prefixUrl, navImages.zoomIn.REST ),
7395                    srcGroup:   resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ),
7396                    srcHover:   resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ),
7397                    srcDown:    resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ),
7398                    onPress:    beginZoomingInHandler,
7399                    onRelease:  endZoomingHandler,
7400                    onClick:    doSingleZoomInHandler,
7401                    onEnter:    beginZoomingInHandler,
7402                    onExit:     endZoomingHandler,
7403                    onFocus:    onFocusHandler,
7404                    onBlur:     onBlurHandler
7405                }));
7406
7407                buttons.push( this.zoomOutButton = new $.Button({
7408                    element:    this.zoomOutButton ? $.getElement( this.zoomOutButton ) : null,
7409                    clickTimeThreshold: this.clickTimeThreshold,
7410                    clickDistThreshold: this.clickDistThreshold,
7411                    tooltip:    $.getString( "Tooltips.ZoomOut" ),
7412                    srcRest:    resolveUrl( this.prefixUrl, navImages.zoomOut.REST ),
7413                    srcGroup:   resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ),
7414                    srcHover:   resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ),
7415                    srcDown:    resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ),
7416                    onPress:    beginZoomingOutHandler,
7417                    onRelease:  endZoomingHandler,
7418                    onClick:    doSingleZoomOutHandler,
7419                    onEnter:    beginZoomingOutHandler,
7420                    onExit:     endZoomingHandler,
7421                    onFocus:    onFocusHandler,
7422                    onBlur:     onBlurHandler
7423                }));
7424            }
7425
7426            if ( this.showHomeControl ) {
7427                buttons.push( this.homeButton = new $.Button({
7428                    element:    this.homeButton ? $.getElement( this.homeButton ) : null,
7429                    clickTimeThreshold: this.clickTimeThreshold,
7430                    clickDistThreshold: this.clickDistThreshold,
7431                    tooltip:    $.getString( "Tooltips.Home" ),
7432                    srcRest:    resolveUrl( this.prefixUrl, navImages.home.REST ),
7433                    srcGroup:   resolveUrl( this.prefixUrl, navImages.home.GROUP ),
7434                    srcHover:   resolveUrl( this.prefixUrl, navImages.home.HOVER ),
7435                    srcDown:    resolveUrl( this.prefixUrl, navImages.home.DOWN ),
7436                    onRelease:  onHomeHandler,
7437                    onFocus:    onFocusHandler,
7438                    onBlur:     onBlurHandler
7439                }));
7440            }
7441
7442            if ( this.showFullPageControl ) {
7443                buttons.push( this.fullPageButton = new $.Button({
7444                    element:    this.fullPageButton ? $.getElement( this.fullPageButton ) : null,
7445                    clickTimeThreshold: this.clickTimeThreshold,
7446                    clickDistThreshold: this.clickDistThreshold,
7447                    tooltip:    $.getString( "Tooltips.FullPage" ),
7448                    srcRest:    resolveUrl( this.prefixUrl, navImages.fullpage.REST ),
7449                    srcGroup:   resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ),
7450                    srcHover:   resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ),
7451                    srcDown:    resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ),
7452                    onRelease:  onFullScreenHandler,
7453                    onFocus:    onFocusHandler,
7454                    onBlur:     onBlurHandler
7455                }));
7456            }
7457
7458            if ( this.showRotationControl ) {
7459                buttons.push( this.rotateLeftButton = new $.Button({
7460                    element:    this.rotateLeftButton ? $.getElement( this.rotateLeftButton ) : null,
7461                    clickTimeThreshold: this.clickTimeThreshold,
7462                    clickDistThreshold: this.clickDistThreshold,
7463                    tooltip:    $.getString( "Tooltips.RotateLeft" ),
7464                    srcRest:    resolveUrl( this.prefixUrl, navImages.rotateleft.REST ),
7465                    srcGroup:   resolveUrl( this.prefixUrl, navImages.rotateleft.GROUP ),
7466                    srcHover:   resolveUrl( this.prefixUrl, navImages.rotateleft.HOVER ),
7467                    srcDown:    resolveUrl( this.prefixUrl, navImages.rotateleft.DOWN ),
7468                    onRelease:  onRotateLeftHandler,
7469                    onFocus:    onFocusHandler,
7470                    onBlur:     onBlurHandler
7471                }));
7472
7473                buttons.push( this.rotateRightButton = new $.Button({
7474                    element:    this.rotateRightButton ? $.getElement( this.rotateRightButton ) : null,
7475                    clickTimeThreshold: this.clickTimeThreshold,
7476                    clickDistThreshold: this.clickDistThreshold,
7477                    tooltip:    $.getString( "Tooltips.RotateRight" ),
7478                    srcRest:    resolveUrl( this.prefixUrl, navImages.rotateright.REST ),
7479                    srcGroup:   resolveUrl( this.prefixUrl, navImages.rotateright.GROUP ),
7480                    srcHover:   resolveUrl( this.prefixUrl, navImages.rotateright.HOVER ),
7481                    srcDown:    resolveUrl( this.prefixUrl, navImages.rotateright.DOWN ),
7482                    onRelease:  onRotateRightHandler,
7483                    onFocus:    onFocusHandler,
7484                    onBlur:     onBlurHandler
7485                }));
7486
7487            }
7488
7489            if ( useGroup ) {
7490                this.buttons = new $.ButtonGroup({
7491                    buttons:            buttons,
7492                    clickTimeThreshold: this.clickTimeThreshold,
7493                    clickDistThreshold: this.clickDistThreshold
7494                });
7495
7496                this.navControl  = this.buttons.element;
7497                this.addHandler( 'open', $.delegate( this, lightUp ) );
7498
7499                if( this.toolbar ){
7500                    this.toolbar.addControl(
7501                        this.navControl,
7502                        {anchor: $.ControlAnchor.TOP_LEFT}
7503                    );
7504                } else {
7505                    this.addControl(
7506                        this.navControl,
7507                        {anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT}
7508                    );
7509                }
7510            }
7511
7512        }
7513        return this;
7514    },
7515    
7516    /**
7517     * Gets the active page of a sequence
7518     * @function
7519     * @return {Number}
7520     */
7521    currentPage: function() {
7522        return THIS[ this.hash ].sequence;
7523    },
7524
7525    /**
7526     * @function
7527     * @return {OpenSeadragon.Viewer} Chainable.
7528     * @fires OpenSeadragon.Viewer.event:page
7529     */
7530    goToPage: function( page ){
7531        if( page >= 0 && page < this.tileSources.length ){
7532            /**
7533             * Raised when the page is changed on a viewer configured with multiple image sources (see {@link OpenSeadragon.Viewer#goToPage}).
7534             *
7535             * @event page
7536             * @memberof OpenSeadragon.Viewer
7537             * @type {Object}
7538             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7539             * @property {Number} page - The page index.
7540             * @property {?Object} userData - Arbitrary subscriber-defined object.
7541             */
7542            this.raiseEvent( 'page', { page: page } );
7543
7544            THIS[ this.hash ].sequence = page;
7545
7546            this._updateSequenceButtons( page );
7547
7548            this.open( this.tileSources[ page ] );
7549
7550            if( this.referenceStrip ){
7551                this.referenceStrip.setFocus( page );
7552            }
7553        }
7554
7555        return this;
7556    },
7557
7558   /**
7559     * Adds an html element as an overlay to the current viewport.  Useful for
7560     * highlighting words or areas of interest on an image or other zoomable
7561     * interface. The overlays added via this method are removed when the viewport
7562     * is closed which include when changing page.
7563     * @method
7564     * @param {Element|String|Object} element - A reference to an element or an id for
7565     *      the element which will overlayed. Or an Object specifying the configuration for the overlay
7566     * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
7567     *      rectangle which will be overlayed.
7568     * @param {OpenSeadragon.OverlayPlacement} placement - The position of the
7569     *      viewport which the location coordinates will be treated as relative
7570     *      to.
7571     * @param {function} onDraw - If supplied the callback is called when the overlay
7572     *      needs to be drawn. It it the responsibility of the callback to do any drawing/positioning.
7573     *      It is passed position, size and element.
7574     * @return {OpenSeadragon.Viewer} Chainable.
7575     * @fires OpenSeadragon.Viewer.event:add-overlay
7576     */
7577    addOverlay: function( element, location, placement, onDraw ) {
7578        var options;
7579        if( $.isPlainObject( element ) ){
7580            options = element;
7581        } else {
7582            options = {
7583                element: element,
7584                location: location,
7585                placement: placement,
7586                onDraw: onDraw
7587            };
7588        }
7589
7590        element = $.getElement( options.element );
7591
7592        if ( getOverlayIndex( this.currentOverlays, element ) >= 0 ) {
7593            // they're trying to add a duplicate overlay
7594            return this;
7595        }
7596        this.currentOverlays.push( getOverlayObject( this, options ) );
7597        THIS[ this.hash ].forceRedraw = true;
7598        /**
7599         * Raised when an overlay is added to the viewer (see {@link OpenSeadragon.Viewer#addOverlay}).
7600         *
7601         * @event add-overlay
7602         * @memberof OpenSeadragon.Viewer
7603         * @type {object}
7604         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7605         * @property {Element} element - The overlay element.
7606         * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
7607         * @property {OpenSeadragon.OverlayPlacement} placement
7608         * @property {?Object} userData - Arbitrary subscriber-defined object.
7609         */
7610        this.raiseEvent( 'add-overlay', {
7611            element: element,
7612            location: options.location,
7613            placement: options.placement
7614        });
7615        return this;
7616    },
7617
7618    /**
7619     * Updates the overlay represented by the reference to the element or
7620     * element id moving it to the new location, relative to the new placement.
7621     * @method
7622     * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
7623     *      rectangle which will be overlayed.
7624     * @param {OpenSeadragon.OverlayPlacement} placement - The position of the
7625     *      viewport which the location coordinates will be treated as relative
7626     *      to.
7627     * @return {OpenSeadragon.Viewer} Chainable.
7628     * @fires OpenSeadragon.Viewer.event:update-overlay
7629     */
7630    updateOverlay: function( element, location, placement ) {
7631        var i;
7632
7633        element = $.getElement( element );
7634        i = getOverlayIndex( this.currentOverlays, element );
7635
7636        if ( i >= 0 ) {
7637            this.currentOverlays[ i ].update( location, placement );
7638            THIS[ this.hash ].forceRedraw = true;
7639            /**
7640             * Raised when an overlay's location or placement changes
7641             * (see {@link OpenSeadragon.Viewer#updateOverlay}).
7642             *
7643             * @event update-overlay
7644             * @memberof OpenSeadragon.Viewer
7645             * @type {object}
7646             * @property {OpenSeadragon.Viewer} eventSource - A reference to the
7647             * Viewer which raised the event.
7648             * @property {Element} element
7649             * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
7650             * @property {OpenSeadragon.OverlayPlacement} placement
7651             * @property {?Object} userData - Arbitrary subscriber-defined object.
7652             */
7653            this.raiseEvent( 'update-overlay', {
7654                element: element,
7655                location: location,
7656                placement: placement
7657            });
7658        }
7659        return this;
7660    },
7661
7662    /**
7663     * Removes an overlay identified by the reference element or element id
7664     * and schedules an update.
7665     * @method
7666     * @param {Element|String} element - A reference to the element or an
7667     *      element id which represent the ovelay content to be removed.
7668     * @return {OpenSeadragon.Viewer} Chainable.
7669     * @fires OpenSeadragon.Viewer.event:remove-overlay
7670     */
7671    removeOverlay: function( element ) {
7672        var i;
7673
7674        element = $.getElement( element );
7675        i = getOverlayIndex( this.currentOverlays, element );
7676
7677        if ( i >= 0 ) {
7678            this.currentOverlays[ i ].destroy();
7679            this.currentOverlays.splice( i, 1 );
7680            THIS[ this.hash ].forceRedraw = true;
7681            /**
7682             * Raised when an overlay is removed from the viewer
7683             * (see {@link OpenSeadragon.Viewer#removeOverlay}).
7684             *
7685             * @event remove-overlay
7686             * @memberof OpenSeadragon.Viewer
7687             * @type {object}
7688             * @property {OpenSeadragon.Viewer} eventSource - A reference to the
7689             * Viewer which raised the event.
7690             * @property {Element} element - The overlay element.
7691             * @property {?Object} userData - Arbitrary subscriber-defined object.
7692             */
7693            this.raiseEvent( 'remove-overlay', {
7694                element: element
7695            });
7696        }
7697        return this;
7698    },
7699
7700    /**
7701     * Removes all currently configured Overlays from this Viewer and schedules
7702     * an update.
7703     * @method
7704     * @return {OpenSeadragon.Viewer} Chainable.
7705     * @fires OpenSeadragon.Viewer.event:clear-overlay
7706     */
7707    clearOverlays: function() {
7708        while ( this.currentOverlays.length > 0 ) {
7709            this.currentOverlays.pop().destroy();
7710        }
7711        THIS[ this.hash ].forceRedraw = true;
7712        /**
7713         * Raised when all overlays are removed from the viewer (see {@link OpenSeadragon.Drawer#clearOverlays}).
7714         *
7715         * @event clear-overlay
7716         * @memberof OpenSeadragon.Viewer
7717         * @type {object}
7718         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
7719         * @property {?Object} userData - Arbitrary subscriber-defined object.
7720         */
7721        this.raiseEvent( 'clear-overlay', {} );
7722        return this;
7723    },
7724
7725    /**
7726     * Updates the sequence buttons.
7727     * @function OpenSeadragon.Viewer.prototype._updateSequenceButtons
7728     * @private
7729     * @param {Number} Sequence Value
7730     */
7731    _updateSequenceButtons: function( page ) {
7732
7733            if ( this.nextButton ) {
7734                if( ( this.tileSources.length - 1 ) === page ) {
7735                    //Disable next button
7736                    if ( !this.navPrevNextWrap ) {
7737                        this.nextButton.disable();
7738                    }
7739                } else {
7740                    this.nextButton.enable();
7741                }
7742            }
7743            if ( this.previousButton ) {
7744                if ( page > 0 ) {
7745                    //Enable previous button
7746                    this.previousButton.enable();
7747                } else {
7748                    if ( !this.navPrevNextWrap ) {
7749                        this.previousButton.disable();
7750                    }
7751                }
7752            }
7753      },
7754      
7755    /**
7756     * Display a message in the viewport
7757     * @function OpenSeadragon.Viewer.prototype._showMessage
7758     * @private
7759     * @param {String} text message
7760     */
7761    _showMessage: function ( message ) {
7762        this._hideMessage();
7763
7764        var div = $.makeNeutralElement( "div" );
7765        div.appendChild( document.createTextNode( message ) );
7766
7767        this.messageDiv = $.makeCenteredNode( div );
7768
7769        $.addClass(this.messageDiv, "openseadragon-message");
7770
7771        this.container.appendChild( this.messageDiv );
7772    },
7773
7774    /**
7775     * Hide any currently displayed viewport message
7776     * @function OpenSeadragon.Viewer.prototype._hideMessage
7777     * @private
7778     */
7779    _hideMessage: function () {
7780        var div = this.messageDiv;
7781        if (div) {
7782            div.parentNode.removeChild(div);
7783            delete this.messageDiv;
7784        }
7785    },
7786
7787    /**
7788     * Gets this viewer's gesture settings for the given pointer device type.
7789     * @method
7790     * @param {String} type - The pointer device type to get the gesture settings for ("mouse", "touch", "pen", etc.).
7791     * @return {OpenSeadragon.GestureSettings}
7792     */
7793    gestureSettingsByDeviceType: function ( type ) {
7794        switch ( type ) {
7795            case 'mouse':
7796                return this.gestureSettingsMouse;
7797            case 'touch':
7798                return this.gestureSettingsTouch;
7799            case 'pen':
7800                return this.gestureSettingsPen;
7801            default:
7802                return this.gestureSettingsUnknown;
7803        }
7804    }
7805
7806});
7807
7808
7809/**
7810 * _getSafeElemSize is like getElementSize(), but refuses to return 0 for x or y,
7811 * which was causing some calling operations in updateOnce and openTileSource to
7812 * return NaN.
7813 * @returns {Point}
7814 * @private
7815 */
7816function _getSafeElemSize (oElement) {
7817    oElement = $.getElement( oElement );
7818
7819    return new $.Point(
7820        (oElement.clientWidth === 0 ? 1 : oElement.clientWidth),
7821        (oElement.clientHeight === 0 ? 1 : oElement.clientHeight)
7822    );
7823}
7824
7825/**
7826 * @function
7827 * @private
7828 */
7829function getTileSourceImplementation( viewer, tileSource, successCallback,
7830    failCallback ) {
7831    var _this = viewer;
7832
7833    //allow plain xml strings or json strings to be parsed here
7834    if ( $.type( tileSource ) == 'string' ) {
7835        if ( tileSource.match( /\s*<.*/ ) ) {
7836            tileSource = $.parseXml( tileSource );
7837        } else if ( tileSource.match( /\s*[\{\[].*/ ) ) {
7838            /*jshint evil:true*/
7839            tileSource = eval( '(' + tileSource + ')' );
7840        }
7841    }
7842
7843    setTimeout( function() {
7844        if ( $.type( tileSource ) == 'string' ) {
7845            //If its still a string it means it must be a url at this point
7846            tileSource = new $.TileSource( tileSource, function( event ) {
7847                successCallback( event.tileSource );
7848            });
7849            tileSource.addHandler( 'open-failed', function( event ) {
7850                failCallback( event );
7851            } );
7852
7853        } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) {
7854            if ( $.isFunction( tileSource.getTileUrl ) ) {
7855                //Custom tile source
7856                var customTileSource = new $.TileSource( tileSource );
7857                customTileSource.getTileUrl = tileSource.getTileUrl;
7858                successCallback( customTileSource );
7859            } else {
7860                //inline configuration
7861                var $TileSource = $.TileSource.determineType( _this, tileSource );
7862                if ( !$TileSource ) {
7863                    failCallback( {
7864                        message: "Unable to load TileSource",
7865                        source: tileSource
7866                    });
7867                    return;
7868                }
7869                var options = $TileSource.prototype.configure.apply( _this, [ tileSource ] );
7870                var readySource = new $TileSource( options );
7871                successCallback( readySource );
7872            }
7873        } else {
7874            //can assume it's already a tile source implementation
7875            successCallback( tileSource );
7876        }
7877    }, 1 );
7878}
7879
7880/**
7881 * @function
7882 * @private
7883 */
7884function openTileSource( viewer, source ) {
7885    var i,
7886        _this = viewer;
7887
7888    if ( _this.source ) {
7889        _this.close( );
7890    }
7891
7892    THIS[ _this.hash ].prevContainerSize = _getSafeElemSize( _this.container );
7893
7894
7895    if( _this.collectionMode ){
7896        _this.source = new $.TileSourceCollection({
7897            rows: _this.collectionRows,
7898            layout: _this.collectionLayout,
7899            tileSize: _this.collectionTileSize,
7900            tileSources: _this.tileSources,
7901            tileMargin: _this.collectionTileMargin
7902        });
7903        _this.viewport = _this.viewport ? _this.viewport : new $.Viewport({
7904            collectionMode:         true,
7905            collectionTileSource:   _this.source,
7906            containerSize:          THIS[ _this.hash ].prevContainerSize,
7907            contentSize:            _this.source.dimensions,
7908            springStiffness:        _this.springStiffness,
7909            animationTime:          _this.animationTime,
7910            showNavigator:          false,
7911            minZoomImageRatio:      1,
7912            maxZoomPixelRatio:      1,
7913            viewer:                 _this,
7914            degrees:                 _this.degrees //,
7915            //TODO: figure out how to support these in a way that makes sense
7916            //minZoomLevel:           this.minZoomLevel,
7917            //maxZoomLevel:           this.maxZoomLevel
7918        });
7919    } else {
7920        if( source ){
7921            _this.source = source;
7922        }
7923        _this.viewport = _this.viewport ? _this.viewport : new $.Viewport({
7924            containerSize:      THIS[ _this.hash ].prevContainerSize,
7925            contentSize:        _this.source.dimensions,
7926            springStiffness:    _this.springStiffness,
7927            animationTime:      _this.animationTime,
7928            minZoomImageRatio:  _this.minZoomImageRatio,
7929            maxZoomPixelRatio:  _this.maxZoomPixelRatio,
7930            visibilityRatio:    _this.visibilityRatio,
7931            wrapHorizontal:     _this.wrapHorizontal,
7932            wrapVertical:       _this.wrapVertical,
7933            defaultZoomLevel:   _this.defaultZoomLevel,
7934            minZoomLevel:       _this.minZoomLevel,
7935            maxZoomLevel:       _this.maxZoomLevel,
7936            viewer:             _this,
7937            degrees:            _this.degrees
7938        });
7939    }
7940
7941    if( _this.preserveViewport ){
7942        _this.viewport.resetContentSize( _this.source.dimensions );
7943    }
7944
7945    _this.source.overlays = _this.source.overlays || [];
7946
7947    _this.drawer = new $.Drawer({
7948        viewer:             _this,
7949        source:             _this.source,
7950        viewport:           _this.viewport,
7951        element:            _this.drawersContainer,
7952        opacity:            _this.opacity,
7953        maxImageCacheCount: _this.maxImageCacheCount,
7954        imageLoaderLimit:   _this.imageLoaderLimit,
7955        minZoomImageRatio:  _this.minZoomImageRatio,
7956        wrapHorizontal:     _this.wrapHorizontal,
7957        wrapVertical:       _this.wrapVertical,
7958        immediateRender:    _this.immediateRender,
7959        blendTime:          _this.blendTime,
7960        alwaysBlend:        _this.alwaysBlend,
7961        minPixelRatio:      _this.collectionMode ? 0 : _this.minPixelRatio,
7962        timeout:            _this.timeout,
7963        debugMode:          _this.debugMode,
7964        debugGridColor:     _this.debugGridColor,
7965        crossOriginPolicy:  _this.crossOriginPolicy
7966    });
7967    _this.drawers = [_this.drawer];
7968
7969    // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons
7970    if (!_this.drawer.canRotate()) {
7971        // Disable/remove the rotate left/right buttons since they aren't supported
7972        if (_this.rotateLeft) {
7973            i = _this.buttons.buttons.indexOf(_this.rotateLeft);
7974            _this.buttons.buttons.splice(i, 1);
7975            _this.buttons.element.removeChild(_this.rotateLeft.element);
7976        }
7977        if (_this.rotateRight) {
7978            i = _this.buttons.buttons.indexOf(_this.rotateRight);
7979            _this.buttons.buttons.splice(i, 1);
7980            _this.buttons.element.removeChild(_this.rotateRight.element);
7981        }
7982    }
7983
7984    //Instantiate a navigator if configured
7985    if ( _this.showNavigator  && !_this.collectionMode ){
7986        // Note: By passing the fully parsed source, the navigator doesn't
7987        // have to load it again.
7988        if ( _this.navigator ) {
7989            _this.navigator.open( source );
7990        } else {
7991            _this.navigator = new $.Navigator({
7992                id:                _this.navigatorId,
7993                position:          _this.navigatorPosition,
7994                sizeRatio:         _this.navigatorSizeRatio,
7995                maintainSizeRatio: _this.navigatorMaintainSizeRatio,
7996                top:               _this.navigatorTop,
7997                left:              _this.navigatorLeft,
7998                width:             _this.navigatorWidth,
7999                height:            _this.navigatorHeight,
8000                autoResize:        _this.navigatorAutoResize,
8001                tileSources:       source,
8002                tileHost:          _this.tileHost,
8003                prefixUrl:         _this.prefixUrl,
8004                viewer:            _this
8005            });
8006        }
8007    }
8008
8009    //Instantiate a referencestrip if configured
8010    if ( _this.showReferenceStrip  && !_this.referenceStrip ){
8011        _this.referenceStrip = new $.ReferenceStrip({
8012            id:          _this.referenceStripElement,
8013            position:    _this.referenceStripPosition,
8014            sizeRatio:   _this.referenceStripSizeRatio,
8015            scroll:      _this.referenceStripScroll,
8016            height:      _this.referenceStripHeight,
8017            width:       _this.referenceStripWidth,
8018            tileSources: _this.tileSources,
8019            tileHost:    _this.tileHost,
8020            prefixUrl:   _this.prefixUrl,
8021            viewer:      _this
8022        });
8023    }
8024
8025    //this.profiler = new $.Profiler();
8026
8027    THIS[ _this.hash ].animating = false;
8028    THIS[ _this.hash ].forceRedraw = true;
8029    _this._updateRequestId = scheduleUpdate( _this, updateMulti );
8030
8031    VIEWERS[ _this.hash ] = _this;
8032
8033    loadOverlays( _this );
8034
8035    /**
8036     * Raised when the viewer has opened and loaded one or more TileSources.
8037     *
8038     * @event open
8039     * @memberof OpenSeadragon.Viewer
8040     * @type {object}
8041     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
8042     * @property {OpenSeadragon.TileSource} source
8043     * @property {?Object} userData - Arbitrary subscriber-defined object.
8044     */
8045    _this.raiseEvent( 'open', { source: source } );
8046
8047    return _this;
8048}
8049
8050function loadOverlays( _this ) {
8051    _this.currentOverlays = [];
8052    for ( var i = 0; i < _this.overlays.length; i++ ) {
8053        _this.currentOverlays[ i ] = getOverlayObject( _this, _this.overlays[ i ] );
8054    }
8055    for ( var j = 0; j < _this.source.overlays.length; j++ ) {
8056        _this.currentOverlays[ i + j ] =
8057            getOverlayObject( _this, _this.source.overlays[ j ] );
8058    }
8059}
8060
8061function getOverlayObject( viewer, overlay ) {
8062    if ( overlay instanceof $.Overlay ) {
8063        return overlay;
8064    }
8065
8066    var element = null;
8067    if ( overlay.element ) {
8068        element = $.getElement( overlay.element );
8069    } else {
8070        var id = overlay.id ?
8071            overlay.id :
8072            "openseadragon-overlay-" + Math.floor( Math.random() * 10000000 );
8073
8074        element = $.getElement( overlay.id );
8075        if ( !element ) {
8076            element         = document.createElement( "a" );
8077            element.href    = "#/overlay/" + id;
8078        }
8079        element.id = id;
8080        $.addClass( element, overlay.className ?
8081            overlay.className :
8082            "openseadragon-overlay"
8083        );
8084    }
8085
8086    var location = overlay.location;
8087    if ( !location ) {
8088        if ( overlay.width && overlay.height ) {
8089            location = overlay.px !== undefined ?
8090                viewer.viewport.imageToViewportRectangle( new $.Rect(
8091                    overlay.px,
8092                    overlay.py,
8093                    overlay.width,
8094                    overlay.height
8095                ) ) :
8096                new $.Rect(
8097                    overlay.x,
8098                    overlay.y,
8099                    overlay.width,
8100                    overlay.height
8101                );
8102        } else {
8103            location = overlay.px !== undefined ?
8104                viewer.viewport.imageToViewportCoordinates( new $.Point(
8105                    overlay.px,
8106                    overlay.py
8107                ) ) :
8108                new $.Point(
8109                    overlay.x,
8110                    overlay.y
8111                );
8112        }
8113    }
8114
8115    var placement = overlay.placement;
8116    if ( placement && ( $.type( placement ) === "string" ) ) {
8117        placement = $.OverlayPlacement[ overlay.placement.toUpperCase() ];
8118    }
8119
8120    return new $.Overlay({
8121        element: element,
8122        location: location,
8123        placement: placement,
8124        onDraw: overlay.onDraw,
8125        checkResize: overlay.checkResize
8126    });
8127}
8128
8129/**
8130 * @private
8131 * @inner
8132 * Determines the index of the given overlay in the given overlays array.
8133 */
8134function getOverlayIndex( overlays, element ) {
8135    var i;
8136    for ( i = overlays.length - 1; i >= 0; i-- ) {
8137        if ( overlays[ i ].element === element ) {
8138            return i;
8139        }
8140    }
8141
8142    return -1;
8143}
8144
8145function drawOverlays( viewport, overlays, container ) {
8146    var i,
8147        length = overlays.length;
8148    for ( i = 0; i < length; i++ ) {
8149        overlays[ i ].drawHTML( container, viewport );
8150    }
8151}
8152
8153///////////////////////////////////////////////////////////////////////////////
8154// Schedulers provide the general engine for animation
8155///////////////////////////////////////////////////////////////////////////////
8156function scheduleUpdate( viewer, updateFunc ){
8157    return $.requestAnimationFrame( function(){
8158        updateFunc( viewer );
8159    } );
8160}
8161
8162
8163//provides a sequence in the fade animation
8164function scheduleControlsFade( viewer ) {
8165    $.requestAnimationFrame( function(){
8166        updateControlsFade( viewer );
8167    });
8168}
8169
8170
8171//initiates an animation to hide the controls
8172function beginControlsAutoHide( viewer ) {
8173    if ( !viewer.autoHideControls ) {
8174        return;
8175    }
8176    viewer.controlsShouldFade = true;
8177    viewer.controlsFadeBeginTime =
8178        $.now() +
8179        viewer.controlsFadeDelay;
8180
8181    window.setTimeout( function(){
8182        scheduleControlsFade( viewer );
8183    }, viewer.controlsFadeDelay );
8184}
8185
8186
8187//determines if fade animation is done or continues the animation
8188function updateControlsFade( viewer ) {
8189    var currentTime,
8190        deltaTime,
8191        opacity,
8192        i;
8193    if ( viewer.controlsShouldFade ) {
8194        currentTime = $.now();
8195        deltaTime = currentTime - viewer.controlsFadeBeginTime;
8196        opacity = 1.0 - deltaTime / viewer.controlsFadeLength;
8197
8198        opacity = Math.min( 1.0, opacity );
8199        opacity = Math.max( 0.0, opacity );
8200
8201        for ( i = viewer.controls.length - 1; i >= 0; i--) {
8202            if (viewer.controls[ i ].autoFade) {
8203                viewer.controls[ i ].setOpacity( opacity );
8204            }
8205        }
8206
8207        if ( opacity > 0 ) {
8208            // fade again
8209            scheduleControlsFade( viewer );
8210        }
8211    }
8212}
8213
8214
8215//stop the fade animation on the controls and show them
8216function abortControlsAutoHide( viewer ) {
8217    var i;
8218    viewer.controlsShouldFade = false;
8219    for ( i = viewer.controls.length - 1; i >= 0; i-- ) {
8220        viewer.controls[ i ].setOpacity( 1.0 );
8221    }
8222}
8223
8224
8225
8226///////////////////////////////////////////////////////////////////////////////
8227// Default view event handlers.
8228///////////////////////////////////////////////////////////////////////////////
8229function onFocus(){
8230    abortControlsAutoHide( this );
8231}
8232
8233function onBlur(){
8234    beginControlsAutoHide( this );
8235
8236}
8237
8238function onCanvasClick( event ) {
8239    var gestureSettings;
8240
8241    if ( !event.preventDefaultAction && this.viewport && event.quick ) {
8242        gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
8243        if ( gestureSettings.clickToZoom ) {
8244            this.viewport.zoomBy(
8245                event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick,
8246                this.viewport.pointFromPixel( event.position, true )
8247            );
8248            this.viewport.applyConstraints();
8249        }
8250    }
8251    /**
8252     * Raised when a mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element.
8253     *
8254     * @event canvas-click
8255     * @memberof OpenSeadragon.Viewer
8256     * @type {object}
8257     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8258     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8259     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8260     * @property {Boolean} quick - True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for differentiating between clicks and drags.
8261     * @property {Boolean} shift - True if the shift key was pressed during this event.
8262     * @property {Object} originalEvent - The original DOM event.
8263     * @property {?Object} userData - Arbitrary subscriber-defined object.
8264     */
8265    this.raiseEvent( 'canvas-click', {
8266        tracker: event.eventSource,
8267        position: event.position,
8268        quick: event.quick,
8269        shift: event.shift,
8270        originalEvent: event.originalEvent
8271    });
8272}
8273
8274function onCanvasDblClick( event ) {
8275    var gestureSettings;
8276
8277    if ( !event.preventDefaultAction && this.viewport ) {
8278        gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
8279        if ( gestureSettings.dblClickToZoom ) {
8280            this.viewport.zoomBy(
8281                event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick,
8282                this.viewport.pointFromPixel( event.position, true )
8283            );
8284            this.viewport.applyConstraints();
8285        }
8286    }
8287    /**
8288     * Raised when a double mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element.
8289     *
8290     * @event canvas-double-click
8291     * @memberof OpenSeadragon.Viewer
8292     * @type {object}
8293     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8294     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8295     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8296     * @property {Boolean} shift - True if the shift key was pressed during this event.
8297     * @property {Object} originalEvent - The original DOM event.
8298     * @property {?Object} userData - Arbitrary subscriber-defined object.
8299     */
8300    this.raiseEvent( 'canvas-double-click', {
8301        tracker: event.eventSource,
8302        position: event.position,
8303        shift: event.shift,
8304        originalEvent: event.originalEvent
8305    });
8306}
8307
8308function onCanvasDrag( event ) {
8309    var gestureSettings;
8310
8311    if ( !event.preventDefaultAction && this.viewport ) {
8312        gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
8313        if( !this.panHorizontal ){
8314            event.delta.x = 0;
8315        }
8316        if( !this.panVertical ){
8317            event.delta.y = 0;
8318        }
8319        this.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta.negate() ), gestureSettings.flickEnabled );
8320        if( this.constrainDuringPan ){
8321            this.viewport.applyConstraints();
8322        }
8323    }
8324    /**
8325     * Raised when a mouse or touch drag operation occurs on the {@link OpenSeadragon.Viewer#canvas} element.
8326     *
8327     * @event canvas-drag
8328     * @memberof OpenSeadragon.Viewer
8329     * @type {object}
8330     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8331     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8332     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8333     * @property {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag.
8334     * @property {Number} speed - Current computed speed, in pixels per second.
8335     * @property {Number} direction - Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
8336     * @property {Boolean} shift - True if the shift key was pressed during this event.
8337     * @property {Object} originalEvent - The original DOM event.
8338     * @property {?Object} userData - Arbitrary subscriber-defined object.
8339     */
8340    this.raiseEvent( 'canvas-drag', {
8341        tracker: event.eventSource,
8342        position: event.position,
8343        delta: event.delta,
8344        speed: event.speed,
8345        direction: event.direction,
8346        shift: event.shift,
8347        originalEvent: event.originalEvent
8348    });
8349}
8350
8351function onCanvasDragEnd( event ) {
8352    var gestureSettings;
8353
8354    if ( !event.preventDefaultAction && this.viewport ) {
8355        gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
8356        if ( gestureSettings.flickEnabled && event.speed >= gestureSettings.flickMinSpeed ) {
8357            var amplitudeX = gestureSettings.flickMomentum * ( event.speed * Math.cos( event.direction ) ),
8358                amplitudeY = gestureSettings.flickMomentum * ( event.speed * Math.sin( event.direction ) ),
8359                center = this.viewport.pixelFromPoint( this.viewport.getCenter( true ) ),
8360                target = this.viewport.pointFromPixel( new $.Point( center.x - amplitudeX, center.y - amplitudeY ) );
8361            if( !this.panHorizontal ) {
8362                target.x = center.x;
8363            }
8364            if( !this.panVertical ) {
8365                target.y = center.y;
8366            }
8367            this.viewport.panTo( target, false );
8368            this.viewport.applyConstraints();
8369        }
8370    }
8371    /**
8372     * Raised when a mouse or touch drag operation ends on the {@link OpenSeadragon.Viewer#canvas} element.
8373     *
8374     * @event canvas-drag-end
8375     * @memberof OpenSeadragon.Viewer
8376     * @type {object}
8377     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8378     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8379     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8380     * @property {Number} speed - Speed at the end of a drag gesture, in pixels per second.
8381     * @property {Number} direction - Direction at the end of a drag gesture, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
8382     * @property {Boolean} shift - True if the shift key was pressed during this event.
8383     * @property {Object} originalEvent - The original DOM event.
8384     * @property {?Object} userData - Arbitrary subscriber-defined object.
8385     */
8386    this.raiseEvent( 'canvas-drag-end', {
8387        tracker: event.eventSource,
8388        position: event.position,
8389        speed: event.speed,
8390        direction: event.direction,
8391        shift: event.shift,
8392        originalEvent: event.originalEvent
8393    });
8394}
8395
8396function onCanvasRelease( event ) {
8397    var gestureSettings;
8398
8399    if ( event.insideElementPressed && this.viewport ) {
8400        gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
8401
8402        if ( !gestureSettings.flickEnabled ) {
8403            this.viewport.applyConstraints();
8404        }
8405    }
8406    /**
8407     * Raised when the mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#canvas} element.
8408     *
8409     * @event canvas-release
8410     * @memberof OpenSeadragon.Viewer
8411     * @type {object}
8412     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8413     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8414     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8415     * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
8416     * @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released.
8417     * @property {Object} originalEvent - The original DOM event.
8418     * @property {?Object} userData - Arbitrary subscriber-defined object.
8419     */
8420    this.raiseEvent( 'canvas-release', {
8421        tracker: event.eventSource,
8422        position: event.position,
8423        insideElementPressed: event.insideElementPressed,
8424        insideElementReleased: event.insideElementReleased,
8425        originalEvent: event.originalEvent
8426    });
8427}
8428
8429function onCanvasPinch( event ) {
8430    var gestureSettings,
8431        centerPt,
8432        lastCenterPt,
8433        panByPt;
8434
8435    if ( !event.preventDefaultAction && this.viewport ) {
8436        gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
8437        if ( gestureSettings.pinchToZoom ) {
8438            centerPt = this.viewport.pointFromPixel( event.center, true );
8439            lastCenterPt = this.viewport.pointFromPixel( event.lastCenter, true );
8440            panByPt = lastCenterPt.minus( centerPt );
8441            if( !this.panHorizontal ) {
8442                panByPt.x = 0;
8443            }
8444            if( !this.panVertical ) {
8445                panByPt.y = 0;
8446            }
8447            this.viewport.zoomBy( event.distance / event.lastDistance, centerPt, true );
8448            this.viewport.panBy( panByPt, true );
8449            this.viewport.applyConstraints();
8450        }
8451    }
8452    /**
8453     * Raised when a pinch event occurs on the {@link OpenSeadragon.Viewer#canvas} element.
8454     *
8455     * @event canvas-pinch
8456     * @memberof OpenSeadragon.Viewer
8457     * @type {object}
8458     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8459     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8460     * @property {Array.<OpenSeadragon.MouseTracker.GesturePoint>} gesturePoints - Gesture points associated with the gesture. Velocity data can be found here.
8461     * @property {OpenSeadragon.Point} lastCenter - The previous center point of the two pinch contact points relative to the tracked element.
8462     * @property {OpenSeadragon.Point} center - The center point of the two pinch contact points relative to the tracked element.
8463     * @property {Number} lastDistance - The previous distance between the two pinch contact points in CSS pixels.
8464     * @property {Number} distance - The distance between the two pinch contact points in CSS pixels.
8465     * @property {Boolean} shift - True if the shift key was pressed during this event.
8466     * @property {Object} originalEvent - The original DOM event.
8467     * @property {?Object} userData - Arbitrary subscriber-defined object.
8468     */
8469    this.raiseEvent('canvas-pinch', {
8470        tracker: event.eventSource,
8471        gesturePoints: event.gesturePoints,
8472        lastCenter: event.lastCenter,
8473        center: event.center,
8474        lastDistance: event.lastDistance,
8475        distance: event.distance,
8476        shift: event.shift,
8477        originalEvent: event.originalEvent
8478    });
8479    //cancels event
8480    return false;
8481}
8482
8483function onCanvasScroll( event ) {
8484    var gestureSettings,
8485        factor;
8486
8487    if ( !event.preventDefaultAction && this.viewport ) {
8488        gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
8489        if ( gestureSettings.scrollToZoom ) {
8490            factor = Math.pow( this.zoomPerScroll, event.scroll );
8491            this.viewport.zoomBy(
8492                factor,
8493                this.viewport.pointFromPixel( event.position, true )
8494            );
8495            this.viewport.applyConstraints();
8496        }
8497    }
8498    /**
8499     * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel).
8500     *
8501     * @event canvas-scroll
8502     * @memberof OpenSeadragon.Viewer
8503     * @type {object}
8504     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8505     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8506     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8507     * @property {Number} scroll - The scroll delta for the event.
8508     * @property {Boolean} shift - True if the shift key was pressed during this event.
8509     * @property {Object} originalEvent - The original DOM event.
8510     * @property {?Object} userData - Arbitrary subscriber-defined object.
8511     */
8512    this.raiseEvent( 'canvas-scroll', {
8513        tracker: event.eventSource,
8514        position: event.position,
8515        scroll: event.scroll,
8516        shift: event.shift,
8517        originalEvent: event.originalEvent
8518    });
8519    //cancels event
8520    return false;
8521}
8522
8523function onContainerExit( event ) {
8524    if ( !event.insideElementPressed ) {
8525        THIS[ this.hash ].mouseInside = false;
8526        if ( !THIS[ this.hash ].animating ) {
8527            beginControlsAutoHide( this );
8528        }
8529    }
8530    /**
8531     * Raised when the cursor leaves the {@link OpenSeadragon.Viewer#container} element.
8532     *
8533     * @event container-exit
8534     * @memberof OpenSeadragon.Viewer
8535     * @type {object}
8536     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8537     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8538     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8539     * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
8540     * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
8541     * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
8542     * @property {Object} originalEvent - The original DOM event.
8543     * @property {?Object} userData - Arbitrary subscriber-defined object.
8544     */
8545    this.raiseEvent( 'container-exit', {
8546        tracker: event.eventSource,
8547        position: event.position,
8548        buttons: event.buttons,
8549        insideElementPressed: event.insideElementPressed,
8550        buttonDownAny: event.buttonDownAny,
8551        originalEvent: event.originalEvent
8552    });
8553}
8554
8555function onContainerPress( event ) {
8556    if ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) {
8557        THIS[ this.hash ].mouseInside = true;
8558        abortControlsAutoHide( this );
8559    }
8560}
8561
8562function onContainerRelease( event ) {
8563    if ( !event.insideElementReleased || ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) ) {
8564        THIS[ this.hash ].mouseInside = false;
8565        if ( !THIS[ this.hash ].animating ) {
8566            beginControlsAutoHide( this );
8567        }
8568    }
8569    /**
8570     * Raised when the mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#container} element.
8571     *
8572     * @event container-release
8573     * @memberof OpenSeadragon.Viewer
8574     * @type {object}
8575     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8576     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8577     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8578     * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
8579     * @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released.
8580     * @property {Object} originalEvent - The original DOM event.
8581     * @property {?Object} userData - Arbitrary subscriber-defined object.
8582     */
8583    this.raiseEvent( 'container-release', {
8584        tracker: event.eventSource,
8585        position: event.position,
8586        insideElementPressed: event.insideElementPressed,
8587        insideElementReleased: event.insideElementReleased,
8588        originalEvent: event.originalEvent
8589    });
8590}
8591
8592function onContainerEnter( event ) {
8593    THIS[ this.hash ].mouseInside = true;
8594    abortControlsAutoHide( this );
8595    /**
8596     * Raised when the cursor enters the {@link OpenSeadragon.Viewer#container} element.
8597     *
8598     * @event container-enter
8599     * @memberof OpenSeadragon.Viewer
8600     * @type {object}
8601     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8602     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
8603     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
8604     * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
8605     * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
8606     * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
8607     * @property {Object} originalEvent - The original DOM event.
8608     * @property {?Object} userData - Arbitrary subscriber-defined object.
8609     */
8610    this.raiseEvent( 'container-enter', {
8611        tracker: event.eventSource,
8612        position: event.position,
8613        buttons: event.buttons,
8614        insideElementPressed: event.insideElementPressed,
8615        buttonDownAny: event.buttonDownAny,
8616        originalEvent: event.originalEvent
8617    });
8618}
8619
8620
8621///////////////////////////////////////////////////////////////////////////////
8622// Page update routines ( aka Views - for future reference )
8623///////////////////////////////////////////////////////////////////////////////
8624
8625function updateMulti( viewer ) {
8626    if ( !viewer.source ) {
8627        viewer._updateRequestId = null;
8628        return;
8629    }
8630
8631    updateOnce( viewer );
8632
8633    // Request the next frame, unless we've been closed during the updateOnce()
8634    if ( viewer.source ) {
8635        viewer._updateRequestId = scheduleUpdate( viewer, updateMulti );
8636    }
8637}
8638
8639function updateOnce( viewer ) {
8640
8641    var containerSize,
8642        animated;
8643
8644    if ( !viewer.source ) {
8645        return;
8646    }
8647
8648    //viewer.profiler.beginUpdate();
8649
8650    if ( viewer.autoResize ) {
8651        containerSize = _getSafeElemSize( viewer.container );
8652        if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) {
8653            // maintain image position
8654            var oldBounds = viewer.viewport.getBounds();
8655            var oldCenter = viewer.viewport.getCenter();
8656            resizeViewportAndRecenter(viewer, containerSize, oldBounds, oldCenter);
8657            THIS[ viewer.hash ].prevContainerSize = containerSize;
8658            THIS[ viewer.hash ].forceRedraw = true;
8659        }
8660    }
8661
8662    animated = viewer.viewport.update();
8663
8664    if( viewer.referenceStrip ){
8665        animated = viewer.referenceStrip.update( viewer.viewport ) || animated;
8666    }
8667
8668    if ( !THIS[ viewer.hash ].animating && animated ) {
8669        /**
8670         * Raised when any spring animation starts (zoom, pan, etc.).
8671         *
8672         * @event animation-start
8673         * @memberof OpenSeadragon.Viewer
8674         * @type {object}
8675         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8676         * @property {?Object} userData - Arbitrary subscriber-defined object.
8677         */
8678        viewer.raiseEvent( "animation-start" );
8679        abortControlsAutoHide( viewer );
8680    }
8681
8682    if ( animated ) {
8683        updateDrawers( viewer );
8684        drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer );
8685        if( viewer.navigator ){
8686            viewer.navigator.update( viewer.viewport );
8687        }
8688        /**
8689         * Raised when any spring animation update occurs (zoom, pan, etc.).
8690         *
8691         * @event animation
8692         * @memberof OpenSeadragon.Viewer
8693         * @type {object}
8694         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8695         * @property {?Object} userData - Arbitrary subscriber-defined object.
8696         */
8697        viewer.raiseEvent( "animation" );
8698    } else if ( THIS[ viewer.hash ].forceRedraw || drawersNeedUpdate( viewer ) ) {
8699        updateDrawers( viewer );
8700        drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer );
8701        if( viewer.navigator ){
8702            viewer.navigator.update( viewer.viewport );
8703        }
8704        THIS[ viewer.hash ].forceRedraw = false;
8705    }
8706
8707    if ( THIS[ viewer.hash ].animating && !animated ) {
8708        /**
8709         * Raised when any spring animation ends (zoom, pan, etc.).
8710         *
8711         * @event animation-finish
8712         * @memberof OpenSeadragon.Viewer
8713         * @type {object}
8714         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
8715         * @property {?Object} userData - Arbitrary subscriber-defined object.
8716         */
8717        viewer.raiseEvent( "animation-finish" );
8718
8719        if ( !THIS[ viewer.hash ].mouseInside ) {
8720            beginControlsAutoHide( viewer );
8721        }
8722    }
8723
8724    THIS[ viewer.hash ].animating = animated;
8725
8726    //viewer.profiler.endUpdate();
8727}
8728
8729// This function resizes the viewport and recenters the image
8730// as it was before resizing.
8731// TODO: better adjust width and height. The new width and height
8732// should depend on the image dimensions and on the dimensions
8733// of the viewport before and after switching mode.
8734function resizeViewportAndRecenter( viewer, containerSize, oldBounds, oldCenter ) {
8735    var viewport = viewer.viewport;
8736
8737    viewport.resize( containerSize, true );
8738
8739    // We try to remove blanks as much as possible
8740    var imageHeight = 1 / viewer.source.aspectRatio;
8741    var newWidth = oldBounds.width <= 1 ? oldBounds.width : 1;
8742    var newHeight = oldBounds.height <= imageHeight ?
8743        oldBounds.height : imageHeight;
8744
8745    var newBounds = new $.Rect(
8746        oldCenter.x - ( newWidth / 2.0 ),
8747        oldCenter.y - ( newHeight / 2.0 ),
8748        newWidth,
8749        newHeight
8750        );
8751    viewport.fitBounds( newBounds, true );
8752}
8753
8754function updateDrawers( viewer ) {
8755    for (var i = 0; i < viewer.drawers.length; i++ ) {
8756        viewer.drawers[i].update();
8757    }
8758}
8759
8760function drawersNeedUpdate( viewer ) {
8761    for (var i = 0; i < viewer.drawers.length; i++ ) {
8762        if (viewer.drawers[i].needsUpdate()) {
8763            return true;
8764        }
8765    }
8766    return false;
8767}
8768
8769///////////////////////////////////////////////////////////////////////////////
8770// Navigation Controls
8771///////////////////////////////////////////////////////////////////////////////
8772function resolveUrl( prefix, url ) {
8773    return prefix ? prefix + url : url;
8774}
8775
8776
8777
8778function beginZoomingIn() {
8779    THIS[ this.hash ].lastZoomTime = $.now();
8780    THIS[ this.hash ].zoomFactor = this.zoomPerSecond;
8781    THIS[ this.hash ].zooming = true;
8782    scheduleZoom( this );
8783}
8784
8785
8786function beginZoomingOut() {
8787    THIS[ this.hash ].lastZoomTime = $.now();
8788    THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond;
8789    THIS[ this.hash ].zooming = true;
8790    scheduleZoom( this );
8791}
8792
8793
8794function endZooming() {
8795    THIS[ this.hash ].zooming = false;
8796}
8797
8798
8799function scheduleZoom( viewer ) {
8800    $.requestAnimationFrame( $.delegate( viewer, doZoom ) );
8801}
8802
8803
8804function doZoom() {
8805    var currentTime,
8806        deltaTime,
8807        adjustedFactor;
8808
8809    if ( THIS[ this.hash ].zooming && this.viewport) {
8810        currentTime     = $.now();
8811        deltaTime       = currentTime - THIS[ this.hash ].lastZoomTime;
8812        adjustedFactor  = Math.pow( THIS[ this.hash ].zoomFactor, deltaTime / 1000 );
8813
8814        this.viewport.zoomBy( adjustedFactor );
8815        this.viewport.applyConstraints();
8816        THIS[ this.hash ].lastZoomTime = currentTime;
8817        scheduleZoom( this );
8818    }
8819}
8820
8821
8822function doSingleZoomIn() {
8823    if ( this.viewport ) {
8824        THIS[ this.hash ].zooming = false;
8825        this.viewport.zoomBy(
8826            this.zoomPerClick / 1.0
8827        );
8828        this.viewport.applyConstraints();
8829    }
8830}
8831
8832
8833function doSingleZoomOut() {
8834    if ( this.viewport ) {
8835        THIS[ this.hash ].zooming = false;
8836        this.viewport.zoomBy(
8837            1.0 / this.zoomPerClick
8838        );
8839        this.viewport.applyConstraints();
8840    }
8841}
8842
8843
8844function lightUp() {
8845    this.buttons.emulateEnter();
8846    this.buttons.emulateExit();
8847}
8848
8849
8850function onHome() {
8851    if ( this.viewport ) {
8852        this.viewport.goHome();
8853    }
8854}
8855
8856
8857function onFullScreen() {
8858    if ( this.isFullPage() && !$.isFullScreen() ) {
8859        // Is fullPage but not fullScreen
8860        this.setFullPage( false );
8861    } else {
8862        this.setFullScreen( !this.isFullPage() );
8863    }
8864    // correct for no mouseout event on change
8865    if ( this.buttons ) {
8866        this.buttons.emulateExit();
8867    }
8868    this.fullPageButton.element.focus();
8869    if ( this.viewport ) {
8870        this.viewport.applyConstraints();
8871    }
8872}
8873
8874/**
8875 * Note: The current rotation feature is limited to 90 degree turns.
8876 */
8877function onRotateLeft() {
8878    if ( this.viewport ) {
8879        var currRotation = this.viewport.getRotation();
8880        if (currRotation === 0) {
8881            currRotation = 270;
8882        }
8883        else {
8884            currRotation -= 90;
8885        }
8886        this.viewport.setRotation(currRotation);
8887    }
8888}
8889
8890/**
8891 * Note: The current rotation feature is limited to 90 degree turns.
8892 */
8893function onRotateRight() {
8894    if ( this.viewport ) {
8895        var currRotation = this.viewport.getRotation();
8896        if (currRotation === 270) {
8897            currRotation = 0;
8898        }
8899        else {
8900            currRotation += 90;
8901        }
8902        this.viewport.setRotation(currRotation);
8903    }
8904}
8905
8906
8907function onPrevious(){
8908    var previous = THIS[ this.hash ].sequence - 1;
8909    if(this.navPrevNextWrap && previous < 0){
8910        previous += this.tileSources.length;
8911    }
8912    this.goToPage( previous );
8913}
8914
8915
8916function onNext(){
8917    var next = THIS[ this.hash ].sequence + 1;
8918    if(this.navPrevNextWrap && next >= this.tileSources.length){
8919        next = 0;
8920    }
8921    this.goToPage( next );
8922}
8923
8924
8925}( OpenSeadragon ));
8926
8927/*
8928 * OpenSeadragon - Navigator
8929 *
8930 * Copyright (C) 2009 CodePlex Foundation
8931 * Copyright (C) 2010-2013 OpenSeadragon contributors
8932 *
8933 * Redistribution and use in source and binary forms, with or without
8934 * modification, are permitted provided that the following conditions are
8935 * met:
8936 *
8937 * - Redistributions of source code must retain the above copyright notice,
8938 *   this list of conditions and the following disclaimer.
8939 *
8940 * - Redistributions in binary form must reproduce the above copyright
8941 *   notice, this list of conditions and the following disclaimer in the
8942 *   documentation and/or other materials provided with the distribution.
8943 *
8944 * - Neither the name of CodePlex Foundation nor the names of its
8945 *   contributors may be used to endorse or promote products derived from
8946 *   this software without specific prior written permission.
8947 *
8948 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
8949 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
8950 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8951 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
8952 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8953 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
8954 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
8955 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
8956 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
8957 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
8958 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
8959 */
8960
8961(function( $ ){
8962
8963/**
8964 * @class Navigator
8965 * @classdesc The Navigator provides a small view of the current image as fixed
8966 * while representing the viewport as a moving box serving as a frame
8967 * of reference in the larger viewport as to which portion of the image
8968 * is currently being examined.  The navigator's viewport can be interacted
8969 * with using the keyboard or the mouse.
8970 *
8971 * @memberof OpenSeadragon
8972 * @extends OpenSeadragon.Viewer
8973 * @extends OpenSeadragon.EventSource
8974 * @param {Object} options
8975 */
8976$.Navigator = function( options ){
8977
8978    var viewer      = options.viewer,
8979        viewerSize,
8980        navigatorSize,
8981        unneededElement;
8982
8983    //We may need to create a new element and id if they did not
8984    //provide the id for the existing element
8985    if( !options.id ){
8986        options.id              = 'navigator-' + $.now();
8987        this.element            = $.makeNeutralElement( "div" );
8988        options.controlOptions  = {
8989            anchor:           $.ControlAnchor.TOP_RIGHT,
8990            attachToViewer:   true,
8991            autoFade:         true
8992        };
8993
8994        if( options.position ){
8995            if( 'BOTTOM_RIGHT' == options.position ){
8996               options.controlOptions.anchor = $.ControlAnchor.BOTTOM_RIGHT;
8997            } else if( 'BOTTOM_LEFT' == options.position ){
8998               options.controlOptions.anchor = $.ControlAnchor.BOTTOM_LEFT;
8999            } else if( 'TOP_RIGHT' == options.position ){
9000               options.controlOptions.anchor = $.ControlAnchor.TOP_RIGHT;
9001            } else if( 'TOP_LEFT' == options.position ){
9002               options.controlOptions.anchor = $.ControlAnchor.TOP_LEFT;
9003            } else if( 'ABSOLUTE' == options.position ){
9004               options.controlOptions.anchor = $.ControlAnchor.ABSOLUTE;
9005               options.controlOptions.top = options.top;
9006               options.controlOptions.left = options.left;
9007               options.controlOptions.height = options.height;
9008               options.controlOptions.width = options.width;
9009            }
9010        }
9011        
9012    } else {
9013        this.element            = document.getElementById( options.id );
9014        options.controlOptions  = {
9015            anchor:           $.ControlAnchor.NONE,
9016            attachToViewer:   false,
9017            autoFade:         false
9018        };
9019    }
9020    this.element.id         = options.id;
9021    this.element.className  += ' navigator';
9022
9023    options = $.extend( true, {
9024        sizeRatio:     $.DEFAULT_SETTINGS.navigatorSizeRatio
9025    }, options, {
9026        element:                this.element,
9027        //These need to be overridden to prevent recursion since
9028        //the navigator is a viewer and a viewer has a navigator
9029        showNavigator:          false,
9030        mouseNavEnabled:        false,
9031        showNavigationControl:  false,
9032        showSequenceControl:    false,
9033        immediateRender:        true,
9034        blendTime:              0,
9035        animationTime:          0,
9036        autoResize:             options.autoResize
9037    });
9038
9039    options.minPixelRatio = this.minPixelRatio = viewer.minPixelRatio;
9040
9041    this.borderWidth = 2;
9042    //At some browser magnification levels the display regions lines up correctly, but at some there appears to
9043    //be a one pixel gap.
9044    this.fudge = new $.Point(1, 1);
9045    this.totalBorderWidths = new $.Point(this.borderWidth*2, this.borderWidth*2).minus(this.fudge);
9046
9047
9048    if ( options.controlOptions.anchor != $.ControlAnchor.NONE ) {
9049        (function( style, borderWidth ){
9050            style.margin        = '0px';
9051            style.border        = borderWidth + 'px solid #555';
9052            style.padding       = '0px';
9053            style.background    = '#000';
9054            style.opacity       = 0.8;
9055            style.overflow      = 'hidden';
9056        }( this.element.style, this.borderWidth));
9057    }
9058
9059    this.displayRegion           = $.makeNeutralElement( "div" );
9060    this.displayRegion.id        = this.element.id + '-displayregion';
9061    this.displayRegion.className = 'displayregion';
9062
9063    (function( style, borderWidth ){
9064        style.position      = 'relative';
9065        style.top           = '0px';
9066        style.left          = '0px';
9067        style.fontSize      = '0px';
9068        style.overflow      = 'hidden';
9069        style.border        = borderWidth + 'px solid #900';
9070        style.margin        = '0px';
9071        style.padding       = '0px';
9072        //TODO: IE doesnt like this property being set
9073        //try{ style.outline  = '2px auto #909'; }catch(e){/*ignore*/}
9074
9075        style.background    = 'transparent';
9076
9077        // We use square bracket notation on the statement below, because float is a keyword.
9078        // This is important for the Google Closure compiler, if nothing else.
9079        /*jshint sub:true */
9080        style['float']      = 'left'; //Webkit
9081
9082        style.cssFloat      = 'left'; //Firefox
9083        style.styleFloat    = 'left'; //IE
9084        style.zIndex        = 999999999;
9085        style.cursor        = 'default';
9086    }( this.displayRegion.style, this.borderWidth ));
9087
9088
9089    this.element.innerTracker = new $.MouseTracker({
9090        element:         this.element,
9091        dragHandler:     $.delegate( this, onCanvasDrag ),
9092        clickHandler:    $.delegate( this, onCanvasClick ),
9093        releaseHandler:  $.delegate( this, onCanvasRelease ),
9094        scrollHandler:   $.delegate( this, onCanvasScroll )
9095    }).setTracking( true );
9096
9097    /*this.displayRegion.outerTracker = new $.MouseTracker({
9098        element:            this.container,
9099        clickTimeThreshold: this.clickTimeThreshold,
9100        clickDistThreshold: this.clickDistThreshold,
9101        enterHandler:       $.delegate( this, onContainerEnter ),
9102        exitHandler:        $.delegate( this, onContainerExit ),
9103        releaseHandler:     $.delegate( this, onContainerRelease )
9104    }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking*/
9105
9106
9107    viewer.addControl(
9108        this.element,
9109        options.controlOptions
9110    );
9111
9112    if ( options.controlOptions.anchor != $.ControlAnchor.ABSOLUTE && options.controlOptions.anchor != $.ControlAnchor.NONE ) {
9113        if ( options.width && options.height ) {
9114            this.element.style.height = typeof ( options.height )  == "number" ? ( options.height + 'px' ) : options.height;
9115            this.element.style.width  = typeof ( options.width )  == "number" ? ( options.width + 'px' ) : options.width;
9116        } else {
9117            viewerSize = $.getElementSize( viewer.element );
9118            this.element.style.height = Math.round( viewerSize.y * options.sizeRatio ) + 'px';
9119            this.element.style.width  = Math.round( viewerSize.x * options.sizeRatio ) + 'px';
9120            this.oldViewerSize = viewerSize;
9121        }
9122        navigatorSize = $.getElementSize( this.element );
9123        this.elementArea = navigatorSize.x * navigatorSize.y;
9124    }
9125
9126    this.oldContainerSize = new $.Point( 0, 0 );
9127
9128    $.Viewer.apply( this, [ options ] );
9129
9130    this.element.getElementsByTagName( 'div' )[0].appendChild( this.displayRegion );
9131    unneededElement = this.element.getElementsByTagName('textarea')[0];
9132    if (unneededElement) {
9133        unneededElement.parentNode.removeChild(unneededElement);
9134    }
9135
9136};
9137
9138$.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /** @lends OpenSeadragon.Navigator.prototype */{
9139
9140    /**
9141     * Used to notify the navigator when its size has changed. 
9142     * Especially useful when {@link OpenSeadragon.Options}.navigatorAutoResize is set to false and the navigator is resizable.
9143     * @function
9144     */
9145    updateSize: function () {
9146        if ( this.viewport ) {
9147            var containerSize = new $.Point(
9148                    (this.container.clientWidth === 0 ? 1 : this.container.clientWidth),
9149                    (this.container.clientHeight === 0 ? 1 : this.container.clientHeight)
9150                );
9151            if ( !containerSize.equals( this.oldContainerSize ) ) {
9152                var oldBounds = this.viewport.getBounds();
9153                var oldCenter = this.viewport.getCenter();
9154                this.viewport.resize( containerSize, true );
9155                var imageHeight = 1 / this.source.aspectRatio;
9156                var newWidth = oldBounds.width <= 1 ? oldBounds.width : 1;
9157                var newHeight = oldBounds.height <= imageHeight ?
9158                    oldBounds.height : imageHeight;
9159                var newBounds = new $.Rect(
9160                    oldCenter.x - ( newWidth / 2.0 ),
9161                    oldCenter.y - ( newHeight / 2.0 ),
9162                    newWidth,
9163                    newHeight
9164                    );
9165                this.viewport.fitBounds( newBounds, true );
9166                this.oldContainerSize = containerSize;
9167                this.drawer.update();
9168            }
9169        }
9170    },
9171
9172    /**
9173     * Used to update the navigator minimap's viewport rectangle when a change in the viewer's viewport occurs.
9174     * @function
9175     * @param {OpenSeadragon.Viewport} The viewport this navigator is tracking.
9176     */
9177    update: function( viewport ) {
9178
9179        var viewerSize,
9180            newWidth,
9181            newHeight,
9182            bounds,
9183            topleft,
9184            bottomright;
9185
9186        viewerSize = $.getElementSize( this.viewer.element );
9187        if ( !viewerSize.equals( this.oldViewerSize ) ) {
9188            this.oldViewerSize = viewerSize;
9189            if ( this.maintainSizeRatio ) {
9190                newWidth  = viewerSize.x * this.sizeRatio;
9191                newHeight = viewerSize.y * this.sizeRatio;
9192            }
9193            else {
9194                newWidth = Math.sqrt(this.elementArea * (viewerSize.x / viewerSize.y));
9195                newHeight = this.elementArea / newWidth;
9196            }
9197            this.element.style.width  = Math.round( newWidth ) + 'px';
9198            this.element.style.height = Math.round( newHeight ) + 'px';
9199            this.updateSize();
9200        }
9201
9202        if( viewport && this.viewport ) {
9203            bounds      = viewport.getBounds( true );
9204            topleft     = this.viewport.pixelFromPoint( bounds.getTopLeft(), false );
9205            bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight(), false ).minus( this.totalBorderWidths );
9206
9207            //update style for navigator-box
9208            (function(style) {
9209
9210                style.top    = Math.round( topleft.y ) + 'px';
9211                style.left   = Math.round( topleft.x ) + 'px';
9212
9213                var width = Math.abs( topleft.x - bottomright.x );
9214                var height = Math.abs( topleft.y - bottomright.y );
9215                // make sure width and height are non-negative so IE doesn't throw
9216                style.width  = Math.round( Math.max( width, 0 ) ) + 'px';
9217                style.height = Math.round( Math.max( height, 0 ) ) + 'px';
9218
9219            }( this.displayRegion.style ));
9220        }
9221
9222    },
9223
9224    open: function( source ) {
9225        this.updateSize();
9226        var containerSize = this.viewer.viewport.containerSize.times( this.sizeRatio );
9227        if( source.tileSize > containerSize.x ||
9228            source.tileSize > containerSize.y ){
9229            this.minPixelRatio = Math.min(
9230                containerSize.x,
9231                containerSize.y
9232            ) / source.tileSize;
9233        } else {
9234            this.minPixelRatio = this.viewer.minPixelRatio;
9235        }
9236        return $.Viewer.prototype.open.apply( this, [ source ] );
9237    }
9238
9239});
9240
9241/**
9242 * @private
9243 * @inner
9244 * @function
9245 */
9246function onCanvasClick( event ) {
9247    var newBounds,
9248        viewerPosition,
9249        dimensions;
9250    if (! this.drag) {
9251        if ( this.viewer.viewport ) {
9252            this.viewer.viewport.panTo( this.viewport.pointFromPixel( event.position ) );
9253            this.viewer.viewport.applyConstraints();
9254        }
9255    }
9256    else {
9257        this.drag = false;
9258    }
9259}
9260
9261/**
9262 * @private
9263 * @inner
9264 * @function
9265 */
9266function onCanvasDrag( event ) {
9267    if ( this.viewer.viewport ) {
9268        this.drag = true;
9269        if( !this.panHorizontal ){
9270            event.delta.x = 0;
9271        }
9272        if( !this.panVertical ){
9273            event.delta.y = 0;
9274        }
9275        this.viewer.viewport.panBy(
9276            this.viewport.deltaPointsFromPixels(
9277                event.delta
9278            )
9279        );
9280    }
9281}
9282
9283
9284/**
9285 * @private
9286 * @inner
9287 * @function
9288 */
9289function onCanvasRelease( event ) {
9290    if ( event.insideElementPressed && this.viewer.viewport ) {
9291        this.viewer.viewport.applyConstraints();
9292    }
9293}
9294
9295
9296/**
9297 * @private
9298 * @inner
9299 * @function
9300 */
9301function onCanvasScroll( event ) {
9302    /**
9303     * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#navigator} element (mouse wheel, touch pinch, etc.).
9304     *
9305     * @event navigator-scroll
9306     * @memberof OpenSeadragon.Viewer
9307     * @type {object}
9308     * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
9309     * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
9310     * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
9311     * @property {Number} scroll - The scroll delta for the event.
9312     * @property {Boolean} shift - True if the shift key was pressed during this event.
9313     * @property {Object} originalEvent - The original DOM event.
9314     * @property {?Object} userData - Arbitrary subscriber-defined object.
9315     */
9316    this.viewer.raiseEvent( 'navigator-scroll', {
9317        tracker: event.eventSource,
9318        position: event.position,
9319        scroll: event.scroll,
9320        shift: event.shift,
9321        originalEvent: event.originalEvent
9322    });
9323
9324    //dont scroll the page up and down if the user is scrolling
9325    //in the navigator
9326    return false;
9327}
9328
9329
9330}( OpenSeadragon ));
9331
9332/*
9333 * OpenSeadragon - getString/setString
9334 *
9335 * Copyright (C) 2009 CodePlex Foundation
9336 * Copyright (C) 2010-2013 OpenSeadragon contributors
9337 *
9338 * Redistribution and use in source and binary forms, with or without
9339 * modification, are permitted provided that the following conditions are
9340 * met:
9341 *
9342 * - Redistributions of source code must retain the above copyright notice,
9343 *   this list of conditions and the following disclaimer.
9344 *
9345 * - Redistributions in binary form must reproduce the above copyright
9346 *   notice, this list of conditions and the following disclaimer in the
9347 *   documentation and/or other materials provided with the distribution.
9348 *
9349 * - Neither the name of CodePlex Foundation nor the names of its
9350 *   contributors may be used to endorse or promote products derived from
9351 *   this software without specific prior written permission.
9352 *
9353 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
9354 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
9355 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
9356 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
9357 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9358 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
9359 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
9360 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
9361 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
9362 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
9363 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9364 */
9365
9366(function( $ ){
9367
9368//TODO: I guess this is where the i18n needs to be reimplemented.  I'll look
9369//      into existing patterns for i18n in javascript but i think that mimicking
9370//      pythons gettext might be a reasonable approach.
9371var I18N = {
9372    Errors: {
9373        Dzc:            "Sorry, we don't support Deep Zoom Collections!",
9374        Dzi:            "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
9375        Xml:            "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
9376        ImageFormat:    "Sorry, we don't support {0}-based Deep Zoom Images.",
9377        Security:       "It looks like a security restriction stopped us from " +
9378                        "loading this Deep Zoom Image.",
9379        Status:         "This space unintentionally left blank ({0} {1}).",
9380        OpenFailed:     "Unable to open {0}: {1}"
9381    },
9382
9383    Tooltips: {
9384        FullPage:       "Toggle full page",
9385        Home:           "Go home",
9386        ZoomIn:         "Zoom in",
9387        ZoomOut:        "Zoom out",
9388        NextPage:       "Next page",
9389        PreviousPage:   "Previous page",
9390        RotateLeft:     "Rotate left",
9391        RotateRight:    "Rotate right"
9392    }
9393};
9394
9395$.extend( $, /** @lends OpenSeadragon */{
9396
9397    /**
9398     * @function
9399     * @param {String} property
9400     */
9401    getString: function( prop ) {
9402
9403        var props   = prop.split('.'),
9404            string  = null,
9405            args    = arguments,
9406            container = I18N,
9407            i;
9408
9409        for ( i = 0; i < props.length-1; i++ ) {
9410            // in case not a subproperty
9411            container = container[ props[ i ] ] || {};
9412        }
9413        string = container[ props[ i ] ];
9414
9415        if ( typeof( string ) != "string" ) {
9416            $.console.debug( "Untranslated source string:", prop );
9417            string = ""; // FIXME: this breaks gettext()-style convention, which would return source
9418        }
9419
9420        return string.replace(/\{\d+\}/g, function(capture) {
9421            var i = parseInt( capture.match( /\d+/ ), 10 ) + 1;
9422            return i < args.length ?
9423                args[ i ] :
9424                "";
9425        });
9426    },
9427
9428    /**
9429     * @function
9430     * @param {String} property
9431     * @param {*} value
9432     */
9433    setString: function( prop, value ) {
9434
9435        var props     = prop.split('.'),
9436            container = I18N,
9437            i;
9438
9439        for ( i = 0; i < props.length - 1; i++ ) {
9440            if ( !container[ props[ i ] ] ) {
9441                container[ props[ i ] ] = {};
9442            }
9443            container = container[ props[ i ] ];
9444        }
9445
9446        container[ props[ i ] ] = value;
9447    }
9448
9449});
9450
9451}( OpenSeadragon ));
9452
9453/*
9454 * OpenSeadragon - Point
9455 *
9456 * Copyright (C) 2009 CodePlex Foundation
9457 * Copyright (C) 2010-2013 OpenSeadragon contributors
9458 *
9459 * Redistribution and use in source and binary forms, with or without
9460 * modification, are permitted provided that the following conditions are
9461 * met:
9462 *
9463 * - Redistributions of source code must retain the above copyright notice,
9464 *   this list of conditions and the following disclaimer.
9465 *
9466 * - Redistributions in binary form must reproduce the above copyright
9467 *   notice, this list of conditions and the following disclaimer in the
9468 *   documentation and/or other materials provided with the distribution.
9469 *
9470 * - Neither the name of CodePlex Foundation nor the names of its
9471 *   contributors may be used to endorse or promote products derived from
9472 *   this software without specific prior written permission.
9473 *
9474 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
9475 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
9476 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
9477 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
9478 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9479 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
9480 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
9481 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
9482 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
9483 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
9484 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9485 */
9486
9487(function( $ ){
9488
9489/**
9490 * @class Point
9491 * @classdesc A Point is really used as a 2-dimensional vector, equally useful for
9492 * representing a point on a plane, or the height and width of a plane
9493 * not requiring any other frame of reference.
9494 *
9495 * @memberof OpenSeadragon
9496 * @param {Number} [x] The vector component 'x'. Defaults to the origin at 0.
9497 * @param {Number} [y] The vector component 'y'. Defaults to the origin at 0.
9498 */
9499$.Point = function( x, y ) {
9500    /**
9501     * The vector component 'x'.
9502     * @member {Number} x
9503     * @memberof OpenSeadragon.Point#
9504     */
9505    this.x = typeof ( x ) == "number" ? x : 0;
9506    /**
9507     * The vector component 'y'.
9508     * @member {Number} y
9509     * @memberof OpenSeadragon.Point#
9510     */
9511    this.y = typeof ( y ) == "number" ? y : 0;
9512};
9513
9514$.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{
9515
9516    /**
9517     * Add another Point to this point and return a new Point.
9518     * @function
9519     * @param {OpenSeadragon.Point} point The point to add vector components.
9520     * @returns {OpenSeadragon.Point} A new point representing the sum of the
9521     *  vector components
9522     */
9523    plus: function( point ) {
9524        return new $.Point(
9525            this.x + point.x,
9526            this.y + point.y
9527        );
9528    },
9529
9530    /**
9531     * Substract another Point to this point and return a new Point.
9532     * @function
9533     * @param {OpenSeadragon.Point} point The point to substract vector components.
9534     * @returns {OpenSeadragon.Point} A new point representing the substraction of the
9535     *  vector components
9536     */
9537    minus: function( point ) {
9538        return new $.Point(
9539            this.x - point.x,
9540            this.y - point.y
9541        );
9542    },
9543
9544    /**
9545     * Multiply this point by a factor and return a new Point.
9546     * @function
9547     * @param {Number} factor The factor to multiply vector components.
9548     * @returns {OpenSeadragon.Point} A new point representing the multiplication
9549     *  of the vector components by the factor
9550     */
9551    times: function( factor ) {
9552        return new $.Point(
9553            this.x * factor,
9554            this.y * factor
9555        );
9556    },
9557
9558    /**
9559     * Divide this point by a factor and return a new Point.
9560     * @function
9561     * @param {Number} factor The factor to divide vector components.
9562     * @returns {OpenSeadragon.Point} A new point representing the division of the
9563     *  vector components by the factor
9564     */
9565    divide: function( factor ) {
9566        return new $.Point(
9567            this.x / factor,
9568            this.y / factor
9569        );
9570    },
9571
9572    /**
9573     * Compute the opposite of this point and return a new Point.
9574     * @function
9575     * @returns {OpenSeadragon.Point} A new point representing the opposite of the
9576     *  vector components
9577     */
9578    negate: function() {
9579        return new $.Point( -this.x, -this.y );
9580    },
9581
9582    /**
9583     * Compute the distance between this point and another point.
9584     * @function
9585     * @param {OpenSeadragon.Point} point The point to compute the distance with.
9586     * @returns {Number} The distance between the 2 points
9587     */
9588    distanceTo: function( point ) {
9589        return Math.sqrt(
9590            Math.pow( this.x - point.x, 2 ) +
9591            Math.pow( this.y - point.y, 2 )
9592        );
9593    },
9594
9595    /**
9596     * Apply a function to each coordinate of this point and return a new point.
9597     * @function
9598     * @param {function} func The function to apply to each coordinate.
9599     * @returns {OpenSeadragon.Point} A new point with the coordinates computed
9600     * by the specified function
9601     */
9602    apply: function( func ) {
9603        return new $.Point( func( this.x ), func( this.y ) );
9604    },
9605
9606    /**
9607     * Check if this point is equal to another one.
9608     * @function
9609     * @param {OpenSeadragon.Point} point The point to compare this point with.
9610     * @returns {Boolean} true if they are equal, false otherwise.
9611     */
9612    equals: function( point ) {
9613        return (
9614            point instanceof $.Point
9615        ) && (
9616            this.x === point.x
9617        ) && (
9618            this.y === point.y
9619        );
9620    },
9621
9622    /**
9623     * Rotates the point around the specified pivot
9624     * From http://stackoverflow.com/questions/4465931/rotate-rectangle-around-a-point
9625     * @function
9626     * @param {Number} degress to rotate around the pivot.
9627     * @param {OpenSeadragon.Point} pivot Point about which to rotate.
9628     * @returns {OpenSeadragon.Point}. A new point representing the point rotated around the specified pivot
9629     */
9630    rotate: function ( degrees, pivot ) {
9631        var angle = degrees * Math.PI / 180.0,
9632            x = Math.cos( angle ) * ( this.x - pivot.x ) - Math.sin( angle ) * ( this.y - pivot.y ) + pivot.x,
9633            y = Math.sin( angle ) * ( this.x - pivot.x ) + Math.
9633cos( angle ) * ( this.y - pivot.y ) + pivot.y;
9634        return new $.Point( x, y );
9635    },
9636
9637    /**
9638     * Convert this point to a string in the format (x,y) where x and y are
9639     * rounded to the nearest integer.
9640     * @function
9641     * @returns {String} A string representation of this point.
9642     */
9643    toString: function() {
9644        return "(" + Math.round(this.x) + "," + Math.round(this.y) + ")";
9645    }
9646};
9647
9648}( OpenSeadragon ));
9649
9650/*
9651 * OpenSeadragon - TileSource
9652 *
9653 * Copyright (C) 2009 CodePlex Foundation
9654 * Copyright (C) 2010-2013 OpenSeadragon contributors
9655 *
9656 * Redistribution and use in source and binary forms, with or without
9657 * modification, are permitted provided that the following conditions are
9658 * met:
9659 *
9660 * - Redistributions of source code must retain the above copyright notice,
9661 *   this list of conditions and the following disclaimer.
9662 *
9663 * - Redistributions in binary form must reproduce the above copyright
9664 *   notice, this list of conditions and the following disclaimer in the
9665 *   documentation and/or other materials provided with the distribution.
9666 *
9667 * - Neither the name of CodePlex Foundation nor the names of its
9668 *   contributors may be used to endorse or promote products derived from
9669 *   this software without specific prior written permission.
9670 *
9671 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
9672 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
9673 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
9674 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
9675 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9676 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
9677 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
9678 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
9679 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
9680 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
9681 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9682 */
9683
9684(function( $ ){
9685
9686
9687/**
9688 * @class TileSource
9689 * @classdesc The TileSource contains the most basic implementation required to create a
9690 * smooth transition between layer in an image pyramid. It has only a single key
9691 * interface that must be implemented to complete it key functionality:
9692 * 'getTileUrl'.  It also has several optional interfaces that can be
9693 * implemented if a new TileSource wishes to support configuration via a simple
9694 * object or array ('configure') and if the tile source supports or requires
9695 * configuration via retreival of a document on the network ala AJAX or JSONP,
9696 * ('getImageInfo').
9697 * <br/>
9698 * By default the image pyramid is split into N layers where the images longest
9699 * side in M (in pixels), where N is the smallest integer which satisfies
9700 *      <strong>2^(N+1) >= M</strong>.
9701 *
9702 * @memberof OpenSeadragon
9703 * @extends OpenSeadragon.EventSource
9704 * @param {Number|Object|Array|String} width
9705 *      If more than a single argument is supplied, the traditional use of
9706 *      positional parameters is supplied and width is expected to be the width
9707 *      source image at its max resolution in pixels.  If a single argument is supplied and
9708 *      it is an Object or Array, the construction is assumed to occur through
9709 *      the extending classes implementation of 'configure'.  Finally if only a
9710 *      single argument is supplied and it is a String, the extending class is
9711 *      expected to implement 'getImageInfo' and 'configure'.
9712 * @param {Number} height
9713 *      Width of the source image at max resolution in pixels.
9714 * @param {Number} tileSize
9715 *      The size of the tiles to assumed to make up each pyramid layer in pixels.
9716 *      Tile size determines the point at which the image pyramid must be
9717 *      divided into a matrix of smaller images.
9718 * @param {Number} tileOverlap
9719 *      The number of pixels each tile is expected to overlap touching tiles.
9720 * @param {Number} minLevel
9721 *      The minimum level to attempt to load.
9722 * @param {Number} maxLevel
9723 *      The maximum level to attempt to load.
9724 */
9725$.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) {
9726    var callback = null,
9727        args = arguments,
9728        options,
9729        i;
9730
9731    if( $.isPlainObject( width ) ){
9732        options = width;
9733    }else{
9734        options = {
9735            width: args[0],
9736            height: args[1],
9737            tileSize: args[2],
9738            tileOverlap: args[3],
9739            minLevel: args[4],
9740            maxLevel: args[5]
9741        };
9742    }
9743
9744    //Tile sources supply some events, namely 'ready' when they must be configured
9745    //by asynchronously fetching their configuration data.
9746    $.EventSource.call( this );
9747
9748    //we allow options to override anything we dont treat as
9749    //required via idiomatic options or which is functionally
9750    //set depending on the state of the readiness of this tile
9751    //source
9752    $.extend( true, this, options );
9753
9754    //Any functions that are passed as arguments are bound to the ready callback
9755    /*jshint loopfunc:true*/
9756    for ( i = 0; i < arguments.length; i++ ) {
9757        if ( $.isFunction( arguments[ i ] ) ) {
9758            callback = arguments[ i ];
9759            this.addHandler( 'ready', function ( event ) {
9760                callback( event );
9761            } );
9762            //only one callback per constructor
9763            break;
9764        }
9765    }
9766
9767    /**
9768     * Ratio of width to height
9769     * @member {Number} aspectRatio
9770     * @memberof OpenSeadragon.TileSource#
9771     */
9772    /**
9773     * Vector storing x and y dimensions ( width and height respectively ).
9774     * @member {OpenSeadragon.Point} dimensions
9775     * @memberof OpenSeadragon.TileSource#
9776     */
9777    /**
9778     * The size of the image tiles used to compose the image.
9779     * @member {Number} tileSize
9780     * @memberof OpenSeadragon.TileSource#
9781     */
9782    /**
9783     * The overlap in pixels each tile shares with its adjacent neighbors.
9784     * @member {Number} tileOverlap
9785     * @memberof OpenSeadragon.TileSource#
9786     */
9787    /**
9788     * The minimum pyramid level this tile source supports or should attempt to load.
9789     * @member {Number} minLevel
9790     * @memberof OpenSeadragon.TileSource#
9791     */
9792    /**
9793     * The maximum pyramid level this tile source supports or should attempt to load.
9794     * @member {Number} maxLevel
9795     * @memberof OpenSeadragon.TileSource#
9796     */
9797    /**
9798     * 
9799     * @member {Boolean} ready
9800     * @memberof OpenSeadragon.TileSource#
9801     */
9802
9803    if( 'string' == $.type( arguments[ 0 ] ) ){
9804        //in case the getImageInfo method is overriden and/or implies an
9805        //async mechanism set some safe defaults first
9806        this.aspectRatio = 1;
9807        this.dimensions  = new $.Point( 10, 10 );
9808        this.tileSize    = 0;
9809        this.tileOverlap = 0;
9810        this.minLevel    = 0;
9811        this.maxLevel    = 0;
9812        this.ready       = false;
9813        //configuration via url implies the extending class
9814        //implements and 'configure'
9815        this.getImageInfo( arguments[ 0 ] );
9816
9817    } else {
9818
9819        //explicit configuration via positional args in constructor
9820        //or the more idiomatic 'options' object
9821        this.ready       = true;
9822        this.aspectRatio = ( options.width && options.height ) ?
9823            (  options.width / options.height ) : 1;
9824        this.dimensions  = new $.Point( options.width, options.height );
9825        this.tileSize    = options.tileSize ? options.tileSize : 0;
9826        this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0;
9827        this.minLevel    = options.minLevel ? options.minLevel : 0;
9828        this.maxLevel    = ( undefined !== options.maxLevel && null !== options.maxLevel ) ?
9829            options.maxLevel : (
9830                ( options.width && options.height ) ? Math.ceil(
9831                    Math.log( Math.max( options.width, options.height ) ) /
9832                    Math.log( 2 )
9833                ) : 0
9834            );
9835        if( callback && $.isFunction( callback ) ){
9836            callback( this );
9837        }
9838    }
9839
9840
9841};
9842
9843
9844$.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{
9845
9846    /**
9847     * @function
9848     * @param {Number} level
9849     */
9850    getLevelScale: function( level ) {
9851
9852        // see https://github.com/openseadragon/openseadragon/issues/22
9853        // we use the tilesources implementation of getLevelScale to generate
9854        // a memoized re-implementation
9855        var levelScaleCache = {},
9856            i;
9857        for( i = 0; i <= this.maxLevel; i++ ){
9858            levelScaleCache[ i ] = 1 / Math.pow(2, this.maxLevel - i);
9859        }
9860        this.getLevelScale = function( _level ){
9861            return levelScaleCache[ _level ];
9862        };
9863        return this.getLevelScale( level );
9864    },
9865
9866    /**
9867     * @function
9868     * @param {Number} level
9869     */
9870    getNumTiles: function( level ) {
9871        var scale = this.getLevelScale( level ),
9872            x = Math.ceil( scale * this.dimensions.x / this.tileSize ),
9873            y = Math.ceil( scale * this.dimensions.y / this.tileSize );
9874
9875        return new $.Point( x, y );
9876    },
9877
9878    /**
9879     * @function
9880     * @param {Number} level
9881     */
9882    getPixelRatio: function( level ) {
9883        var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ),
9884            rx = 1.0 / imageSizeScaled.x,
9885            ry = 1.0 / imageSizeScaled.y;
9886
9887        return new $.Point(rx, ry);
9888    },
9889
9890
9891    /**
9892     * @function
9893     * @param {Number} level
9894     */
9895    getClosestLevel: function( rect ) {
9896        var i,
9897            tilesPerSide = Math.floor( Math.max( rect.x, rect.y ) / this.tileSize ),
9898            tiles;
9899        for( i = this.minLevel; i < this.maxLevel; i++ ){
9900            tiles = this.getNumTiles( i );
9901            if( Math.max( tiles.x, tiles.y ) + 1 >= tilesPerSide ){
9902                break;
9903            }
9904        }
9905        return Math.max( 0, i - 1 );
9906    },
9907
9908    /**
9909     * @function
9910     * @param {Number} level
9911     * @param {OpenSeadragon.Point} point
9912     */
9913    getTileAtPoint: function( level, point ) {
9914        var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ),
9915            tx = Math.floor( pixel.x / this.tileSize ),
9916            ty = Math.floor( pixel.y / this.tileSize );
9917
9918        return new $.Point( tx, ty );
9919    },
9920
9921    /**
9922     * @function
9923     * @param {Number} level
9924     * @param {Number} x
9925     * @param {Number} y
9926     */
9927    getTileBounds: function( level, x, y ) {
9928        var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
9929            px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap,
9930            py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap,
9931            sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap,
9932            sy = this.tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap,
9933            scale = 1.0 / dimensionsScaled.x;
9934
9935        sx = Math.min( sx, dimensionsScaled.x - px );
9936        sy = Math.min( sy, dimensionsScaled.y - py );
9937
9938        return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
9939    },
9940
9941
9942    /**
9943     * Responsible for retrieving, and caching the
9944     * image metadata pertinent to this TileSources implementation.
9945     * @function
9946     * @param {String} url
9947     * @throws {Error}
9948     */
9949    getImageInfo: function( url ) {
9950        var _this = this,
9951            callbackName,
9952            callback,
9953            readySource,
9954            options,
9955            urlParts,
9956            filename,
9957            lastDot;
9958
9959
9960        if( url ) {
9961            urlParts = url.split( '/' );
9962            filename = urlParts[ urlParts.length - 1 ];
9963            lastDot  = filename.lastIndexOf( '.' );
9964            if ( lastDot > -1 ) {
9965                urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot );
9966            }
9967        }
9968
9969        callback = function( data ){
9970            if( typeof(data) === "string" ) {
9971                data = $.parseXml( data );
9972            }
9973            var $TileSource = $.TileSource.determineType( _this, data, url );
9974            if ( !$TileSource ) {
9975                /**
9976                 * Raised when an error occurs loading a TileSource.
9977                 *
9978                 * @event open-failed
9979                 * @memberof OpenSeadragon.TileSource
9980                 * @type {object}
9981                 * @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event.
9982                 * @property {String} message
9983                 * @property {String} source
9984                 * @property {?Object} userData - Arbitrary subscriber-defined object.
9985                 */
9986                _this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } );
9987                return;
9988            }
9989
9990            options = $TileSource.prototype.configure.apply( _this, [ data, url ]);
9991            readySource = new $TileSource( options );
9992            _this.ready = true;
9993            /**
9994             * Raised when a TileSource is opened and initialized.
9995             *
9996             * @event ready
9997             * @memberof OpenSeadragon.TileSource
9998             * @type {object}
9999             * @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event.
10000             * @property {Object} tileSource
10001             * @property {?Object} userData - Arbitrary subscriber-defined object.
10002             */
10003            _this.raiseEvent( 'ready', { tileSource: readySource } );
10004        };
10005
10006        if( url.match(/\.js$/) ){
10007            //TODO: Its not very flexible to require tile sources to end jsonp
10008            //      request for info  with a url that ends with '.js' but for
10009            //      now it's the only way I see to distinguish uniformly.
10010            callbackName = url.split( '/' ).pop().replace('.js','');
10011            $.jsonp({
10012                url: url,
10013                async: false,
10014                callbackName: callbackName,
10015                callback: callback
10016            });
10017        } else {
10018            // request info via xhr asynchronously.
10019            $.makeAjaxRequest( url, function( xhr ) {
10020                var data = processResponse( xhr );
10021                callback( data );
10022            }, function ( xhr, exc ) {
10023                var msg;
10024
10025                /*
10026                    IE < 10 will block XHR requests to different origins. Any property access on the request
10027                    object will raise an exception which we'll attempt to handle by formatting the original
10028                    exception rather than the second one raised when we try to access xhr.status
10029                 */
10030                try {
10031                    msg = "HTTP " + xhr.status + " attempting to load TileSource";
10032                } catch ( e ) {
10033                    var formattedExc;
10034                    if ( typeof( exc ) == "undefined" || !exc.toString ) {
10035                        formattedExc = "Unknown error";
10036                    } else {
10037                        formattedExc = exc.toString();
10038                    }
10039
10040                    msg = formattedExc + " attempting to load TileSource";
10041                }
10042
10043                /***
10044                 * Raised when an error occurs loading a TileSource.
10045                 *
10046                 * @event open-failed
10047                 * @memberof OpenSeadragon.TileSource
10048                 * @type {object}
10049                 * @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event.
10050                 * @property {String} message
10051                 * @property {String} source
10052                 * @property {?Object} userData - Arbitrary subscriber-defined object.
10053                 */
10054                _this.raiseEvent( 'open-failed', {
10055                    message: msg,
10056                    source: url
10057                });
10058            });
10059        }
10060
10061    },
10062
10063    /**
10064     * Responsible determining if a the particular TileSource supports the
10065     * data format ( and allowed to apply logic against the url the data was
10066     * loaded from, if any ). Overriding implementations are expected to do
10067     * something smart with data and / or url to determine support.  Also
10068     * understand that iteration order of TileSources is not guarunteed so
10069     * please make sure your data or url is expressive enough to ensure a simple
10070     * and sufficient mechanisim for clear determination.
10071     * @function
10072     * @param {String|Object|Array|Document} data
10073     * @param {String} url - the url the data was loaded
10074     *      from if any.
10075     * @return {Boolean}
10076     */
10077    supports: function( data, url ) {
10078        return false;
10079    },
10080
10081    /**
10082     * Responsible for parsing and configuring the
10083     * image metadata pertinent to this TileSources implementation.
10084     * This method is not implemented by this class other than to throw an Error
10085     * announcing you have to implement it.  Because of the variety of tile
10086     * server technologies, and various specifications for building image
10087     * pyramids, this method is here to allow easy integration.
10088     * @function
10089     * @param {String|Object|Array|Document} data
10090     * @param {String} url - the url the data was loaded
10091     *      from if any.
10092     * @return {Object} options - A dictionary of keyword arguments sufficient
10093     *      to configure this tile sources constructor.
10094     * @throws {Error}
10095     */
10096    configure: function( data, url ) {
10097        throw new Error( "Method not implemented." );
10098    },
10099
10100    /**
10101     * Responsible for retriving the url which will return an image for the
10102     * region speified by the given x, y, and level components.
10103     * This method is not implemented by this class other than to throw an Error
10104     * announcing you have to implement it.  Because of the variety of tile
10105     * server technologies, and various specifications for building image
10106     * pyramids, this method is here to allow easy integration.
10107     * @function
10108     * @param {Number} level
10109     * @param {Number} x
10110     * @param {Number} y
10111     * @throws {Error}
10112     */
10113    getTileUrl: function( level, x, y ) {
10114        throw new Error( "Method not implemented." );
10115    },
10116
10117    /**
10118     * @function
10119     * @param {Number} level
10120     * @param {Number} x
10121     * @param {Number} y
10122     */
10123    tileExists: function( level, x, y ) {
10124        var numTiles = this.getNumTiles( level );
10125        return  level >= this.minLevel &&
10126                level <= this.maxLevel &&
10127                x >= 0 &&
10128                y >= 0 &&
10129                x < numTiles.x &&
10130                y < numTiles.y;
10131    }
10132};
10133
10134
10135$.extend( true, $.TileSource.prototype, $.EventSource.prototype );
10136
10137
10138/**
10139 * Decides whether to try to process the response as xml, json, or hand back
10140 * the text
10141 * @private
10142 * @inner
10143 * @function
10144 * @param {XMLHttpRequest} xhr - the completed network request
10145 */
10146function processResponse( xhr ){
10147    var responseText = xhr.responseText,
10148        status       = xhr.status,
10149        statusText,
10150        data;
10151
10152    if ( !xhr ) {
10153        throw new Error( $.getString( "Errors.Security" ) );
10154    } else if ( xhr.status !== 200 && xhr.status !== 0 ) {
10155        status     = xhr.status;
10156        statusText = ( status == 404 ) ?
10157            "Not Found" :
10158            xhr.statusText;
10159        throw new Error( $.getString( "Errors.Status", status, statusText ) );
10160    }
10161
10162    if( responseText.match(/\s*<.*/) ){
10163        try{
10164        data = ( xhr.responseXML && xhr.responseXML.documentElement ) ?
10165            xhr.responseXML :
10166            $.parseXml( responseText );
10167        } catch (e){
10168            data = xhr.responseText;
10169        }
10170    }else if( responseText.match(/\s*[\{\[].*/) ){
10171        /*jshint evil:true*/
10172        data = eval( '('+responseText+')' );
10173    }else{
10174        data = responseText;
10175    }
10176    return data;
10177}
10178
10179
10180/**
10181 * Determines the TileSource Implementation by introspection of OpenSeadragon
10182 * namespace, calling each TileSource implementation of 'isType'
10183 * @private
10184 * @inner
10185 * @function
10186 * @param {Object|Array|Document} data - the tile source configuration object
10187 * @param {String} url - the url where the tile source configuration object was
10188 *      loaded from, if any.
10189 */
10190$.TileSource.determineType = function( tileSource, data, url ){
10191    var property;
10192    for( property in OpenSeadragon ){
10193        if( property.match(/.+TileSource$/) &&
10194            $.isFunction( OpenSeadragon[ property ] ) &&
10195            $.isFunction( OpenSeadragon[ property ].prototype.supports ) &&
10196            OpenSeadragon[ property ].prototype.supports.call( tileSource, data, url )
10197        ){
10198            return OpenSeadragon[ property ];
10199        }
10200    }
10201
10202    $.console.error( "No TileSource was able to open %s %s", url, data );
10203};
10204
10205
10206}( OpenSeadragon ));
10207
10208/*
10209 * OpenSeadragon - DziTileSource
10210 *
10211 * Copyright (C) 2009 CodePlex Foundation
10212 * Copyright (C) 2010-2013 OpenSeadragon contributors
10213 *
10214 * Redistribution and use in source and binary forms, with or without
10215 * modification, are permitted provided that the following conditions are
10216 * met:
10217 *
10218 * - Redistributions of source code must retain the above copyright notice,
10219 *   this list of conditions and the following disclaimer.
10220 *
10221 * - Redistributions in binary form must reproduce the above copyright
10222 *   notice, this list of conditions and the following disclaimer in the
10223 *   documentation and/or other materials provided with the distribution.
10224 *
10225 * - Neither the name of CodePlex Foundation nor the names of its
10226 *   contributors may be used to endorse or promote products derived from
10227 *   this software without specific prior written permission.
10228 *
10229 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10230 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
10231 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
10232 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
10233 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10234 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
10235 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
10236 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
10237 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
10238 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
10239 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10240 */
10241
10242(function( $ ){
10243
10244/**
10245 * @class DziTileSource
10246 * @memberof OpenSeadragon
10247 * @extends OpenSeadragon.TileSource
10248 * @param {Number|Object} width - the pixel width of the image or the idiomatic
10249 *      options object which is used instead of positional arguments.
10250 * @param {Number} height
10251 * @param {Number} tileSize
10252 * @param {Number} tileOverlap
10253 * @param {String} tilesUrl
10254 * @param {String} fileFormat
10255 * @param {OpenSeadragon.DisplayRect[]} displayRects
10256 * @property {String} tilesUrl
10257 * @property {String} fileFormat
10258 * @property {OpenSeadragon.DisplayRect[]} displayRects
10259 */
10260$.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects, minLevel, maxLevel ) {
10261    var i,
10262        rect,
10263        level,
10264        options;
10265
10266    if( $.isPlainObject( width ) ){
10267        options = width;
10268    }else{
10269        options = {
10270            width: arguments[ 0 ],
10271            height: arguments[ 1 ],
10272            tileSize: arguments[ 2 ],
10273            tileOverlap: arguments[ 3 ],
10274            tilesUrl: arguments[ 4 ],
10275            fileFormat: arguments[ 5 ],
10276            displayRects: arguments[ 6 ],
10277            minLevel: arguments[ 7 ],
10278            maxLevel: arguments[ 8 ]
10279        };
10280    }
10281
10282    this._levelRects  = {};
10283    this.tilesUrl     = options.tilesUrl;
10284    this.fileFormat   = options.fileFormat;
10285    this.displayRects = options.displayRects;
10286
10287    if ( this.displayRects ) {
10288        for ( i = this.displayRects.length - 1; i >= 0; i-- ) {
10289            rect = this.displayRects[ i ];
10290            for ( level = rect.minLevel; level <= rect.maxLevel; level++ ) {
10291                if ( !this._levelRects[ level ] ) {
10292                    this._levelRects[ level ] = [];
10293                }
10294                this._levelRects[ level ].push( rect );
10295            }
10296        }
10297    }
10298
10299    $.TileSource.apply( this, [ options ] );
10300
10301};
10302
10303$.extend( $.DziTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.DziTileSource.prototype */{
10304
10305
10306    /**
10307     * Determine if the data and/or url imply the image service is supported by
10308     * this tile source.
10309     * @function
10310     * @param {Object|Array} data
10311     * @param {String} optional - url
10312     */
10313    supports: function( data, url ){
10314        var ns;
10315        if ( data.Image ) {
10316            ns = data.Image.xmlns;
10317        } else if ( data.documentElement && "Image" == data.documentElement.tagName ) {
10318            ns = data.documentElement.namespaceURI;
10319        }
10320
10321        return ( "http://schemas.microsoft.com/deepzoom/2008" == ns ||
10322            "http://schemas.microsoft.com/deepzoom/2009" == ns );
10323    },
10324
10325    /**
10326     *
10327     * @function
10328     * @param {Object|XMLDocument} data - the raw configuration
10329     * @param {String} url - the url the data was retreived from if any.
10330     * @return {Object} options - A dictionary of keyword arguments sufficient
10331     *      to configure this tile sources constructor.
10332     */
10333    configure: function( data, url ){
10334
10335        var options;
10336
10337        if( !$.isPlainObject(data) ){
10338
10339            options = configureFromXML( this, data );
10340
10341        }else{
10342
10343            options = configureFromObject( this, data );
10344        }
10345
10346        if (url && !options.tilesUrl) {
10347            options.tilesUrl = url.replace(/([^\/]+)\.(dzi|xml|js)(\?.*|$)/, '$1_files/');
10348
10349            if (url.search(/\.(dzi|xml|js)\?/) != -1) {
10350                options.queryParams = url.match(/\?.*/);
10351            }else{
10352                options.queryParams = '';
10353            }
10354        }
10355
10356        return options;
10357    },
10358
10359
10360    /**
10361     * @function
10362     * @param {Number} level
10363     * @param {Number} x
10364     * @param {Number} y
10365     */
10366    getTileUrl: function( level, x, y ) {
10367        return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat, this.queryParams ].join( '' );
10368    },
10369
10370
10371    /**
10372     * @function
10373     * @param {Number} level
10374     * @param {Number} x
10375     * @param {Number} y
10376     */
10377    tileExists: function( level, x, y ) {
10378        var rects = this._levelRects[ level ],
10379            rect,
10380            scale,
10381            xMin,
10382            yMin,
10383            xMax,
10384            yMax,
10385            i;
10386
10387        if ( !rects || !rects.length ) {
10388            return true;
10389        }
10390
10391        for ( i = rects.length - 1; i >= 0; i-- ) {
10392            rect = rects[ i ];
10393
10394            if ( level < rect.minLevel || level > rect.maxLevel ) {
10395                continue;
10396            }
10397
10398            scale = this.getLevelScale( level );
10399            xMin = rect.x * scale;
10400            yMin = rect.y * scale;
10401            xMax = xMin + rect.width * scale;
10402            yMax = yMin + rect.height * scale;
10403
10404            xMin = Math.floor( xMin / this.tileSize );
10405            yMin = Math.floor( yMin / this.tileSize );
10406            xMax = Math.ceil( xMax / this.tileSize );
10407            yMax = Math.ceil( yMax / this.tileSize );
10408
10409            if ( xMin <= x && x < xMax && yMin <= y && y < yMax ) {
10410                return true;
10411            }
10412        }
10413
10414        return false;
10415    }
10416});
10417
10418
10419/**
10420 * @private
10421 * @inner
10422 * @function
10423 */
10424function configureFromXML( tileSource, xmlDoc ){
10425
10426    if ( !xmlDoc || !xmlDoc.documentElement ) {
10427        throw new Error( $.getString( "Errors.Xml" ) );
10428    }
10429
10430    var root           = xmlDoc.documentElement,
10431        rootName       = root.tagName,
10432        configuration  = null,
10433        displayRects   = [],
10434        dispRectNodes,
10435        dispRectNode,
10436        rectNode,
10437        sizeNode,
10438        i;
10439
10440    if ( rootName == "Image" ) {
10441
10442        try {
10443            sizeNode = root.getElementsByTagName( "Size" )[ 0 ];
10444            configuration = {
10445                Image: {
10446                    xmlns:       "http://schemas.microsoft.com/deepzoom/2008",
10447                    Url:         root.getAttribute( "Url" ),
10448                    Format:      root.getAttribute( "Format" ),
10449                    DisplayRect: null,
10450                    Overlap:     parseInt( root.getAttribute( "Overlap" ), 10 ),
10451                    TileSize:    parseInt( root.getAttribute( "TileSize" ), 10 ),
10452                    Size: {
10453                        Height: parseInt( sizeNode.getAttribute( "Height" ), 10 ),
10454                        Width:  parseInt( sizeNode.getAttribute( "Width" ), 10 )
10455                    }
10456                }
10457            };
10458
10459            if ( !$.imageFormatSupported( configuration.Image.Format ) ) {
10460                throw new Error(
10461                    $.getString( "Errors.ImageFormat", configuration.Image.Format.toUpperCase() )
10462                );
10463            }
10464
10465            dispRectNodes = root.getElementsByTagName( "DisplayRect" );
10466            for ( i = 0; i < dispRectNodes.length; i++ ) {
10467                dispRectNode = dispRectNodes[ i ];
10468                rectNode     = dispRectNode.getElementsByTagName( "Rect" )[ 0 ];
10469
10470                displayRects.push({
10471                    Rect: {
10472                        X: parseInt( rectNode.getAttribute( "X" ), 10 ),
10473                        Y: parseInt( rectNode.getAttribute( "Y" ), 10 ),
10474                        Width: parseInt( rectNode.getAttribute( "Width" ), 10 ),
10475                        Height: parseInt( rectNode.getAttribute( "Height" ), 10 ),
10476                        MinLevel: parseInt( dispRectNode.getAttribute( "MinLevel" ), 10 ),
10477                        MaxLevel: parseInt( dispRectNode.getAttribute( "MaxLevel" ), 10 )
10478                    }
10479                });
10480            }
10481
10482            if( displayRects.length ){
10483                configuration.Image.DisplayRect = displayRects;
10484            }
10485
10486            return configureFromObject( tileSource, configuration );
10487
10488        } catch ( e ) {
10489            throw (e instanceof Error) ?
10490                e :
10491                new Error( $.getString("Errors.Dzi") );
10492        }
10493    } else if ( rootName == "Collection" ) {
10494        throw new Error( $.getString( "Errors.Dzc" ) );
10495    } else if ( rootName == "Error" ) {
10496        return $._processDZIError( root );
10497    }
10498
10499    throw new Error( $.getString( "Errors.Dzi" ) );
10500}
10501
10502/**
10503 * @private
10504 * @inner
10505 * @function
10506 */
10507function configureFromObject( tileSource, configuration ){
10508    var imageData     = configuration.Image,
10509        tilesUrl      = imageData.Url,
10510        fileFormat    = imageData.Format,
10511        sizeData      = imageData.Size,
10512        dispRectData  = imageData.DisplayRect || [],
10513        width         = parseInt( sizeData.Width, 10 ),
10514        height        = parseInt( sizeData.Height, 10 ),
10515        tileSize      = parseInt( imageData.TileSize, 10 ),
10516        tileOverlap   = parseInt( imageData.Overlap, 10 ),
10517        displayRects  = [],
10518        rectData,
10519        i;
10520
10521    //TODO: need to figure out out to better handle image format compatibility
10522    //      which actually includes additional file formats like xml and pdf
10523    //      and plain text for various tilesource implementations to avoid low
10524    //      level errors.
10525    //
10526    //      For now, just don't perform the check.
10527    //
10528    /*if ( !imageFormatSupported( fileFormat ) ) {
10529        throw new Error(
10530            $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() )
10531        );
10532    }*/
10533
10534    for ( i = 0; i < dispRectData.length; i++ ) {
10535        rectData = dispRectData[ i ].Rect;
10536
10537        displayRects.push( new $.DisplayRect(
10538            parseInt( rectData.X, 10 ),
10539            parseInt( rectData.Y, 10 ),
10540            parseInt( rectData.Width, 10 ),
10541            parseInt( rectData.Height, 10 ),
10542            parseInt( rectData.MinLevel, 10 ),
10543            parseInt( rectData.MaxLevel, 10 )
10544        ));
10545    }
10546
10547    return $.extend(true, {
10548        width: width, /* width *required */
10549        height: height, /* height *required */
10550        tileSize: tileSize, /* tileSize *required */
10551        tileOverlap: tileOverlap, /* tileOverlap *required */
10552        minLevel: null, /* minLevel */
10553        maxLevel: null, /* maxLevel */
10554        tilesUrl: tilesUrl, /* tilesUrl */
10555        fileFormat: fileFormat, /* fileFormat */
10556        displayRects: displayRects /* displayRects */
10557    }, configuration );
10558
10559}
10560
10561}( OpenSeadragon ));
10562
10563/*
10564 * OpenSeadragon - IIIFTileSource
10565 *
10566 * Copyright (C) 2009 CodePlex Foundation
10567 * Copyright (C) 2010-2013 OpenSeadragon contributors
10568 *
10569 * Redistribution and use in source and binary forms, with or without
10570 * modification, are permitted provided that the following conditions are
10571 * met:
10572 *
10573 * - Redistributions of source code must retain the above copyright notice,
10574 *   this list of conditions and the following disclaimer.
10575 *
10576 * - Redistributions in binary form must reproduce the above copyright
10577 *   notice, this list of conditions and the following disclaimer in the
10578 *   documentation and/or other materials provided with the distribution.
10579 *
10580 * - Neither the name of CodePlex Foundation nor the names of its
10581 *   contributors may be used to endorse or promote products derived from
10582 *   this software without specific prior written permission.
10583 *
10584 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10585 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
10586 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
10587 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
10588 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10589 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
10590 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
10591 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
10592 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
10593 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
10594 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10595 */
10596
10597/*
10598 * The getTileUrl implementation is based on Jon Stroop's Python version,
10599 * which is released under the New BSD license:
10600 * https://gist.github.com/jpstroop/4624253
10601 */
10602
10603
10604(function( $ ){
10605
10606/**
10607 * @class IIIFTileSource
10608 * @classdesc A client implementation of the International Image Interoperability
10609 * Format: Image API Draft 0.2
10610 *
10611 * @memberof OpenSeadragon
10612 * @extends OpenSeadragon.TileSource
10613 * @see http://library.stanford.edu/iiif/image-api/
10614 */
10615$.IIIFTileSource = function( options ){
10616
10617    $.extend( true, this, options );
10618
10619    if( !(this.height && this.width && this.identifier && this.tilesUrl ) ){
10620        throw new Error('IIIF required parameters not provided.');
10621    }
10622
10623    //TODO: at this point the base tile source implementation assumes
10624    //      a tile is a square and so only has one property tileSize
10625    //      to store it.  It may be possible to make tileSize a vector
10626    //      OpenSeadraon.Point but would require careful implementation
10627    //      to preserve backward compatibility.
10628    options.tileSize = this.tile_width;
10629
10630    if (! options.maxLevel ) {
10631        var mf = -1;
10632        var scfs = this.scale_factors || this.scale_factor;
10633        if ( scfs instanceof Array ) {
10634            for ( var i = 0; i < scfs.length; i++ ) {
10635                var cf = Number( scfs[i] );
10636                if ( !isNaN( cf ) && cf > mf ) { mf = cf; }
10637            }
10638        }
10639        if ( mf < 0 ) { options.maxLevel = Number(Math.ceil(Math.log(Math.max(this.width, this.height), 2))); }
10640        else { options.maxLevel = mf; }
10641    }
10642
10643    $.TileSource.apply( this, [ options ] );
10644};
10645
10646$.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.IIIFTileSource.prototype */{
10647    /**
10648     * Determine if the data and/or url imply the image service is supported by
10649     * this tile source.
10650     * @method
10651     * @param {Object|Array} data
10652     * @param {String} optional - url
10653     */
10654    supports: function( data, url ){
10655        return (
10656            data.ns &&
10657            "http://library.stanford.edu/iiif/image-api/ns/" == data.ns
10658        ) || (
10659            data.profile && (
10660                "http://library.stanford.edu/iiif/image-api/compliance.html#level1" == data.profile ||
10661                "http://library.stanford.edu/iiif/image-api/compliance.html#level2" == data.profile ||
10662                "http://library.stanford.edu/iiif/image-api/compliance.html#level3" == data.profile ||
10663                "http://library.stanford.edu/iiif/image-api/compliance.html" == data.profile
10664            )
10665        ) || (
10666            data.documentElement &&
10667            "info" == data.documentElement.tagName &&
10668            "http://library.stanford.edu/iiif/image-api/ns/" ==
10669                data.documentElement.namespaceURI
10670        );
10671    },
10672
10673   /**
10674     *
10675     * @method
10676     * @param {Object|XMLDocument} data - the raw configuration
10677     * @param {String} url - the url the data was retreived from if any.
10678     * @return {Object} options - A dictionary of keyword arguments sufficient
10679     *      to configure this tile source via its constructor.
10680     */
10681    configure: function( data, url ){
10682        var service,
10683            options,
10684            host;
10685
10686        if( !$.isPlainObject(data) ){
10687
10688            options = configureFromXml( this, data );
10689
10690        }else{
10691
10692            options = configureFromObject( this, data );
10693        }
10694
10695        if( url && !options.tilesUrl ){
10696            service = url.split('/');
10697            service.pop(); //info.json or info.xml
10698            service = service.join('/');
10699            if( 'http' !== url.substring( 0, 4 ) ){
10700                host = location.protocol + '//' + location.host;
10701                service = host + service;
10702            }
10703            options.tilesUrl = service.replace(
10704                data.identifier,
10705                ''
10706            );
10707        }
10708
10709        return options;
10710    },
10711
10712    /**
10713     * Responsible for retreiving the url which will return an image for the
10714     * region speified by the given x, y, and level components.
10715     * @method
10716     * @param {Number} level - z index
10717     * @param {Number} x
10718     * @param {Number} y
10719     * @throws {Error}
10720     */
10721    getTileUrl: function( level, x, y ){
10722
10723        //# constants
10724        var IIIF_ROTATION = '0',
10725            IIIF_QUALITY = 'native.jpg',
10726
10727            //## get the scale (level as a decimal)
10728            scale = Math.pow( 0.5, this.maxLevel - level ),
10729
10730            //## get iiif size
10731            // iiif_size = 'pct:' + ( scale * 100 ),
10732
10733            //# image dimensions at this level
10734            level_width = Math.ceil( this.width * scale ),
10735            level_height = Math.ceil( this.height * scale ),
10736
10737            //## iiif region
10738            iiif_tile_size_width = Math.ceil( this.tileSize / scale ),
10739            iiif_tile_size_height = Math.ceil( this.tileSize / scale ),
10740            iiif_region,
10741            iiif_tile_x,
10742            iiif_tile_y,
10743            iiif_tile_w,
10744            iiif_tile_h,
10745            iiif_size;
10746
10747
10748        if ( level_width < this.tile_width && level_height < this.tile_height ){
10749            iiif_size = level_width + ","; // + level_height; only one dim. for IIIF level 1 compliance
10750            iiif_region = 'full';
10751        } else {
10752            iiif_tile_x = x * iiif_tile_size_width;
10753            iiif_tile_y = y * iiif_tile_size_height;
10754            iiif_tile_w = Math.min( iiif_tile_size_width, this.width - iiif_tile_x );
10755            iiif_tile_h = Math.min( iiif_tile_size_height, this.height - iiif_tile_y );
10756            iiif_size = Math.ceil(iiif_tile_w * scale) + ",";
10757            iiif_region = [ iiif_tile_x, iiif_tile_y, iiif_tile_w, iiif_tile_h ].join(',');
10758        }
10759
10760        return [
10761            this.tilesUrl,
10762            this.identifier,
10763            iiif_region,
10764            iiif_size,
10765            IIIF_ROTATION,
10766            IIIF_QUALITY
10767        ].join('/');
10768    }
10769
10770
10771});
10772
10773/**
10774 * @private
10775 * @inner
10776 * @function
10777 * @example
10778 *   <?xml version="1.0" encoding="UTF-8"?>
10779 *   <info xmlns="http://library.stanford.edu/iiif/image-api/ns/">
10780 *     <identifier>1E34750D-38DB-4825-A38A-B60A345E591C</identifier>
10781 *     <width>6000</width>
10782 *     <height>4000</height>
10783 *     <scale_factors>
10784 *       <scale_factor>1</scale_factor>
10785 *       <scale_factor>2</scale_factor>
10786 *       <scale_factor>4</scale_factor>
10787 *     </scale_factors>
10788 *     <tile_width>1024</tile_width>
10789 *     <tile_height>1024</tile_height>
10790 *     <formats>
10791 *       <format>jpg</format>
10792 *       <format>png</format>
10793 *     </formats>
10794 *     <qualities>
10795 *       <quality>native</quality>
10796 *       <quality>grey</quality>
10797 *     </qualities>
10798 *   </info>
10799 */
10800function configureFromXml( tileSource, xmlDoc ){
10801
10802    //parse the xml
10803    if ( !xmlDoc || !xmlDoc.documentElement ) {
10804        throw new Error( $.getString( "Errors.Xml" ) );
10805    }
10806
10807    var root            = xmlDoc.documentElement,
10808        rootName        = root.tagName,
10809        configuration   = null;
10810
10811    if ( rootName == "info" ) {
10812
10813        try {
10814
10815            configuration = {
10816                "ns": root.namespaceURI
10817            };
10818
10819            parseXML( root, configuration );
10820
10821            return configureFromObject( tileSource, configuration );
10822
10823        } catch ( e ) {
10824            throw (e instanceof Error) ?
10825                e :
10826                new Error( $.getString("Errors.IIIF") );
10827        }
10828    }
10829
10830    throw new Error( $.getString( "Errors.IIIF" ) );
10831
10832}
10833
10834
10835/**
10836 * @private
10837 * @inner
10838 * @function
10839 */
10840function parseXML( node, configuration, property ){
10841    var i,
10842        value;
10843    if( node.nodeType == 3 && property ){//text node
10844        value = node.nodeValue.trim();
10845        if( value.match(/^\d*$/)){
10846            value = Number( value );
10847        }
10848        if( !configuration[ property ] ){
10849            configuration[ property ] = value;
10850        }else{
10851            if( !$.isArray( configuration[ property ] ) ){
10852                configuration[ property ] = [ configuration[ property ] ];
10853            }
10854            configuration[ property ].push( value );
10855        }
10856    } else if( node.nodeType == 1 ){
10857        for( i = 0; i < node.childNodes.length; i++ ){
10858            parseXML( node.childNodes[ i ], configuration, node.nodeName );
10859        }
10860    }
10861}
10862
10863
10864/**
10865 * @private
10866 * @inner
10867 * @function
10868 * @example
10869 *   {
10870 *       "profile" : "http://library.stanford.edu/iiif/image-api/compliance.html#level1",
10871 *       "identifier" : "1E34750D-38DB-4825-A38A-B60A345E591C",
10872 *       "width" : 6000,
10873 *       "height" : 4000,
10874 *       "scale_factors" : [ 1, 2, 4 ],
10875 *       "tile_width" : 1024,
10876 *       "tile_height" : 1024,
10877 *       "formats" : [ "jpg", "png" ],
10878 *       "quality" : [ "native", "grey" ]
10879 *   }
10880 */
10881function configureFromObject( tileSource, configuration ){
10882    //the image_host property is not part of the iiif standard but is included here to
10883    //allow the info.json and info.xml specify a different server to load the
10884    //images from so we can test the implementation.
10885    if( configuration.image_host ){
10886        configuration.tilesUrl = configuration.image_host;
10887    }
10888    return configuration;
10889}
10890
10891}( OpenSeadragon ));
10892
10893/*
10894 * OpenSeadragon - IIIF1_1TileSource
10895 *
10896 * Copyright (C) 2009 CodePlex Foundation
10897 * Copyright (C) 2010-2013 OpenSeadragon contributors
10898 *
10899 * Redistribution and use in source and binary forms, with or without
10900 * modification, are permitted provided that the following conditions are
10901 * met:
10902 *
10903 * - Redistributions of source code must retain the above copyright notice,
10904 *   this list of conditions and the following disclaimer.
10905 *
10906 * - Redistributions in binary form must reproduce the above copyright
10907 *   notice, this list of conditions and the following disclaimer in the
10908 *   documentation and/or other materials provided with the distribution.
10909 *
10910 * - Neither the name of CodePlex Foundation nor the names of its
10911 *   contributors may be used to endorse or promote products derived from
10912 *   this software without specific prior written permission.
10913 *
10914 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10915 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
10916 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
10917 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
10918 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10919 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
10920 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
10921 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
10922 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
10923 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
10924 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10925 */
10926
10927(function( $ ){
10928
10929/**
10930 * @class IIIF1_1TileSource
10931 * @classdesc A client implementation of the International Image Interoperability
10932 * Format: Image API 1.1
10933 *
10934 * @memberof OpenSeadragon
10935 * @extends OpenSeadragon.TileSource
10936 * @see http://library.stanford.edu/iiif/image-api/
10937 */
10938$.IIIF1_1TileSource = function( options ){
10939
10940
10941    $.extend( true, this, options );
10942
10943
10944    if ( !( this.height && this.width && this['@id'] ) ){
10945        throw new Error( 'IIIF required parameters not provided.' );
10946    }
10947
10948    if ( ( this.profile &&
10949        this.profile == "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0" ) ){
10950        // what if not reporting a profile?
10951        throw new Error( 'IIIF Image API 1.1 compliance level 1 or greater is required.' );
10952    }
10953
10954    if ( this.tile_width ) {
10955        options.tileSize = this.tile_width;
10956    } else if ( this.tile_height ) {
10957        options.tileSize = this.tile_height;
10958    } else {
10959        // use the largest of tileOptions that is smaller than the short
10960        // dimension
10961
10962        var shortDim = Math.min( this.height, this.width ),
10963            tileOptions = [256,512,1024],
10964            smallerTiles = [];
10965
10966            for ( var c = 0; c < tileOptions.length; c++ ) {
10967                if ( tileOptions[c] <= shortDim ) {
10968                    smallerTiles.push( tileOptions[c] );
10969                }
10970            }
10971
10972        if ( smallerTiles.length > 0 ) {
10973            options.tileSize = Math.max.apply( null, smallerTiles );
10974        } else {
10975            // If we're smaller than 256, just use the short side.
10976            options.tileSize = shortDim;
10977        }
10978        this.tile_width = options.tileSize;  // So that 'full' gets used for 
10979        this.tile_height = options.tileSize; // the region below
10980    }
10981
10982    if ( !options.maxLevel ) {
10983        var mf = -1;
10984        var scfs = this.scale_factors || this.scale_factor;
10985        if ( scfs instanceof Array ) {
10986            for ( var i = 0; i < scfs.length; i++ ) {
10987                var cf = Number( scfs[i] );
10988                if ( !isNaN( cf ) && cf > mf ) { mf = cf; }
10989            }
10990        }
10991        if ( mf < 0 ) { options.maxLevel = Number( Math.ceil( Math.log( Math.max( this.width, this.height ), 2 ) ) ); }
10992        else { options.maxLevel = mf; }
10993    }
10994
10995    $.TileSource.apply( this, [ options ] );
10996};
10997
10998$.extend( $.IIIF1_1TileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.IIIF1_1TileSource.prototype */{
10999    /**
11000     * Determine if the data and/or url imply the image service is supported by
11001     * this tile source.
11002     * @function
11003     * @param {Object|Array} data
11004     * @param {String} optional - url
11005     */
11006    supports: function( data, url ) {
11007        return ( data['@context'] &&
11008            data['@context'] == "http://library.stanford.edu/iiif/image-api/1.1/context.json" );
11009    },
11010
11011    /**
11012     *
11013     * @function
11014     * @param {Object} data - the raw configuration
11015     * @example <caption>IIIF 1.1 Info Looks like this (XML syntax is no more)</caption>
11016     * {
11017     *   "@context" : "http://library.stanford.edu/iiif/image-api/1.1/context.json",
11018     *   "@id" : "http://iiif.example.com/prefix/1E34750D-38DB-4825-A38A-B60A345E591C",
11019     *   "width" : 6000,
11020     *   "height" : 4000,
11021     *   "scale_factors" : [ 1, 2, 4 ],
11022     *   "tile_width" : 1024,
11023     *   "tile_height" : 1024,
11024     *   "formats" : [ "jpg", "png" ],
11025     *   "qualities" : [ "native", "grey" ],
11026     *   "profile" : "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0"
11027     * }
11028     */
11029    configure: function( data ){
11030      return data;
11031    },
11032    /**
11033     * Responsible for retreiving the url which will return an image for the
11034     * region specified by the given x, y, and level components.
11035     * @function
11036     * @param {Number} level - z index
11037     * @param {Number} x
11038     * @param {Number} y
11039     * @throws {Error}
11040     */
11041    getTileUrl: function( level, x, y ){
11042
11043        //# constants
11044
11045        var IIIF_ROTATION = '0',
11046            IIIF_QUALITY = 'native.jpg',
11047
11048            //## get the scale (level as a decimal)
11049            scale = Math.pow( 0.5, this.maxLevel - level ),
11050
11051            //# image dimensions at this level
11052            levelWidth = Math.ceil( this.width * scale ),
11053            levelHeight = Math.ceil( this.height * scale ),
11054
11055            //## iiif region
11056            iiifTileSizeWidth = Math.ceil( this.tileSize / scale ),
11057            iiifTileSizeHeight = Math.ceil( this.tileSize / scale ),
11058            iiifRegion,
11059            iiifTileX,
11060            iiifTileY,
11061            iiifTileW,
11062            iiifTileH,
11063            iiifSize,
11064            uri;
11065
11066        if ( levelWidth < this.tile_width && levelHeight < this.tile_height ){
11067            iiifSize = levelWidth + ",";
11068            iiifRegion = 'full';
11069        } else {
11070            iiifTileX = x * iiifTileSizeWidth;
11071            iiifTileY = y * iiifTileSizeHeight;
11072            iiifTileW = Math.min( iiifTileSizeWidth, this.width - iiifTileX );
11073            iiifTileH = Math.min( iiifTileSizeHeight, this.height - iiifTileY );
11074
11075            iiifSize = Math.ceil( iiifTileW * scale ) + ",";
11076
11077            iiifRegion = [ iiifTileX, iiifTileY, iiifTileW, iiifTileH ].join( ',' );
11078        }
11079        uri = [ this['@id'], iiifRegion, iiifSize, IIIF_ROTATION, IIIF_QUALITY ].join( '/' );
11080        return uri;
11081    }
11082  });
11083
11084}( OpenSeadragon ));
11085
11086/*
11087 * OpenSeadragon - OsmTileSource
11088 *
11089 * Copyright (C) 2009 CodePlex Foundation
11090 * Copyright (C) 2010-2013 OpenSeadragon contributors
11091 *
11092 * Redistribution and use in source and binary forms, with or without
11093 * modification, are permitted provided that the following conditions are
11094 * met:
11095 *
11096 * - Redistributions of source code must retain the above copyright notice,
11097 *   this list of conditions and the following disclaimer.
11098 *
11099 * - Redistributions in binary form must reproduce the above copyright
11100 *   notice, this list of conditions and the following disclaimer in the
11101 *   documentation and/or other materials provided with the distribution.
11102 *
11103 * - Neither the name of CodePlex Foundation nor the names of its
11104 *   contributors may be used to endorse or promote products derived from
11105 *   this software without specific prior written permission.
11106 *
11107 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11108 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11109 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11110 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
11111 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11112 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
11113 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
11114 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
11115 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
11116 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
11117 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11118 */
11119
11120/*
11121 * Derived from the OSM tile source in Rainer Simon's seajax-utils project
11122 * <http://github.com/rsimon/seajax-utils>.  Rainer Simon has contributed
11123 * the included code to the OpenSeadragon project under the New BSD license;
11124 * see <https://github.com/openseadragon/openseadragon/issues/58>.
11125 */
11126
11127
11128(function( $ ){
11129
11130/**
11131 * @class OsmTileSource
11132 * @classdesc A tilesource implementation for OpenStreetMap.<br><br>
11133 *
11134 * Note 1. Zoomlevels. Deep Zoom and OSM define zoom levels differently. In  Deep
11135 * Zoom, level 0 equals an image of 1x1 pixels. In OSM, level 0 equals an image of
11136 * 256x256 levels (see http://gasi.ch/blog/inside-deep-zoom-2). I.e. there is a
11137 * difference of log2(256)=8 levels.<br><br>
11138 *
11139 * Note 2. Image dimension. According to the OSM Wiki
11140 * (http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels)
11141 * the highest Mapnik zoom level has 256.144x256.144 tiles, with a 256x256
11142 * pixel size. I.e. the Deep Zoom image dimension is 65.572.864x65.572.864
11143 * pixels.
11144 *
11145 * @memberof OpenSeadragon
11146 * @extends OpenSeadragon.TileSource
11147 * @param {Number|Object} width - the pixel width of the image or the idiomatic
11148 *      options object which is used instead of positional arguments.
11149 * @param {Number} height
11150 * @param {Number} tileSize
11151 * @param {Number} tileOverlap
11152 * @param {String} tilesUrl
11153 */
11154$.OsmTileSource = function( width, height, tileSize, tileOverlap, tilesUrl ) {
11155    var options;
11156
11157    if( $.isPlainObject( width ) ){
11158        options = width;
11159    }else{
11160        options = {
11161            width: arguments[0],
11162            height: arguments[1],
11163            tileSize: arguments[2],
11164            tileOverlap: arguments[3],
11165            tilesUrl: arguments[4]
11166        };
11167    }
11168    //apply default setting for standard public OpenStreatMaps service
11169    //but allow them to be specified so fliks can host there own instance
11170    //or apply against other services supportting the same standard
11171    if( !options.width || !options.height ){
11172        options.width = 65572864;
11173        options.height = 65572864;
11174    }
11175    if( !options.tileSize ){
11176        options.tileSize = 256;
11177        options.tileOverlap = 0;
11178    }
11179    if( !options.tilesUrl ){
11180        options.tilesUrl = "http://tile.openstreetmap.org/";
11181    }
11182    options.minLevel = 8;
11183
11184    $.TileSource.apply( this, [ options ] );
11185
11186};
11187
11188$.extend( $.OsmTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.OsmTileSource.prototype */{
11189
11190
11191    /**
11192     * Determine if the data and/or url imply the image service is supported by
11193     * this tile source.
11194     * @function
11195     * @param {Object|Array} data
11196     * @param {String} optional - url
11197     */
11198    supports: function( data, url ){
11199        return (
11200            data.type &&
11201            "openstreetmaps" == data.type
11202        );
11203    },
11204
11205    /**
11206     *
11207     * @function
11208     * @param {Object} data - the raw configuration
11209     * @param {String} url - the url the data was retreived from if any.
11210     * @return {Object} options - A dictionary of keyword arguments sufficient
11211     *      to configure this tile sources constructor.
11212     */
11213    configure: function( data, url ){
11214        return data;
11215    },
11216
11217
11218    /**
11219     * @function
11220     * @param {Number} level
11221     * @param {Number} x
11222     * @param {Number} y
11223     */
11224    getTileUrl: function( level, x, y ) {
11225        return this.tilesUrl + (level - 8) + "/" + x + "/" + y + ".png";
11226    }
11227});
11228
11229
11230}( OpenSeadragon ));
11231
11232/*
11233 * OpenSeadragon - TmsTileSource
11234 *
11235 * Copyright (C) 2009 CodePlex Foundation
11236 * Copyright (C) 2010-2013 OpenSeadragon contributors
11237 *
11238 * Redistribution and use in source and binary forms, with or without
11239 * modification, are permitted provided that the following conditions are
11240 * met:
11241 *
11242 * - Redistributions of source code must retain the above copyright notice,
11243 *   this list of conditions and the following disclaimer.
11244 *
11245 * - Redistributions in binary form must reproduce the above copyright
11246 *   notice, this list of conditions and the following disclaimer in the
11247 *   documentation and/or other materials provided with the distribution.
11248 *
11249 * - Neither the name of CodePlex Foundation nor the names of its
11250 *   contributors may be used to endorse or promote products derived from
11251 *   this software without specific prior written permission.
11252 *
11253 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11254 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11255 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11256 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
11257 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11258 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
11259 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
11260 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
11261 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
11262 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
11263 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11264 */
11265
11266/*
11267 * Derived from the TMS tile source in Rainer Simon's seajax-utils project
11268 * <http://github.com/rsimon/seajax-utils>.  Rainer Simon has contributed
11269 * the included code to the OpenSeadragon project under the New BSD license;
11270 * see <https://github.com/openseadragon/openseadragon/issues/58>.
11271 */
11272
11273
11274(function( $ ){
11275
11276/**
11277 * @class TmsTileSource
11278 * @classdesc A tilesource implementation for Tiled Map Services (TMS).
11279 * TMS tile scheme ( [ as supported by OpenLayers ] is described here
11280 * ( http://openlayers.org/dev/examples/tms.html ).
11281 *
11282 * @memberof OpenSeadragon
11283 * @extends OpenSeadragon.TileSource
11284 * @param {Number|Object} width - the pixel width of the image or the idiomatic
11285 *      options object which is used instead of positional arguments.
11286 * @param {Number} height
11287 * @param {Number} tileSize
11288 * @param {Number} tileOverlap
11289 * @param {String} tilesUrl
11290 */
11291$.TmsTileSource = function( width, height, tileSize, tileOverlap, tilesUrl ) {
11292    var options;
11293
11294    if( $.isPlainObject( width ) ){
11295        options = width;
11296    }else{
11297        options = {
11298            width: arguments[0],
11299            height: arguments[1],
11300            tileSize: arguments[2],
11301            tileOverlap: arguments[3],
11302            tilesUrl: arguments[4]
11303        };
11304    }
11305    // TMS has integer multiples of 256 for width/height and adds buffer
11306    // if necessary -> account for this!
11307    var bufferedWidth = Math.ceil(options.width / 256) * 256,
11308        bufferedHeight = Math.ceil(options.height / 256) * 256,
11309        max;
11310
11311    // Compute number of zoomlevels in this tileset
11312    if (bufferedWidth > bufferedHeight) {
11313        max = bufferedWidth / 256;
11314    } else {
11315        max = bufferedHeight / 256;
11316    }
11317    options.maxLevel = Math.ceil(Math.log(max)/Math.log(2)) - 1;
11318    options.tileSize = 256;
11319    options.width = bufferedWidth;
11320    options.height = bufferedHeight;
11321
11322    $.TileSource.apply( this, [ options ] );
11323
11324};
11325
11326$.extend( $.TmsTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.TmsTileSource.prototype */{
11327
11328
11329    /**
11330     * Determine if the data and/or url imply the image service is supported by
11331     * this tile source.
11332     * @function
11333     * @param {Object|Array} data
11334     * @param {String} optional - url
11335     */
11336    supports: function( data, url ){
11337        return ( data.type && "tiledmapservice" == data.type );
11338    },
11339
11340    /**
11341     *
11342     * @function
11343     * @param {Object} data - the raw configuration
11344     * @param {String} url - the url the data was retreived from if any.
11345     * @return {Object} options - A dictionary of keyword arguments sufficient
11346     *      to configure this tile sources constructor.
11347     */
11348    configure: function( data, url ){
11349        return data;
11350    },
11351
11352
11353    /**
11354     * @function
11355     * @param {Number} level
11356     * @param {Number} x
11357     * @param {Number} y
11358     */
11359    getTileUrl: function( level, x, y ) {
11360        // Convert from Deep Zoom definition to TMS zoom definition
11361        var yTiles = this.getNumTiles( level ).y - 1;
11362
11363        return this.tilesUrl + level + "/" + x + "/" +  (yTiles - y) + ".png";
11364    }
11365});
11366
11367
11368}( OpenSeadragon ));
11369
11370/*
11371 * OpenSeadragon - LegacyTileSource
11372 *
11373 * Copyright (C) 2009 CodePlex Foundation
11374 * Copyright (C) 2010-2013 OpenSeadragon contributors
11375 *
11376 * Redistribution and use in source and binary forms, with or without
11377 * modification, are permitted provided that the following conditions are
11378 * met:
11379 *
11380 * - Redistributions of source code must retain the above copyright notice,
11381 *   this list of conditions and the following disclaimer.
11382 *
11383 * - Redistributions in binary form must reproduce the above copyright
11384 *   notice, this list of conditions and the following disclaimer in the
11385 *   documentation and/or other materials provided with the distribution.
11386 *
11387 * - Neither the name of CodePlex Foundation nor the names of its
11388 *   contributors may be used to endorse or promote products derived from
11389 *   this software without specific prior written permission.
11390 *
11391 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11392 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11393 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11394 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
11395 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11396 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
11397 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
11398 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
11399 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
11400 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
11401 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11402 */
11403
11404(function( $ ){
11405
11406/**
11407 * @class LegacyTileSource
11408 * @classdesc The LegacyTileSource allows simple, traditional image pyramids to be loaded
11409 * into an OpenSeadragon Viewer.  Basically, this translates to the historically
11410 * common practice of starting with a 'master' image, maybe a tiff for example,
11411 * and generating a set of 'service' images like one or more thumbnails, a medium
11412 * resolution image and a high resolution image in standard web formats like
11413 * png or jpg.
11414 *
11415 * @memberof OpenSeadragon
11416 * @extends OpenSeadragon.TileSource
11417 * @param {Array} levels An array of file descriptions, each is an object with
11418 *      a 'url', a 'width', and a 'height'.  Overriding classes can expect more
11419 *      properties but these properties are sufficient for this implementation.
11420 *      Additionally, the levels are required to be listed in order from
11421 *      smallest to largest.
11422 * @property {Number} aspectRatio
11423 * @property {Number} dimensions
11424 * @property {Number} tileSize
11425 * @property {Number} tileOverlap
11426 * @property {Number} minLevel
11427 * @property {Number} maxLevel
11428 * @property {Array}  levels
11429 */
11430$.LegacyTileSource = function( levels ) {
11431
11432    var options,
11433        width,
11434        height;
11435
11436    if( $.isArray( levels ) ){
11437        options = {
11438            type: 'legacy-image-pyramid',
11439            levels: levels
11440        };
11441    }
11442
11443    //clean up the levels to make sure we support all formats
11444    options.levels = filterFiles( options.levels );
11445
11446    if ( options.levels.length > 0 ) {
11447        width = options.levels[ options.levels.length - 1 ].width;
11448        height = options.levels[ options.levels.length - 1 ].height;
11449    }
11450    else {
11451        width = 0;
11452        height = 0;
11453        $.console.error( "No supported image formats found" );
11454    }
11455
11456    $.extend( true, options, {
11457        width: width,
11458        height: height,
11459        tileSize: Math.max( height, width ),
11460        tileOverlap: 0,
11461        minLevel: 0,
11462        maxLevel: options.levels.length > 0 ? options.levels.length - 1 : 0
11463    } );
11464
11465    $.TileSource.apply( this, [ options ] );
11466
11467    this.levels = options.levels;
11468};
11469
11470$.extend( $.LegacyTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.LegacyTileSource.prototype */{
11471    /**
11472     * Determine if the data and/or url imply the image service is supported by
11473     * this tile source.
11474     * @function
11475     * @param {Object|Array} data
11476     * @param {String} optional - url
11477     */
11478    supports: function( data, url ){
11479        return (
11480            data.type &&
11481            "legacy-image-pyramid" == data.type
11482        ) || (
11483            data.documentElement &&
11484            "legacy-image-pyramid" == data.documentElement.getAttribute('type')
11485        );
11486    },
11487
11488
11489    /**
11490     *
11491     * @function
11492     * @param {Object|XMLDocument} configuration - the raw configuration
11493     * @param {String} dataUrl - the url the data was retreived from if any.
11494     * @return {Object} options - A dictionary of keyword arguments sufficient
11495     *      to configure this tile sources constructor.
11496     */
11497    configure: function( configuration, dataUrl ){
11498
11499        var options;
11500
11501        if( !$.isPlainObject(configuration) ){
11502
11503            options = configureFromXML( this, configuration );
11504
11505        }else{
11506
11507            options = configureFromObject( this, configuration );
11508        }
11509
11510        return options;
11511
11512    },
11513
11514    /**
11515     * @function
11516     * @param {Number} level
11517     */
11518    getLevelScale: function ( level ) {
11519        var levelScale = NaN;
11520        if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) {
11521            levelScale =
11522                this.levels[ level ].width /
11523                this.levels[ this.maxLevel ].width;
11524        }
11525        return levelScale;
11526    },
11527
11528    /**
11529     * @function
11530     * @param {Number} level
11531     */
11532    getNumTiles: function( level ) {
11533        var scale = this.getLevelScale( level );
11534        if ( scale ){
11535            return new $.Point( 1, 1 );
11536        } else {
11537            return new $.Point( 0, 0 );
11538        }
11539    },
11540
11541    /**
11542     * @function
11543     * @param {Number} level
11544     * @param {OpenSeadragon.Point} point
11545     */
11546    getTileAtPoint: function( level, point ) {
11547        return new $.Point( 0, 0 );
11548    },
11549
11550
11551    /**
11552     * This method is not implemented by this class other than to throw an Error
11553     * announcing you have to implement it.  Because of the variety of tile
11554     * server technologies, and various specifications for building image
11555     * pyramids, this method is here to allow easy integration.
11556     * @function
11557     * @param {Number} level
11558     * @param {Number} x
11559     * @param {Number} y
11560     * @throws {Error}
11561     */
11562    getTileUrl: function ( level, x, y ) {
11563        var url = null;
11564        if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) {
11565            url = this.levels[ level ].url;
11566        }
11567        return url;
11568    }
11569} );
11570
11571/**
11572 * This method removes any files from the Array which dont conform to our
11573 * basic requirements for a 'level' in the LegacyTileSource.
11574 * @private
11575 * @inner
11576 * @function
11577 */
11578function filterFiles( files ){
11579    var filtered = [],
11580        file,
11581        i;
11582    for( i = 0; i < files.length; i++ ){
11583        file = files[ i ];
11584        if( file.height &&
11585            file.width &&
11586            file.url && (
11587                file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)$/) || (
11588                    file.mimetype &&
11589                    file.mimetype.toLowerCase().match(/^.*\/(png|jpg|jpeg|gif)$/)
11590                )
11591            ) ){
11592            //This is sufficient to serve as a level
11593            filtered.push({
11594                url: file.url,
11595                width: Number( file.width ),
11596                height: Number( file.height )
11597            });
11598        }
11599        else {
11600            $.console.error( 'Unsupported image format: %s', file.url ? file.url : '<no URL>' );
11601        }
11602    }
11603
11604    return filtered.sort(function(a,b){
11605        return a.height - b.height;
11606    });
11607
11608}
11609
11610/**
11611 * @private
11612 * @inner
11613 * @function
11614 */
11615function configureFromXML( tileSource, xmlDoc ){
11616
11617    if ( !xmlDoc || !xmlDoc.documentElement ) {
11618        throw new Error( $.getString( "Errors.Xml" ) );
11619    }
11620
11621    var root         = xmlDoc.documentElement,
11622        rootName     = root.tagName,
11623        conf         = null,
11624        levels       = [],
11625        level,
11626        i;
11627
11628    if ( rootName == "image" ) {
11629
11630        try {
11631            conf = {
11632                type:        root.getAttribute( "type" ),
11633                levels:      []
11634            };
11635
11636            levels = root.getElementsByTagName( "level" );
11637            for ( i = 0; i < levels.length; i++ ) {
11638                level = levels[ i ];
11639
11640                conf.levels .push({
11641                    url:    level.getAttribute( "url" ),
11642                    width:  parseInt( level.getAttribute( "width" ), 10 ),
11643                    height: parseInt( level.getAttribute( "height" ), 10 )
11644                });
11645            }
11646
11647            return configureFromObject( tileSource, conf );
11648
11649        } catch ( e ) {
11650            throw (e instanceof Error) ?
11651                e :
11652                new Error( 'Unknown error parsing Legacy Image Pyramid XML.' );
11653        }
11654    } else if ( rootName == "collection" ) {
11655        throw new Error( 'Legacy Image Pyramid Collections not yet supported.' );
11656    } else if ( rootName == "error" ) {
11657        throw new Error( 'Error: ' + xmlDoc );
11658    }
11659
11660    throw new Error( 'Unknown element ' + rootName );
11661}
11662
11663/**
11664 * @private
11665 * @inner
11666 * @function
11667 */
11668function configureFromObject( tileSource, configuration ){
11669
11670    return configuration.levels;
11671
11672}
11673
11674}( OpenSeadragon ));
11675
11676/*
11677 * OpenSeadragon - TileSourceCollection
11678 *
11679 * Copyright (C) 2009 CodePlex Foundation
11680 * Copyright (C) 2010-2013 OpenSeadragon contributors
11681 *
11682 * Redistribution and use in source and binary forms, with or without
11683 * modification, are permitted provided that the following conditions are
11684 * met:
11685 *
11686 * - Redistributions of source code must retain the above copyright notice,
11687 *   this list of conditions and the following disclaimer.
11688 *
11689 * - Redistributions in binary form must reproduce the above copyright
11690 *   notice, this list of conditions and the following disclaimer in the
11691 *   documentation and/or other materials provided with the distribution.
11692 *
11693 * - Neither the name of CodePlex Foundation nor the names of its
11694 *   contributors may be used to endorse or promote products derived from
11695 *   this software without specific prior written permission.
11696 *
11697 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11698 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11699 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11700 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
11701 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11702 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
11703 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
11704 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
11705 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
11706 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
11707 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11708 */
11709
11710(function( $ ){
11711
11712/**
11713 * @class TileSourceCollection
11714 * @memberof OpenSeadragon
11715 * @extends OpenSeadragon.TileSource
11716 */
11717$.TileSourceCollection = function( tileSize, tileSources, rows, layout  ) {
11718    var options;
11719
11720    if( $.isPlainObject( tileSize ) ){
11721        options = tileSize;
11722    }else{
11723        options = {
11724            tileSize: arguments[ 0 ],
11725            tileSources: arguments[ 1 ],
11726            rows: arguments[ 2 ],
11727            layout: arguments[ 3 ]
11728        };
11729    }
11730
11731    if( !options.layout ){
11732        options.layout = 'horizontal';
11733    }
11734
11735    var minLevel = 0,
11736        levelSize = 1.0,
11737        tilesPerRow = Math.ceil( options.tileSources.length / options.rows ),
11738        longSide = tilesPerRow >= options.rows ?
11739            tilesPerRow :
11740            options.rows;
11741
11742    if( 'horizontal' == options.layout ){
11743        options.width = ( options.tileSize ) * tilesPerRow;
11744        options.height = ( options.tileSize ) * options.rows;
11745    } else {
11746        options.height = ( options.tileSize ) * tilesPerRow;
11747        options.width = ( options.tileSize ) * options.rows;
11748    }
11749
11750    options.tileOverlap = -options.tileMargin;
11751    options.tilesPerRow = tilesPerRow;
11752
11753    //Set min level to avoid loading sublevels since collection is a
11754    //different kind of abstraction
11755
11756    while( levelSize  <  ( options.tileSize ) * longSide ){
11757        //$.console.log( '%s levelSize %s minLevel %s', options.tileSize * longSide, levelSize, minLevel );
11758        levelSize = levelSize * 2.0;
11759        minLevel++;
11760    }
11761    options.minLevel = minLevel;
11762
11763    //for( var name in options ){
11764    //    $.console.log( 'Collection %s %s', name, options[ name ] );
11765    //}
11766
11767    $.TileSource.apply( this, [ options ] );
11768
11769};
11770
11771$.extend( $.TileSourceCollection.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.TileSourceCollection.prototype */{
11772
11773    /**
11774     * @function
11775     * @param {Number} level
11776     * @param {Number} x
11777     * @param {Number} y
11778     */
11779    getTileBounds: function( level, x, y ) {
11780        var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
11781            px = this.tileSize * x - this.tileOverlap,
11782            py = this.tileSize * y - this.tileOverlap,
11783            sx = this.tileSize + 1 * this.tileOverlap,
11784            sy = this.tileSize + 1 * this.tileOverlap,
11785            scale = 1.0 / dimensionsScaled.x;
11786
11787        sx = Math.min( sx, dimensionsScaled.x - px );
11788        sy = Math.min( sy, dimensionsScaled.y - py );
11789
11790        return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
11791    },
11792
11793    /**
11794     *
11795     * @function
11796     */
11797    configure: function( data, url ){
11798        return;
11799    },
11800
11801
11802    /**
11803     * @function
11804     * @param {Number} level
11805     * @param {Number} x
11806     * @param {Number} y
11807     */
11808    getTileUrl: function( level, x, y ) {
11809        //$.console.log([  level, '/', x, '_', y ].join( '' ));
11810        return null;
11811    }
11812
11813
11814
11815});
11816
11817
11818}( OpenSeadragon ));
11819
11820/*
11821 * OpenSeadragon - Button
11822 *
11823 * Copyright (C) 2009 CodePlex Foundation
11824 * Copyright (C) 2010-2013 OpenSeadragon contributors
11825 *
11826 * Redistribution and use in source and binary forms, with or without
11827 * modification, are permitted provided that the following conditions are
11828 * met:
11829 *
11830 * - Redistributions of source code must retain the above copyright notice,
11831 *   this list of conditions and the following disclaimer.
11832 *
11833 * - Redistributions in binary form must reproduce the above copyright
11834 *   notice, this list of conditions and the following disclaimer in the
11835 *   documentation and/or other materials provided with the distribution.
11836 *
11837 * - Neither the name of CodePlex Foundation nor the names of its
11838 *   contributors may be used to endorse or promote products derived from
11839 *   this software without specific prior written permission.
11840 *
11841 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11842 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11843 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11844 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
11845 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11846 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
11847 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
11848 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
11849 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
11850 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
11851 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11852 */
11853
11854(function( $ ){
11855
11856/**
11857 * An enumeration of button states
11858 * @member ButtonState
11859 * @memberof OpenSeadragon
11860 * @static
11861 * @type {Object}
11862 * @property {Number} REST
11863 * @property {Number} GROUP
11864 * @property {Number} HOVER
11865 * @property {Number} DOWN
11866 */
11867$.ButtonState = {
11868    REST:   0,
11869    GROUP:  1,
11870    HOVER:  2,
11871    DOWN:   3
11872};
11873
11874/**
11875 * @class Button
11876 * @classdesc Manages events, hover states for individual buttons, tool-tips, as well
11877 * as fading the buttons out when the user has not interacted with them
11878 * for a specified period.
11879 *
11880 * @memberof OpenSeadragon
11881 * @extends OpenSeadragon.EventSource
11882 * @param {Object} options
11883 * @param {Element} [options.element=null] Element to use as the button. If not specified, an HTML &lt;button&gt; element is created.
11884 * @param {String} [options.tooltip=null] Provides context help for the button when the
11885 *  user hovers over it.
11886 * @param {String} [options.srcRest=null] URL of image to use in 'rest' state.
11887 * @param {String} [options.srcGroup=null] URL of image to use in 'up' state.
11888 * @param {String} [options.srcHover=null] URL of image to use in 'hover' state.
11889 * @param {String} [options.srcDown=null] URL of image to use in 'down' state.
11890 * @param {Number} [options.fadeDelay=0] How long to wait before fading.
11891 * @param {Number} [options.fadeLength=2000] How long should it take to fade the button.
11892 * @param {OpenSeadragon.EventHandler} [options.onPress=null] Event handler callback for {@link OpenSeadragon.Button.event:press}.
11893 * @param {OpenSeadragon.EventHandler} [options.onRelease=null] Event handler callback for {@link OpenSeadragon.Button.event:release}.
11894 * @param {OpenSeadragon.EventHandler} [options.onClick=null] Event handler callback for {@link OpenSeadragon.Button.event:click}.
11895 * @param {OpenSeadragon.EventHandler} [options.onEnter=null] Event handler callback for {@link OpenSeadragon.Button.event:enter}.
11896 * @param {OpenSeadragon.EventHandler} [options.onExit=null] Event handler callback for {@link OpenSeadragon.Button.event:exit}.
11897 * @param {OpenSeadragon.EventHandler} [options.onFocus=null] Event handler callback for {@link OpenSeadragon.Button.event:focus}.
11898 * @param {OpenSeadragon.EventHandler} [options.onBlur=null] Event handler callback for {@link OpenSeadragon.Button.event:blur}.
11899 */
11900$.Button = function( options ) {
11901
11902    var _this = this;
11903
11904    $.EventSource.call( this );
11905
11906    $.extend( true, this, {
11907
11908        tooltip:            null,
11909        srcRest:            null,
11910        srcGroup:           null,
11911        srcHover:           null,
11912        srcDown:            null,
11913        clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold,
11914        clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold,
11915        /**
11916         * How long to wait before fading.
11917         * @member {Number} fadeDelay
11918         * @memberof OpenSeadragon.Button#
11919         */
11920        fadeDelay:          0,
11921        /**
11922         * How long should it take to fade the button.
11923         * @member {Number} fadeLength
11924         * @memberof OpenSeadragon.Button#
11925         */
11926        fadeLength:         2000,
11927        onPress:            null,
11928        onRelease:          null,
11929        onClick:            null,
11930        onEnter:            null,
11931        onExit:             null,
11932        onFocus:            null,
11933        onBlur:             null
11934
11935    }, options );
11936
11937    /**
11938     * The button element.
11939     * @member {Element} element
11940     * @memberof OpenSeadragon.Button#
11941     */
11942    this.element        = options.element   || $.makeNeutralElement( "div" );
11943
11944    //if the user has specified the element to bind the control to explicitly
11945    //then do not add the default control images
11946    if ( !options.element ) {
11947        this.imgRest      = $.makeTransparentImage( this.srcRest );
11948        this.imgGroup     = $.makeTransparentImage( this.srcGroup );
11949        this.imgHover     = $.makeTransparentImage( this.srcHover );
11950        this.imgDown      = $.makeTransparentImage( this.srcDown );
11951
11952        this.imgRest.alt  =
11953        this.imgGroup.alt =
11954        this.imgHover.alt =
11955        this.imgDown.alt  =
11956            this.tooltip;
11957
11958        this.element.style.position = "relative";
11959
11960        this.imgGroup.style.position =
11961        this.imgHover.style.position =
11962        this.imgDown.style.position  =
11963            "absolute";
11964
11965        this.imgGroup.style.top =
11966        this.imgHover.style.top =
11967        this.imgDown.style.top  =
11968            "0px";
11969
11970        this.imgGroup.style.left =
11971        this.imgHover.style.left =
11972        this.imgDown.style.left  =
11973            "0px";
11974
11975        this.imgHover.style.visibility =
11976        this.imgDown.style.visibility  =
11977            "hidden";
11978
11979        if ( $.Browser.vendor == $.BROWSERS.FIREFOX  && $.Browser.version < 3 ){
11980            this.imgGroup.style.top =
11981            this.imgHover.style.top =
11982            this.imgDown.style.top  =
11983                "";
11984        }
11985
11986        this.element.appendChild( this.imgRest );
11987        this.element.appendChild( this.imgGroup );
11988        this.element.appendChild( this.imgHover );
11989        this.element.appendChild( this.imgDown );
11990    }
11991
11992
11993    this.addHandler( "press",     this.onPress );
11994    this.addHandler( "release",   this.onRelease );
11995    this.addHandler( "click",     this.onClick );
11996    this.addHandler( "enter",     this.onEnter );
11997    this.addHandler( "exit",      this.onExit );
11998    this.addHandler( "focus",     this.onFocus );
11999    this.addHandler( "blur",      this.onBlur );
12000
12001    /**
12002     * The button's current state.
12003     * @member {OpenSeadragon.ButtonState} currentState
12004     * @memberof OpenSeadragon.Button#
12005     */
12006    this.currentState = $.ButtonState.GROUP;
12007
12008    // When the button last began to fade.
12009    this.fadeBeginTime  = null;
12010    // Whether this button should fade after user stops interacting with the viewport.
12011    this.shouldFade     = false;
12012
12013    this.element.style.display  = "inline-block";
12014    this.element.style.position = "relative";
12015    this.element.title          = this.tooltip;
12016
12017    /**
12018     * Tracks mouse/touch/key events on the button.
12019     * @member {OpenSeadragon.MouseTracker} tracker
12020     * @memberof OpenSeadragon.Button#
12021     */
12022    this.tracker = new $.MouseTracker({
12023
12024        element:            this.element,
12025        clickTimeThreshold: this.clickTimeThreshold,
12026        clickDistThreshold: this.clickDistThreshold,
12027
12028        enterHandler: function( event ) {
12029            if ( event.insideElementPressed ) {
12030                inTo( _this, $.ButtonState.DOWN );
12031                /**
12032                 * Raised when the cursor enters the Button element.
12033                 *
12034                 * @event enter
12035                 * @memberof OpenSeadragon.Button
12036                 * @type {object}
12037                 * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12038                 * @property {Object} originalEvent - The original DOM event.
12039                 * @property {?Object} userData - Arbitrary subscriber-defined object.
12040                 */
12041                _this.raiseEvent( "enter", { originalEvent: event.originalEvent } );
12042            } else if ( !event.buttonDownAny ) {
12043                inTo( _this, $.ButtonState.HOVER );
12044            }
12045        },
12046
12047        focusHandler: function ( event ) {
12048            this.enterHandler( event );
12049            /**
12050             * Raised when the Button element receives focus.
12051             *
12052             * @event focus
12053             * @memberof OpenSeadragon.Button
12054             * @type {object}
12055             * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12056             * @property {Object} originalEvent - The original DOM event.
12057             * @property {?Object} userData - Arbitrary subscriber-defined object.
12058             */
12059            _this.raiseEvent( "focus", { originalEvent: event.originalEvent } );
12060        },
12061
12062        exitHandler: function( event ) {
12063            outTo( _this, $.ButtonState.GROUP );
12064            if ( event.insideElementPressed ) {
12065                /**
12066                 * Raised when the cursor leaves the Button element.
12067                 *
12068                 * @event exit
12069                 * @memberof OpenSeadragon.Button
12070                 * @type {object}
12071                 * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12072                 * @property {Object} originalEvent - The original DOM event.
12073                 * @property {?Object} userData - Arbitrary subscriber-defined object.
12074                 */
12075                _this.raiseEvent( "exit", { originalEvent: event.originalEvent } );
12076            }
12077        },
12078
12079        blurHandler: function ( event ) {
12080            this.exitHandler( event );
12081            /**
12082             * Raised when the Button element loses focus.
12083             *
12084             * @event blur
12085             * @memberof OpenSeadragon.Button
12086             * @type {object}
12087             * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12088             * @property {Object} originalEvent - The original DOM event.
12089             * @property {?Object} userData - Arbitrary subscriber-defined object.
12090             */
12091            _this.raiseEvent( "blur", { originalEvent: event.originalEvent } );
12092        },
12093
12094        pressHandler: function ( event ) {
12095            inTo( _this, $.ButtonState.DOWN );
12096            /**
12097             * Raised when a mouse button is pressed or touch occurs in the Button element.
12098             *
12099             * @event press
12100             * @memberof OpenSeadragon.Button
12101             * @type {object}
12102             * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12103             * @property {Object} originalEvent - The original DOM event.
12104             * @property {?Object} userData - Arbitrary subscriber-defined object.
12105             */
12106            _this.raiseEvent( "press", { originalEvent: event.originalEvent } );
12107        },
12108
12109        releaseHandler: function( event ) {
12110            if ( event.insideElementPressed && event.insideElementReleased ) {
12111                outTo( _this, $.ButtonState.HOVER );
12112                /**
12113                 * Raised when the mouse button is released or touch ends in the Button element.
12114                 *
12115                 * @event release
12116                 * @memberof OpenSeadragon.Button
12117                 * @type {object}
12118                 * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12119                 * @property {Object} originalEvent - The original DOM event.
12120                 * @property {?Object} userData - Arbitrary subscriber-defined object.
12121                 */
12122                _this.raiseEvent( "release", { originalEvent: event.originalEvent } );
12123            } else if ( event.insideElementPressed ) {
12124                outTo( _this, $.ButtonState.GROUP );
12125            } else {
12126                inTo( _this, $.ButtonState.HOVER );
12127            }
12128        },
12129
12130        clickHandler: function( event ) {
12131            if ( event.quick ) {
12132                /**
12133                 * Raised when a mouse button is pressed and released or touch is initiated and ended in the Button element within the time and distance threshold.
12134                 *
12135                 * @event click
12136                 * @memberof OpenSeadragon.Button
12137                 * @type {object}
12138                 * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12139                 * @property {Object} originalEvent - The original DOM event.
12140                 * @property {?Object} userData - Arbitrary subscriber-defined object.
12141                 */
12142                _this.raiseEvent("click", { originalEvent: event.originalEvent });
12143            }
12144        },
12145
12146        keyHandler: function( event ){
12147            //console.log( "%s : handling key %s!", _this.tooltip, event.keyCode);
12148            if( 13 === event.keyCode ){
12149                /***
12150                 * Raised when a mouse button is pressed and released or touch is initiated and ended in the Button element within the time and distance threshold.
12151                 *
12152                 * @event click
12153                 * @memberof OpenSeadragon.Button
12154                 * @type {object}
12155                 * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12156                 * @property {Object} originalEvent - The original DOM event.
12157                 * @property {?Object} userData - Arbitrary subscriber-defined object.
12158                 */
12159                _this.raiseEvent( "click", { originalEvent: event.originalEvent } );
12160                /***
12161                 * Raised when the mouse button is released or touch ends in the Button element.
12162                 *
12163                 * @event release
12164                 * @memberof OpenSeadragon.Button
12165                 * @type {object}
12166                 * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event.
12167                 * @property {Object} originalEvent - The original DOM event.
12168                 * @property {?Object} userData - Arbitrary subscriber-defined object.
12169                 */
12170                _this.raiseEvent( "release", { originalEvent: event.originalEvent } );
12171                return false;
12172            }
12173            return true;
12174        }
12175
12176    }).setTracking( true );
12177
12178    outTo( this, $.ButtonState.REST );
12179};
12180
12181$.extend( $.Button.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.Button.prototype */{
12182
12183    /**
12184     * TODO: Determine what this function is intended to do and if it's actually
12185     * useful as an API point.
12186     * @function
12187     */
12188    notifyGroupEnter: function() {
12189        inTo( this, $.ButtonState.GROUP );
12190    },
12191
12192    /**
12193     * TODO: Determine what this function is intended to do and if it's actually
12194     * useful as an API point.
12195     * @function
12196     */
12197    notifyGroupExit: function() {
12198        outTo( this, $.ButtonState.REST );
12199    },
12200
12201    /**
12202     * @function
12203     */
12204    disable: function(){
12205        this.notifyGroupExit();
12206        this.element.disabled = true;
12207        $.setElementOpacity( this.element, 0.2, true );
12208    },
12209
12210    /**
12211     * @function
12212     */
12213    enable: function(){
12214        this.element.disabled = false;
12215        $.setElementOpacity( this.element, 1.0, true );
12216        this.notifyGroupEnter();
12217    }
12218
12219});
12220
12221
12222function scheduleFade( button ) {
12223    $.requestAnimationFrame(function(){
12224        updateFade( button );
12225    });
12226}
12227
12228function updateFade( button ) {
12229    var currentTime,
12230        deltaTime,
12231        opacity;
12232
12233    if ( button.shouldFade ) {
12234        currentTime = $.now();
12235        deltaTime   = currentTime - button.fadeBeginTime;
12236        opacity     = 1.0 - deltaTime / button.fadeLength;
12237        opacity     = Math.min( 1.0, opacity );
12238        opacity     = Math.max( 0.0, opacity );
12239
12240        if( button.imgGroup ){
12241            $.setElementOpacity( button.imgGroup, opacity, true );
12242        }
12243        if ( opacity > 0 ) {
12244            // fade again
12245            scheduleFade( button );
12246        }
12247    }
12248}
12249
12250function beginFading( button ) {
12251    button.shouldFade = true;
12252    button.fadeBeginTime = $.now() + button.fadeDelay;
12253    window.setTimeout( function(){
12254        scheduleFade( button );
12255    }, button.fadeDelay );
12256}
12257
12258function stopFading( button ) {
12259    button.shouldFade = false;
12260    if( button.imgGroup ){
12261        $.setElementOpacity( button.imgGroup, 1.0, true );
12262    }
12263}
12264
12265function inTo( button, newState ) {
12266
12267    if( button.element.disabled ){
12268        return;
12269    }
12270
12271    if ( newState >= $.ButtonState.GROUP &&
12272         button.currentState == $.ButtonState.REST ) {
12273        stopFading( button );
12274        button.currentState = $.ButtonState.GROUP;
12275    }
12276
12277    if ( newState >= $.ButtonState.HOVER &&
12278         button.currentState == $.ButtonState.GROUP ) {
12279        if( button.imgHover ){
12280            button.imgHover.style.visibility = "";
12281        }
12282        button.currentState = $.ButtonState.HOVER;
12283    }
12284
12285    if ( newState >= $.ButtonState.DOWN &&
12286         button.currentState == $.ButtonState.HOVER ) {
12287        if( button.imgDown ){
12288            button.imgDown.style.visibility = "";
12289        }
12290        button.currentState = $.ButtonState.DOWN;
12291    }
12292}
12293
12294
12295function outTo( button, newState ) {
12296
12297    if( button.element.disabled ){
12298        return;
12299    }
12300
12301    if ( newState <= $.ButtonState.HOVER &&
12302         button.currentState == $.ButtonState.DOWN ) {
12303        if( button.imgDown ){
12304            button.imgDown.style.visibility = "hidden";
12305        }
12306        button.currentState = $.ButtonState.HOVER;
12307    }
12308
12309    if ( newState <= $.ButtonState.GROUP &&
12310         button.currentState == $.ButtonState.HOVER ) {
12311        if( button.imgHover ){
12312            button.imgHover.style.visibility = "hidden";
12313        }
12314        button.currentState = $.ButtonState.GROUP;
12315    }
12316
12317    if ( newState <= $.ButtonState.REST &&
12318         button.currentState == $.ButtonState.GROUP ) {
12319        beginFading( button );
12320        button.currentState = $.ButtonState.REST;
12321    }
12322}
12323
12324
12325
12326}( OpenSeadragon ));
12327
12328/*
12329 * OpenSeadragon - ButtonGroup
12330 *
12331 * Copyright (C) 2009 CodePlex Foundation
12332 * Copyright (C) 2010-2013 OpenSeadragon contributors
12333 *
12334 * Redistribution and use in source and binary forms, with or without
12335 * modification, are permitted provided that the following conditions are
12336 * met:
12337 *
12338 * - Redistributions of source code must retain the above copyright notice,
12339 *   this list of conditions and the following disclaimer.
12340 *
12341 * - Redistributions in binary form must reproduce the above copyright
12342 *   notice, this list of conditions and the following disclaimer in the
12343 *   documentation and/or other materials provided with the distribution.
12344 *
12345 * - Neither the name of CodePlex Foundation nor the names of its
12346 *   contributors may be used to endorse or promote products derived from
12347 *   this software without specific prior written permission.
12348 *
12349 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
12350 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12351 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
12352 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
12353 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
12354 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
12355 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
12356 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
12357 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
12358 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
12359 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12360 */
12361
12362(function( $ ){
12363/**
12364 * @class ButtonGroup
12365 * @classdesc Manages events on groups of buttons.
12366 *
12367 * @memberof OpenSeadragon
12368 * @param {Object} options - A dictionary of settings applied against the entire group of buttons.
12369 * @param {Array} options.buttons Array of buttons
12370 * @param {Element} [options.element] Element to use as the container
12371 **/
12372$.ButtonGroup = function( options ) {
12373
12374    $.extend( true, this, {
12375        /**
12376         * An array containing the buttons themselves.
12377         * @member {Array} buttons
12378         * @memberof OpenSeadragon.ButtonGroup#
12379         */
12380        buttons:            [],
12381        clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold,
12382        clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold,
12383        labelText:          ""
12384    }, options );
12385
12386    // copy the button elements  TODO: Why?
12387    var buttons = this.buttons.concat([]),
12388        _this = this,
12389        i;
12390
12391    /**
12392     * The shared container for the buttons.
12393     * @member {Element} element
12394     * @memberof OpenSeadragon.ButtonGroup#
12395     */
12396    this.element = options.element || $.makeNeutralElement( "div" );
12397
12398    // TODO What if there IS an options.group specified? 
12399    if( !options.group ){
12400        this.label   = $.makeNeutralElement( "label" );
12401        //TODO: support labels for ButtonGroups
12402        //this.label.innerHTML = this.labelText;
12403        this.element.style.display = "inline-block";
12404        this.element.appendChild( this.label );
12405        for ( i = 0; i < buttons.length; i++ ) {
12406            this.element.appendChild( buttons[ i ].element );
12407        }
12408    }
12409
12410    /**
12411     * Tracks mouse/touch/key events accross the group of buttons.
12412     * @member {OpenSeadragon.MouseTracker} tracker
12413     * @memberof OpenSeadragon.ButtonGroup#
12414     */
12415    this.tracker = new $.MouseTracker({
12416        element:            this.element,
12417        clickTimeThreshold: this.clickTimeThreshold,
12418        clickDistThreshold: this.clickDistThreshold,
12419        enterHandler: function ( event ) {
12420            var i;
12421            for ( i = 0; i < _this.buttons.length; i++ ) {
12422                _this.buttons[ i ].notifyGroupEnter();
12423            }
12424        },
12425        exitHandler: function ( event ) {
12426            var i;
12427            if ( !event.insideElementPressed ) {
12428                for ( i = 0; i < _this.buttons.length; i++ ) {
12429                    _this.buttons[ i ].notifyGroupExit();
12430                }
12431            }
12432        },
12433        pressHandler: function ( event ) {
12434            if ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) {
12435                var i;
12436                for ( i = 0; i < _this.buttons.length; i++ ) {
12437                    _this.buttons[ i ].notifyGroupEnter();
12438                }
12439            }
12440        },
12441        releaseHandler: function ( event ) {
12442            var i;
12443            if ( !event.insideElementReleased || ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) ) {
12444                for ( i = 0; i < _this.buttons.length; i++ ) {
12445                    _this.buttons[ i ].notifyGroupExit();
12446                }
12447            }
12448        }
12449    }).setTracking( true );
12450};
12451
12452$.ButtonGroup.prototype = /** @lends OpenSeadragon.ButtonGroup.prototype */{
12453
12454    /**
12455     * TODO: Figure out why this is used on the public API and if a more useful
12456     * api can be created.
12457     * @function
12458     * @private
12459     */
12460    emulateEnter: function() {
12461        this.tracker.enterHandler( { eventSource: this.tracker } );
12462    },
12463
12464    /**
12465     * TODO: Figure out why this is used on the public API and if a more useful
12466     * api can be created.
12467     * @function
12468     * @private
12469     */
12470    emulateExit: function() {
12471        this.tracker.exitHandler( { eventSource: this.tracker } );
12472    }
12473};
12474
12475
12476}( OpenSeadragon ));
12477
12478/*
12479 * OpenSeadragon - Rect
12480 *
12481 * Copyright (C) 2009 CodePlex Foundation
12482 * Copyright (C) 2010-2013 OpenSeadragon contributors
12483 *
12484 * Redistribution and use in source and binary forms, with or without
12485 * modification, are permitted provided that the following conditions are
12486 * met:
12487 *
12488 * - Redistributions of source code must retain the above copyright notice,
12489 *   this list of conditions and the following disclaimer.
12490 *
12491 * - Redistributions in binary form must reproduce the above copyright
12492 *   notice, this list of conditions and the following disclaimer in the
12493 *   documentation and/or other materials provided with the distribution.
12494 *
12495 * - Neither the name of CodePlex Foundation nor the names of its
12496 *   contributors may be used to endorse or promote products derived from
12497 *   this software without specific prior written permission.
12498 *
12499 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
12500 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12501 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
12502 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
12503 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
12504 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
12505 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
12506 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
12507 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
12508 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
12509 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12510 */
12511
12512(function( $ ){
12513
12514/**
12515 * @class Rect
12516 * @classdesc A Rectangle really represents a 2x2 matrix where each row represents a
12517 * 2 dimensional vector component, the first is (x,y) and the second is
12518 * (width, height).  The latter component implies the equation of a simple
12519 * plane.
12520 *
12521 * @memberof OpenSeadragon
12522 * @param {Number} x The vector component 'x'.
12523 * @param {Number} y The vector component 'y'.
12524 * @param {Number} width The vector component 'height'.
12525 * @param {Number} height The vector component 'width'.
12526 */
12527$.Rect = function( x, y, width, height ) {
12528    /**
12529     * The vector component 'x'.
12530     * @member {Number} x
12531     * @memberof OpenSeadragon.Rect#
12532     */
12533    this.x = typeof ( x ) == "number" ? x : 0;
12534    /**
12535     * The vector component 'y'.
12536     * @member {Number} y
12537     * @memberof OpenSeadragon.Rect#
12538     */
12539    this.y = typeof ( y ) == "number" ? y : 0;
12540    /**
12541     * The vector component 'width'.
12542     * @member {Number} width
12543     * @memberof OpenSeadragon.Rect#
12544     */
12545    this.width  = typeof ( width )  == "number" ? width : 0;
12546    /**
12547     * The vector component 'height'.
12548     * @member {Number} height
12549     * @memberof OpenSeadragon.Rect#
12550     */
12551    this.height = typeof ( height ) == "number" ? height : 0;
12552};
12553
12554$.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{
12555
12556    /**
12557     * The aspect ratio is simply the ratio of width to height.
12558     * @function
12559     * @returns {Number} The ratio of width to height.
12560     */
12561    getAspectRatio: function() {
12562        return this.width / this.height;
12563    },
12564
12565    /**
12566     * Provides the coordinates of the upper-left corner of the rectangle as a
12567     * point.
12568     * @function
12569     * @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of
12570     *  the rectangle.
12571     */
12572    getTopLeft: function() {
12573        return new $.Point(
12574            this.x,
12575            this.y
12576        );
12577    },
12578
12579    /**
12580     * Provides the coordinates of the bottom-right corner of the rectangle as a
12581     * point.
12582     * @function
12583     * @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of
12584     *  the rectangle.
12585     */
12586    getBottomRight: function() {
12587        return new $.Point(
12588            this.x + this.width,
12589            this.y + this.height
12590        );
12591    },
12592
12593    /**
12594     * Provides the coordinates of the top-right corner of the rectangle as a
12595     * point.
12596     * @function
12597     * @returns {OpenSeadragon.Point} The coordinate of the top-right corner of
12598     *  the rectangle.
12599     */
12600    getTopRight: function() {
12601        return new $.Point(
12602            this.x + this.width,
12603            this.y
12604        );
12605    },
12606
12607    /**
12608     * Provides the coordinates of the bottom-left corner of the rectangle as a
12609     * point.
12610     * @function
12611     * @returns {OpenSeadragon.Point} The coordinate of the bottom-left corner of
12612     *  the rectangle.
12613     */
12614    getBottomLeft: function() {
12615        return new $.Point(
12616            this.x,
12617            this.y + this.height
12618        );
12619    },
12620
12621    /**
12622     * Computes the center of the rectangle.
12623     * @function
12624     * @returns {OpenSeadragon.Point} The center of the rectangle as represented
12625     *  as represented by a 2-dimensional vector (x,y)
12626     */
12627    getCenter: function() {
12628        return new $.Point(
12629            this.x + this.width / 2.0,
12630            this.y + this.height / 2.0
12631        );
12632    },
12633
12634    /**
12635     * Returns the width and height component as a vector OpenSeadragon.Point
12636     * @function
12637     * @returns {OpenSeadragon.Point} The 2 dimensional vector representing the
12638     *  the width and height of the rectangle.
12639     */
12640    getSize: function() {
12641        return new $.Point( this.width, this.height );
12642    },
12643
12644    /**
12645     * Determines if two Rectangles have equivalent components.
12646     * @function
12647     * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to.
12648     * @return {Boolean}
12648 'true' if all components are equal, otherwise 'false'.
12649     */
12650    equals: function( other ) {
12651        return ( other instanceof $.Rect ) &&
12652            ( this.x === other.x ) &&
12653            ( this.y === other.y ) &&
12654            ( this.width === other.width ) &&
12655            ( this.height === other.height );
12656    },
12657
12658    /**
12659     * Rotates a rectangle around a point. Currently only 90, 180, and 270
12660     * degrees are supported.
12661     * @function
12662     * @param {Number} degrees The angle in degrees to rotate.
12663     * @param {OpenSeadragon.Point} pivot The point about which to rotate.
12664     * Defaults to the center of the rectangle.
12665     * @return {OpenSeadragon.Rect}
12666     */
12667    rotate: function( degrees, pivot ) {
12668        // TODO support arbitrary rotation
12669        var width = this.width,
12670            height = this.height,
12671            newTopLeft;
12672
12673        degrees = ( degrees + 360 ) % 360;
12674        if( degrees % 90 !== 0 ) {
12675            throw new Error('Currently only 0, 90, 180, and 270 degrees are supported.');
12676        }
12677
12678        if( degrees === 0 ){
12679            return new $.Rect(
12680                this.x,
12681                this.y,
12682                this.width,
12683                this.height
12684            );
12685        }
12686
12687        pivot = pivot || this.getCenter();
12688
12689        switch ( degrees ) {
12690            case 90:
12691                newTopLeft = this.getBottomLeft();
12692                width = this.height;
12693                height = this.width;
12694                break;
12695            case 180:
12696                newTopLeft = this.getBottomRight();
12697                break;
12698            case 270:
12699                newTopLeft = this.getTopRight();
12700                width = this.height;
12701                height = this.width;
12702                break;
12703            default:
12704                newTopLeft = this.getTopLeft();
12705                break;
12706        }
12707
12708        newTopLeft = newTopLeft.rotate(degrees, pivot);
12709
12710        return new $.Rect(newTopLeft.x, newTopLeft.y, width, height);
12711    },
12712
12713    /**
12714     * Provides a string representation of the rectangle which is useful for
12715     * debugging.
12716     * @function
12717     * @returns {String} A string representation of the rectangle.
12718     */
12719    toString: function() {
12720        return "[" +
12721            Math.round(this.x*100) + "," +
12722            Math.round(this.y*100) + "," +
12723            Math.round(this.width*100) + "x" +
12724            Math.round(this.height*100) +
12725        "]";
12726    }
12727};
12728
12729
12730}( OpenSeadragon ));
12731
12732/*
12733 * OpenSeadragon - ReferenceStrip
12734 *
12735 * Copyright (C) 2009 CodePlex Foundation
12736 * Copyright (C) 2010-2013 OpenSeadragon contributors
12737 *
12738 * Redistribution and use in source and binary forms, with or without
12739 * modification, are permitted provided that the following conditions are
12740 * met:
12741 *
12742 * - Redistributions of source code must retain the above copyright notice,
12743 *   this list of conditions and the following disclaimer.
12744 *
12745 * - Redistributions in binary form must reproduce the above copyright
12746 *   notice, this list of conditions and the following disclaimer in the
12747 *   documentation and/or other materials provided with the distribution.
12748 *
12749 * - Neither the name of CodePlex Foundation nor the names of its
12750 *   contributors may be used to endorse or promote products derived from
12751 *   this software without specific prior written permission.
12752 *
12753 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
12754 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12755 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
12756 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
12757 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
12758 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
12759 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
12760 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
12761 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
12762 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
12763 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12764 */
12765
12766(function ( $ ) {
12767
12768// dictionary from id to private properties
12769var THIS = {};
12770
12771/**
12772 *  The CollectionDrawer is a reimplementation if the Drawer API that
12773 *  focuses on allowing a viewport to be redefined as a collection
12774 *  of smaller viewports, defined by a clear number of rows and / or
12775 *  columns of which each item in the matrix of viewports has its own
12776 *  source.
12777 *
12778 *  This idea is a reexpression of the idea of dzi collections
12779 *  which allows a clearer algorithm to reuse the tile sources already
12780 *  supported by OpenSeadragon, in heterogenious or homogenious
12781 *  sequences just like mixed groups already supported by the viewer
12782 *  for the purpose of image sequnces.
12783 *
12784 *  TODO:   The difficult part of this feature is figuring out how to express
12785 *          this functionality as a combination of the functionality already
12786 *          provided by Drawer, Viewport, TileSource, and Navigator.  It may
12787 *          require better abstraction at those points in order to effeciently
12788 *          reuse those paradigms.
12789 */
12790/**
12791 * @class ReferenceStrip
12792 * @memberof OpenSeadragon
12793 * @param {Object} options
12794 */
12795$.ReferenceStrip = function ( options ) {
12796
12797    var _this       = this,
12798        viewer      = options.viewer,
12799        viewerSize  = $.getElementSize( viewer.element ),
12800        element,
12801        style,
12802        i;
12803
12804    //We may need to create a new element and id if they did not
12805    //provide the id for the existing element
12806    if ( !options.id ) {
12807        options.id              = 'referencestrip-' + $.now();
12808        this.element            = $.makeNeutralElement( "div" );
12809        this.element.id         = options.id;
12810        this.element.className  = 'referencestrip';
12811    }
12812
12813    options = $.extend( true, {
12814        sizeRatio:  $.DEFAULT_SETTINGS.referenceStripSizeRatio,
12815        position:   $.DEFAULT_SETTINGS.referenceStripPosition,
12816        scroll:     $.DEFAULT_SETTINGS.referenceStripScroll,
12817        clickTimeThreshold:  $.DEFAULT_SETTINGS.clickTimeThreshold
12818    }, options, {
12819        //required overrides
12820        element:                this.element,
12821        //These need to be overridden to prevent recursion since
12822        //the navigator is a viewer and a viewer has a navigator
12823        showNavigator:          false,
12824        mouseNavEnabled:        false,
12825        showNavigationControl:  false,
12826        showSequenceControl:    false
12827    } );
12828
12829    $.extend( this, options );
12830    //Private state properties
12831    THIS[this.id] = {
12832        "animating":           false
12833    };
12834
12835    this.minPixelRatio = this.viewer.minPixelRatio;
12836
12837    style = this.element.style;
12838    style.marginTop     = '0px';
12839    style.marginRight   = '0px';
12840    style.marginBottom  = '0px';
12841    style.marginLeft    = '0px';
12842    style.left          = '0px';
12843    style.bottom        = '0px';
12844    style.border        = '0px';
12845    style.background    = '#000';
12846    style.position      = 'relative';
12847
12848    $.setElementOpacity( this.element, 0.8 );
12849
12850    this.viewer = viewer;
12851    this.innerTracker = new $.MouseTracker( {
12852        element:        this.element,
12853        dragHandler:    $.delegate( this, onStripDrag ),
12854        scrollHandler:  $.delegate( this, onStripScroll ),
12855        enterHandler:   $.delegate( this, onStripEnter ),
12856        exitHandler:    $.delegate( this, onStripExit ),
12857        keyHandler:     $.delegate( this, onKeyPress )
12858    } ).setTracking( true );
12859
12860    //Controls the position and orientation of the reference strip and sets the
12861    //appropriate width and height
12862    if ( options.width && options.height ) {
12863        this.element.style.width  = options.width + 'px';
12864        this.element.style.height = options.height + 'px';
12865        viewer.addControl(
12866            this.element,
12867            { anchor: $.ControlAnchor.BOTTOM_LEFT }
12868        );
12869    } else {
12870        if ( "horizontal" == options.scroll ) {
12871            this.element.style.width = (
12872                viewerSize.x *
12873                options.sizeRatio *
12874                viewer.tileSources.length
12875            ) + ( 12 * viewer.tileSources.length ) + 'px';
12876
12877            this.element.style.height = (
12878                viewerSize.y *
12879                options.sizeRatio
12880            ) + 'px';
12881
12882            viewer.addControl(
12883                this.element,
12884                { anchor: $.ControlAnchor.BOTTOM_LEFT }
12885            );
12886        } else {
12887            this.element.style.height = (
12888                viewerSize.y *
12889                options.sizeRatio *
12890                viewer.tileSources.length
12891            ) + ( 12 * viewer.tileSources.length ) + 'px';
12892
12893            this.element.style.width = (
12894                viewerSize.x *
12895                options.sizeRatio
12896            ) + 'px';
12897
12898            viewer.addControl(
12899                this.element,
12900                { anchor: $.ControlAnchor.TOP_LEFT }
12901            );
12902
12903        }
12904    }
12905
12906    this.panelWidth = ( viewerSize.x * this.sizeRatio ) + 8;
12907    this.panelHeight = ( viewerSize.y * this.sizeRatio ) + 8;
12908    this.panels = [];
12909
12910    /*jshint loopfunc:true*/
12911    for ( i = 0; i < viewer.tileSources.length; i++ ) {
12912
12913        element = $.makeNeutralElement( 'div' );
12914        element.id = this.element.id + "-" + i;
12915
12916        element.style.width         = _this.panelWidth + 'px';
12917        element.style.height        = _this.panelHeight + 'px';
12918        element.style.display       = 'inline';
12919        element.style.float         = 'left'; //Webkit
12920        element.style.cssFloat      = 'left'; //Firefox
12921        element.style.styleFloat    = 'left'; //IE
12922        element.style.padding       = '2px';
12923
12924        element.innerTracker = new $.MouseTracker( {
12925            element:            element,
12926            clickTimeThreshold: this.clickTimeThreshold,
12927            clickDistThreshold: this.clickDistThreshold,
12928            pressHandler: function ( event ) {
12929                event.eventSource.dragging = $.now();
12930            },
12931            releaseHandler: function ( event ) {
12932                var tracker = event.eventSource,
12933                    id      = tracker.element.id,
12934                    page    = Number( id.split( '-' )[2] ),
12935                    now     = $.now();
12936
12937                if ( event.insideElementPressed &&
12938                     event.insideElementReleased &&
12939                     tracker.dragging &&
12940                     ( now - tracker.dragging ) < tracker.clickTimeThreshold ) {
12941                    tracker.dragging = null;
12942                    viewer.goToPage( page );
12943                }
12944            }
12945        } ).setTracking( true );
12946
12947        this.element.appendChild( element );
12948
12949        element.activePanel = false;
12950
12951        this.panels.push( element );
12952
12953    }
12954    loadPanels( this, this.scroll == 'vertical' ? viewerSize.y : viewerSize.y, 0 );
12955    this.setFocus( 0 );
12956
12957};
12958
12959$.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototype, /** @lends OpenSeadragon.ReferenceStrip.prototype */{
12960
12961    /**
12962     * @function
12963     */
12964    setFocus: function ( page ) {
12965        var element      = $.getElement( this.element.id + '-' + page ),
12966            viewerSize   = $.getElementSize( this.viewer.canvas ),
12967            scrollWidth  = Number( this.element.style.width.replace( 'px', '' ) ),
12968            scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ),
12969            offsetLeft   = -Number( this.element.style.marginLeft.replace( 'px', '' ) ),
12970            offsetTop    = -Number( this.element.style.marginTop.replace( 'px', '' ) ),
12971            offset;
12972
12973        if ( this.currentSelected !== element ) {
12974            if ( this.currentSelected ) {
12975                this.currentSelected.style.background = '#000';
12976            }
12977            this.currentSelected = element;
12978            this.currentSelected.style.background = '#999';
12979
12980            if ( 'horizontal' == this.scroll ) {
12981                //right left
12982                offset = ( Number( page ) ) * ( this.panelWidth + 3 );
12983                if ( offset > offsetLeft + viewerSize.x - this.panelWidth ) {
12984                    offset = Math.min( offset, ( scrollWidth - viewerSize.x ) );
12985                    this.element.style.marginLeft = -offset + 'px';
12986                    loadPanels( this, viewerSize.x, -offset );
12987                } else if ( offset < offsetLeft ) {
12988                    offset = Math.max( 0, offset - viewerSize.x / 2 );
12989                    this.element.style.marginLeft = -offset + 'px';
12990                    loadPanels( this, viewerSize.x, -offset );
12991                }
12992            } else {
12993                offset = ( Number( page ) ) * ( this.panelHeight + 3 );
12994                if ( offset > offsetTop + viewerSize.y - this.panelHeight ) {
12995                    offset = Math.min( offset, ( scrollHeight - viewerSize.y ) );
12996                    this.element.style.marginTop = -offset + 'px';
12997                    loadPanels( this, viewerSize.y, -offset );
12998                } else if ( offset < offsetTop ) {
12999                    offset = Math.max( 0, offset - viewerSize.y / 2 );
13000                    this.element.style.marginTop = -offset + 'px';
13001                    loadPanels( this, viewerSize.y, -offset );
13002                }
13003            }
13004
13005            this.currentPage = page;
13006            $.getElement( element.id + '-displayregion' ).focus();
13007            onStripEnter.call( this, { eventSource: this.innerTracker } );
13008        }
13009    },
13010
13011    /**
13012     * @function
13013     */
13014    update: function () {
13015        if ( THIS[this.id].animating ) {
13016            $.console.log( 'image reference strip update' );
13017            return true;
13018        }
13019        return false;
13020    }
13021
13022} );
13023
13024
13025
13026
13027/**
13028 * @private
13029 * @inner
13030 * @function
13031 */
13032function onStripDrag( event ) {
13033
13034    var offsetLeft   = Number( this.element.style.marginLeft.replace( 'px', '' ) ),
13035        offsetTop    = Number( this.element.style.marginTop.replace( 'px', '' ) ),
13036        scrollWidth  = Number( this.element.style.width.replace( 'px', '' ) ),
13037        scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ),
13038        viewerSize   = $.getElementSize( this.viewer.canvas );
13039    this.dragging = true;
13040    if ( this.element ) {
13041        if ( 'horizontal' == this.scroll ) {
13042            if ( -event.delta.x > 0 ) {
13043                //forward
13044                if ( offsetLeft > -( scrollWidth - viewerSize.x ) ) {
13045                    this.element.style.marginLeft = ( offsetLeft + ( event.delta.x * 2 ) ) + 'px';
13046                    loadPanels( this, viewerSize.x, offsetLeft + ( event.delta.x * 2 ) );
13047                }
13048            } else if ( -event.delta.x < 0 ) {
13049                //reverse
13050                if ( offsetLeft < 0 ) {
13051                    this.element.style.marginLeft = ( offsetLeft + ( event.delta.x * 2 ) ) + 'px';
13052                    loadPanels( this, viewerSize.x, offsetLeft + ( event.delta.x * 2 ) );
13053                }
13054            }
13055        } else {
13056            if ( -event.delta.y > 0 ) {
13057                //forward
13058                if ( offsetTop > -( scrollHeight - viewerSize.y ) ) {
13059                    this.element.style.marginTop = ( offsetTop + ( event.delta.y * 2 ) ) + 'px';
13060                    loadPanels( this, viewerSize.y, offsetTop + ( event.delta.y * 2 ) );
13061                }
13062            } else if ( -event.delta.y < 0 ) {
13063                //reverse
13064                if ( offsetTop < 0 ) {
13065                    this.element.style.marginTop = ( offsetTop + ( event.delta.y * 2 ) ) + 'px';
13066                    loadPanels( this, viewerSize.y, offsetTop + ( event.delta.y * 2 ) );
13067                }
13068            }
13069        }
13070    }
13071    return false;
13072
13073}
13074
13075
13076
13077/**
13078 * @private
13079 * @inner
13080 * @function
13081 */
13082function onStripScroll( event ) {
13083    var offsetLeft   = Number( this.element.style.marginLeft.replace( 'px', '' ) ),
13084        offsetTop    = Number( this.element.style.marginTop.replace( 'px', '' ) ),
13085        scrollWidth  = Number( this.element.style.width.replace( 'px', '' ) ),
13086        scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ),
13087        viewerSize   = $.getElementSize( this.viewer.canvas );
13088    if ( this.element ) {
13089        if ( 'horizontal' == this.scroll ) {
13090            if ( event.scroll > 0 ) {
13091                //forward
13092                if ( offsetLeft > -( scrollWidth - viewerSize.x ) ) {
13093                    this.element.style.marginLeft = ( offsetLeft - ( event.scroll * 60 ) ) + 'px';
13094                    loadPanels( this, viewerSize.x, offsetLeft - ( event.scroll * 60 ) );
13095                }
13096            } else if ( event.scroll < 0 ) {
13097                //reverse
13098                if ( offsetLeft < 0 ) {
13099                    this.element.style.marginLeft = ( offsetLeft - ( event.scroll * 60 ) ) + 'px';
13100                    loadPanels( this, viewerSize.x, offsetLeft - ( event.scroll * 60 ) );
13101                }
13102            }
13103        } else {
13104            if ( event.scroll < 0 ) {
13105                //scroll up
13106                if ( offsetTop > viewerSize.y - scrollHeight ) {
13107                    this.element.style.marginTop = ( offsetTop + ( event.scroll * 60 ) ) + 'px';
13108                    loadPanels( this, viewerSize.y, offsetTop + ( event.scroll * 60 ) );
13109                }
13110            } else if ( event.scroll > 0 ) {
13111                //scroll dowm
13112                if ( offsetTop < 0 ) {
13113                    this.element.style.marginTop = ( offsetTop + ( event.scroll * 60 ) ) + 'px';
13114                    loadPanels( this, viewerSize.y, offsetTop + ( event.scroll * 60 ) );
13115                }
13116            }
13117        }
13118    }
13119    //cancels event
13120    return false;
13121}
13122
13123
13124function loadPanels( strip, viewerSize, scroll ) {
13125    var panelSize,
13126        activePanelsStart,
13127        activePanelsEnd,
13128        miniViewer,
13129        style,
13130        i,
13131        element;
13132    if ( 'horizontal' == strip.scroll ) {
13133        panelSize = strip.panelWidth;
13134    } else {
13135        panelSize = strip.panelHeight;
13136    }
13137    activePanelsStart = Math.ceil( viewerSize / panelSize ) + 5;
13138    activePanelsEnd = Math.ceil( ( Math.abs( scroll ) + viewerSize ) / panelSize ) + 1;
13139    activePanelsStart = activePanelsEnd - activePanelsStart;
13140    activePanelsStart = activePanelsStart < 0 ? 0 : activePanelsStart;
13141
13142    for ( i = activePanelsStart; i < activePanelsEnd && i < strip.panels.length; i++ ) {
13143        element = strip.panels[i];
13144        if ( !element.activePanel ) {
13145            miniViewer = new $.Viewer( {
13146                id:                     element.id,
13147                tileSources:            [strip.viewer.tileSources[i]],
13148                element:                element,
13149                navigatorSizeRatio:     strip.sizeRatio,
13150                showNavigator:          false,
13151                mouseNavEnabled:        false,
13152                showNavigationControl:  false,
13153                showSequenceControl:    false,
13154                immediateRender:        true,
13155                blendTime:              0,
13156                animationTime:          0
13157            } );
13158
13159            miniViewer.displayRegion           = $.makeNeutralElement( "textarea" );
13160            miniViewer.displayRegion.id        = element.id + '-displayregion';
13161            miniViewer.displayRegion.className = 'displayregion';
13162
13163            style               = miniViewer.displayRegion.style;
13164            style.position      = 'relative';
13165            style.top           = '0px';
13166            style.left          = '0px';
13167            style.fontSize      = '0px';
13168            style.overflow      = 'hidden';
13169            style.float         = 'left'; //Webkit
13170            style.cssFloat      = 'left'; //Firefox
13171            style.styleFloat    = 'left'; //IE
13172            style.zIndex        = 999999999;
13173            style.cursor        = 'default';
13174            style.width         = ( strip.panelWidth - 4 ) + 'px';
13175            style.height        = ( strip.panelHeight - 4 ) + 'px';
13176
13177            miniViewer.displayRegion.innerTracker = new $.MouseTracker( {
13178                element: miniViewer.displayRegion
13179            } );
13180
13181            element.getElementsByTagName( 'div' )[0].appendChild(
13182                miniViewer.displayRegion
13183            );
13184
13185            element.activePanel = true;
13186        }
13187    }
13188}
13189
13190
13191/**
13192 * @private
13193 * @inner
13194 * @function
13195 */
13196function onStripEnter( event ) {
13197    var element = event.eventSource.element;
13198    
13199    //$.setElementOpacity(element, 0.8);
13200
13201    //element.style.border = '1px solid #555';
13202    //element.style.background = '#000';
13203
13204    if ( 'horizontal' == this.scroll ) {
13205
13206        //element.style.paddingTop = "0px";
13207        element.style.marginBottom = "0px";
13208
13209    } else {
13210
13211        //element.style.paddingRight = "0px";
13212        element.style.marginLeft = "0px";
13213
13214    }
13215    return false;
13216}
13217
13218
13219/**
13220 * @private
13221 * @inner
13222 * @function
13223 */
13224function onStripExit( event ) {
13225    var element = event.eventSource.element;
13226    
13227    if ( 'horizontal' == this.scroll ) {
13228
13229        //element.style.paddingTop = "10px";
13230        element.style.marginBottom = "-" + ( $.getElementSize( element ).y / 2 ) + "px";
13231
13232    } else {
13233
13234        //element.style.paddingRight = "10px";
13235        element.style.marginLeft = "-" + ( $.getElementSize( element ).x / 2 ) + "px";
13236
13237    }
13238    return false;
13239}
13240
13241
13242
13243/**
13244 * @private
13245 * @inner
13246 * @function
13247 */
13248function onKeyPress( event ) {
13249    //console.log( event.keyCode );
13250
13251    switch ( event.keyCode ) {
13252        case 61: //=|+
13253            onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } );
13254            return false;
13255        case 45: //-|_
13256            onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } );
13257            return false;
13258        case 48: //0|)
13259        case 119: //w
13260        case 87: //W
13261        case 38: //up arrow
13262            onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } );
13263            return false;
13264        case 115: //s
13265        case 83: //S
13266        case 40: //down arrow
13267            onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } );
13268            return false;
13269        case 97: //a
13270        case 37: //left arrow
13271            onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } );
13272            return false;
13273        case 100: //d
13274        case 39: //right arrow
13275            onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } );
13276            return false;
13277        default:
13278            //console.log( 'navigator keycode %s', event.keyCode );
13279            return true;
13280    }
13281}
13282
13283
13284
13285} ( OpenSeadragon ) );
13286
13287/*
13288 * OpenSeadragon - DisplayRect
13289 *
13290 * Copyright (C) 2009 CodePlex Foundation
13291 * Copyright (C) 2010-2013 OpenSeadragon contributors
13292 *
13293 * Redistribution and use in source and binary forms, with or without
13294 * modification, are permitted provided that the following conditions are
13295 * met:
13296 *
13297 * - Redistributions of source code must retain the above copyright notice,
13298 *   this list of conditions and the following disclaimer.
13299 *
13300 * - Redistributions in binary form must reproduce the above copyright
13301 *   notice, this list of conditions and the following disclaimer in the
13302 *   documentation and/or other materials provided with the distribution.
13303 *
13304 * - Neither the name of CodePlex Foundation nor the names of its
13305 *   contributors may be used to endorse or promote products derived from
13306 *   this software without specific prior written permission.
13307 *
13308 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
13309 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
13310 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
13311 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
13312 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13313 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
13314 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
13315 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
13316 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
13317 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
13318 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13319 */
13320
13321(function( $ ){
13322
13323/**
13324 * @class DisplayRect
13325 * @classdesc A display rectangle is very similar to {@link OpenSeadragon.Rect} but adds two
13326 * fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels
13327 * for this rectangle.
13328 *
13329 * @memberof OpenSeadragon
13330 * @extends OpenSeadragon.Rect
13331 * @param {Number} x The vector component 'x'.
13332 * @param {Number} y The vector component 'y'.
13333 * @param {Number} width The vector component 'height'.
13334 * @param {Number} height The vector component 'width'.
13335 * @param {Number} minLevel The lowest zoom level supported.
13336 * @param {Number} maxLevel The highest zoom level supported.
13337 */
13338$.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) {
13339    $.Rect.apply( this, [ x, y, width, height ] );
13340
13341    /**
13342     * The lowest zoom level supported.
13343     * @member {Number} minLevel
13344     * @memberof OpenSeadragon.DisplayRect#
13345     */
13346    this.minLevel = minLevel;
13347    /**
13348     * The highest zoom level supported.
13349     * @member {Number} maxLevel
13350     * @memberof OpenSeadragon.DisplayRect#
13351     */
13352    this.maxLevel = maxLevel;
13353};
13354
13355$.extend( $.DisplayRect.prototype, $.Rect.prototype );
13356
13357}( OpenSeadragon ));
13358
13359/*
13360 * OpenSeadragon - Spring
13361 *
13362 * Copyright (C) 2009 CodePlex Foundation
13363 * Copyright (C) 2010-2013 OpenSeadragon contributors
13364 *
13365 * Redistribution and use in source and binary forms, with or without
13366 * modification, are permitted provided that the following conditions are
13367 * met:
13368 *
13369 * - Redistributions of source code must retain the above copyright notice,
13370 *   this list of conditions and the following disclaimer.
13371 *
13372 * - Redistributions in binary form must reproduce the above copyright
13373 *   notice, this list of conditions and the following disclaimer in the
13374 *   documentation and/or other materials provided with the distribution.
13375 *
13376 * - Neither the name of CodePlex Foundation nor the names of its
13377 *   contributors may be used to endorse or promote products derived from
13378 *   this software without specific prior written permission.
13379 *
13380 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
13381 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
13382 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
13383 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
13384 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13385 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
13386 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
13387 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
13388 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
13389 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
13390 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13391 */
13392
13393(function( $ ){
13394
13395/**
13396 * @class Spring
13397 * @memberof OpenSeadragon
13398 * @param {Object} options - Spring configuration settings.
13399 * @param {Number} options.initial - Initial value of spring, default to 0 so
13400 *  spring is not in motion initally by default.
13401 * @param {Number} options.springStiffness - Spring stiffness.
13402 * @param {Number} options.animationTime - Animation duration per spring.
13403 */
13404$.Spring = function( options ) {
13405    var args = arguments;
13406
13407    if( typeof( options ) != 'object' ){
13408        //allows backward compatible use of ( initialValue, config ) as
13409        //constructor parameters
13410        options = {
13411            initial: args.length && typeof ( args[ 0 ] ) == "number" ?
13412                args[ 0 ] :
13413                0,
13414            /**
13415             * Spring stiffness.
13416             * @member {Number} springStiffness
13417             * @memberof OpenSeadragon.Spring#
13418             */
13419            springStiffness: args.length > 1 ?
13420                args[ 1 ].springStiffness :
13421                5.0,
13422            /**
13423             * Animation duration per spring.
13424             * @member {Number} animationTime
13425             * @memberof OpenSeadragon.Spring#
13426             */
13427            animationTime: args.length > 1 ?
13428                args[ 1 ].animationTime :
13429                1.5
13430        };
13431    }
13432
13433    $.extend( true, this, options);
13434
13435    /**
13436     * @member {Object} current
13437     * @memberof OpenSeadragon.Spring#
13438     * @property {Number} value
13439     * @property {Number} time
13440     */
13441    this.current = {
13442        value: typeof ( this.initial ) == "number" ?
13443            this.initial :
13444            0,
13445        time:  $.now() // always work in milliseconds
13446    };
13447
13448    /**
13449     * @member {Object} start
13450     * @memberof OpenSeadragon.Spring#
13451     * @property {Number} value
13452     * @property {Number} time
13453     */
13454    this.start = {
13455        value: this.current.value,
13456        time:  this.current.time
13457    };
13458
13459    /**
13460     * @member {Object} target
13461     * @memberof OpenSeadragon.Spring#
13462     * @property {Number} value
13463     * @property {Number} time
13464     */
13465    this.target = {
13466        value: this.current.value,
13467        time:  this.current.time
13468    };
13469};
13470
13471$.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{
13472
13473    /**
13474     * @function
13475     * @param {Number} target
13476     */
13477    resetTo: function( target ) {
13478        this.target.value = target;
13479        this.target.time  = this.current.time;
13480        this.start.value  = this.target.value;
13481        this.start.time   = this.target.time;
13482    },
13483
13484    /**
13485     * @function
13486     * @param {Number} target
13487     */
13488    springTo: function( target ) {
13489        this.start.value  = this.current.value;
13490        this.start.time   = this.current.time;
13491        this.target.value = target;
13492        this.target.time  = this.start.time + 1000 * this.animationTime;
13493    },
13494
13495    /**
13496     * @function
13497     * @param {Number} delta
13498     */
13499    shiftBy: function( delta ) {
13500        this.start.value  += delta;
13501        this.target.value += delta;
13502    },
13503
13504    /**
13505     * @function
13506     */
13507    update: function() {
13508        this.current.time  = $.now();
13509        this.current.value = (this.current.time >= this.target.time) ?
13510            this.target.value :
13511            this.start.value +
13512                ( this.target.value - this.start.value ) *
13513                transform(
13514                    this.springStiffness,
13515                    ( this.current.time - this.start.time ) /
13516                    ( this.target.time  - this.start.time )
13517                );
13518    }
13519};
13520
13521/**
13522 * @private
13523 */
13524function transform( stiffness, x ) {
13525    return ( 1.0 - Math.exp( stiffness * -x ) ) /
13526        ( 1.0 - Math.exp( -stiffness ) );
13527}
13528
13529}( OpenSeadragon ));
13530
13531/*
13532 * OpenSeadragon - ImageLoader
13533 *
13534 * Copyright (C) 2009 CodePlex Foundation
13535 * Copyright (C) 2010-2013 OpenSeadragon contributors
13536 
13537 * Redistribution and use in source and binary forms, with or without
13538 * modification, are permitted provided that the following conditions are
13539 * met:
13540 *
13541 * - Redistributions of source code must retain the above copyright notice,
13542 *   this list of conditions and the following disclaimer.
13543 *
13544 * - Redistributions in binary form must reproduce the above copyright
13545 *   notice, this list of conditions and the following disclaimer in the
13546 *   documentation and/or other materials provided with the distribution.
13547 *
13548 * - Neither the name of CodePlex Foundation nor the names of its
13549 *   contributors may be used to endorse or promote products derived from
13550 *   this software without specific prior written permission.
13551 *
13552 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
13553 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
13554 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
13555 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
13556 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13557 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
13558 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
13559 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
13560 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
13561 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
13562 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13563 */
13564
13565(function( $ ){
13566
13567/**
13568 * @private
13569 * @class ImageJob
13570 * @classdesc Handles loading a single image for use in a single {@link OpenSeadragon.Tile}.
13571 *
13572 * @memberof OpenSeadragon
13573 * @param {String} source - URL of image to download.
13574 * @param {String} crossOriginPolicy - CORS policy to use for downloads
13575 * @param {Function} callback - Called once image has finished downloading.
13576 */
13577function ImageJob ( options ) {
13578    
13579    $.extend( true, this, {
13580        timeout:        $.DEFAULT_SETTINGS.timeout,
13581        jobId:          null
13582    }, options );
13583    
13584    /**
13585     * Image object which will contain downloaded image.
13586     * @member {Image} image
13587     * @memberof OpenSeadragon.ImageJob#
13588     */
13589    this.image = null;
13590}
13591
13592ImageJob.prototype = {
13593
13594    /**
13595     * Initiates downloading of associated image.
13596     * @method
13597     */
13598    start: function(){
13599        var _this = this;
13600
13601        this.image = new Image();
13602
13603        if ( this.crossOriginPolicy !== false ) {
13604            this.image.crossOrigin = this.crossOriginPolicy;
13605        }
13606
13607        this.image.onload = function(){
13608            _this.finish( true );
13609        };
13610        this.image.onabort = this.image.onerror = function(){
13611            _this.finish( false );
13612        };
13613
13614        this.jobId = window.setTimeout( function(){
13615            _this.finish( false );
13616        }, this.timeout);
13617
13618        this.image.src = this.src;
13619    },
13620
13621    finish: function( successful ) {
13622        this.image.onload = this.image.onerror = this.image.onabort = null;
13623        if (!successful) {
13624            this.image = null;
13625        }
13626
13627        if ( this.jobId ) {
13628            window.clearTimeout( this.jobId );
13629        }
13630
13631        this.callback( this );
13632    }
13633
13634};
13635
13636/**
13637 * @class
13638 * @classdesc Handles downloading of a set of images using asynchronous queue pattern.
13639 */
13640$.ImageLoader = function() {
13641    
13642    $.extend( true, this, {
13643        jobLimit:       $.DEFAULT_SETTINGS.imageLoaderLimit,
13644        jobQueue:       [],
13645        jobsInProgress: 0
13646    });
13647
13648};
13649
13650$.ImageLoader.prototype = {
13651    
13652    /**
13653     * Add an unloaded image to the loader queue.
13654     * @method
13655     * @param {String} src - URL of image to download.
13656     * @param {String} crossOriginPolicy - CORS policy to use for downloads
13657     * @param {Function} callback - Called once image has been downloaded.
13658     */
13659    addJob: function( options ) {
13660        var _this = this,
13661            complete = function( job ) {
13662                completeJob( _this, job, options.callback );
13663            },
13664            jobOptions = {
13665                src: options.src,
13666                crossOriginPolicy: options.crossOriginPolicy,
13667                callback: complete
13668            },
13669            newJob = new ImageJob( jobOptions );
13670
13671        if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) {
13672            newJob.start();
13673            this.jobsInProgress++;
13674        }
13675        else {
13676           this.jobQueue.push( newJob );
13677        }
13678
13679    }
13680};
13681
13682/**
13683 * Cleans up ImageJob once completed.
13684 * @method
13685 * @private
13686 * @param loader - ImageLoader used to start job.
13687 * @param job - The ImageJob that has completed.
13688 * @param callback - Called once cleanup is finished.
13689 */
13690function completeJob( loader, job, callback ) {
13691    var nextJob;
13692
13693    loader.jobsInProgress--;
13694
13695    if ( (!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.jobQueue.length > 0) {
13696        nextJob = loader.jobQueue.shift();
13697        nextJob.start();
13698    }
13699
13700    callback( job.image );
13701}
13702
13703}( OpenSeadragon ));
13704
13705
13706/*
13707 * OpenSeadragon - Tile
13708 *
13709 * Copyright (C) 2009 CodePlex Foundation
13710 * Copyright (C) 2010-2013 OpenSeadragon contributors
13711 *
13712 * Redistribution and use in source and binary forms, with or without
13713 * modification, are permitted provided that the following conditions are
13714 * met:
13715 *
13716 * - Redistributions of source code must retain the above copyright notice,
13717 *   this list of conditions and the following disclaimer.
13718 *
13719 * - Redistributions in binary form must reproduce the above copyright
13720 *   notice, this list of conditions and the following disclaimer in the
13721 *   documentation and/or other materials provided with the distribution.
13722 *
13723 * - Neither the name of CodePlex Foundation nor the names of its
13724 *   contributors may be used to endorse or promote products derived from
13725 *   this software without specific prior written permission.
13726 *
13727 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
13728 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
13729 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
13730 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
13731 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13732 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
13733 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
13734 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
13735 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
13736 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
13737 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13738 */
13739
13740(function( $ ){
13741    var TILE_CACHE       = {};
13742/**
13743 * @class Tile
13744 * @memberof OpenSeadragon
13745 * @param {Number} level The zoom level this tile belongs to.
13746 * @param {Number} x The vector component 'x'.
13747 * @param {Number} y The vector component 'y'.
13748 * @param {OpenSeadragon.Point}
13748 bounds Where this tile fits, in normalized
13749 *      coordinates.
13750 * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has
13751 *      this tile failed to load? )
13752 * @param {String} url The URL of this tile's image.
13753 */
13754$.Tile = function(level, x, y, bounds, exists, url) {
13755    /**
13756     * The zoom level this tile belongs to.
13757     * @member {Number} level
13758     * @memberof OpenSeadragon.Tile#
13759     */
13760    this.level   = level;
13761    /**
13762     * The vector component 'x'.
13763     * @member {Number} x
13764     * @memberof OpenSeadragon.Tile#
13765     */
13766    this.x       = x;
13767    /**
13768     * The vector component 'y'.
13769     * @member {Number} y
13770     * @memberof OpenSeadragon.Tile#
13771     */
13772    this.y       = y;
13773    /**
13774     * Where this tile fits, in normalized coordinates
13775     * @member {OpenSeadragon.Point} bounds
13776     * @memberof OpenSeadragon.Tile#
13777     */
13778    this.bounds  = bounds;
13779    /**
13780     * Is this tile a part of a sparse image? Also has this tile failed to load?
13781     * @member {Boolean} exists
13782     * @memberof OpenSeadragon.Tile#
13783     */
13784    this.exists  = exists;
13785    /**
13786     * The URL of this tile's image.
13787     * @member {String} url
13788     * @memberof OpenSeadragon.Tile#
13789     */
13790    this.url     = url;
13791    /**
13792     * Is this tile loaded?
13793     * @member {Boolean} loaded
13794     * @memberof OpenSeadragon.Tile#
13795     */
13796    this.loaded  = false;
13797    /**
13798     * Is this tile loading?
13799     * @member {Boolean} loading
13800     * @memberof OpenSeadragon.Tile#
13801     */
13802    this.loading = false;
13803
13804    /**
13805     * The HTML div element for this tile
13806     * @member {Element} element
13807     * @memberof OpenSeadragon.Tile#
13808     */
13809    this.element    = null;
13810    /**
13811     * The HTML img element for this tile.
13812     * @member {Element} imgElement
13813     * @memberof OpenSeadragon.Tile#
13814     */
13815    this.imgElement = null;
13816    /**
13817     * The Image object for this tile.
13818     * @member {Object} image
13819     * @memberof OpenSeadragon.Tile#
13820     */
13821    this.image      = null;
13822
13823    /**
13824     * The alias of this.element.style.
13825     * @member {String} style
13826     * @memberof OpenSeadragon.Tile#
13827     */
13828    this.style      = null;
13829    /**
13830     * This tile's position on screen, in pixels.
13831     * @member {OpenSeadragon.Point} position
13832     * @memberof OpenSeadragon.Tile#
13833     */
13834    this.position   = null;
13835    /**
13836     * This tile's size on screen, in pixels.
13837     * @member {OpenSeadragon.Point} size
13838     * @memberof OpenSeadragon.Tile#
13839     */
13840    this.size       = null;
13841    /**
13842     * The start time of this tile's blending.
13843     * @member {Number} blendStart
13844     * @memberof OpenSeadragon.Tile#
13845     */
13846    this.blendStart = null;
13847    /**
13848     * The current opacity this tile should be.
13849     * @member {Number} opacity
13850     * @memberof OpenSeadragon.Tile#
13851     */
13852    this.opacity    = null;
13853    /**
13854     * The distance of this tile to the viewport center.
13855     * @member {Number} distance
13856     * @memberof OpenSeadragon.Tile#
13857     */
13858    this.distance   = null;
13859    /**
13860     * The visibility score of this tile.
13861     * @member {Number} visibility
13862     * @memberof OpenSeadragon.Tile#
13863     */
13864    this.visibility = null;
13865
13866    /**
13867     * Whether this tile is currently being drawn.
13868     * @member {Boolean} beingDrawn
13869     * @memberof OpenSeadragon.Tile#
13870     */
13871    this.beingDrawn     = false;
13872    /**
13873     * Timestamp the tile was last touched.
13874     * @member {Number} lastTouchTime
13875     * @memberof OpenSeadragon.Tile#
13876     */
13877    this.lastTouchTime  = 0;
13878};
13879
13880$.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{
13881
13882    /**
13883     * Provides a string representation of this tiles level and (x,y)
13884     * components.
13885     * @function
13886     * @returns {String}
13887     */
13888    toString: function() {
13889        return this.level + "/" + this.x + "_" + this.y;
13890    },
13891
13892    /**
13893     * Renders the tile in an html container.
13894     * @function
13895     * @param {Element} container
13896     */
13897    drawHTML: function( container ) {
13898        if ( !this.loaded || !this.image ) {
13899            $.console.warn(
13900                "Attempting to draw tile %s when it's not yet loaded.",
13901                this.toString()
13902            );
13903            return;
13904        }
13905
13906        //EXPERIMENTAL - trying to figure out how to scale the container
13907        //               content during animation of the container size.
13908
13909        if ( !this.element ) {
13910            this.element                              = $.makeNeutralElement( "div" );
13911            this.imgElement                           = $.makeNeutralElement( "img" );
13912            this.imgElement.src                       = this.url;
13913            this.imgElement.style.msInterpolationMode = "nearest-neighbor";
13914            this.imgElement.style.width               = "100%";
13915            this.imgElement.style.height              = "100%";
13916
13917            this.style                     = this.element.style;
13918            this.style.position            = "absolute";
13919        }
13920        if ( this.element.parentNode != container ) {
13921            container.appendChild( this.element );
13922        }
13923        if ( this.imgElement.parentNode != this.element ) {
13924            this.element.appendChild( this.imgElement );
13925        }
13926
13927        this.style.top     = this.position.y + "px";
13928        this.style.left    = this.position.x + "px";
13929        this.style.height  = this.size.y + "px";
13930        this.style.width   = this.size.x + "px";
13931
13932        $.setElementOpacity( this.element, this.opacity );
13933    },
13934
13935    /**
13936     * Renders the tile in a canvas-based context.
13937     * @function
13938     * @param {Canvas} context
13939     * @param {Function} method for firing the drawing event. drawingHandler({context, tile, rendered})
13940     * where <code>rendered</code> is the context with the pre-drawn image.
13941     */
13942    drawCanvas: function( context, drawingHandler ) {
13943
13944        var position = this.position,
13945            size     = this.size,
13946            rendered,
13947            canvas;
13948
13949        if ( !this.loaded || !( this.image || TILE_CACHE[ this.url ] ) ){
13950            $.console.warn(
13951                "Attempting to draw tile %s when it's not yet loaded.",
13952                this.toString()
13953            );
13954            return;
13955        }
13956        context.globalAlpha = this.opacity;
13957
13958        //context.save();
13959
13960        //if we are supposed to be rendering fully opaque rectangle,
13961        //ie its done fading or fading is turned off, and if we are drawing
13962        //an image with an alpha channel, then the only way
13963        //to avoid seeing the tile underneath is to clear the rectangle
13964        if( context.globalAlpha == 1 && this.url.match('.png') ){
13965            //clearing only the inside of the rectangle occupied
13966            //by the png prevents edge flikering
13967            context.clearRect(
13968                position.x+1,
13969                position.y+1,
13970                size.x-2,
13971                size.y-2
13972            );
13973
13974        }
13975
13976        if( !TILE_CACHE[ this.url ] ){
13977            canvas = document.createElement( 'canvas' );
13978            canvas.width = this.image.width;
13979            canvas.height = this.image.height;
13980            rendered = canvas.getContext('2d');
13981            rendered.drawImage( this.image, 0, 0 );
13982            TILE_CACHE[ this.url ] = rendered;
13983            //since we are caching the prerendered image on a canvas
13984            //allow the image to not be held in memory
13985            this.image = null;
13986        }
13987
13988        rendered = TILE_CACHE[ this.url ];
13989
13990        // This gives the application a chance to make image manipulation changes as we are rendering the image
13991        drawingHandler({context: context, tile: this, rendered: rendered});
13992
13993        //rendered.save();
13994        context.drawImage(
13995            rendered.canvas,
13996            0,
13997            0,
13998            rendered.canvas.width,
13999            rendered.canvas.height,
14000            position.x,
14001            position.y,
14002            size.x,
14003            size.y
14004        );
14005        //rendered.restore();
14006
14007        //context.restore();
14008    },
14009
14010    /**
14011     * Removes tile from its container.
14012     * @function
14013     */
14014    unload: function() {
14015        if ( this.imgElement && this.imgElement.parentNode ) {
14016            this.imgElement.parentNode.removeChild( this.imgElement );
14017        }
14018        if ( this.element && this.element.parentNode ) {
14019            this.element.parentNode.removeChild( this.element );
14020        }
14021        if ( TILE_CACHE[ this.url ]){
14022            delete TILE_CACHE[ this.url ];
14023        }
14024
14025        this.element    = null;
14026        this.imgElement = null;
14027        this.image      = null;
14028        this.loaded     = false;
14029        this.loading    = false;
14030    }
14031};
14032
14033}( OpenSeadragon ));
14034
14035/*
14036 * OpenSeadragon - Overlay
14037 *
14038 * Copyright (C) 2009 CodePlex Foundation
14039 * Copyright (C) 2010-2013 OpenSeadragon contributors
14040 *
14041 * Redistribution and use in source and binary forms, with or without
14042 * modification, are permitted provided that the following conditions are
14043 * met:
14044 *
14045 * - Redistributions of source code must retain the above copyright notice,
14046 *   this list of conditions and the following disclaimer.
14047 *
14048 * - Redistributions in binary form must reproduce the above copyright
14049 *   notice, this list of conditions and the following disclaimer in the
14050 *   documentation and/or other materials provided with the distribution.
14051 *
14052 * - Neither the name of CodePlex Foundation nor the names of its
14053 *   contributors may be used to endorse or promote products derived from
14054 *   this software without specific prior written permission.
14055 *
14056 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
14057 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
14058 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
14059 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
14060 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
14061 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
14062 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
14063 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
14064 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
14065 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
14066 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14067 */
14068
14069(function( $ ){
14070
14071    /**
14072     * An enumeration of positions that an overlay may be assigned relative to
14073     * the viewport.
14074     * @member OverlayPlacement
14075     * @memberof OpenSeadragon
14076     * @static
14077     * @type {Object}
14078     * @property {Number} CENTER
14079     * @property {Number} TOP_LEFT
14080     * @property {Number} TOP
14081     * @property {Number} TOP_RIGHT
14082     * @property {Number} RIGHT
14083     * @property {Number} BOTTOM_RIGHT
14084     * @property {Number} BOTTOM
14085     * @property {Number} BOTTOM_LEFT
14086     * @property {Number} LEFT
14087     */
14088    $.OverlayPlacement = {
14089        CENTER:       0,
14090        TOP_LEFT:     1,
14091        TOP:          2,
14092        TOP_RIGHT:    3,
14093        RIGHT:        4,
14094        BOTTOM_RIGHT: 5,
14095        BOTTOM:       6,
14096        BOTTOM_LEFT:  7,
14097        LEFT:         8
14098    };
14099
14100    /**
14101     * @class Overlay
14102     * @classdesc Provides a way to float an HTML element on top of the viewer element.
14103     *
14104     * @memberof OpenSeadragon
14105     * @param {Object} options
14106     * @param {Element} options.element
14107     * @param {OpenSeadragon.Point|OpenSeadragon.Rect} options.location - The
14108     * location of the overlay on the image. If a {@link OpenSeadragon.Point}
14109     * is specified, the overlay will keep a constant size independently of the
14110     * zoom. If a {@link OpenSeadragon.Rect} is specified, the overlay size will
14111     * be adjusted when the zoom changes.
14112     * @param {OpenSeadragon.OverlayPlacement} [options.placement=OpenSeadragon.OverlayPlacement.TOP_LEFT]
14113     * Relative position to the viewport.
14114     * Only used if location is a {@link OpenSeadragon.Point}.
14115     * @param {OpenSeadragon.Overlay.OnDrawCallback} [options.onDraw]
14116     * @param {Boolean} [options.checkResize=true] Set to false to avoid to
14117     * check the size of the overlay everytime it is drawn when using a
14118     * {@link OpenSeadragon.Point} as options.location. It will improve
14119     * performances but will cause a misalignment if the overlay size changes.
14120     */
14121    $.Overlay = function( element, location, placement ) {
14122
14123        /**
14124         * onDraw callback signature used by {@link OpenSeadragon.Overlay}.
14125         *
14126         * @callback OnDrawCallback
14127         * @memberof OpenSeadragon.Overlay
14128         * @param {OpenSeadragon.Point} position
14129         * @param {OpenSeadragon.Point} size
14130         * @param {Element} element
14131         */
14132
14133        var options;
14134        if ( $.isPlainObject( element ) ) {
14135            options = element;
14136        } else {
14137            options = {
14138                element: element,
14139                location: location,
14140                placement: placement
14141            };
14142        }
14143        
14144        this.element    = options.element;
14145        this.scales     = options.location instanceof $.Rect;
14146        this.bounds     = new $.Rect(
14147            options.location.x,
14148            options.location.y,
14149            options.location.width,
14150            options.location.height
14151        );
14152        this.position   = new $.Point(
14153            options.location.x,
14154            options.location.y
14155        );
14156        this.size       = new $.Point(
14157            options.location.width,
14158            options.location.height
14159        );
14160        this.style      = options.element.style;
14161        // rects are always top-left
14162        this.placement  = options.location instanceof $.Point ?
14163            options.placement :
14164            $.OverlayPlacement.TOP_LEFT;
14165        this.onDraw = options.onDraw;
14166        this.checkResize = options.checkResize === undefined ?
14167            true : options.checkResize;
14168    };
14169
14170    $.Overlay.prototype = /** @lends OpenSeadragon.Overlay.prototype */{
14171
14172        /**
14173         * @function
14174         * @param {OpenSeadragon.OverlayPlacement} position
14175         * @param {OpenSeadragon.Point} size
14176         */
14177        adjust: function( position, size ) {
14178            switch ( this.placement ) {
14179                case $.OverlayPlacement.TOP_LEFT:
14180                    break;
14181                case $.OverlayPlacement.TOP:
14182                    position.x -= size.x / 2;
14183                    break;
14184                case $.OverlayPlacement.TOP_RIGHT:
14185                    position.x -= size.x;
14186                    break;
14187                case $.OverlayPlacement.RIGHT:
14188                    position.x -= size.x;
14189                    position.y -= size.y / 2;
14190                    break;
14191                case $.OverlayPlacement.BOTTOM_RIGHT:
14192                    position.x -= size.x;
14193                    position.y -= size.y;
14194                    break;
14195                case $.OverlayPlacement.BOTTOM:
14196                    position.x -= size.x / 2;
14197                    position.y -= size.y;
14198                    break;
14199                case $.OverlayPlacement.BOTTOM_LEFT:
14200                    position.y -= size.y;
14201                    break;
14202                case $.OverlayPlacement.LEFT:
14203                    position.y -= size.y / 2;
14204                    break;
14205                default:
14206                case $.OverlayPlacement.CENTER:
14207                    position.x -= size.x / 2;
14208                    position.y -= size.y / 2;
14209                    break;
14210            }
14211        },
14212
14213        /**
14214         * @function
14215         */
14216        destroy: function() {
14217            var element = this.element,
14218                style   = this.style;
14219
14220            if ( element.parentNode ) {
14221                element.parentNode.removeChild( element );
14222                //this should allow us to preserve overlays when required between
14223                //pages
14224                if ( element.prevElementParent ) {
14225                    style.display = 'none';
14226                    //element.prevElementParent.insertBefore(
14227                    //    element,
14228                    //    element.prevNextSibling
14229                    //);
14230                    document.body.appendChild( element );
14231                }
14232            }
14233
14234            // clear the onDraw callback
14235            this.onDraw = null;
14236
14237            style.top = "";
14238            style.left = "";
14239            style.position = "";
14240
14241            if ( this.scales ) {
14242                style.width = "";
14243                style.height = "";
14244            }
14245        },
14246
14247        /**
14248         * @function
14249         * @param {Element} container
14250         */
14251        drawHTML: function( container, viewport ) {
14252            var element = this.element,
14253                style   = this.style,
14254                scales  = this.scales,
14255                degrees  = viewport.degrees,
14256                position = viewport.pixelFromPoint(
14257                    this.bounds.getTopLeft(),
14258                    true
14259                ),
14260                size,
14261                overlayCenter;
14262
14263            if ( element.parentNode != container ) {
14264                //save the source parent for later if we need it
14265                element.prevElementParent  = element.parentNode;
14266                element.prevNextSibling    = element.nextSibling;
14267                container.appendChild( element );
14268                this.size = $.getElementSize( element );
14269            }
14270
14271            if ( scales ) {
14272                size = viewport.deltaPixelsFromPoints(
14273                    this.bounds.getSize(),
14274                    true
14275                );
14276            } else if ( this.checkResize ) {
14277                size = $.getElementSize( element );
14278            } else {
14279                size = this.size;
14280            }
14281
14282            this.position = position;
14283            this.size     = size;
14284
14285            this.adjust( position, size );
14286
14287            position = position.apply( Math.floor );
14288            size     = size.apply( Math.ceil );
14289
14290            // rotate the position of the overlay
14291            // TODO only rotate overlays if in canvas mode
14292            // TODO replace the size rotation with CSS3 transforms
14293            // TODO add an option to overlays to not rotate with the image
14294            // Currently only rotates position and size
14295            if( degrees !== 0 && this.scales ) {
14296                overlayCenter = new $.Point( size.x / 2, size.y / 2 );
14297
14298                var drawerCenter = new $.Point(
14299                    viewport.viewer.drawer.canvas.width / 2,
14300                    viewport.viewer.drawer.canvas.height / 2
14301                );
14302                position = position.plus( overlayCenter ).rotate(
14303                    degrees,
14304                    drawerCenter
14305                ).minus( overlayCenter );
14306
14307                size = size.rotate( degrees, new $.Point( 0, 0 ) );
14308                size = new $.Point( Math.abs( size.x ), Math.abs( size.y ) );
14309            }
14310
14311            // call the onDraw callback if it exists to allow one to overwrite
14312            // the drawing/positioning/sizing of the overlay
14313            if ( this.onDraw ) {
14314                this.onDraw( position, size, element );
14315            } else {
14316                style.left     = position.x + "px";
14317                style.top      = position.y + "px";
14318                style.position = "absolute";
14319                style.display  = 'block';
14320
14321                if ( scales ) {
14322                    style.width  = size.x + "px";
14323                    style.height = size.y + "px";
14324                }
14325            }
14326        },
14327
14328        /**
14329         * @function
14330         * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location
14331         * @param {OpenSeadragon.OverlayPlacement} position
14332         */
14333        update: function( location, placement ) {
14334            this.scales     = location instanceof $.Rect;
14335            this.bounds     = new $.Rect(
14336                location.x,
14337                location.y,
14338                location.width,
14339                location.height
14340            );
14341            // rects are always top-left
14342            this.placement  = location instanceof $.Point ?
14343                placement :
14344                $.OverlayPlacement.TOP_LEFT;
14345        }
14346
14347    };
14348
14349}( OpenSeadragon ));
14350
14351/*
14352 * OpenSeadragon - Drawer
14353 *
14354 * Copyright (C) 2009 CodePlex Foundation
14355 * Copyright (C) 2010-2013 OpenSeadragon contributors
14356 *
14357 * Redistribution and use in source and binary forms, with or without
14358 * modification, are permitted provided that the following conditions are
14359 * met:
14360 *
14361 * - Redistributions of source code must retain the above copyright notice,
14362 *   this list of conditions and the following disclaimer.
14363 *
14364 * - Redistributions in binary form must reproduce the above copyright
14365 *   notice, this list of conditions and the following disclaimer in the
14366 *   documentation and/or other materials provided with the distribution.
14367 *
14368 * - Neither the name of CodePlex Foundation nor the names of its
14369 *   contributors may be used to endorse or promote products derived from
14370 *   this software without specific prior written permission.
14371 *
14372 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
14373 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
14374 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
14375 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
14376 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
14377 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
14378 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
14379 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
14380 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
14381 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
14382 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14383 */
14384
14385(function( $ ){
14386
14387var DEVICE_SCREEN       = $.getWindowSize(),
14388    BROWSER             = $.Browser.vendor,
14389    BROWSER_VERSION     = $.Browser.version,
14390
14391    SUBPIXEL_RENDERING = (
14392        ( BROWSER == $.BROWSERS.FIREFOX ) ||
14393        ( BROWSER == $.BROWSERS.OPERA )   ||
14394        ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) ||
14395        ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) ||
14396        ( BROWSER == $.BROWSERS.IE     && BROWSER_VERSION >= 9 )
14397    );
14398
14399
14400/**
14401 * @class Drawer
14402 * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. 
14403 * A new instance is created for each TileSource opened (see {@link OpenSeadragon.Viewer#drawer}).
14404 *
14405 * @memberof OpenSeadragon
14406 * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source.
14407 * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport.
14408 * @param {Element} element - Parent element.
14409 */
14410$.Drawer = function( options ) {
14411
14412    //backward compatibility for positional args while prefering more
14413    //idiomatic javascript options object as the only argument
14414    var args  = arguments,
14415        i;
14416
14417    if( !$.isPlainObject( options ) ){
14418        options = {
14419            source:     args[ 0 ], // Reference to Viewer tile source.
14420            viewport:   args[ 1 ], // Reference to Viewer viewport.
14421            element:    args[ 2 ]  // Parent element.
14422        };
14423    }
14424
14425    $.extend( true, this, {
14426
14427        //internal state properties
14428        viewer:         null,
14429        imageLoader:    new $.ImageLoader(),
14430        tilesMatrix:    {},    // A '3d' dictionary [level][x][y] --> Tile.
14431        tilesLoaded:    [],    // An unordered list of Tiles with loaded images.
14432        coverage:       {},    // A '3d' dictionary [level][x][y] --> Boolean.
14433        lastDrawn:      [],    // An unordered list of Tiles drawn last frame.
14434        lastResetTime:  0,     // Last time for which the drawer was reset.
14435        midUpdate:      false, // Is the drawer currently updating the viewport?
14436        updateAgain:    true,  // Does the drawer need to update the viewort again?
14437
14438
14439        //internal state / configurable settings
14440        collectionOverlays: {}, // For collection mode. Here an overlay is actually a viewer.
14441
14442        //configurable settings
14443        opacity:            $.DEFAULT_SETTINGS.opacity,
14444        maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount,
14445        minZoomImageRatio:  $.DEFAULT_SETTINGS.minZoomImageRatio,
14446        wrapHorizontal:     $.DEFAULT_SETTINGS.wrapHorizontal,
14447        wrapVertical:       $.DEFAULT_SETTINGS.wrapVertical,
14448        immediateRender:    $.DEFAULT_SETTINGS.immediateRender,
14449        blendTime:          $.DEFAULT_SETTINGS.blendTime,
14450        alwaysBlend:        $.DEFAULT_SETTINGS.alwaysBlend,
14451        minPixelRatio:      $.DEFAULT_SETTINGS.minPixelRatio,
14452        debugMode:          $.DEFAULT_SETTINGS.debugMode,
14453        timeout:            $.DEFAULT_SETTINGS.timeout,
14454        crossOriginPolicy:  $.DEFAULT_SETTINGS.crossOriginPolicy
14455
14456    }, options );
14457
14458    this.useCanvas  = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true );
14459    /**
14460     * The parent element of this Drawer instance, passed in when the Drawer was created.
14461     * The parent of {@link OpenSeadragon.Drawer#canvas}.
14462     * @member {Element} container
14463     * @memberof OpenSeadragon.Drawer#
14464     */
14465    this.container  = $.getElement( this.element );
14466    /**
14467     * A &lt;canvas&gt; element if the browser supports them, otherwise a &lt;div&gt; element.
14468     * Child element of {@link OpenSeadragon.Drawer#container}.
14469     * @member {Element} canvas
14470     * @memberof OpenSeadragon.Drawer#
14471     */
14472    this.canvas     = $.makeNeutralElement( this.useCanvas ? "canvas" : "div" );
14473    /**
14474     * 2d drawing context for {@link OpenSeadragon.Drawer#canvas} if it's a &lt;canvas&gt; element, otherwise null.
14475     * @member {Object} context
14476     * @memberof OpenSeadragon.Drawer#
14477     */
14478    this.context    = this.useCanvas ? this.canvas.getContext( "2d" ) : null;
14479    // Ratio of zoomable image height to width.
14480    this.normHeight = this.source.dimensions.y / this.source.dimensions.x;
14481    /**
14482     * @member {Element} element
14483     * @memberof OpenSeadragon.Drawer#
14484     * @deprecated Alias for {@link OpenSeadragon.Drawer#container}.
14485     */
14486    this.element    = this.container;
14487
14488    // We force our container to ltr because our drawing math doesn't work in rtl.
14489    // This issue only affects our canvas renderer, but we do it always for consistency.
14490    // Note that this means overlays you want to be rtl need to be explicitly set to rtl.
14491    this.container.dir = 'ltr';
14492
14493    this.canvas.style.width     = "100%";
14494    this.canvas.style.height    = "100%";
14495    this.canvas.style.position  = "absolute";
14496    $.setElementOpacity( this.canvas, this.opacity, true );
14497
14498    // explicit left-align
14499    this.container.style.textAlign = "left";
14500    this.container.appendChild( this.canvas );
14501
14502    //this.profiler    = new $.Profiler();
14503};
14504
14505$.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{
14506
14507    /**
14508     * Adds an html element as an overlay to the current viewport.  Useful for
14509     * highlighting words or areas of interest on an image or other zoomable
14510     * interface.
14511     * @method
14512     * @param {Element|String|Object} element - A reference to an element or an id for
14513     *      the element which will overlayed. Or an Object specifying the configuration for the overlay
14514     * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
14515     *      rectangle which will be overlayed.
14516     * @param {OpenSeadragon.OverlayPlacement} placement - The position of the
14517     *      viewport which the location coordinates will be treated as relative
14518     *      to.
14519     * @param {function} onDraw - If supplied the callback is called when the overlay
14520     *      needs to be drawn. It it the responsibility of the callback to do any drawing/positioning.
14521     *      It is passed position, size and element.
14522     * @fires OpenSeadragon.Viewer.event:add-overlay
14523     * @deprecated - use {@link OpenSeadragon.Viewer#addOverlay} instead.
14524     */
14525    addOverlay: function( element, location, placement, onDraw ) {
14526        $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead.");
14527        this.viewer.addOverlay( element, location, placement, onDraw );
14528        return this;
14529    },
14530
14531    /**
14532     * Updates the overlay represented by the reference to the element or
14533     * element id moving it to the new location, relative to the new placement.
14534     * @method
14535     * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
14536     *      rectangle which will be overlayed.
14537     * @param {OpenSeadragon.OverlayPlacement} placement - The position of the
14538     *      viewport which the location coordinates will be treated as relative
14539     *      to.
14540     * @return {OpenSeadragon.Drawer} Chainable.
14541     * @fires OpenSeadragon.Viewer.event:update-overlay
14542     * @deprecated - use {@link OpenSeadragon.Viewer#updateOverlay} instead.
14543     */
14544    updateOverlay: function( element, location, placement ) {
14545        $.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead.");
14546        this.viewer.updateOverlay( element, location, placement );
14547        return this;
14548    },
14549
14550    /**
14551     * Removes and overlay identified by the reference element or element id
14552     *      and schedules and update.
14553     * @method
14554     * @param {Element|String} element - A reference to the element or an
14555     *      element id which represent the ovelay content to be removed.
14556     * @return {OpenSeadragon.Drawer} Chainable.
14557     * @fires OpenSeadragon.Viewer.event:remove-overlay
14558     * @deprecated - use {@link OpenSeadragon.Viewer#removeOverlay} instead.
14559     */
14560    removeOverlay: function( element ) {
14561        $.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead.");
14562        this.viewer.removeOverlay( element );
14563        return this;
14564    },
14565
14566    /**
14567     * Removes all currently configured Overlays from this Drawer and schedules
14568     *      and update.
14569     * @method
14570     * @return {OpenSeadragon.Drawer} Chainable.
14571     * @fires OpenSeadragon.Viewer.event:clear-overlay
14572     * @deprecated - use {@link OpenSeadragon.Viewer#clearOverlays} instead.
14573     */
14574    clearOverlays: function() {
14575        $.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead.");
14576        this.viewer.clearOverlays();
14577        return this;
14578    },
14579
14580    /**
14581     * Set the opacity of the drawer.
14582     * @method
14583     * @param {Number} opacity
14584     * @return {OpenSeadragon.Drawer} Chainable.
14585     */
14586    setOpacity: function( opacity ) {
14587        this.opacity = opacity;
14588        $.setElementOpacity( this.canvas, this.opacity, true );
14589        return this;
14590    },
14591
14592    /**
14593     * Get the opacity of the drawer.
14594     * @method
14595     * @returns {Number}
14596     */
14597    getOpacity: function() {
14598        return this.opacity;
14599    },
14600    /**
14601     * Returns whether the Drawer is scheduled for an update at the
14602     *      soonest possible opportunity.
14603     * @method
14604     * @returns {Boolean} - Whether the Drawer is scheduled for an update at the
14605     *      soonest possible opportunity.
14606     */
14607    needsUpdate: function() {
14608        return this.updateAgain;
14609    },
14610
14611    /**
14612     * Returns the total number of tiles that have been loaded by this Drawer.
14613     * @method
14614     * @returns {Number} - The total number of tiles that have been loaded by
14615     *      this Drawer.
14616     */
14617    numTilesLoaded: function() {
14618        return this.tilesLoaded.length;
14619    },
14620
14621    /**
14622     * Clears all tiles and triggers an update on the next call to
14623     * Drawer.prototype.update().
14624     * @method
14625     * @return {OpenSeadragon.Drawer} Chainable.
14626     */
14627    reset: function() {
14628        clearTiles( this );
14629        this.lastResetTime = $.now();
14630        this.updateAgain = true;
14631        return this;
14632    },
14633
14634    /**
14635     * Forces the Drawer to update.
14636     * @method
14637     * @return {OpenSeadragon.Drawer} Chainable.
14638     */
14639    update: function() {
14640        //this.profiler.beginUpdate();
14641        this.midUpdate = true;
14642        updateViewport( this );
14643        this.midUpdate = false;
14644        //this.profiler.endUpdate();
14645        return this;
14646    },
14647
14648    /**
14649     * Returns whether rotation is supported or not.
14650     * @method
14651     * @return {Boolean} True if rotation is supported.
14652     */
14653    canRotate: function() {
14654        return this.useCanvas;
14655    },
14656
14657    /**
14658     * Destroy the drawer (unload current loaded tiles)
14659     * @method
14660     * @return null
14661     */
14662    destroy: function() {
14663        //unload current loaded tiles (=empty TILE_CACHE)
14664        for ( var i = 0; i < this.tilesLoaded.length; ++i ) {
14665            this.tilesLoaded[i].unload();
14666        }
14667
14668        //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed)
14669        this.canvas.width  = 1;
14670        this.canvas.height = 1;
14671    }
14672};
14673
14674/**
14675 * @private
14676 * @inner
14677 * Pretty much every other line in this needs to be documented so it's clear
14678 * how each piece of this routine contributes to the drawing process.  That's
14679 * why there are so many TODO's inside this function.
14680 */
14681function updateViewport( drawer ) {
14682
14683    drawer.updateAgain = false;
14684
14685    if( drawer.viewer ){
14686        /**
14687         * <em>- Needs documentation -</em>
14688         *
14689         * @event update-viewport
14690         * @memberof OpenSeadragon.Viewer
14691         * @type {object}
14692         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
14693         * @property {?Object} userData - Arbitrary subscriber-defined object.
14694         */
14695        drawer.viewer.raiseEvent( 'update-viewport', {} );
14696    }
14697
14698    var tile,
14699        level,
14700        best            = null,
14701        haveDrawn       = false,
14702        currentTime     = $.now(),
14703        viewportSize    = drawer.viewport.getContainerSize(),
14704        viewportBounds  = drawer.viewport.getBounds( true ),
14705        viewportTL      = viewportBounds.getTopLeft(),
14706        viewportBR      = viewportBounds.getBottomRight(),
14707        zeroRatioC      = drawer.viewport.deltaPixelsFromPoints(
14708            drawer.source.getPixelRatio( 0 ),
14709            true
14710        ).x,
14711        lowestLevel     = Math.max(
14712            drawer.source.minLevel,
14713            Math.floor(
14714                Math.log( drawer.minZoomImageRatio ) /
14715                Math.log( 2 )
14716            )
14717        ),
14718        highestLevel    = Math.min(
14719            Math.abs(drawer.source.maxLevel),
14720            Math.abs(Math.floor(
14721                Math.log( zeroRatioC / drawer.minPixelRatio ) /
14722                Math.log( 2 )
14723            ))
14724        ),
14725        degrees         = drawer.viewport.degrees,
14726        renderPixelRatioC,
14727        renderPixelRatioT,
14728        zeroRatioT,
14729        optimalRatio,
14730        levelOpacity,
14731        levelVisibility;
14732
14733    // Reset tile's internal drawn state
14734    while ( drawer.lastDrawn.length > 0 ) {
14735        tile = drawer.lastDrawn.pop();
14736        tile.beingDrawn = false;
14737    }
14738
14739    // Clear canvas
14740    drawer.canvas.innerHTML   = "";
14741    if ( drawer.useCanvas ) {
14742        if( drawer.canvas.width  != viewportSize.x ||
14743            drawer.canvas.height != viewportSize.y ){
14744            drawer.canvas.width  = viewportSize.x;
14745            drawer.canvas.height = viewportSize.y;
14746        }
14747        drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y );
14748    }
14749
14750    //Change bounds for rotation
14751    if (degrees === 90 || degrees === 270) {
14752        var rotatedBounds = viewportBounds.rotate( degrees );
14753        viewportTL = rotatedBounds.getTopLeft();
14754        viewportBR = rotatedBounds.getBottomRight();
14755    }
14756
14757    //Don't draw if completely outside of the viewport
14758    if  ( !drawer.wrapHorizontal &&
14759        ( viewportBR.x < 0 || viewportTL.x > 1 ) ) {
14760        return;
14761    } else if
14762        ( !drawer.wrapVertical &&
14763        ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) {
14764        return;
14765    }
14766
14767    // Calculate viewport rect / bounds
14768    if ( !drawer.wrapHorizontal ) {
14769        viewportTL.x = Math.max( viewportTL.x, 0 );
14770        viewportBR.x = Math.min( viewportBR.x, 1 );
14771    }
14772    if ( !drawer.wrapVertical ) {
14773        viewportTL.y = Math.max( viewportTL.y, 0 );
14774        viewportBR.y = Math.min( viewportBR.y, drawer.normHeight );
14775    }
14776
14777    // Calculations for the interval of levels to draw
14778    // (above in initial var statement)
14779    // can return invalid intervals; fix that here if necessary
14780    lowestLevel = Math.min( lowestLevel, highestLevel );
14781
14782    // Update any level that will be drawn
14783    var drawLevel; // FIXME: drawLevel should have a more explanatory name
14784    for ( level = highestLevel; level >= lowestLevel; level-- ) {
14785        drawLevel = false;
14786
14787        //Avoid calculations for draw if we have already drawn this
14788        renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints(
14789            drawer.source.getPixelRatio( level ),
14790            true
14791        ).x;
14792
14793        if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) ||
14794             ( level == lowestLevel ) ) {
14795            drawLevel = true;
14796            haveDrawn = true;
14797        } else if ( !haveDrawn ) {
14798            continue;
14799        }
14800
14801        //Perform calculations for draw if we haven't drawn this
14802        renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints(
14803            drawer.source.getPixelRatio( level ),
14804            false
14805        ).x;
14806
14807        zeroRatioT      = drawer.viewport.deltaPixelsFromPoints(
14808            drawer.source.getPixelRatio(
14809                Math.max(
14810                    drawer.source.getClosestLevel( drawer.viewport.containerSize ) - 1,
14811                    0
14812                )
14813            ),
14814            false
14815        ).x;
14816
14817        optimalRatio    = drawer.immediateRender ?
14818            1 :
14819            zeroRatioT;
14820
14821        levelOpacity    = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 );
14822
14823        levelVisibility = optimalRatio / Math.abs(
14824            optimalRatio - renderPixelRatioT
14825        );
14826
14827        // Update the level and keep track of 'best' tile to load
14828        best = updateLevel(
14829            drawer,
14830            haveDrawn,
14831            drawLevel,
14832            level,
14833            levelOpacity,
14834            levelVisibility,
14835            viewportTL,
14836            viewportBR,
14837            currentTime,
14838            best
14839        );
14840
14841        // Stop the loop if lower-res tiles would all be covered by
14842        // already drawn tiles
14843        if (  providesCoverage( drawer.coverage, level ) ) {
14844            break;
14845        }
14846    }
14847
14848    // Perform the actual drawing
14849    drawTiles( drawer, drawer.lastDrawn );
14850
14851    // Load the new 'best' tile
14852    if ( best ) {
14853        loadTile( drawer, best, currentTime );
14854        // because we haven't finished drawing, so
14855        drawer.updateAgain = true;
14856    }
14857
14858}
14859
14860
14861function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){
14862
14863    var x, y,
14864        tileTL,
14865        tileBR,
14866        numberOfTiles,
14867        viewportCenter  = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() );
14868
14869
14870    if( drawer.viewer ){
14871        /**
14872         * <em>- Needs documentation -</em>
14873         *
14874         * @event update-level
14875         * @memberof OpenSeadragon.Viewer
14876         * @type {object}
14877         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
14878         * @property {Object} havedrawn
14879         * @property {Object} level
14880         * @property {Object} opacity
14881         * @property {Object} visibility
14882         * @property {Object} topleft
14883         * @property {Object} bottomright
14884         * @property {Object} currenttime
14885         * @property {Object} best
14886         * @property {?Object} userData - Arbitrary subscriber-defined object.
14887         */
14888        drawer.viewer.raiseEvent( 'update-level', {
14889            havedrawn: haveDrawn,
14890            level: level,
14891            opacity: levelOpacity,
14892            visibility: levelVisibility,
14893            topleft: viewportTL,
14894            bottomright: viewportBR,
14895            currenttime: currentTime,
14896            best: best
14897        });
14898    }
14899
14900    //OK, a new drawing so do your calculations
14901    tileTL    = drawer.source.getTileAtPoint( level, viewportTL );
14902    tileBR    = drawer.source.getTileAtPoint( level, viewportBR );
14903    numberOfTiles  = drawer.source.getNumTiles( level );
14904
14905    resetCoverage( drawer.coverage, level );
14906
14907    if ( !drawer.wrapHorizontal ) {
14908        tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 );
14909    }
14910    if ( !drawer.wrapVertical ) {
14911        tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 );
14912    }
14913
14914    for ( x = tileTL.x; x <= tileBR.x; x++ ) {
14915        for ( y = tileTL.y; y <= tileBR.y; y++ ) {
14916
14917            best = updateTile(
14918                drawer,
14919                drawLevel,
14920                haveDrawn,
14921                x, y,
14922                level,
14923                levelOpacity,
14924                levelVisibility,
14925                viewportCenter,
14926                numberOfTiles,
14927                currentTime,
14928                best
14929            );
14930
14931        }
14932    }
14933
14934    return best;
14935}
14936
14937function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
14938
14939    var tile = getTile(
14940            x, y,
14941            level,
14942            drawer.source,
14943            drawer.tilesMatrix,
14944            currentTime,
14945            numberOfTiles,
14946            drawer.normHeight
14947        ),
14948        drawTile = drawLevel;
14949
14950    if( drawer.viewer ){
14951        /**
14952         * <em>- Needs documentation -</em>
14953         *
14954         * @event update-tile
14955         * @memberof OpenSeadragon.Viewer
14956         * @type {object}
14957         * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
14958         * @property {OpenSeadragon.Tile} tile
14959         * @property {?Object} userData - Arbitrary subscriber-defined object.
14960         */
14961        drawer.viewer.raiseEvent( 'update-tile', {
14962            tile: tile
14963        });
14964    }
14965
14966    setCoverage( drawer.coverage, level, x, y, false );
14967
14968    if ( !tile.exists ) {
14969        return best;
14970    }
14971
14972    if ( haveDrawn && !drawTile ) {
14973        if ( isCovered( drawer.coverage, level, x, y ) ) {
14974            setCoverage( drawer.coverage, level, x, y, true );
14975        } else {
14976            drawTile = true;
14977        }
14978    }
14979
14980    if ( !drawTile ) {
14981        return best;
14982    }
14983
14984    positionTile(
14985        tile,
14986        drawer.source.tileOverlap,
14987        drawer.viewport,
14988        viewportCenter,
14989        levelVisibility
14990    );
14991
14992    if ( tile.loaded ) {
14993        var needsUpdate = blendTile(
14994            drawer,
14995            tile,
14996            x, y,
14997            level,
14998            levelOpacity,
14999            currentTime
15000        );
15001
15002        if ( needsUpdate ) {
15003            drawer.updateAgain = true;
15004        }
15005    } else if ( tile.loading ) {
15006        // the tile is already in the download queue
15007        // thanks josh1093 for finally translating this typo
15008    } else {
15009        best = compareTiles( best, tile );
15010    }
15011
15012    return best;
15013}
15014
15015function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) {
15016    var xMod,
15017        yMod,
15018        bounds,
15019        exists,
15020        url,
15021        tile;
15022
15023    if ( !tilesMatrix[ level ] ) {
15024        tilesMatrix[ level ] = {};
15025    }
15026    if ( !tilesMatrix[ level ][ x ] ) {
15027        tilesMatrix[ level ][ x ] = {};
15028    }
15029
15030    if ( !tilesMatrix[ level ][ x ][ y ] ) {
15031        xMod    = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
15032        yMod    = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
15033        bounds  = tileSource.getTileBounds( level, xMod, yMod );
15034        exists  = tileSource.tileExists( level, xMod, yMod );
15035        url     = tileSource.getTileUrl( level, xMod, yMod );
15036
15037        bounds.x += 1.0 * ( x - xMod ) / numTiles.x;
15038        bounds.y += normHeight * ( y - yMod ) / numTiles.y;
15039
15040        tilesMatrix[ level ][ x ][ y ] = new $.Tile(
15041            level,
15042            x,
15043            y,
15044            bounds,
15045            exists,
15046            url
15047        );
15048    }
15049
15050    tile = tilesMatrix[ level ][ x ][ y ];
15051    tile.lastTouchTime = time;
15052
15053    return tile;
15054}
15055
15056function loadTile( drawer, tile, time ) {
15057    if( drawer.viewport.collectionMode ){
15058        drawer.midUpdate = false;
15059        onTileLoad( drawer, tile, time );
15060    } else {
15061        tile.loading = true;
15062        drawer.imageLoader.addJob({
15063            src: tile.url,
15064            crossOriginPolicy: drawer.crossOriginPolicy,
15065            callback: function( image ){
15066                onTileLoad( drawer, tile, time, image );
15067            }
15068        });
15069    }
15070}
15071
15072function onTileLoad( drawer, tile, time, image ) {
15073    var insertionIndex,
15074        cutoff,
15075        worstTile,
15076        worstTime,
15077        worstLevel,
15078        worstTileIndex,
15079        prevTile,
15080        prevTime,
15081        prevLevel,
15082        i;
15083
15084    tile.loading = false;
15085
15086    if ( drawer.midUpdate ) {
15087        $.console.warn( "Tile load callback in middle of drawing routine." );
15088        return;
15089    } else if ( !image  && !drawer.viewport.collectionMode ) {
15090        $.console.log( "Tile %s failed to load: %s", tile, tile.url );
15091        if( !drawer.debugMode ){
15092            tile.exists = false;
15093            return;
15094        }
15095    } else if ( time < drawer.lastResetTime ) {
15096        $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url );
15097        return;
15098    }
15099
15100    tile.loaded = true;
15101    tile.image  = image;
15102
15103
15104    insertionIndex = drawer.tilesLoaded.length;
15105
15106    if ( drawer.tilesLoaded.length >
15106= drawer.maxImageCacheCount ) {
15107        cutoff = Math.ceil( Math.log( drawer.source.tileSize ) / Math.log( 2 ) );
15108
15109        worstTile       = null;
15110        worstTileIndex  = -1;
15111
15112        for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) {
15113            prevTile = drawer.tilesLoaded[ i ];
15114
15115            if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) {
15116                continue;
15117            } else if ( !worstTile ) {
15118                worstTile       = prevTile;
15119                worstTileIndex  = i;
15120                continue;
15121            }
15122
15123            prevTime    = prevTile.lastTouchTime;
15124            worstTime   = worstTile.lastTouchTime;
15125            prevLevel   = prevTile.level;
15126            worstLevel  = worstTile.level;
15127
15128            if ( prevTime < worstTime ||
15129               ( prevTime == worstTime && prevLevel > worstLevel ) ) {
15130                worstTile       = prevTile;
15131                worstTileIndex  = i;
15132            }
15133        }
15134
15135        if ( worstTile && worstTileIndex >= 0 ) {
15136            worstTile.unload();
15137            insertionIndex = worstTileIndex;
15138        }
15139    }
15140
15141    drawer.tilesLoaded[ insertionIndex ] = tile;
15142    drawer.updateAgain = true;
15143}
15144
15145
15146function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){
15147    var boundsTL     = tile.bounds.getTopLeft(),
15148        boundsSize   = tile.bounds.getSize(),
15149        positionC    = viewport.pixelFromPoint( boundsTL, true ),
15150        positionT    = viewport.pixelFromPoint( boundsTL, false ),
15151        sizeC        = viewport.deltaPixelsFromPoints( boundsSize, true ),
15152        sizeT        = viewport.deltaPixelsFromPoints( boundsSize, false ),
15153        tileCenter   = positionT.plus( sizeT.divide( 2 ) ),
15154        tileDistance = viewportCenter.distanceTo( tileCenter );
15155
15156    if ( !overlap ) {
15157        sizeC = sizeC.plus( new $.Point( 1, 1 ) );
15158    }
15159
15160    tile.position   = positionC;
15161    tile.size       = sizeC;
15162    tile.distance   = tileDistance;
15163    tile.visibility = levelVisibility;
15164}
15165
15166
15167function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){
15168    var blendTimeMillis = 1000 * drawer.blendTime,
15169        deltaTime,
15170        opacity;
15171
15172    if ( !tile.blendStart ) {
15173        tile.blendStart = currentTime;
15174    }
15175
15176    deltaTime   = currentTime - tile.blendStart;
15177    opacity     = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1;
15178
15179    if ( drawer.alwaysBlend ) {
15180        opacity *= levelOpacity;
15181    }
15182
15183    tile.opacity = opacity;
15184
15185    drawer.lastDrawn.push( tile );
15186
15187    if ( opacity == 1 ) {
15188        setCoverage( drawer.coverage, level, x, y, true );
15189    } else if ( deltaTime < blendTimeMillis ) {
15190        return true;
15191    }
15192
15193    return false;
15194}
15195
15196
15197function clearTiles( drawer ) {
15198    drawer.tilesMatrix = {};
15199    drawer.tilesLoaded = [];
15200}
15201
15202/**
15203 * @private
15204 * @inner
15205 * Returns true if the given tile provides coverage to lower-level tiles of
15206 * lower resolution representing the same content. If neither x nor y is
15207 * given, returns true if the entire visible level provides coverage.
15208 *
15209 * Note that out-of-bounds tiles provide coverage in this sense, since
15210 * there's no content that they would need to cover. Tiles at non-existent
15211 * levels that are within the image bounds, however, do not.
15212 */
15213function providesCoverage( coverage, level, x, y ) {
15214    var rows,
15215        cols,
15216        i, j;
15217
15218    if ( !coverage[ level ] ) {
15219        return false;
15220    }
15221
15222    if ( x === undefined || y === undefined ) {
15223        rows = coverage[ level ];
15224        for ( i in rows ) {
15225            if ( rows.hasOwnProperty( i ) ) {
15226                cols = rows[ i ];
15227                for ( j in cols ) {
15228                    if ( cols.hasOwnProperty( j ) && !cols[ j ] ) {
15229                        return false;
15230                    }
15231                }
15232            }
15233        }
15234
15235        return true;
15236    }
15237
15238    return (
15239        coverage[ level ][ x] === undefined ||
15240        coverage[ level ][ x ][ y ] === undefined ||
15241        coverage[ level ][ x ][ y ] === true
15242    );
15243}
15244
15245/**
15246 * @private
15247 * @inner
15248 * Returns true if the given tile is completely covered by higher-level
15249 * tiles of higher resolution representing the same content. If neither x
15250 * nor y is given, returns true if the entire visible level is covered.
15251 */
15252function isCovered( coverage, level, x, y ) {
15253    if ( x === undefined || y === undefined ) {
15254        return providesCoverage( coverage, level + 1 );
15255    } else {
15256        return (
15257             providesCoverage( coverage, level + 1, 2 * x, 2 * y ) &&
15258             providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) &&
15259             providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) &&
15260             providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 )
15261        );
15262    }
15263}
15264
15265/**
15266 * @private
15267 * @inner
15268 * Sets whether the given tile provides coverage or not.
15269 */
15270function setCoverage( coverage, level, x, y, covers ) {
15271    if ( !coverage[ level ] ) {
15272        $.console.warn(
15273            "Setting coverage for a tile before its level's coverage has been reset: %s",
15274            level
15275        );
15276        return;
15277    }
15278
15279    if ( !coverage[ level ][ x ] ) {
15280        coverage[ level ][ x ] = {};
15281    }
15282
15283    coverage[ level ][ x ][ y ] = covers;
15284}
15285
15286/**
15287 * @private
15288 * @inner
15289 * Resets coverage information for the given level. This should be called
15290 * after every draw routine. Note that at the beginning of the next draw
15291 * routine, coverage for every visible tile should be explicitly set.
15292 */
15293function resetCoverage( coverage, level ) {
15294    coverage[ level ] = {};
15295}
15296
15297/**
15298 * @private
15299 * @inner
15300 * Determines whether the 'last best' tile for the area is better than the
15301 * tile in question.
15302 */
15303function compareTiles( previousBest, tile ) {
15304    if ( !previousBest ) {
15305        return tile;
15306    }
15307
15308    if ( tile.visibility > previousBest.visibility ) {
15309        return tile;
15310    } else if ( tile.visibility == previousBest.visibility ) {
15311        if ( tile.distance < previousBest.distance ) {
15312            return tile;
15313        }
15314    }
15315
15316    return previousBest;
15317}
15318
15319function drawTiles( drawer, lastDrawn ){
15320    var i,
15321        tile,
15322        tileKey,
15323        viewer,
15324        viewport,
15325        position,
15326        tileSource,
15327        collectionTileSource;
15328
15329    // We need a callback to give image manipulation a chance to happen
15330    var drawingHandler = function(args) {
15331        if (drawer.viewer) {
15332          /**
15333           * This event is fired just before the tile is drawn giving the application a chance to alter the image.
15334           *
15335           * NOTE: This event is only fired when the drawer is using a <canvas>.
15336           *
15337           * @event tile-drawing
15338           * @memberof OpenSeadragon.Viewer
15339           * @type {object}
15340           * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
15341           * @property {OpenSeadragon.Tile} tile
15342           * @property {?Object} userData - 'context', 'tile' and 'rendered'.
15343           */
15344            drawer.viewer.raiseEvent('tile-drawing', args);
15345        }
15346    };
15347
15348    for ( i = lastDrawn.length - 1; i >= 0; i-- ) {
15349        tile = lastDrawn[ i ];
15350
15351        //We dont actually 'draw' a collection tile, rather its used to house
15352        //an overlay which does the drawing in its own viewport
15353        if( drawer.viewport.collectionMode ){
15354
15355            tileKey = tile.x + '/' + tile.y;
15356            viewport = drawer.viewport;
15357            collectionTileSource = viewport.collectionTileSource;
15358
15359            if( !drawer.collectionOverlays[ tileKey ] ){
15360
15361                position = collectionTileSource.layout == 'horizontal' ?
15362                    tile.y + ( tile.x * collectionTileSource.rows ) :
15363                    tile.x + ( tile.y * collectionTileSource.rows );
15364
15365                if (position < collectionTileSource.tileSources.length) {
15366                    tileSource = collectionTileSource.tileSources[ position ];
15367                } else {
15368                    tileSource = null;
15369                }
15370
15371                //$.console.log("Rendering collection tile %s | %s | %s", tile.y, tile.y, position);
15372                if( tileSource ){
15373                    drawer.collectionOverlays[ tileKey ] = viewer = new $.Viewer({
15374                        hash:                   viewport.viewer.hash + "-" + tileKey,
15375                        element:                $.makeNeutralElement( "div" ),
15376                        mouseNavEnabled:        false,
15377                        showNavigator:          false,
15378                        showSequenceControl:    false,
15379                        showNavigationControl:  false,
15380                        tileSources: [
15381                            tileSource
15382                        ]
15383                    });
15384
15385                    //TODO: IE seems to barf on this, not sure if its just the border
15386                    //      but we probably need to clear this up with a better
15387                    //      test of support for various css features
15388                    if( SUBPIXEL_RENDERING ){
15389                        viewer.element.style.border = '1px solid rgba(255,255,255,0.38)';
15390                        viewer.element.style['-webkit-box-reflect'] =
15391                            'below 0px -webkit-gradient('+
15392                                'linear,left '+
15393                                'top,left '+
15394                                'bottom,from(transparent),color-stop(62%,transparent),to(rgba(255,255,255,0.62))'+
15395                            ')';
15396                    }
15397
15398                    drawer.viewer.addOverlay(
15399                        viewer.element,
15400                        tile.bounds
15401                    );
15402                }
15403
15404            }else{
15405                viewer = drawer.collectionOverlays[ tileKey ];
15406                if( viewer.viewport ){
15407                    viewer.viewport.resize( tile.size, true );
15408                    viewer.viewport.goHome( true );
15409                }
15410            }
15411
15412        } else {
15413
15414            if ( drawer.useCanvas ) {
15415                // TODO do this in a more performant way
15416                // specifically, don't save,rotate,restore every time we draw a tile
15417                if( drawer.viewport.degrees !== 0 ) {
15418                    offsetForRotation( tile, drawer.canvas, drawer.context, drawer.viewport.degrees );
15419                    tile.drawCanvas( drawer.context, drawingHandler );
15420                    restoreRotationChanges( tile, drawer.canvas, drawer.context );
15421                } else {
15422                    tile.drawCanvas( drawer.context, drawingHandler );
15423                }
15424            } else {
15425                tile.drawHTML( drawer.canvas );
15426            }
15427
15428
15429            tile.beingDrawn = true;
15430        }
15431
15432        if( drawer.debugMode ){
15433            try{
15434                drawDebugInfo( drawer, tile, lastDrawn.length, i );
15435            }catch(e){
15436                $.console.error(e);
15437            }
15438        }
15439
15440        if( drawer.viewer ){
15441            /**
15442             * <em>- Needs documentation -</em>
15443             *
15444             * @event tile-drawn
15445             * @memberof OpenSeadragon.Viewer
15446             * @type {object}
15447             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
15448             * @property {OpenSeadragon.Tile} tile
15449             * @property {?Object} userData - Arbitrary subscriber-defined object.
15450             */
15451            drawer.viewer.raiseEvent( 'tile-drawn', {
15452                tile: tile
15453            });
15454        }
15455    }
15456}
15457
15458function offsetForRotation( tile, canvas, context, degrees ){
15459    var cx = canvas.width / 2,
15460        cy = canvas.height / 2,
15461        px = tile.position.x - cx,
15462        py = tile.position.y - cy;
15463
15464    context.save();
15465
15466    context.translate(cx, cy);
15467    context.rotate( Math.PI / 180 * degrees);
15468    tile.position.x = px;
15469    tile.position.y = py;
15470}
15471
15472function restoreRotationChanges( tile, canvas, context ){
15473    var cx = canvas.width / 2,
15474        cy = canvas.height / 2,
15475        px = tile.position.x + cx,
15476        py = tile.position.y + cy;
15477
15478    tile.position.x = px;
15479    tile.position.y = py;
15480
15481    context.restore();
15482}
15483
15484
15485function drawDebugInfo( drawer, tile, count, i ){
15486
15487    if ( drawer.useCanvas ) {
15488        drawer.context.save();
15489        drawer.context.lineWidth = 2;
15490        drawer.context.font = 'small-caps bold 13px ariel';
15491        drawer.context.strokeStyle = drawer.debugGridColor;
15492        drawer.context.fillStyle = drawer.debugGridColor;
15493        drawer.context.strokeRect(
15494            tile.position.x,
15495            tile.position.y,
15496            tile.size.x,
15497            tile.size.y
15498        );
15499        if( tile.x === 0 && tile.y === 0 ){
15500            drawer.context.fillText(
15501                "Zoom: " + drawer.viewport.getZoom(),
15502                tile.position.x,
15503                tile.position.y - 30
15504            );
15505            drawer.context.fillText(
15506                "Pan: " + drawer.viewport.getBounds().toString(),
15507                tile.position.x,
15508                tile.position.y - 20
15509            );
15510        }
15511        drawer.context.fillText(
15512            "Level: " + tile.level,
15513            tile.position.x + 10,
15514            tile.position.y + 20
15515        );
15516        drawer.context.fillText(
15517            "Column: " + tile.x,
15518            tile.position.x + 10,
15519            tile.position.y + 30
15520        );
15521        drawer.context.fillText(
15522            "Row: " + tile.y,
15523            tile.position.x + 10,
15524            tile.position.y + 40
15525        );
15526        drawer.context.fillText(
15527            "Order: " + i + " of " + count,
15528            tile.position.x + 10,
15529            tile.position.y + 50
15530        );
15531        drawer.context.fillText(
15532            "Size: " + tile.size.toString(),
15533            tile.position.x + 10,
15534            tile.position.y + 60
15535        );
15536        drawer.context.fillText(
15537            "Position: " + tile.position.toString(),
15538            tile.position.x + 10,
15539            tile.position.y + 70
15540        );
15541        drawer.context.restore();
15542    }
15543}
15544
15545
15546}( OpenSeadragon ));
15547
15548/*
15549 * OpenSeadragon - Viewport
15550 *
15551 * Copyright (C) 2009 CodePlex Foundation
15552 * Copyright (C) 2010-2013 OpenSeadragon contributors
15553 *
15554 * Redistribution and use in source and binary forms, with or without
15555 * modification, are permitted provided that the following conditions are
15556 * met:
15557 *
15558 * - Redistributions of source code must retain the above copyright notice,
15559 *   this list of conditions and the following disclaimer.
15560 *
15561 * - Redistributions in binary form must reproduce the above copyright
15562 *   notice, this list of conditions and the following disclaimer in the
15563 *   documentation and/or other materials provided with the distribution.
15564 *
15565 * - Neither the name of CodePlex Foundation nor the names of its
15566 *   contributors may be used to endorse or promote products derived from
15567 *   this software without specific prior written permission.
15568 *
15569 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15570 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
15571 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
15572 * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
15573 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
15574 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
15575 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
15576 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
15577 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
15578 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
15579 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15580 */
15581
15582(function( $ ){
15583
15584
15585/**
15586 * @class Viewport
15587 * @classdesc Handles coordinate-related functionality (zoom, pan, rotation, etc.) for an {@link OpenSeadragon.Viewer}.
15588 * A new instance is created for each TileSource opened (see {@link OpenSeadragon.Viewer#viewport}).
15589 *
15590 * @memberof OpenSeadragon
15591 */
15592$.Viewport = function( options ) {
15593
15594    //backward compatibility for positional args while prefering more
15595    //idiomatic javascript options object as the only argument
15596    var args = arguments;
15597    if(  args.length && args[ 0 ] instanceof $.Point ){
15598        options = {
15599            containerSize:  args[ 0 ],
15600            contentSize:    args[ 1 ],
15601            config:         args[ 2 ]
15602        };
15603    }
15604
15605    //options.config and the general config argument are deprecated
15606    //in favor of the more direct specification of optional settings
15607    //being passed directly on the options object
15608    if ( options.config ){
15609        $.extend( true, options, options.config );
15610        delete options.config;
15611    }
15612
15613    $.extend( true, this, {
15614
15615        //required settings
15616        containerSize:      null,
15617        contentSize:        null,
15618
15619        //internal state properties
15620        zoomPoint:          null,
15621        viewer:           null,
15622
15623        //configurable options
15624        springStiffness:    $.DEFAULT_SETTINGS.springStiffness,
15625        animationTime:      $.DEFAULT_SETTINGS.animationTime,
15626        minZoomImageRatio:  $.DEFAULT_SETTINGS.minZoomImageRatio,
15627        maxZoomPixelRatio:  $.DEFAULT_SETTINGS.maxZoomPixelRatio,
15628        visibilityRatio:    $.DEFAULT_SETTINGS.visibilityRatio,
15629        wrapHorizontal:     $.DEFAULT_SETTINGS.wrapHorizontal,
15630        wrapVertical:       $.DEFAULT_SETTINGS.wrapVertical,
15631        defaultZoomLevel:   $.DEFAULT_SETTINGS.defaultZoomLevel,
15632        minZoomLevel:       $.DEFAULT_SETTINGS.minZoomLevel,
15633        maxZoomLevel:       $.DEFAULT_SETTINGS.maxZoomLevel,
15634        degrees:            $.DEFAULT_SETTINGS.degrees
15635
15636    }, options );
15637
15638    this.centerSpringX = new $.Spring({
15639        initial: 0,
15640        springStiffness: this.springStiffness,
15641        animationTime:   this.animationTime
15642    });
15643    this.centerSpringY = new $.Spring({
15644        initial: 0,
15645        springStiffness: this.springStiffness,
15646        animationTime:   this.animationTime
15647    });
15648    this.zoomSpring    = new $.Spring({
15649        initial: 1,
15650        springStiffness: this.springStiffness,
15651        animationTime:   this.animationTime
15652    });
15653
15654    this.resetContentSize( this.contentSize );
15655    this.goHome( true );
15656    this.update();
15657};
15658
15659$.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{
15660
15661    /**
15662     * @function
15663     * @return {OpenSeadragon.Viewport} Chainable.
15664     * @fires OpenSeadragon.Viewer.event:reset-size
15665     */
15666    resetContentSize: function( contentSize ){
15667        this.contentSize    = contentSize;
15668        this.contentAspectX = this.contentSize.x / this.contentSize.y;
15669        this.contentAspectY = this.contentSize.y / this.contentSize.x;
15670        this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectY );
15671        this.fitHeightBounds = new $.Rect( 0, 0, this.contentAspectY, this.contentAspectY);
15672
15673        this.homeBounds = new $.Rect( 0, 0, 1, this.contentAspectY );
15674
15675        if( this.viewer ){
15676            /**
15677             * Raised when the viewer's content size is reset (see {@link OpenSeadragon.Viewport#resetContentSize}).
15678             *
15679             * @event reset-size
15680             * @memberof OpenSeadragon.Viewer
15681             * @type {object}
15682             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
15683             * @property {OpenSeadragon.Point} contentSize
15684             * @property {?Object} userData - Arbitrary subscriber-defined object.
15685             */
15686            this.viewer.raiseEvent( 'reset-size', {
15687                contentSize: contentSize
15688            });
15689        }
15690
15691        return this;
15692    },
15693
15694    /**
15695     * @function
15696     */
15697    getHomeZoom: function() {
15698        var aspectFactor =
15699            this.contentAspectX / this.getAspectRatio();
15700
15701        if( this.defaultZoomLevel ){
15702            return this.defaultZoomLevel;
15703        } else {
15704            return ( aspectFactor >= 1 ) ?
15705                1 :
15706                aspectFactor;
15707        }
15708    },
15709
15710    /**
15711     * @function
15712     */
15713    getHomeBounds: function() {
15714        var center = this.homeBounds.getCenter( ),
15715            width  = 1.0 / this.getHomeZoom( ),
15716            height = width / this.getAspectRatio();
15717
15718        return new $.Rect(
15719            center.x - ( width / 2.0 ),
15720            center.y - ( height / 2.0 ),
15721            width,
15722            height
15723        );
15724    },
15725
15726    /**
15727     * @function
15728     * @param {Boolean} immediately
15729     * @fires OpenSeadragon.Viewer.event:home
15730     */
15731    goHome: function( immediately ) {
15732        if( this.viewer ){
15733            /**
15734             * Raised when the "home" operation occurs (see {@link OpenSeadragon.Viewport#goHome}).
15735             *
15736             * @event home
15737             * @memberof OpenSeadragon.Viewer
15738             * @type {object}
15739             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
15740             * @property {Boolean} immediately
15741             * @property {?Object} userData - Arbitrary subscriber-defined object.
15742             */
15743            this.viewer.raiseEvent( 'home', {
15744                immediately: immediately
15745            });
15746        }
15747        return this.fitBounds( this.getHomeBounds(), immediately );
15748    },
15749
15750    /**
15751     * @function
15752     */
15753    getMinZoom: function() {
15754        var homeZoom = this.getHomeZoom(),
15755            zoom = this.minZoomLevel ?
15756            this.minZoomLevel :
15757                this.minZoomImageRatio * homeZoom;
15758
15759        return Math.min( zoom, homeZoom );
15760    },
15761
15762    /**
15763     * @function
15764     */
15765    getMaxZoom: function() {
15766        var zoom = this.maxZoomLevel ?
15767            this.maxZoomLevel :
15768                ( this.contentSize.x * this.maxZoomPixelRatio / this.containerSize.x );
15769
15770        return Math.max( zoom, this.getHomeZoom() );
15771    },
15772
15773    /**
15774     * @function
15775     */
15776    getAspectRatio: function() {
15777        return this.containerSize.x / this.containerSize.y;
15778    },
15779
15780    /**
15781     * @function
15782     */
15783    getContainerSize: function() {
15784        return new $.Point(
15785            this.containerSize.x,
15786            this.containerSize.y
15787        );
15788    },
15789
15790    /**
15791     * @function
15792     * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
15793     */
15794    getBounds: function( current ) {
15795        var center = this.getCenter( current ),
15796            width  = 1.0 / this.getZoom( current ),
15797            height = width / this.getAspectRatio();
15798
15799        return new $.Rect(
15800            center.x - ( width / 2.0 ),
15801            center.y - ( height / 2.0 ),
15802            width,
15803            height
15804        );
15805    },
15806
15807    /**
15808     * @function
15809     * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
15810     */
15811    getCenter: function( current ) {
15812        var centerCurrent = new $.Point(
15813                this.centerSpringX.current.value,
15814                this.centerSpringY.current.value
15815            ),
15816            centerTarget = new $.Point(
15817                this.centerSpringX.target.value,
15818                this.centerSpringY.target.value
15819            ),
15820            oldZoomPixel,
15821            zoom,
15822            width,
15823            height,
15824            bounds,
15825            newZoomPixel,
15826            deltaZoomPixels,
15827            deltaZoomPoints;
15828
15829        if ( current ) {
15830            return centerCurrent;
15831        } else if ( !this.zoomPoint ) {
15832            return centerTarget;
15833        }
15834
15835        oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
15836
15837        zoom    = this.getZoom();
15838        width   = 1.0 / zoom;
15839        height  = width / this.getAspectRatio();
15840        bounds  = new $.Rect(
15841            centerCurrent.x - width / 2.0,
15842            centerCurrent.y - height / 2.0,
15843            width,
15844            height
15845        );
15846
15847        newZoomPixel    = this.zoomPoint.minus(
15848            bounds.getTopLeft()
15849        ).times(
15850            this.containerSize.x / bounds.width
15851        );
15852        deltaZoomPixels = newZoomPixel.minus( oldZoomPixel );
15853        deltaZoomPoints = deltaZoomPixels.divide( this.containerSize.x * zoom );
15854
15855        return centerTarget.plus( deltaZoomPoints );
15856    },
15857
15858    /**
15859     * @function
15860     * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
15861     */
15862    getZoom: function( current ) {
15863        if ( current ) {
15864            return this.zoomSpring.current.value;
15865        } else {
15866            return this.zoomSpring.target.value;
15867        }
15868    },
15869
15870    /**
15871     * @function
15872     * @return {OpenSeadragon.Viewport} Chainable.
15873     * @fires OpenSeadragon.Viewer.event:constrain
15874     */
15875    applyConstraints: function( immediately ) {
15876        var actualZoom = this.getZoom(),
15877            constrainedZoom = Math.max(
15878                Math.min( actualZoom, this.getMaxZoom() ),
15879                this.getMinZoom()
15880            ),
15881            bounds,
15882            horizontalThreshold,
15883            verticalThreshold,
15884            left,
15885            right,
15886            top,
15887            bottom,
15888            dx = 0,
15889            dy = 0;
15890
15891        if ( actualZoom != constrainedZoom ) {
15892            this.zoomTo( constrainedZoom, this.zoomPoint, immediately );
15893        }
15894
15895        bounds = this.getBounds();
15896
15897        horizontalThreshold = this.visibilityRatio * bounds.width;
15898        verticalThreshold   = this.visibilityRatio * bounds.height;
15899
15900        left   = bounds.x + bounds.width;
15901        right  = 1 - bounds.x;
15902        top    = bounds.y + bounds.height;
15903        bottom = this.contentAspectY - bounds.y;
15904
15905        if ( this.wrapHorizontal ) {
15906            //do nothing
15907        } else {
15908            if ( left < horizontalThreshold ) {
15909                dx = horizontalThreshold - left;
15910            }
15911            if ( right < horizontalThreshold ) {
15912                dx = dx ?
15913                    ( dx + right - horizontalThreshold ) / 2 :
15914                    ( right - horizontalThreshold );
15915            }
15916        }
15917
15918        if ( this.wrapVertical ) {
15919            //do nothing
15920        } else {
15921            if ( top < verticalThreshold ) {
15922                dy = ( verticalThreshold - top );
15923            }
15924            if ( bottom < verticalThreshold ) {
15925                dy =  dy ?
15926                    ( dy + bottom - verticalThreshold ) / 2 :
15927                    ( bottom - verticalThreshold );
15928            }
15929        }
15930
15931        if ( dx || dy || immediately ) {
15932            bounds.x += dx;
15933            bounds.y += dy;
15934            if( bounds.width > 1  ){
15935                bounds.x = 0.5 - bounds.width/2;
15936            }
15937            if( bounds.height > this.contentAspectY ){
15938                bounds.y = this.contentAspectY/2 - bounds.height/2;
15939            }
15940            this.fitBounds( bounds, immediately );
15941        }
15942
15943        if( this.viewer ){
15944            /**
15945             * Raised when the viewport constraints are applied (see {@link OpenSeadragon.Viewport#applyConstraints}).
15946             *
15947             * @event constrain
15948             * @memberof OpenSeadragon.Viewer
15949             * @type {object}
15950             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
15951             * @property {Boolean} immediately
15952             * @property {?Object} userData - Arbitrary subscriber-defined object.
15953             */
15954            this.viewer.raiseEvent( 'constrain', {
15955                immediately: immediately
15956            });
15957        }
15958
15959        return this;
15960    },
15961
15962    /**
15963     * @function
15964     * @param {Boolean} immediately
15965     */
15966    ensureVisible: function( immediately ) {
15967        return this.applyConstraints( immediately );
15968    },
15969
15970    /**
15971     * @function
15972     * @param {OpenSeadragon.Rect} bounds
15973     * @param {Boolean} immediately
15974     * @return {OpenSeadragon.Viewport} Chainable.
15975     */
15976    fitBounds: function( bounds, immediately ) {
15977        var aspect = this.getAspectRatio(),
15978            center = bounds.getCenter(),
15979            newBounds = new $.Rect(
15980                bounds.x,
15981                bounds.y,
15982                bounds.width,
15983                bounds.height
15984            ),
15985            oldBounds,
15986            oldZoom,
15987            newZoom,
15988            referencePoint;
15989
15990        if ( newBounds.getAspectRatio() >= aspect ) {
15991            newBounds.height = bounds.width / aspect;
15992            newBounds.y      = center.y - newBounds.height / 2;
15993        } else {
15994            newBounds.width = bounds.height * aspect;
15995            newBounds.x     = center.x - newBounds.width / 2;
15996        }
15997
15998        this.panTo( this.getCenter( true ), true );
15999        this.zoomTo( this.getZoom( true ), null, true );
16000
16001        oldBounds = this.getBounds();
16002        oldZoom   = this.getZoom();
16003        newZoom   = 1.0 / newBounds.width;
16004        if ( newZoom == oldZoom || newBounds.width == oldBounds.width ) {
16005            return this.panTo( center, immediately );
16006        }
16007
16008        referencePoint = oldBounds.getTopLeft().times(
16009            this.containerSize.x / oldBounds.width
16010        ).minus(
16011            newBounds.getTopLeft().times(
16012                this.containerSize.x / newBounds.width
16013            )
16014        ).divide(
16015            this.containerSize.x / oldBounds.width -
16016            this.containerSize.x / newBounds.width
16017        );
16018
16019        return this.zoomTo( newZoom, referencePoint, immediately );
16020    },
16021
16022
16023    /**
16024     * @function
16025     * @param {Boolean} immediately
16026     * @return {OpenSeadragon.Viewport} Chainable.
16027     */
16028    fitVertically: function( immediately ) {
16029        var center = this.getCenter();
16030
16031        if ( this.wrapHorizontal ) {
16032            center.x = ( 1 + ( center.x % 1 ) ) % 1;
16033            this.centerSpringX.resetTo( center.x );
16034            this.centerSpringX.update();
16035        }
16036
16037        if ( this.wrapVertical ) {
16038            center.y = (
16039                this.contentAspectY + ( center.y % this.contentAspectY )
16040            ) % this.contentAspectY;
16041            this.centerSpringY.resetTo( center.y );
16042            this.centerSpringY.update();
16043        }
16044
16045        return this.fitBounds( this.fitHeightBounds, immediately );
16046    },
16047
16048    /**
16049     * @function
16050     * @param {Boolean} immediately
16051     * @return {OpenSeadragon.Viewport} Chainable.
16052     */
16053    fitHorizontally: function( immediately ) {
16054        var center = this.getCenter();
16055
16056        if ( this.wrapHorizontal ) {
16057            center.x = (
16058                this.contentAspectX + ( center.x % this.contentAspectX )
16059            ) % this.contentAspectX;
16060            this.centerSpringX.resetTo( center.x );
16061            this.centerSpringX.update();
16062        }
16063
16064        if ( this.wrapVertical ) {
16065            center.y = ( 1 + ( center.y % 1 ) ) % 1;
16066            this.centerSpringY.resetTo( center.y );
16067            this.centerSpringY.update();
16068        }
16069
16070        return this.fitBounds( this.fitWidthBounds, immediately );
16071    },
16072
16073
16074    /**
16075     * @function
16076     * @param {OpenSeadragon.Point} delta
16077     * @param {Boolean} immediately
16078     * @return {OpenSeadragon.Viewport} Chainable.
16079     * @fires OpenSeadragon.Viewer.event:pan
16080     */
16081    panBy: function( delta, immediately ) {
16082        var center = new $.Point(
16083            this.centerSpringX.target.value,
16084            this.centerSpringY.target.value
16085        );
16086        delta = delta.rotate( -this.degrees, new $.Point( 0, 0 ) );
16087        return this.panTo( center.plus( delta ), immediately );
16088    },
16089
16090    /**
16091     * @function
16092     * @param {OpenSeadragon.Point} center
16093     * @param {Boolean} immediately
16094     * @return {OpenSeadragon.Viewport} Chainable.
16095     * @fires OpenSeadragon.Viewer.event:pan
16096     */
16097    panTo: function( center, immediately ) {
16098        if ( immediately ) {
16099            this.centerSpringX.resetTo( center.x );
16100            this.centerSpringY.resetTo( center.y );
16101        } else {
16102            this.centerSpringX.springTo( center.x );
16103            this.centerSpringY.springTo( center.y );
16104        }
16105
16106        if( this.viewer ){
16107            /**
16108             * Raised when the viewport is panned (see {@link OpenSeadragon.Viewport#panBy} and {@link OpenSeadragon.Viewport#panTo}).
16109             *
16110             * @event pan
16111             * @memberof OpenSeadragon.Viewer
16112             * @type {object}
16113             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
16114             * @property {OpenSeadragon.Point} center
16115             * @property {Boolean} immediately
16116             * @property {?Object} userData - Arbitrary subscriber-defined object.
16117             */
16118            this.viewer.raiseEvent( 'pan', {
16119                center: center,
16120                immediately: immediately
16121            });
16122        }
16123
16124        return this;
16125    },
16126
16127    /**
16128     * @function
16129     * @return {OpenSeadragon.Viewport} Chainable.
16130     * @fires OpenSeadragon.Viewer.event:zoom
16131     */
16132    zoomBy: function( factor, refPoint, immediately ) {
16133        if( refPoint instanceof $.Point && !isNaN( refPoint.x ) && !isNaN( refPoint.y ) ) {
16134            refPoint = refPoint.rotate(
16135                -this.degrees,
16136                new $.Point( this.centerSpringX.target.value, this.centerSpringY.target.value )
16137            );
16138        }
16139        return this.zoomTo( this.zoomSpring.target.value * factor, refPoint, immediately );
16140    },
16141
16142    /**
16143     * @function
16144     * @return {OpenSeadragon.Viewport} Chainable.
16145     * @fires OpenSeadragon.Viewer.event:zoom
16146     */
16147    zoomTo: function( zoom, refPoint, immediately ) {
16148
16149        this.zoomPoint = refPoint instanceof $.Point &&
16150            !isNaN(refPoint.x) &&
16151            !isNaN(refPoint.y) ?
16152            refPoint :
16153            null;
16154
16155        if ( immediately ) {
16156            this.zoomSpring.resetTo( zoom );
16157        } else {
16158            this.zoomSpring.springTo( zoom );
16159        }
16160
16161        if( this.viewer ){
16162            /**
16163             * Raised when the viewport zoom level changes (see {@link OpenSeadragon.Viewport#zoomBy} and {@link OpenSeadragon.Viewport#zoomTo}).
16164             *
16165             * @event zoom
16166             * @memberof OpenSeadragon.Viewer
16167             * @type {object}
16168             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
16169             * @property {Number} zoom
16170             * @property {OpenSeadragon.Point} refPoint
16171             * @property {Boolean} immediately
16172             * @property {?Object} userData - Arbitrary subscriber-defined object.
16173             */
16174            this.viewer.raiseEvent( 'zoom', {
16175                zoom: zoom,
16176                refPoint: refPoint,
16177                immediately: immediately
16178            });
16179        }
16180
16181        return this;
16182    },
16183
16184    /**
16185     * Currently only 90 degree rotation is supported and it only works
16186     * with the canvas. Additionally, the navigator does not rotate yet,
16187     * debug mode doesn't rotate yet, and overlay rotation is only
16188     * partially supported.
16189     * @function
16190     * @return {OpenSeadragon.Viewport} Chainable.
16191     */
16192    setRotation: function( degrees ) {
16193        if( !( this.viewer && this.viewer.drawer.canRotate() ) ) {
16194            return this;
16195        }
16196
16197        degrees = ( degrees + 360 ) % 360;
16198        if( degrees % 90 !== 0 ) {
16199            throw new Error('Currently only 0, 90, 180, and 270 degrees are supported.');
16200        }
16201        this.degrees = degrees;
16202        this.viewer.forceRedraw();
16203        
16204        return this;
16205    },
16206
16207    /**
16208     * Gets the current rotation in degrees.
16209     * @function
16210     * @return {Number} The current rotation in degrees.
16211     */
16212    getRotation: function() {
16213        return this.degrees;
16214    },
16215
16216    /**
16217     * @function
16218     * @return {OpenSeadragon.Viewport} Chainable.
16219     * @fires OpenSeadragon.Viewer.event:resize
16220     */
16221    resize: function( newContainerSize, maintain ) {
16222        var oldBounds = this.getBounds(),
16223            newBounds = oldBounds,
16224            widthDeltaFactor;
16225
16226        this.containerSize = new $.Point(
16227            newContainerSize.x,
16228            newContainerSize.y
16229        );
16230
16231        if ( maintain ) {
16232            widthDeltaFactor = newContainerSize.x / this.containerSize.x;
16233            newBounds.width  = oldBounds.width * widthDeltaFactor;
16234            newBounds.height = newBounds.width / this.getAspectRatio();
16235        }
16236
16237        if( this.viewer ){
16238            /**
16239             * Raised when the viewer is resized (see {@link OpenSeadragon.Viewport#resize}).
16240             *
16241             * @event resize
16242             * @memberof OpenSeadragon.Viewer
16243             * @type {object}
16244             * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
16245             * @property {OpenSeadragon.Point} newContainerSize
16246             * @property {Boolean} maintain
16247             * @property {?Object} userData - Arbitrary subscriber-defined object.
16248             */
16249            this.viewer.raiseEvent( 'resize', {
16250                newContainerSize: newContainerSize,
16251                maintain: maintain
16252            });
16253        }
16254
16255        return this.fitBounds( newBounds, true );
16256    },
16257
16258    /**
16259     * @function
16260     */
16261    update: function() {
16262        var oldCenterX = this.centerSpringX.current.value,
16263            oldCenterY = this.centerSpringY.current.value,
16264            oldZoom    = this.zoomSpring.current.value,
16265            oldZoomPixel,
16266            newZoomPixel,
16267            deltaZoomPixels,
16268            deltaZoomPoints;
16269
16270        if (this.zoomPoint) {
16271            oldZoomPixel = this.pixelFromPoint( this.zoomPoint, true );
16272        }
16273
16274        this.zoomSpring.update();
16275
16276        if (this.zoomPoint && this.zoomSpring.current.value != oldZoom) {
16277            newZoomPixel    = this.pixelFromPoint( this.zoomPoint, true );
16278            deltaZoomPixels = newZoomPixel.minus( oldZoomPixel );
16279            deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true );
16280
16281            this.centerSpringX.shiftBy( deltaZoomPoints.x );
16282            this.centerSpringY.shiftBy( deltaZoomPoints.y );
16283        } else {
16284            this.zoomPoint = null;
16285        }
16286
16287        this.centerSpringX.update();
16288        this.centerSpringY.update();
16289
16290        return this.centerSpringX.current.value != oldCenterX ||
16291            this.centerSpringY.current.value != oldCenterY ||
16292            this.zoomSpring.current.value != oldZoom;
16293    },
16294
16295
16296    /**
16297     * Convert a delta (translation vector) from pixels coordinates to viewport coordinates
16298     * @function
16299     * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
16300     */
16301    deltaPixelsFromPoints: function( deltaPoints, current ) {
16302        return deltaPoints.times(
16303            this.containerSize.x * this.getZoom( current )
16304        );
16305    },
16306
16307    /**
16308     * Convert a delta (translation vector) from viewport coordinates to pixels coordinates.
16309     * @function
16310     * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
16311     */
16312    deltaPointsFromPixels: function( deltaPixels, current ) {
16313        return deltaPixels.divide(
16314            this.containerSize.x * this.getZoom( current )
16315        );
16316    },
16317
16318    /**
16319     * Convert image pixel coordinates to viewport coordinates.
16320     * @function
16321     * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
16322     */
16323    pixelFromPoint: function( point, current ) {
16324        var bounds = this.getBounds( current );
16325        return point.minus(
16326            bounds.getTopLeft()
16327        ).times(
16328            this.containerSize.x / bounds.width
16329        );
16330    },
16331
16332    /**
16333     * Convert viewport coordinates to image pixel coordinates.
16334     * @function
16335     * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
16336     */
16337    pointFromPixel: function( pixel, current ) {
16338        var bounds = this.getBounds( current );
16339        return pixel.divide(
16340            this.containerSize.x / bounds.width
16341        ).plus(
16342            bounds.getTopLeft()
16343        );
16344    },
16345
16346    /**
16347     * Translates from OpenSeadragon viewer coordinate system to image coordinate system.
16348     * This method can be called either by passing X,Y coordinates or an
16349     * OpenSeadragon.Point
16350     * @function
16351     * @param {OpenSeadragon.Point} viewerX the point in viewport coordinate system.
16352     * @param {Number} viewerX X coordinate in viewport coordinate system.
16353     * @param {Number} viewerY Y coordinate in viewport coordinate system.
16354     * @return {OpenSeadragon.Point} a point representing the coordinates in the image.
16355     */
16356    viewportToImageCoordinates: function( viewerX, viewerY ) {
16357        if ( arguments.length == 1 ) {
16358            //they passed a point instead of individual components
16359            return this.viewportToImageCoordinates( viewerX.x, viewerX.y );
16360        }
16361        return new $.Point( viewerX * this.contentSize.x, viewerY * this.contentSize.y * this.contentAspectX );
16362    },
16363
16364    /**
16365     * Translates from image coordinate system to OpenSeadragon viewer coordinate system
16366     * This method can be called either by passing X,Y coordinates or an
16367     * OpenSeadragon.Point
16368     * @function
16369     * @param {OpenSeadragon.Point} imageX the point in image coordinate system.
16370     * @param {Number} imageX X coordinate in image coordinate system.
16371     * @param {Number} imageY Y coordinate in image coordinate system.
16372     * @return {OpenSeadragon.Point} a point representing the coordinates in the viewport.
16373     */
16374    imageToViewportCoordinates: function( imageX, imageY ) {
16375        if ( arguments.length == 1 ) {
16376            //they passed a point instead of individual components
16377            return this.imageToViewportCoordinates( imageX.x, imageX.y );
16378        }
16379        return new $.Point( imageX / this.contentSize.x, imageY / this.contentSize.y / this.contentAspectX );
16380    },
16381
16382    /**
16383     * Translates from a rectangle which describes a portion of the image in
16384     * pixel coordinates to OpenSeadragon viewport rectangle coordinates.
16385     * This method can be called either by passing X,Y,width,height or an
16386     * OpenSeadragon.Rect
16387     * @function
16388     * @param {OpenSeadragon.Rect} imageX the rectangle in image coordinate system.
16389     * @param {Number} imageX the X coordinate of the top left corner of the rectangle
16390     * in image coordinate system.
16391     * @param {Number} imageY the Y coordinate of the top left corner of the rectangle
16392     * in image coordinate system.
16393     * @param {Number} pixelWidth the width in pixel of the rectangle.
16394     * @param {Number} pixelHeight the height in pixel of the rectangle.
16395     */
16396    imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight ) {
16397        var coordA,
16398            coordB,
16399            rect;
16400        if( arguments.length == 1 ) {
16401            //they passed a rectangle instead of individual components
16402            rect = imageX;
16403            return this.imageToViewportRectangle(
16404                rect.x, rect.y, rect.width, rect.height
16405            );
16406        }
16407        coordA = this.imageToViewportCoordinates(
16408            imageX, imageY
16409        );
16410        coordB = this.imageToViewportCoordinates(
16411            pixelWidth, pixelHeight
16412        );
16413        return new $.Rect(
16414            coordA.x,
16415            coordA.y,
16416            coordB.x,
16417            coordB.y
16418        );
16419    },
16420
16421    /**
16422     * Translates from a rectangle which describes a portion of
16423     * the viewport in point coordinates to image rectangle coordinates.
16424     * This method can be called either by passing X,Y,width,height or an
16425     * OpenSeadragon.Rect
16426     * @function
16427     * @param {OpenSeadragon.Rect} viewerX the rectangle in viewport coordinate system.
16428     * @param {Number} viewerX the X coordinate of the top left corner of the rectangle
16429     * in viewport coordinate system.
16430     * @param {Number} imageY the Y coordinate of the top left corner of the rectangle
16431     * in viewport coordinate system.
16432     * @param {Number} pointWidth the width of the rectangle in viewport coordinate system.
16433     * @param {Number} pointHeight the height of the rectangle in viewport coordinate system.
16434     */
16435    viewportToImageRectangle: function( viewerX, viewerY, pointWidth, pointHeight ) {
16436        var coordA,
16437            coordB,
16438            rect;
16439        if ( arguments.length == 1 ) {
16440            //they passed a rectangle instead of individual components
16441            rect = viewerX;
16442            return this.viewportToImageRectangle(
16443                rect.x, rect.y, rect.width, rect.height
16444            );
16445        }
16446        coordA = this.viewportToImageCoordinates( viewerX, viewerY );
16447        coordB = this.viewportToImageCoordinates( pointWidth, pointHeight );
16448        return new $.Rect(
16449            coordA.x,
16450            coordA.y,
16451            coordB.x,
16452            coordB.y
16453        );
16454    },
16455
16456    /**
16457     * Convert pixel coordinates relative to the viewer element to image
16458     * coordinates.
16459     * @param {OpenSeadragon.Point} pixel
16460     * @returns {OpenSeadragon.Point}
16461     */
16462    viewerElementToImageCoordinates: function( pixel ) {
16463        var point = this.pointFromPixel( pixel, true );
16464        return this.viewportToImageCoordinates( point );
16465    },
16466
16467    /**
16468     * Convert pixel coordinates relative to the image to
16469     * viewer element coordinates.
16470     * @param {OpenSeadragon.Point} pixel
16471     * @returns {OpenSeadragon.Point}
16472     */
16473    imageToViewerElementCoordinates: function( pixel ) {
16474        var point = this.imageToViewportCoordinates( pixel );
16475        return this.pixelFromPoint( point, true );
16476    },
16477
16478    /**
16479     * Convert pixel coordinates relative to the window to image coordinates.
16480     * @param {OpenSeadragon.Point} pixel
16481     * @returns {OpenSeadragon.Point}
16482     */
16483    windowToImageCoordinates: function( pixel ) {
16484        var viewerCoordinates = pixel.minus(
16485                OpenSeadragon.getElementPosition( this.viewer.element ));
16486        return this.viewerElementToImageCoordinates( viewerCoordinates );
16487    },
16488
16489    /**
16490     * Convert image coordinates to pixel coordinates relative to the window.
16491     * @param {OpenSeadragon.Point} pixel
16492     * @returns {OpenSeadragon.Point}
16493     */
16494    imageToWindowCoordinates: function( pixel ) {
16495        var viewerCoordinates = this.imageToViewerElementCoordinates( pixel );
16496        return viewerCoordinates.plus(
16497                OpenSeadragon.getElementPosition( this.viewer.element ));
16498    },
16499
16500    /**
16501     * Convert pixel coordinates relative to the viewer element to viewport
16502     * coordinates.
16503     * @param {OpenSeadragon.Point} pixel
16504     * @returns {OpenSeadragon.Point}
16505     */
16506    viewerElementToViewportCoordinates: function( pixel ) {
16507        return this.pointFromPixel( pixel, true );
16508    },
16509
16510    /**
16511     * Convert viewport coordinates to pixel coordinates relative to the
16512     * viewer element.
16513     * @param {OpenSeadragon.Point} point
16514     * @returns {OpenSeadragon.Point}
16515     */
16516    viewportToViewerElementCoordinates: function( point ) {
16517        return this.pixelFromPoint( point, true );
16518    },
16519
16520    /**
16521     * Convert pixel coordinates relative to the window to viewport coordinates.
16522     * @param {OpenSeadragon.Point} pixel
16523     * @returns {OpenSeadragon.Point}
16524     */
16525    windowToViewportCoordinates: function( pixel ) {
16526        var viewerCoordinates = pixel.minus(
16527                OpenSeadragon.getElementPosition( this.viewer.element ));
16528        return this.viewerElementToViewportCoordinates( viewerCoordinates );
16529    },
16530
16531    /**
16532     * Convert viewport coordinates to pixel coordinates relative to the window.
16533     * @param {OpenSeadragon.Point} point
16534     * @returns {OpenSeadragon.Point}
16535     */
16536    viewportToWindowCoordinates: function( point ) {
16537        var viewerCoordinates = this.viewportToViewerElementCoordinates( point );
16538        return viewerCoordinates.plus(
16539                OpenSeadragon.getElementPosition( this.viewer.element ));
16540    },
16541    
16542    /**
16543     * Convert a viewport zoom to an image zoom.
16544     * Image zoom: ratio of the original image size to displayed image size.
16545     * 1 means original image size, 0.5 half size...
16546     * Viewport zoom: ratio of the displayed image's width to viewport'
16546s width.
16547     * 1 means identical width, 2 means image's width is twice the viewport's width...
16548     * @function
16549     * @param {Number} viewportZoom The viewport zoom
16550     * target zoom.
16551     * @returns {Number} imageZoom The image zoom
16552     */
16553    viewportToImageZoom: function( viewportZoom ) {
16554        var imageWidth = this.viewer.source.dimensions.x;
16555        var containerWidth = this.getContainerSize().x;
16556        var viewportToImageZoomRatio = containerWidth / imageWidth;
16557        return viewportZoom * viewportToImageZoomRatio;
16558    },
16559    
16560    /**
16561     * Convert an image zoom to a viewport zoom.
16562     * Image zoom: ratio of the original image size to displayed image size.
16563     * 1 means original image size, 0.5 half size...
16564     * Viewport zoom: ratio of the displayed image's width to viewport's width.
16565     * 1 means identical width, 2 means image's width is twice the viewport's width...
16566     * @function
16567     * @param {Number} imageZoom The image zoom
16568     * target zoom.
16569     * @returns {Number} viewportZoom The viewport zoom
16570     */
16571    imageToViewportZoom: function( imageZoom ) {
16572        var imageWidth = this.viewer.source.dimensions.x;
16573        var containerWidth = this.getContainerSize().x;
16574        var viewportToImageZoomRatio = imageWidth / containerWidth;
16575        return imageZoom * viewportToImageZoomRatio;
16576    }
16577};
16578
16579}( OpenSeadragon ));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.