diff --git a/exact/exact/annotations/static/annotations/js/exact-browser-sync.js b/exact/exact/annotations/static/annotations/js/exact-browser-sync.js
index 6ecbde83..10ec92dd 100644
--- a/exact/exact/annotations/static/annotations/js/exact-browser-sync.js
+++ b/exact/exact/annotations/static/annotations/js/exact-browser-sync.js
@@ -31,6 +31,14 @@ class EXACTBrowserSync {
event.userData.requestAllOpenImages();
}, this);
+ viewer.addHandler("sync_NoRegistrationsFound", function (event) {
+
+ // Clear any stale registration UI left over from a previous image
+ $('#registration_selector').find('option:not([value=""])').remove();
+ $('#registration_selector').hide();
+ document.getElementById('registrationField').textContent = '';
+ }, this);
+
viewer.addHandler("sync_TabAnnotationCreated", function (event) {
@@ -124,9 +132,9 @@ class EXACTBrowserSync {
} else{
$("#open_registration_image_visibility").hide();
this.registration = undefined;
- return
+ return
}
-
+
}
this.openTabImageInformations[registration_pair.source_image.id] = registration_pair.source_image;
@@ -139,24 +147,45 @@ class EXACTBrowserSync {
}
this.registration = new EXACTRegistrationHandler(this.viewer, registration_pair, this);
+
+ // Keep the status-bar selector in sync
+ $("#registration_selector").val($("select#sync_browser_image").val());
}
initUiEvents() {
- $('#search_browserimages_btn').click(this.requestAllOpenImages.bind(this));
+ $('#search_browserimages_btn').click(this.requestAllOpenImages.bind(this));
+
+ // Status-bar registration selector: mirrors sync_browser_image
+ $("#registration_selector").on("change", function() {
+ $("#sync_browser_image").val($(this).val()).trigger("change");
+ });
}
requestAllOpenImages() {
// set all registration pairs at UI
$('#sync_browser_image').empty();
+ $('#registration_selector').find('option:not([value=""])').remove();
let image_list = $('#sync_browser_image');
+ let reg_selector = $('#registration_selector');
for (let registration_pair of Object.values(this.exact_registration_sync.registeredImagePairs)) {
image_list.append(``);
+ reg_selector.append(``);
+ }
+
+ // show the selector only when there are multiple registrations to choose from
+ if (Object.keys(this.exact_registration_sync.registeredImagePairs).length > 1) {
+ $('#registration_selector').show();
+ } else {
+ $('#registration_selector').hide();
}
// set all segmentation pairs at UI
@@ -171,7 +200,7 @@ class EXACTBrowserSync {
var name1 = this.source_image.name.split('.').slice(0, -1).join('.');
var name2 = imageName.split('.').slice(0, -1).join('.');
-
+
if (name1 === name2 && this.source_image.name !== imageName) {
image_list.append(``);
diff --git a/exact/exact/annotations/static/annotations/js/exact-quad-tree.js b/exact/exact/annotations/static/annotations/js/exact-quad-tree.js
index 01f5d0c2..4f58c927 100644
--- a/exact/exact/annotations/static/annotations/js/exact-quad-tree.js
+++ b/exact/exact/annotations/static/annotations/js/exact-quad-tree.js
@@ -6,7 +6,6 @@ class EXACTRegistrationHandler {
this.browser_sync = browser_sync;
this.viewer = viewer;
this.background_viewer = undefined;
- this.show_registration = ("image_set" in registration_pair.source_image) ? registration_pair.source_image.image_set.show_registration : false;
// Check if the OpenCv js is loaded
this.check_opencv = undefined;
@@ -54,7 +53,7 @@ class EXACTRegistrationHandler {
this.updateHomographyUI();
// Load current registration
- if (this.show_registration && $("#OverlayRegImage-enabled").prop("checked") == false) {
+ if ($("#OverlayRegImage-enabled").prop("checked") == false) {
$("#OverlayRegImage-enabled").prop("checked", true);
this.enableOverlayRegImageSlider();
@@ -100,7 +99,14 @@ class EXACTRegistrationHandler {
method=' (qt)';
}
- document.getElementById('registrationField').textContent = 'Registered to: ' + this.registration_pair.source_image.name+method;
+ if ($('#registration_selector').is(':hidden')) {
+ document.getElementById('registrationField').textContent = 'Registered to: ' + this.registration_pair.source_image.name + method;
+ }
+ $("#registration_selector").val(this.registration_pair.source_image.name);
+ var self = this;
+ this.viewer.addHandler('animation', function() {
+ self.syncViewBackgroundForeground();
+ });
this.background_viewer.addHandler("open", function (event) {
@@ -120,26 +126,37 @@ class EXACTRegistrationHandler {
}
+
+
syncViewBackgroundForeground () {
if (this.background_viewer !== undefined) {
let bounds = this.viewer.viewport.getBounds(true);
let imageRect = this.viewer.viewport.viewportToImageRectangle(bounds);
-
- let [xmin_trans, ymin_trans] = this.transformAffineInv(imageRect.x, imageRect.y);
-
+
+ // Transform all 4 corners of the current viewport into the overlay image space,
+ // then take the axis-aligned bounding box. This is correct for any rotation or
+ // scale and avoids relying on OpenSeadragon's Rect.degrees handling, which
+ // behaves asymmetrically for +90° vs -90° viewer rotations.
+ let corners = [
+ this.transformAffineInv(imageRect.x, imageRect.y),
+ this.transformAffineInv(imageRect.x + imageRect.width, imageRect.y),
+ this.transformAffineInv(imageRect.x, imageRect.y + imageRect.height),
+ this.transformAffineInv(imageRect.x + imageRect.width, imageRect.y + imageRect.height),
+ ];
+ let xs = corners.map(c => c[0]);
+ let ys = corners.map(c => c[1]);
+ let x_min = Math.min(...xs), x_max = Math.max(...xs);
+ let y_min = Math.min(...ys), y_max = Math.max(...ys);
+
this.background_viewer.viewport.setRotation(this.rotation_angle);
- const vpRect = this.background_viewer.viewport.imageToViewportRectangle(new OpenSeadragon.Rect(
- xmin_trans,
- ymin_trans,
- imageRect.width * this.inv_mpp_x_scale,
- imageRect.height * this.inv_mpp_y_scale,
- -this.rotation_angle
- ));
+ const vpRect = this.background_viewer.viewport.imageToViewportRectangle(
+ new OpenSeadragon.Rect(x_min, y_min, x_max - x_min, y_max - y_min)
+ );
- this.background_viewer.viewport.fitBoundsWithConstraints(vpRect);
+ this.background_viewer.viewport.fitBoundsWithConstraints(vpRect, true);
}
}
diff --git a/exact/exact/annotations/static/annotations/js/exact-sync.js b/exact/exact/annotations/static/annotations/js/exact-sync.js
index 77673110..314dc1bf 100644
--- a/exact/exact/annotations/static/annotations/js/exact-sync.js
+++ b/exact/exact/annotations/static/annotations/js/exact-sync.js
@@ -50,25 +50,71 @@ class EXACTRegistrationSync {
}
- loadRegistrationInformation(url,context) {
+ loadRegistrationInformation(url, context) {
- $.ajax(this.API_1_REGISTRATION_BASE_URL + url
- +'&fields=id,transformation_matrix,file,rotation_angle,inv_matrix,get_scale,get_inv_scale,source_image.name,source_image.id,target_image.name,target_image.id,source_image.image_set.show_registration'
+ $.ajax(this.API_1_REGISTRATION_BASE_URL + url
+ +'&fields=id,transformation_matrix,file,rotation_angle,inv_matrix,get_scale,get_inv_scale,source_image.name,source_image.id,target_image.name,target_image.id'
+'&expand=target_image,source_image,source_image.image_set', {
type: 'GET',
- headers: this.gHeaders,
+ headers: context.gHeaders,
dataType: 'json',
- success: function (registrations, textStatus, jqXHR) {
+ success: function (registrations) {
for (let registration of registrations.results) {
-
context.registeredImagePairs[registration.source_image.name] = registration;
}
- if (registrations.results.length > 0) {
- let reg = registrations.results[0]
- context.viewer.raiseEvent('sync_RegistrationLoaded', { reg });
- }
+ // Also load registrations where the current image is the source,
+ // and present them as flipped pairs so the overlay works in both directions.
+ $.ajax(context.API_1_REGISTRATION_BASE_URL
+ + '?source_image=' + context.imageInformation.id
+ + '&fields=id,transformation_matrix,file,inv_matrix,get_scale,get_inv_scale,source_image.name,source_image.id,target_image.name,target_image.id'
+ + '&expand=target_image,source_image,target_image.image_set', {
+ type: 'GET',
+ headers: context.gHeaders,
+ dataType: 'json',
+ success: function (inverseRegistrations) {
+
+ for (let reg of inverseRegistrations.results) {
+ // Only add if not already covered by a forward registration
+ if (reg.target_image.name in context.registeredImagePairs) continue;
+
+ const inv = reg.inv_matrix;
+ context.registeredImagePairs[reg.target_image.name] = {
+ id: reg.id,
+ source_image: reg.target_image,
+ target_image: reg.source_image,
+ transformation_matrix: reg.inv_matrix,
+ inv_matrix: reg.transformation_matrix,
+ get_scale: reg.get_inv_scale,
+ get_inv_scale: reg.get_scale,
+ rotation_angle: -Math.atan2(inv.t_01, inv.t_00) * 180 / Math.PI,
+ file: null,
+ };
+ }
+
+ if (Object.keys(context.registeredImagePairs).length > 0) {
+ let reg = Object.values(context.registeredImagePairs)[0];
+ context.viewer.raiseEvent('sync_RegistrationLoaded', { reg });
+ } else {
+ context.viewer.raiseEvent('sync_NoRegistrationsFound', {});
+ }
+ },
+ error: function (request, status, error) {
+ // Still raise the event if forward registrations were found
+ if (Object.keys(context.registeredImagePairs).length > 0) {
+ let reg = Object.values(context.registeredImagePairs)[0];
+ context.viewer.raiseEvent('sync_RegistrationLoaded', { reg });
+ } else {
+ context.viewer.raiseEvent('sync_NoRegistrationsFound', {});
+ }
+ if (request.responseText !== undefined) {
+ $.notify(request.responseText, { position: "bottom center", className: "error" });
+ } else {
+ $.notify(`Server ERR_CONNECTION_TIMED_OUT`, { position: "bottom center", className: "error" });
+ }
+ }
+ });
},
error: function (request, status, error) {
if (request.responseText !== undefined) {
diff --git a/exact/exact/annotations/static/annotations/js/openseadragon.js b/exact/exact/annotations/static/annotations/js/openseadragon.js
new file mode 100644
index 00000000..0f245029
--- /dev/null
+++ b/exact/exact/annotations/static/annotations/js/openseadragon.js
@@ -0,0 +1,33353 @@
+//! openseadragon 6.0.2
+//! Built on 2026-03-12
+//! Git commit: v6.0.2-0-7842cd92
+//! http://openseadragon.github.io
+//! License: http://openseadragon.github.io/license/
+
+/*
+ * OpenSeadragon
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Portions of this source file taken from jQuery:
+ *
+ * Copyright 2011 John Resig
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Portions of this source file taken from mattsnider.com:
+ *
+ * Copyright (c) 2006-2013 Matt Snider
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+ * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+
+/**
+ * @namespace OpenSeadragon
+ * @version openseadragon 6.0.2
+ * @classdesc The root namespace for OpenSeadragon. All utility methods
+ * and classes are defined on or below this namespace.
+ *
+ */
+
+
+// Typedefs
+
+ /**
+ * All required and optional settings for instantiating a new instance of an OpenSeadragon image viewer.
+ *
+ * @typedef {Object} Options
+ * @memberof OpenSeadragon
+ *
+ * @property {String} id
+ * Id of the element to append the viewer's container element to. If not provided, the 'element' property must be provided.
+ * If both the element and id properties are specified, the viewer is appended to the element provided in the element property.
+ *
+ * @property {Element} element
+ * The element to append the viewer's container element to. If not provided, the 'id' property must be provided.
+ * If both the element and id properties are specified, the viewer is appended to the element provided in the element property.
+ *
+ * @property {Array|String|Function|Object} [tileSources=null]
+ * Tile source(s) to open initially. This is a complex parameter; see
+ * {@link OpenSeadragon.Viewer#open} for details.
+ *
+ * @property {Number} [tabIndex=0]
+ * Tabbing order index to assign to the viewer element. Positive values are selected in increasing order. When tabIndex is 0
+ * source order is used. A negative value omits the viewer from the tabbing order.
+ *
+ * @property {Array} overlays Array of objects defining permanent overlays of
+ * the viewer. The overlays added via this option and later removed with
+ * {@link OpenSeadragon.Viewer#removeOverlay} will be added back when a new
+ * image is opened.
+ * To add overlays which can be definitively removed, one must use
+ * {@link OpenSeadragon.Viewer#addOverlay}
+ * If displaying a sequence of images, the overlays can be associated
+ * with a specific page by passing the overlays array to the page's
+ * tile source configuration.
+ * Expected properties:
+ * * x, y, (or px, py for pixel coordinates) to define the location.
+ * * width, height in point if using x,y or in pixels if using px,py. If width
+ * and height are specified, the overlay size is adjusted when zooming,
+ * otherwise the size stays the size of the content (or the size defined by CSS).
+ * * className to associate a class to the overlay
+ * * id to set the overlay element. If an element with this id already exists,
+ * it is reused, otherwise it is created. If not specified, a new element is
+ * created.
+ * * placement a string to define the relative position to the viewport.
+ * Only used if no width and height are specified. Default: 'TOP_LEFT'.
+ * See {@link OpenSeadragon.Placement} for possible values.
+ *
+ * @property {String} [xmlPath=null]
+ * DEPRECATED. A relative path to load a DZI file from the server.
+ * Prefer the newer Options.tileSources.
+ *
+ * @property {String} [prefixUrl='/images/']
+ * Prepends the prefixUrl to navImages paths, which is very useful
+ * since the default paths are rarely useful for production
+ * environments.
+ *
+ * @property {OpenSeadragon.NavImages} [navImages]
+ * An object with a property for each button or other built-in navigation
+ * control, eg the current 'zoomIn', 'zoomOut', 'home', and 'fullpage'.
+ * Each of those in turn provides an image path for each state of the button
+ * or navigation control, eg 'REST', 'GROUP', 'HOVER', 'PRESS'. Finally the
+ * image paths, by default assume there is a folder on the servers root path
+ * called '/images', eg '/images/zoomin_rest.png'. If you need to adjust
+ * these paths, prefer setting the option.prefixUrl rather than overriding
+ * every image path directly through this setting.
+ *
+ * @property {Boolean} [debugMode=false]
+ * TODO: provide an in-screen panel providing event detail feedback.
+ *
+ * @property {String} [debugGridColor=['#437AB2', '#1B9E77', '#D95F02', '#7570B3', '#E7298A', '#66A61E', '#E6AB02', '#A6761D', '#666666']]
+ * The colors of grids in debug mode. Each tiled image's grid uses a consecutive color.
+ * If there are more tiled images than provided colors, the color vector is recycled.
+ *
+ * @property {Boolean} [silenceMultiImageWarnings=false]
+ * Silences warnings when calling viewport coordinate functions with multi-image.
+ * Useful when you're overlaying multiple images on top of one another.
+ *
+ * @property {Number} [blendTime=0]
+ * Specifies the duration of animation as higher or lower level tiles are
+ * replacing the existing tile.
+ *
+ * @property {Boolean} [alwaysBlend=false]
+ * Forces the tile to always blend. By default the tiles skip blending
+ * when the blendTime is surpassed and the current animation frame would
+ * not complete the blend.
+ *
+ * @property {Boolean} [autoHideControls=true]
+ * If the user stops interacting with the viewport, fade the navigation
+ * controls. Useful for presentation since the controls are by default
+ * floated on top of the image the user is viewing.
+ *
+ * @property {Boolean} [immediateRender=false]
+ * Render the best closest level first, ignoring the lowering levels which
+ * provide the effect of very blurry to sharp. It is recommended to change
+ * setting to true for mobile devices.
+ *
+ * @property {Number} [defaultZoomLevel=0]
+ * Zoom level to use when image is first opened or the home button is clicked.
+ * If 0, adjusts to fit viewer.
+ *
+ * @property {String|DrawerImplementation|Array} [drawer = ['auto', 'webgl', 'canvas', 'html']]
+ * Which drawer to use. Valid strings are 'auto', 'webgl', 'canvas', and 'html'.
+ * The string 'auto' is converted to one or more drawer type strings depending
+ * on the platform. On iOS-like devices it becomes 'canvas' due to performance
+ * limitations with the webgl drawer. On all other platforms it becomes ['webgl', 'canvas']
+ * meaning that webgl is tried first, and canvas is available as a fallback if webgl is not supported.
+ *
+ * The 'webgl' drawer automatically uses WebGL2 when available, falling back to WebGL1.
+ *
+ * External drawer plugins can register additional drawer types as strings.
+ * Valid drawer implementations are constructors of classes that extend OpenSeadragon.DrawerBase.
+ * An array of strings and/or constructors can be used to indicate the priority
+ * of different implementations, which will be tried in order based on browser support.
+ * The 'webgl' drawer can automatically fall back to canvas as needed, for example to draw
+ * images that do not have CORS headers set which makes them tainted and unavailable to webgl.
+ * This behavior depends on 'canvas' being included in the list of drawer candidates. If
+ * webgl is needed and canvas fallback is not desired, use 'webgl' without including 'canvas' in the list.
+ *
+ * @property {Object} drawerOptions
+ * Options to pass to the selected drawer implementation. For details
+ * please see {@link OpenSeadragon.DrawerOptions}.
+ *
+ * @property {Number} [opacity=1]
+ * Default proportional opacity of the tiled images (1=opaque, 0=hidden)
+ * Hidden images do not draw and only load when preloading is allowed.
+ *
+ * @property {Boolean} [preload=false]
+ * Default switch for loading hidden images (true loads, false blocks)
+ *
+ * @property {String} [compositeOperation=null]
+ * Valid values are 'source-over', 'source-atop', 'source-in', 'source-out',
+ * 'destination-over', 'destination-atop', 'destination-in', 'destination-out',
+ * 'lighter', 'difference', 'copy', 'xor', etc.
+ * For complete list of modes, please @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation/ globalCompositeOperation}
+ *
+ * @property {Boolean} [imageSmoothingEnabled=true]
+ * Image smoothing for rendering. Supported by the canvas and webgl drawers,
+ * and may also be supported by external drawer plugins. Note: Ignored by some
+ * (especially older) browsers which do not support this canvas property.
+ * This property can be changed in {@link Viewer.DrawerBase.setImageSmoothingEnabled}.
+ *
+ * @property {String|CanvasGradient|CanvasPattern|Function} [placeholderFillStyle=null]
+ * Draws a colored rectangle behind the tile if it is not loaded yet.
+ * You can pass a CSS color value like "#FF8800".
+ * When passing a function the tiledImage and canvas context are available as argument which is useful when you draw a gradient or pattern.
+ *
+ * @property {Object} [subPixelRoundingForTransparency=null]
+ * Determines when subpixel rounding should be applied for tiles when rendering images that support transparency.
+ * This property is a subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}.
+ * The key is a {@link BROWSERS} value, and the value is one of {@link SUBPIXEL_ROUNDING_OCCURRENCES},
+ * indicating, for a given browser, when to apply subpixel rounding.
+ * Key '*' is the fallback value for any browser not specified in the dictionary.
+ * This property has a simple mode, and one can set it directly to
+ * {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}
+ * in order to apply this rule for all browser. The values {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS} would be equivalent to { '*', SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS }.
+ * The default is {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} for all browsers, for backward compatibility reason.
+ *
+ * @property {Number} [degrees=0]
+ * Initial rotation.
+ *
+ * @property {Boolean} [flipped=false]
+ * Initial flip state.
+ *
+ * @property {Boolean} [overlayPreserveContentDirection=true]
+ * When the viewport is flipped (by pressing 'f'), the overlay is flipped using ScaleX.
+ * Normally, this setting (default true) keeps the overlay's content readable by flipping it back.
+ * To make the content flip with the overlay, set overlayPreserveContentDirection to false.
+ *
+ * @property {Number} [minZoomLevel=null]
+ *
+ * @property {Number} [maxZoomLevel=null]
+ *
+ * @property {Boolean} [homeFillsViewer=false]
+ * Make the 'home' button fill the viewer and clip the image, instead
+ * of fitting the image to the viewer and letterboxing.
+ *
+ * @property {Boolean} [panHorizontal=true]
+ * Allow horizontal pan.
+ *
+ * @property {Boolean} [panVertical=true]
+ * Allow vertical pan.
+ *
+ * @property {Boolean} [constrainDuringPan=false]
+ *
+ * @property {Boolean} [wrapHorizontal=false]
+ * Set to true to force the image to wrap horizontally within the viewport.
+ * Useful for maps or images representing the surface of a sphere or cylinder.
+ *
+ * @property {Boolean} [wrapVertical=false]
+ * Set to true to force the image to wrap vertically within the viewport.
+ * Useful for maps or images representing the surface of a sphere or cylinder.
+ *
+ * @property {Number} [minZoomImageRatio=0.9]
+ * The minimum percentage ( expressed as a number between 0 and 1 ) of
+ * the viewport height or width at which the zoom out will be constrained.
+ * Setting it to 0, for example will allow you to zoom out infinity.
+ *
+ * @property {Number} [maxZoomPixelRatio=1.1]
+ * The maximum ratio to allow a zoom-in to affect the highest level pixel
+ * ratio. This can be set to Infinity to allow 'infinite' zooming into the
+ * image though it is less effective visually if the HTML5 Canvas is not
+ * available on the viewing device.
+ *
+ * @property {Number} [smoothTileEdgesMinZoom=1.1]
+ * A zoom percentage ( where 1 is 100% ) of the highest resolution level.
+ * When zoomed in beyond this value alternative compositing will be used to
+ * smooth out the edges between tiles. This will have a performance impact.
+ * Can be set to Infinity to turn it off.
+ * Note: This setting is ignored on iOS devices due to a known bug (See {@link https://github.com/openseadragon/openseadragon/issues/952})
+ *
+ * @property {Boolean} [iOSDevice=?]
+ * True if running on an iOS device, false otherwise.
+ * Used to disable certain features that behave differently on iOS devices.
+ *
+ * @property {Boolean} [autoResize=true]
+ * Set to false to prevent polling for viewer size changes. Useful for providing custom resize behavior.
+ *
+ * @property {Boolean} [preserveImageSizeOnResize=false]
+ * Set to true to have the image size preserved when the viewer is resized. This requires autoResize=true (default).
+ *
+ * @property {Number} [minScrollDeltaTime=50]
+ * Number of milliseconds between canvas-scroll events. This value helps normalize the rate of canvas-scroll
+ * events between different devices, causing the faster devices to slow down enough to make the zoom control
+ * more manageable.
+ *
+ * @property {Number} [rotationIncrement=90]
+ * The number of degrees to rotate right or left when the rotate buttons or keyboard shortcuts are activated.
+ *
+ * @property {Number} [maxTilesPerFrame=1]
+ * The number of tiles loaded per frame. As the frame rate of the client's machine is usually high (e.g., 50 fps),
+ * one tile per frame should be a good choice. However, for large screens or lower frame rates, the number of
+ * loaded tiles per frame can be adjusted here. Reasonable values might be 2 or 3 tiles per frame.
+ * (Note that the actual frame rate is given by the client's browser and machine).
+ *
+ * @property {Number} [pixelsPerWheelLine=40]
+ * For pixel-resolution scrolling devices, the number of pixels equal to one scroll line.
+ *
+ * @property {Number} [pixelsPerArrowPress=40]
+ * The number of pixels viewport moves when an arrow key is pressed.
+ *
+ * @property {Number} [visibilityRatio=0.5]
+ * The percentage ( as a number from 0 to 1 ) of the source image which
+ * must be kept within the viewport. If the image is dragged beyond that
+ * limit, it will 'bounce' back until the minimum visibility ratio is
+ * achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to
+ * true will provide the effect of an infinitely scrolling viewport.
+ *
+ * @property {Object} [viewportMargins={}]
+ * Pushes the "home" region in from the sides by the specified amounts.
+ * Possible subproperties (Numbers, in screen coordinates): left, top, right, bottom.
+ *
+ * @property {Number} [imageLoaderLimit=0]
+ * The maximum number of image requests to make concurrently. By default
+ * it is set to 0 allowing the browser to make the maximum number of
+ * image requests in parallel as allowed by the browsers policy.
+ *
+ * @property {Number} [clickTimeThreshold=300]
+ * The number of milliseconds within which a pointer down-up event combination
+ * will be treated as a click gesture.
+ *
+ * @property {Number} [clickDistThreshold=5]
+ * The maximum distance allowed between a pointer down event and a pointer up event
+ * to be treated as a click gesture.
+ *
+ * @property {Number} [dblClickTimeThreshold=300]
+ * The number of milliseconds within which two pointer down-up event combinations
+ * will be treated as a double-click gesture.
+ *
+ * @property {Number} [dblClickDistThreshold=20]
+ * The maximum distance allowed between two pointer click events
+ * to be treated as a double-click gesture.
+ *
+ * @property {Number} [springStiffness=6.5]
+ *
+ * @property {Number} [animationTime=1.2]
+ * Specifies the animation duration per each {@link OpenSeadragon.Spring}
+ * which occur when the image is dragged, zoomed or rotated.
+ *
+ * @property {Boolean} [loadDestinationTilesOnAnimation=true]
+ * If true, tiles are loaded only at the destination of an animation.
+ * If false, tiles are loaded along the animation path during the animation.
+ * @property {OpenSeadragon.GestureSettings} [gestureSettingsMouse]
+ * Settings for gestures generated by a mouse pointer device. (See {@link OpenSeadragon.GestureSettings})
+ * @property {Boolean} [gestureSettingsMouse.dragToPan=true] - Pan on drag gesture
+ * @property {Boolean} [gestureSettingsMouse.scrollToZoom=true] - Zoom on scroll gesture
+ * @property {Boolean} [gestureSettingsMouse.clickToZoom=true] - Zoom on click gesture
+ * @property {Boolean} [gestureSettingsMouse.dblClickToZoom=false] - Zoom on double-click gesture. Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+ * @property {Boolean} [gestureSettingsMouse.dblClickDragToZoom=false] - Zoom on dragging through
+ * double-click gesture ( single click and next click to drag). Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+ * @property {Boolean} [gestureSettingsMouse.pinchToZoom=false] - Zoom on pinch gesture
+ * @property {Boolean} [gestureSettingsMouse.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
+ * the zoom is centered at the canvas center.
+ * @property {Boolean} [gestureSettingsMouse.flickEnabled=false] - Enable flick gesture
+ * @property {Number} [gestureSettingsMouse.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
+ * @property {Number} [gestureSettingsMouse.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
+ * @property {Boolean} [gestureSettingsMouse.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
+ *
+ * @property {OpenSeadragon.GestureSettings} [gestureSettingsTouch]
+ * Settings for gestures generated by a touch pointer device. (See {@link OpenSeadragon.GestureSettings})
+ * @property {Boolean} [gestureSettingsTouch.dragToPan=true] - Pan on drag gesture
+ * @property {Boolean} [gestureSettingsTouch.scrollToZoom=false] - Zoom on scroll gesture
+ * @property {Boolean} [gestureSettingsTouch.clickToZoom=false] - Zoom on click gesture
+ * @property {Boolean} [gestureSettingsTouch.dblClickToZoom=true] - Zoom on double-click gesture. Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+ * @property {Boolean} [gestureSettingsTouch.dblClickDragToZoom=true] - Zoom on dragging through
+ * double-click gesture ( single click and next click to drag). Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+
+ * @property {Boolean} [gestureSettingsTouch.pinchToZoom=true] - Zoom on pinch gesture
+ * @property {Boolean} [gestureSettingsTouch.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
+ * the zoom is centered at the canvas center.
+ * @property {Boolean} [gestureSettingsTouch.flickEnabled=true] - Enable flick gesture
+ * @property {Number} [gestureSettingsTouch.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
+ * @property {Number} [gestureSettingsTouch.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
+ * @property {Boolean} [gestureSettingsTouch.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
+ *
+ * @property {OpenSeadragon.GestureSettings} [gestureSettingsPen]
+ * Settings for gestures generated by a pen pointer device. (See {@link OpenSeadragon.GestureSettings})
+ * @property {Boolean} [gestureSettingsPen.dragToPan=true] - Pan on drag gesture
+ * @property {Boolean} [gestureSettingsPen.scrollToZoom=false] - Zoom on scroll gesture
+ * @property {Boolean} [gestureSettingsPen.clickToZoom=true] - Zoom on click gesture
+ * @property {Boolean} [gestureSettingsPen.dblClickToZoom=false] - Zoom on double-click gesture. Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+ * @property {Boolean} [gestureSettingsPen.pinchToZoom=false] - Zoom on pinch gesture
+ * @property {Boolean} [gestureSettingsPen.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
+ * the zoom is centered at the canvas center.
+ * @property {Boolean} [gestureSettingsPen.flickEnabled=false] - Enable flick gesture
+ * @property {Number} [gestureSettingsPen.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
+ * @property {Number} [gestureSettingsPen.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
+ * @property {Boolean} [gestureSettingsPen.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
+ *
+ * @property {OpenSeadragon.GestureSettings} [gestureSettingsUnknown]
+ * Settings for gestures generated by unknown pointer devices. (See {@link OpenSeadragon.GestureSettings})
+ * @property {Boolean} [gestureSettingsUnknown.dragToPan=true] - Pan on drag gesture
+ * @property {Boolean} [gestureSettingsUnknown.scrollToZoom=true] - Zoom on scroll gesture
+ * @property {Boolean} [gestureSettingsUnknown.clickToZoom=false] - Zoom on click gesture
+ * @property {Boolean} [gestureSettingsUnknown.dblClickToZoom=true] - Zoom on double-click gesture. Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+ * @property {Boolean} [gestureSettingsUnknown.dblClickDragToZoom=false] - Zoom on dragging through
+ * double-click gesture ( single click and next click to drag). Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+ * @property {Boolean} [gestureSettingsUnknown.pinchToZoom=true] - Zoom on pinch gesture
+ * @property {Boolean} [gestureSettingsUnknown.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
+ * the zoom is centered at the canvas center.
+ * @property {Boolean} [gestureSettingsUnknown.flickEnabled=true] - Enable flick gesture
+ * @property {Number} [gestureSettingsUnknown.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
+ * @property {Number} [gestureSettingsUnknown.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
+ * @property {Boolean} [gestureSettingsUnknown.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
+ *
+ * @property {Number} [zoomPerClick=2.0]
+ * The "zoom distance" per mouse click or touch tap. Note: Setting this to 1.0 effectively disables the click-to-zoom feature (also see gestureSettings[Mouse|Touch|Pen].clickToZoom/dblClickToZoom).
+ *
+ * @property {Number} [zoomPerScroll=1.2]
+ * The "zoom distance" per mouse scroll or touch pinch. Note: Setting this to 1.0 effectively disables the mouse-wheel zoom feature (also see gestureSettings[Mouse|Touch|Pen].scrollToZoom}).
+ *
+ * @property {Number} [zoomPerDblClickDrag=1.2]
+ * The "zoom distance" per double-click mouse drag. Note: Setting this to 1.0 effectively disables the double-click-drag-to-Zoom feature (also see gestureSettings[Mouse|Touch|Pen].dblClickDragToZoom).
+ *
+ * @property {Number} [zoomPerSecond=1.0]
+ * Sets the zoom amount per second when zoomIn/zoomOut buttons are pressed and held.
+ * The value is a factor of the current zoom, so 1.0 (the default) disables zooming when the zoomIn/zoomOut buttons
+ * are held. Higher values will increase the rate of zoom when the zoomIn/zoomOut buttons are held. Note that values
+ * < 1.0 will reverse the operation of the zoomIn/zoomOut buttons (zoomIn button will decrease the zoom, zoomOut will
+ * increase the zoom).
+ *
+ * @property {Boolean} [showNavigator=false]
+ * Set to true to make the navigator minimap appear.
+ *
+ * @property {Element} [navigatorElement=null]
+ * The element to hold the navigator minimap.
+ * If an element is specified, the Id option (see navigatorId) is ignored.
+ * If no element nor ID is specified, a div element will be generated accordingly.
+ *
+ * @property {String} [navigatorId=navigator-GENERATED DATE]
+ * The ID of a div to hold the navigator minimap.
+ * If an ID is specified, the navigatorPosition, navigatorSizeRatio, navigatorMaintainSizeRatio, navigator[Top|Left|Height|Width] and navigatorAutoFade options will be ignored.
+ * If an ID is not specified, a div element will be generated and placed on top of the main image.
+ *
+ * @property {String} [navigatorPosition='TOP_RIGHT']
+ * Valid values are 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT', or 'ABSOLUTE'.
+ * If 'ABSOLUTE' is specified, then navigator[Top|Left|Height|Width] determines the size and position of the navigator minimap in the viewer, and navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.
+ * For 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', and 'BOTTOM_RIGHT', the navigatorSizeRatio or navigator[Height|Width] values determine the size of the navigator minimap.
+ *
+ * @property {Number} [navigatorSizeRatio=0.2]
+ * Ratio of navigator size to viewer size. Ignored if navigator[Height|Width] are specified.
+ *
+ * @property {Boolean} [navigatorMaintainSizeRatio=false]
+ * If true, the navigator minimap is resized (using navigatorSizeRatio) when the viewer size changes.
+ *
+ * @property {Number|String} [navigatorTop=null]
+ * Specifies the location of the navigator minimap (see navigatorPosition).
+ *
+ * @property {Number|String} [navigatorLeft=null]
+ * Specifies the location of the navigator minimap (see navigatorPosition).
+ *
+ * @property {Number|String} [navigatorHeight=null]
+ * Specifies the size of the navigator minimap (see navigatorPosition).
+ * If specified, navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.
+ *
+ * @property {Number|String} [navigatorWidth=null]
+ * Specifies the size of the navigator minimap (see navigatorPosition).
+ * If specified, navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.
+ *
+ * @property {Boolean} [navigatorAutoFade=true]
+ * If the user stops interacting with the viewport, fade the navigator minimap.
+ * Setting to false will make the navigator minimap always visible.
+ *
+ * @property {Boolean} [navigatorRotate=true]
+ * If true, the navigator will be rotated together with the viewer.
+ *
+ * @property {String} [navigatorBackground='#000']
+ * Specifies the background color of the navigator minimap
+ *
+ * @property {Number} [navigatorOpacity=0.8]
+ * Specifies the opacity of the navigator minimap.
+ *
+ * @property {String} [navigatorBorderColor='#555']
+ * Specifies the border color of the navigator minimap
+ *
+ * @property {String} [navigatorDisplayRegionColor='#900']
+ * Specifies the border color of the display region rectangle of the navigator minimap
+ *
+ * @property {Number} [controlsFadeDelay=2000]
+ * The number of milliseconds to wait once the user has stopped interacting
+ * with the interface before beginning to fade the controls. Assumes
+ * showNavigationControl and autoHideControls are both true.
+ *
+ * @property {Number} [controlsFadeLength=1500]
+ * The number of milliseconds to animate the controls fading out.
+ *
+ * @property {Number} [maxImageCacheCount=200]
+ * The max number of images we should keep in memory (per drawer).
+ *
+ * @property {Number} [timeout=30000]
+ * The max number of milliseconds that an image job may take to complete.
+ *
+ * @property {Number} [tileRetryMax=0]
+ * The max number of retries when a tile download fails. By default it's 0, so retries are disabled.
+ *
+ * @property {Number} [tileRetryDelay=2500]
+ * Milliseconds to wait after each tile retry if tileRetryMax is set.
+ *
+ * @property {Boolean} [useCanvas=true]
+ * Deprecated. Use the `drawer` option to specify preferred renderer.
+ *
+ * @property {Number} [minPixelRatio=0.5]
+ * The higher the minPixelRatio, the lower the quality of the image that
+ * is considered sufficient to stop rendering a given zoom level. For
+ * example, if you are targeting mobile devices with less bandwidth you may
+ * try setting this to 1.5 or higher.
+ *
+ * @property {Boolean} [mouseNavEnabled=true]
+ * Is the user able to interact with the image via mouse or touch. Default
+ * interactions include draging the image in a plane, and zooming in toward
+ * and away from the image.
+ *
+ * @property {boolean} [keyboardNavEnabled=true]
+ * Is the user able to interact with the image via keyboard.
+ *
+ * @property {Boolean} [showNavigationControl=true]
+ * Set to false to prevent the appearance of the default navigation controls.
+ * Note that if set to false, the customs buttons set by the options
+ * zoomInButton, zoomOutButton etc, are rendered inactive.
+ *
+ * @property {OpenSeadragon.ControlAnchor} [navigationControlAnchor=TOP_LEFT]
+ * Placement of the default navigation controls.
+ * To set the placement of the sequence controls, see the
+ * sequenceControlAnchor option.
+ *
+ * @property {Boolean} [showZoomControl=true]
+ * If true then + and - buttons to zoom in and out are displayed.
+ * Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
+ * this setting when set to false.
+ *
+ * @property {Boolean} [showHomeControl=true]
+ * If true then the 'Go home' button is displayed to go back to the original
+ * zoom and pan.
+ * Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
+ * this setting when set to false.
+ *
+ * @property {Boolean} [showFullPageControl=true]
+ * If true then the 'Toggle full page' button is displayed to switch
+ * between full page and normal mode.
+ * Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
+ * this setting when set to false.
+ *
+ * @property {Boolean} [showRotationControl=false]
+ * If true then the rotate left/right controls will be displayed as part of the
+ * standard controls. This is also subject to the browser support for rotate
+ * (e.g. viewer.drawer.canRotate()).
+ * Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
+ * this setting when set to false.
+ *
+ * @property {Boolean} [showFlipControl=false]
+ * If true then the flip controls will be displayed as part of the
+ * standard controls.
+ *
+ * @property {Boolean} [showSequenceControl=true]
+ * If sequenceMode is true, then provide buttons for navigating forward and
+ * backward through the images.
+ *
+ * @property {OpenSeadragon.ControlAnchor} [sequenceControlAnchor=TOP_LEFT]
+ * Placement of the default sequence controls.
+ *
+ * @property {Boolean} [navPrevNextWrap=false]
+ * If true then the 'previous' button will wrap to the last image when
+ * viewing the first image and the 'next' button will wrap to the first
+ * image when viewing the last image.
+ *
+ *@property {String|Element} zoomInButton
+ * Set the id or element of the custom 'Zoom in' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {String|Element} zoomOutButton
+ * Set the id or element of the custom 'Zoom out' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {String|Element} homeButton
+ * Set the id or element of the custom 'Go home' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {String|Element} fullPageButton
+ * Set the id or element of the custom 'Toggle full page' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {String|Element} rotateLeftButton
+ * Set the id or element of the custom 'Rotate left' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {String|Element} rotateRightButton
+ * Set the id or element of the custom 'Rotate right' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {String|Element} previousButton
+ * Set the id or element of the custom 'Previous page' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {String|Element} nextButton
+ * Set the id or element of the custom 'Next page' button to use.
+ * This is useful to have a custom button anywhere in the web page.
+ * To only change the button images, consider using
+ * {@link OpenSeadragon.Options.navImages}
+ *
+ * @property {Boolean} [sequenceMode=false]
+ * Set to true to have the viewer treat your tilesources as a sequence of images to
+ * be opened one at a time rather than all at once.
+ *
+ * @property {Number} [initialPage=0]
+ * If sequenceMode is true, display this page initially.
+ *
+ * @property {Boolean} [preserveViewport=false]
+ * If sequenceMode is true, then normally navigating through each image resets the
+ * viewport to 'home' position. If preserveViewport is set to true, then the viewport
+ * position is preserved when navigating between images in the sequence.
+ *
+ * @property {Boolean} [preserveOverlays=false]
+ * If sequenceMode is true, then normally navigating through each image
+ * resets the overlays.
+ * If preserveOverlays is set to true, then the overlays added with {@link OpenSeadragon.Viewer#addOverlay}
+ * are preserved when navigating between images in the sequence.
+ * Note: setting preserveOverlays overrides any overlays specified in the global
+ * "overlays" option for the Viewer. It's also not compatible with specifying
+ * per-tileSource overlays via the options, as those overlays will persist
+ * even after the tileSource is closed.
+ *
+ * @property {Boolean} [showReferenceStrip=false]
+ * If sequenceMode is true, then display a scrolling strip of image thumbnails for
+ * navigating through the images.
+ *
+ * @property {String} [referenceStripScroll='horizontal']
+ *
+ * @property {Element} [referenceStripElement=null]
+ *
+ * @property {Number} [referenceStripHeight=null]
+ *
+ * @property {Number} [referenceStripWidth=null]
+ *
+ * @property {String} [referenceStripPosition='BOTTOM_LEFT']
+ *
+ * @property {Number} [referenceStripSizeRatio=0.2]
+ *
+ * @property {Boolean} [collectionMode=false]
+ * Set to true to have the viewer arrange your TiledImages in a grid or line.
+ *
+ * @property {Number} [collectionRows=3]
+ * If collectionMode is true, specifies how many rows the grid should have. Use 1 to make a line.
+ * If collectionLayout is 'vertical', specifies how many columns instead.
+ *
+ * @property {Number} [collectionColumns=0]
+ * If collectionMode is true, specifies how many columns the grid should have. Use 1 to make a line.
+ * If collectionLayout is 'vertical', specifies how many rows instead. Ignored if collectionRows is not set to a falsy value.
+ *
+ * @property {String} [collectionLayout='horizontal']
+ * If collectionMode is true, specifies whether to arrange vertically or horizontally.
+ *
+ * @property {Number} [collectionTileSize=800]
+ * If collectionMode is true, specifies the size, in viewport coordinates, for each TiledImage to fit into.
+ * The TiledImage will be centered within a square of the specified size.
+ *
+ * @property {Number} [collectionTileMargin=80]
+ * If collectionMode is true, specifies the margin, in viewport coordinates, between each TiledImage.
+ *
+ * @property {String|Boolean} [crossOriginPolicy=false]
+ * Valid values are 'Anonymous', 'use-credentials', and false. If false, canvas requests will
+ * not use CORS, and the canvas will be tainted.
+ *
+ * @property {Boolean} [ajaxWithCredentials=false]
+ * Whether to set the withCredentials XHR flag for AJAX requests.
+ * Note that this can be overridden at the {@link OpenSeadragon.TileSource} level.
+ *
+ * @property {Boolean} [loadTilesWithAjax=false]
+ * Whether to load tile data using AJAX requests.
+ * Note that this can be overridden at the {@link OpenSeadragon.TileSource} level.
+ *
+ * @property {Object} [ajaxHeaders={}]
+ * A set of headers to include when making AJAX requests for tile sources or tiles.
+ *
+ * @property {Boolean} [splitHashDataForPost=false]
+ * Allows to treat _first_ hash ('#') symbol as a separator for POST data:
+ * URL to be opened by a {@link OpenSeadragon.TileSource} can thus look like: http://some.url#postdata=here.
+ * The whole URL is used to fetch image info metadata and it is then split to 'http://some.url' and
+ * 'postdata=here'; post data is given to the {@link OpenSeadragon.TileSource} of the choice and can be further
+ * used within tile requests (see TileSource methods).
+ * NOTE: {@link OpenSeadragon.TileSource.prototype.configure} return value should contain the post data
+ * if you want to use it later - so that it is given to your constructor later.
+ * NOTE: usually, post data is expected to be ampersand-separated (just like GET parameters), and is NOT USED
+ * to fetch tile image data unless explicitly programmed, or if loadTilesWithAjax=false 4
+ * (but it is still used for the initial image info request).
+ * NOTE: passing POST data from URL by this feature only supports string values, however,
+ * TileSource can send any data using POST as long as the header is correct
+ * (@see OpenSeadragon.TileSource.prototype.getTilePostData)
+ *
+ * @property {Boolean} [callTileLoadedWithCachedData=false]
+ * tile-loaded event is called only for tiles that downloaded new data or
+ * their data is stored in the original form in a suplementary cache object.
+ * Caches that render directly from re-used cache does not trigger this event again,
+ * as possible modifications would be applied twice.
+ */
+
+ /**
+ * Settings for gestures generated by a pointer device.
+ *
+ * @typedef {Object} GestureSettings
+ * @memberof OpenSeadragon
+ *
+ * @property {Boolean} dragToPan
+ * Set to false to disable panning on drag gestures.
+ *
+ * @property {Boolean} scrollToZoom
+ * Set to false to disable zooming on scroll gestures.
+ *
+ * @property {Boolean} clickToZoom
+ * Set to false to disable zooming on click gestures.
+ *
+ * @property {Boolean} dblClickToZoom
+ * Set to false to disable zooming on double-click gestures. Note: If set to true
+ * then clickToZoom should be set to false to prevent multiple zooms.
+ *
+ * @property {Boolean} pinchToZoom
+ * Set to false to disable zooming on pinch gestures.
+ *
+ * @property {Boolean} flickEnabled
+ * Set to false to disable the kinetic panning effect (flick) at the end of a drag gesture.
+ *
+ * @property {Number} flickMinSpeed
+ * 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.
+ *
+ * @property {Number} flickMomentum
+ * 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.
+ * A larger value will make the flick feel "lighter", while a smaller value will make the flick feel "heavier".
+ * Note: springStiffness and animationTime also affect the "spring" used to stop the flick animation.
+ *
+ */
+
+/**
+ * @typedef {OpenSeadragon.BaseDrawerOptions} OpenSeadragon.WebGLDrawerOptions
+ * @memberof OpenSeadragon
+ * @property {Boolean} [unpackWithPremultipliedAlpha=false]
+ * Whether to enable gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL when uploading textures.
+ */
+
+/**
+ * @typedef {Object.} DrawerOptions
+ * Can support any drawer key as long as a drawer is registered with the drawer id = map key.
+ * Therefore, one can register a new drawer that extends a drawer base and submit a custom key in the options.
+ * @memberof OpenSeadragon
+ * @property {OpenSeadragon.WebGLDrawerOptions} webgl - options if the WebGLDrawer is used.
+ * @property {OpenSeadragon.BaseDrawerOptions} canvas - options if the CanvasDrawer is used.
+ * @property {OpenSeadragon.BaseDrawerOptions} html - options if the HTMLDrawer is used.
+ * @property {OpenSeadragon.BaseDrawerOptions} custom - options if a custom drawer is used.
+ */
+
+
+/**
+ * The names for the image resources used for the image navigation buttons.
+ *
+ * @typedef {Object} NavImages
+ * @memberof OpenSeadragon
+ *
+ * @property {Object} zoomIn - Images for the zoom-in button.
+ * @property {String} zoomIn.REST
+ * @property {String} zoomIn.GROUP
+ * @property {String} zoomIn.HOVER
+ * @property {String} zoomIn.DOWN
+ *
+ * @property {Object} zoomOut - Images for the zoom-out button.
+ * @property {String} zoomOut.REST
+ * @property {String} zoomOut.GROUP
+ * @property {String} zoomOut.HOVER
+ * @property {String} zoomOut.DOWN
+ *
+ * @property {Object} home - Images for the home button.
+ * @property {String} home.REST
+ * @property {String} home.GROUP
+ * @property {String} home.HOVER
+ * @property {String} home.DOWN
+ *
+ * @property {Object} fullpage - Images for the full-page button.
+ * @property {String} fullpage.REST
+ * @property {String} fullpage.GROUP
+ * @property {String} fullpage.HOVER
+ * @property {String} fullpage.DOWN
+ *
+ * @property {Object} rotateleft - Images for the rotate left button.
+ * @property {String} rotateleft.REST
+ * @property {String} rotateleft.GROUP
+ * @property {String} rotateleft.HOVER
+ * @property {String} rotateleft.DOWN
+ *
+ * @property {Object} rotateright - Images for the rotate right button.
+ * @property {String} rotateright.REST
+ * @property {String} rotateright.GROUP
+ * @property {String} rotateright.HOVER
+ * @property {String} rotateright.DOWN
+ *
+ * @property {Object} flip - Images for the flip button.
+ * @property {String} flip.REST
+ * @property {String} flip.GROUP
+ * @property {String} flip.HOVER
+ * @property {String} flip.DOWN
+ *
+ * @property {Object} previous - Images for the previous button.
+ * @property {String} previous.REST
+ * @property {String} previous.GROUP
+ * @property {String} previous.HOVER
+ * @property {String} previous.DOWN
+ *
+ * @property {Object} next - Images for the next button.
+ * @property {String} next.REST
+ * @property {String} next.GROUP
+ * @property {String} next.HOVER
+ * @property {String} next.DOWN
+ *
+ */
+
+/* eslint-disable no-redeclare */
+function OpenSeadragon( options ){
+ return new OpenSeadragon.Viewer( options );
+}
+
+(function( $ ){
+
+
+ /**
+ * The OpenSeadragon version.
+ *
+ * @member {Object} OpenSeadragon.version
+ * @property {String} versionStr - The version number as a string ('major.minor.revision').
+ * @property {Number} major - The major version number.
+ * @property {Number} minor - The minor version number.
+ * @property {Number} revision - The revision number.
+ * @since 1.0.0
+ */
+ $.version = {
+ versionStr: '6.0.2',
+ major: parseInt('6', 10),
+ minor: parseInt('0', 10),
+ revision: parseInt('2', 10)
+ };
+
+
+ /**
+ * Taken from jquery 1.6.1
+ * [[Class]] -> type pairs
+ * @private
+ */
+ const class2type = {
+ '[object Boolean]': 'boolean',
+ '[object Number]': 'number',
+ '[object String]': 'string',
+ '[object Function]': 'function',
+ '[object AsyncFunction]': 'function',
+ '[object Promise]': 'promise',
+ '[object Array]': 'array',
+ '[object Date]': 'date',
+ '[object RegExp]': 'regexp',
+ '[object Object]': 'object',
+ '[object HTMLUnknownElement]': 'dom-node',
+ '[object HTMLImageElement]': 'image',
+ '[object HTMLCanvasElement]': 'canvas',
+ '[object CanvasRenderingContext2D]': 'context2d'
+ };
+ // Save a reference to some core methods
+ const toString = Object.prototype.toString;
+ const hasOwn = Object.prototype.hasOwnProperty;
+
+ /**
+ * Taken from jQuery 1.6.1
+ * @function isFunction
+ * @memberof OpenSeadragon
+ * @see {@link http://www.jquery.com/ jQuery}
+ */
+ $.isFunction = function( obj ) {
+ return $.type(obj) === "function";
+ };
+
+ /**
+ * Taken from jQuery 1.6.1
+ * @function isArray
+ * @memberof OpenSeadragon
+ * @see {@link http://www.jquery.com/ jQuery}
+ */
+ $.isArray = Array.isArray || function( obj ) {
+ return $.type(obj) === "array";
+ };
+
+
+ /**
+ * A crude way of determining if an object is a window.
+ * Taken from jQuery 1.6.1
+ * @function isWindow
+ * @memberof OpenSeadragon
+ * @see {@link http://www.jquery.com/ jQuery}
+ */
+ $.isWindow = function( obj ) {
+ return obj && typeof obj === "object" && "setInterval" in obj;
+ };
+
+
+ /**
+ * Taken from jQuery 1.6.1
+ * @function type
+ * @memberof OpenSeadragon
+ * @see {@link http://www.jquery.com/ jQuery}
+ */
+ $.type = function( obj ) {
+ return ( obj === null ) || ( obj === undefined ) ?
+ String( obj ) :
+ class2type[ toString.call(obj) ] || "object";
+ };
+
+
+ /**
+ * Taken from jQuery 1.6.1
+ * @function isPlainObject
+ * @memberof OpenSeadragon
+ * @see {@link http://www.jquery.com/ jQuery}
+ */
+ $.isPlainObject = function( obj ) {
+ // Must be an Object.
+ // Because of IE, we also have to check the presence of the constructor property.
+ // Make sure that DOM nodes and window objects don't pass through, as well
+ if ( !obj || OpenSeadragon.type(obj) !== "object" || obj.nodeType || $.isWindow( obj ) ) {
+ return false;
+ }
+
+ // Not own constructor property must be Object
+ if ( obj.constructor &&
+ !hasOwn.call(obj, "constructor") &&
+ !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+ return false;
+ }
+
+ // Own properties are enumerated firstly, so to speed up,
+ // if last one is own, then all properties are own.
+
+ let lastKey;
+ for (const key in obj ) {
+ lastKey = key;
+ }
+
+ return lastKey === undefined || hasOwn.call( obj, lastKey );
+ };
+
+
+ /**
+ * Taken from jQuery 1.6.1
+ * @function isEmptyObject
+ * @memberof OpenSeadragon
+ * @see {@link http://www.jquery.com/ jQuery}
+ */
+ $.isEmptyObject = function( obj ) {
+ for ( const name in obj ) {
+ return false;
+ }
+ return true;
+ };
+
+ /**
+ * Shim around Object.freeze. Does nothing if Object.freeze is not supported.
+ * @param {Object} obj The object to freeze.
+ * @returns {Object} obj The frozen object.
+ */
+ $.freezeObject = function(obj) {
+ if (Object.freeze) {
+ $.freezeObject = Object.freeze;
+ } else {
+ $.freezeObject = function(obj) {
+ return obj;
+ };
+ }
+ return $.freezeObject(obj);
+ };
+
+ /**
+ * True if the browser supports the HTML5 canvas element
+ * @member {Boolean} supportsCanvas
+ * @memberof OpenSeadragon
+ */
+ $.supportsCanvas = (function () {
+ const canvasElement = document.createElement( 'canvas' );
+ return !!( $.isFunction( canvasElement.getContext ) &&
+ canvasElement.getContext( '2d' ) );
+ }());
+
+ /**
+ * Test whether the submitted canvas is tainted or not.
+ * @argument {Canvas} canvas The canvas to test.
+ * @returns {Boolean} True if the canvas is tainted.
+ */
+ $.isCanvasTainted = function(canvas) {
+ let isTainted = false;
+ try {
+ // We test if the canvas is tainted by retrieving data from it.
+ // An exception will be raised if the canvas is tainted.
+ canvas.getContext('2d').getImageData(0, 0, 1, 1);
+ } catch (e) {
+ isTainted = true;
+ }
+ return isTainted;
+ };
+
+ /**
+ * True if the browser supports the EventTarget.addEventListener() method
+ * @member {Boolean} supportsAddEventListener
+ * @memberof OpenSeadragon
+ */
+ $.supportsAddEventListener = (function () {
+ return !!(document.documentElement.addEventListener && document.addEventListener);
+ }());
+
+ /**
+ * True if the browser supports the EventTarget.removeEventListener() method
+ * @member {Boolean} supportsRemoveEventListener
+ * @memberof OpenSeadragon
+ */
+ $.supportsRemoveEventListener = (function () {
+ return !!(document.documentElement.removeEventListener && document.removeEventListener);
+ }());
+
+ /**
+ * True if the browser supports the newer EventTarget.addEventListener options argument
+ * @member {Boolean} supportsEventListenerOptions
+ * @memberof OpenSeadragon
+ */
+ $.supportsEventListenerOptions = (function () {
+ let supported = 0;
+
+ if ( $.supportsAddEventListener ) {
+ try {
+ const options = {
+ get capture() {
+ supported++;
+ return false;
+ },
+ get once() {
+ supported++;
+ return false;
+ },
+ get passive() {
+ supported++;
+ return false;
+ }
+ };
+ window.addEventListener("test", null, options);
+ window.removeEventListener("test", null, options);
+ } catch ( e ) {
+ supported = 0;
+ }
+ }
+
+ return supported >= 3;
+ }());
+
+ /**
+ * If true, OpenSeadragon uses async execution, else it uses synchronous execution.
+ * Note that disabling async means no plugins that use Promises / async will work with OSD.
+ * @member {boolean}
+ * @memberof OpenSeadragon
+ */
+ $.supportsAsync = true;
+
+ /**
+ * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density,
+ * clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser.
+ * @function getCurrentPixelDensityRatio
+ * @memberof OpenSeadragon
+ * @returns {Number}
+ */
+ $.getCurrentPixelDensityRatio = function() {
+ if ( $.supportsCanvas ) {
+ const context = document.createElement('canvas').getContext('2d');
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ const backingStoreRatio = context.webkitBackingStorePixelRatio ||
+ context.mozBackingStorePixelRatio ||
+ context.msBackingStorePixelRatio ||
+ context.oBackingStorePixelRatio ||
+ context.backingStorePixelRatio || 1;
+ return Math.max(devicePixelRatio, 1) / backingStoreRatio;
+ } else {
+ return 1;
+ }
+ };
+
+ /**
+ * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density,
+ * clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser.
+ * @member {Number} pixelDensityRatio
+ * @memberof OpenSeadragon
+ */
+ $.pixelDensityRatio = $.getCurrentPixelDensityRatio();
+
+}( OpenSeadragon ));
+
+/**
+ * This closure defines all static methods available to the OpenSeadragon
+ * namespace. Many, if not most, are taken directly from jQuery for use
+ * to simplify and reduce common programming patterns. More static methods
+ * from jQuery may eventually make their way into this though we are
+ * attempting to avoid an explicit dependency on jQuery only because
+ * OpenSeadragon is a broadly useful code base and would be made less broad
+ * by requiring jQuery fully.
+ *
+ * Some static methods have also been refactored from the original OpenSeadragon
+ * project.
+ */
+(function( $ ){
+
+ /**
+ * Taken from jQuery 1.6.1
+ * @function extend
+ * @memberof OpenSeadragon
+ * @see {@link http://www.jquery.com/ jQuery}
+ */
+ $.extend = function() {
+ let options;
+ let name;
+ let src;
+ let copy;
+ let copyIsArray;
+ let clone;
+ let target = arguments[ 0 ] || {};
+ const length = arguments.length;
+ let deep = false;
+ let i = 1;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[ 1 ] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) {
+ target = {};
+ }
+
+ // extend jQuery itself if only one argument is passed
+ if ( length === i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ options = arguments[ i ];
+ if ( options !== null || options !== undefined ) {
+ // Extend the base object
+ for ( name in options ) {
+ const descriptor = Object.getOwnPropertyDescriptor(options, name);
+
+ if (descriptor !== undefined) {
+ if (descriptor.get || descriptor.set) {
+ Object.defineProperty(target, name, descriptor);
+ continue;
+ }
+
+ copy = descriptor.value;
+ } else {
+ $.console.warn('Could not copy inherited property "' + name + '".');
+ continue;
+ }
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( OpenSeadragon.isPlainObject( copy ) || ( copyIsArray = OpenSeadragon.isArray( copy ) ) ) ) {
+ src = target[ name ];
+
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && OpenSeadragon.isArray( src ) ? src : [];
+
+ } else {
+ clone = src && OpenSeadragon.isPlainObject( src ) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = OpenSeadragon.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+ };
+
+ const isIOSDevice = function () {
+ if (typeof navigator !== 'object') {
+ return false;
+ }
+ const userAgent = navigator.userAgent;
+ if (typeof userAgent !== 'string') {
+ return false;
+ }
+ return userAgent.indexOf('iPhone') !== -1 ||
+ userAgent.indexOf('iPad') !== -1 ||
+ userAgent.indexOf('iPod') !== -1;
+ };
+
+ $.extend( $, /** @lends OpenSeadragon */{
+ /**
+ * The default values for the optional settings documented at {@link OpenSeadragon.Options}.
+ * @static
+ * @type {Object}
+ */
+ DEFAULT_SETTINGS: {
+ //DATA SOURCE DETAILS
+ xmlPath: null,
+ tileSources: null,
+ tileHost: null,
+ initialPage: 0,
+ crossOriginPolicy: false,
+ ajaxWithCredentials: false,
+ loadTilesWithAjax: false,
+ ajaxHeaders: {},
+ splitHashDataForPost: false,
+ callTileLoadedWithCachedData: false,
+
+ //PAN AND ZOOM SETTINGS AND CONSTRAINTS
+ panHorizontal: true,
+ panVertical: true,
+ constrainDuringPan: false,
+ wrapHorizontal: false,
+ wrapVertical: false,
+ visibilityRatio: 0.5, //-> how much of the viewer can be negative space
+ minPixelRatio: 0.5, //->closer to 0 draws tiles meant for a higher zoom at this zoom
+ defaultZoomLevel: 0,
+ minZoomLevel: null,
+ maxZoomLevel: null,
+ homeFillsViewer: false,
+
+ //UI RESPONSIVENESS AND FEEL
+ clickTimeThreshold: 300,
+ clickDistThreshold: 5,
+ dblClickTimeThreshold: 300,
+ dblClickDistThreshold: 20,
+ springStiffness: 6.5,
+ animationTime: 1.2,
+ loadDestinationTilesOnAnimation: true,
+ gestureSettingsMouse: {
+ dragToPan: true,
+ scrollToZoom: true,
+ clickToZoom: true,
+ dblClickToZoom: false,
+ dblClickDragToZoom: false,
+ pinchToZoom: false,
+ zoomToRefPoint: true,
+ flickEnabled: false,
+ flickMinSpeed: 120,
+ flickMomentum: 0.25,
+ pinchRotate: false
+ },
+ gestureSettingsTouch: {
+ dragToPan: true,
+ scrollToZoom: false,
+ clickToZoom: false,
+ dblClickToZoom: true,
+ dblClickDragToZoom: true,
+ pinchToZoom: true,
+ zoomToRefPoint: true,
+ flickEnabled: true,
+ flickMinSpeed: 120,
+ flickMomentum: 0.25,
+ pinchRotate: false
+ },
+ gestureSettingsPen: {
+ dragToPan: true,
+ scrollToZoom: false,
+ clickToZoom: true,
+ dblClickToZoom: false,
+ dblClickDragToZoom: false,
+ pinchToZoom: false,
+ zoomToRefPoint: true,
+ flickEnabled: false,
+ flickMinSpeed: 120,
+ flickMomentum: 0.25,
+ pinchRotate: false
+ },
+ gestureSettingsUnknown: {
+ dragToPan: true,
+ scrollToZoom: false,
+ clickToZoom: false,
+ dblClickToZoom: true,
+ dblClickDragToZoom: false,
+ pinchToZoom: true,
+ zoomToRefPoint: true,
+ flickEnabled: true,
+ flickMinSpeed: 120,
+ flickMomentum: 0.25,
+ pinchRotate: false
+ },
+ zoomPerClick: 2,
+ zoomPerScroll: 1.2,
+ zoomPerDblClickDrag: 1.2,
+ zoomPerSecond: 1.0,
+ blendTime: 0,
+ alwaysBlend: false,
+ autoHideControls: true,
+ immediateRender: false,
+ minZoomImageRatio: 0.9, //-> closer to 0 allows zoom out to infinity
+ maxZoomPixelRatio: 1.1, //-> higher allows 'over zoom' into pixels
+ smoothTileEdgesMinZoom: 1.1, //-> higher than maxZoomPixelRatio disables it
+ iOSDevice: isIOSDevice(),
+ pixelsPerWheelLine: 40,
+ pixelsPerArrowPress: 40,
+ autoResize: true,
+ preserveImageSizeOnResize: false, // requires autoResize=true
+ minScrollDeltaTime: 50,
+ rotationIncrement: 90,
+ maxTilesPerFrame: 1,
+
+ //DEFAULT CONTROL SETTINGS
+ showSequenceControl: true, //SEQUENCE
+ sequenceControlAnchor: null, //SEQUENCE
+ preserveViewport: false, //SEQUENCE
+ preserveOverlays: false, //SEQUENCE
+ navPrevNextWrap: false, //SEQUENCE
+ showNavigationControl: true, //ZOOM/HOME/FULL/ROTATION
+ navigationControlAnchor: null, //ZOOM/HOME/FULL/ROTATION
+ showZoomControl: true, //ZOOM
+ showHomeControl: true, //HOME
+ showFullPageControl: true, //FULL
+ showRotationControl: false, //ROTATION
+ showFlipControl: false, //FLIP
+ controlsFadeDelay: 2000, //ZOOM/HOME/FULL/SEQUENCE
+ controlsFadeLength: 1500, //ZOOM/HOME/FULL/SEQUENCE
+ mouseNavEnabled: true, //GENERAL MOUSE INTERACTIVITY
+ keyboardNavEnabled: true, //GENERAL KEYBOARD INTERACTIVITY
+
+ //VIEWPORT NAVIGATOR SETTINGS
+ showNavigator: false,
+ navigatorElement: null,
+ navigatorId: null,
+ navigatorPosition: null,
+ navigatorSizeRatio: 0.2,
+ navigatorMaintainSizeRatio: false,
+ navigatorTop: null,
+ navigatorLeft: null,
+ navigatorHeight: null,
+ navigatorWidth: null,
+ navigatorAutoFade: true,
+ navigatorRotate: true,
+ navigatorBackground: '#000',
+ navigatorOpacity: 0.8,
+ navigatorBorderColor: '#555',
+ navigatorDisplayRegionColor: '#900',
+
+ // INITIAL ROTATION
+ degrees: 0,
+
+ // INITIAL FLIP STATE
+ flipped: false,
+ overlayPreserveContentDirection: true,
+
+ // APPEARANCE
+ opacity: 1, // to be passed into each TiledImage
+ compositeOperation: null, // to be passed into each TiledImage
+
+ // DRAWER SETTINGS
+ drawer: ['auto', 'webgl', 'canvas', 'html'], // prefer using auto, then webgl (with WebGL2 if available), then canvas (i.e. context2d), then fallback to html
+ // DRAWER CONFIGURATIONS
+ drawerOptions: {
+ // [drawer-id]: {options} map
+ },
+
+ // TILED IMAGE SETTINGS
+ preload: false, // to be passed into each TiledImage
+ imageSmoothingEnabled: true, // to be passed into each TiledImage
+ placeholderFillStyle: null, // to be passed into each TiledImage
+ subPixelRoundingForTransparency: null, // to be passed into each TiledImage
+
+ //REFERENCE STRIP SETTINGS
+ showReferenceStrip: false,
+ referenceStripScroll: 'horizontal',
+ referenceStripElement: null,
+ referenceStripHeight: null,
+ referenceStripWidth: null,
+ referenceStripPosition: 'BOTTOM_LEFT',
+ referenceStripSizeRatio: 0.2,
+
+ //COLLECTION VISUALIZATION SETTINGS
+ collectionRows: 3, //or columns depending on layout
+ collectionColumns: 0, //columns in horizontal layout, rows in vertical layout
+ collectionLayout: 'horizontal', //vertical
+ collectionMode: false,
+ collectionTileSize: 800,
+ collectionTileMargin: 80,
+
+ //PERFORMANCE SETTINGS
+ imageLoaderLimit: 0,
+ maxImageCacheCount: 200,
+ timeout: 30000,
+ tileRetryMax: 0,
+ tileRetryDelay: 2500,
+
+ //INTERFACE RESOURCE SETTINGS
+ prefixUrl: "/images/",
+ navImages: {
+ zoomIn: {
+ REST: 'zoomin_rest.png',
+ GROUP: 'zoomin_grouphover.png',
+ HOVER: 'zoomin_hover.png',
+ DOWN: 'zoomin_pressed.png'
+ },
+ zoomOut: {
+ REST: 'zoomout_rest.png',
+ GROUP: 'zoomout_grouphover.png',
+ HOVER: 'zoomout_hover.png',
+ DOWN: 'zoomout_pressed.png'
+ },
+ home: {
+ REST: 'home_rest.png',
+ GROUP: 'home_grouphover.png',
+ HOVER: 'home_hover.png',
+ DOWN: 'home_pressed.png'
+ },
+ fullpage: {
+ REST: 'fullpage_rest.png',
+ GROUP: 'fullpage_grouphover.png',
+ HOVER: 'fullpage_hover.png',
+ DOWN: 'fullpage_pressed.png'
+ },
+ rotateleft: {
+ REST: 'rotateleft_rest.png',
+ GROUP: 'rotateleft_grouphover.png',
+ HOVER: 'rotateleft_hover.png',
+ DOWN: 'rotateleft_pressed.png'
+ },
+ rotateright: {
+ REST: 'rotateright_rest.png',
+ GROUP: 'rotateright_grouphover.png',
+ HOVER: 'rotateright_hover.png',
+ DOWN: 'rotateright_pressed.png'
+ },
+ flip: { // Flip icon designed by Yaroslav Samoylov from the Noun Project and modified by Nelson Campos ncampos@criteriamarathon.com, https://thenounproject.com/term/flip/136289/
+ REST: 'flip_rest.png',
+ GROUP: 'flip_grouphover.png',
+ HOVER: 'flip_hover.png',
+ DOWN: 'flip_pressed.png'
+ },
+ previous: {
+ REST: 'previous_rest.png',
+ GROUP: 'previous_grouphover.png',
+ HOVER: 'previous_hover.png',
+ DOWN: 'previous_pressed.png'
+ },
+ next: {
+ REST: 'next_rest.png',
+ GROUP: 'next_grouphover.png',
+ HOVER: 'next_hover.png',
+ DOWN: 'next_pressed.png'
+ }
+ },
+
+ //DEVELOPER SETTINGS
+ debugMode: false,
+ debugGridColor: ['#437AB2', '#1B9E77', '#D95F02', '#7570B3', '#E7298A', '#66A61E', '#E6AB02', '#A6761D', '#666666'],
+ silenceMultiImageWarnings: false
+
+ },
+
+ /**
+ * Returns a function which invokes the method as if it were a method belonging to the object.
+ * @function
+ * @param {Object} object
+ * @param {Function} method
+ * @returns {Function}
+ */
+ delegate: function( object, method ) {
+ return function(){
+ let args = arguments;
+ if ( args === undefined ){
+ args = [];
+ }
+ return method.apply( object, args );
+ };
+ },
+
+
+ /**
+ * An enumeration of Browser vendors.
+ * @static
+ * @type {Object}
+ * @property {Number} UNKNOWN
+ * @property {Number} IE
+ * @property {Number} FIREFOX
+ * @property {Number} SAFARI
+ * @property {Number} CHROME
+ * @property {Number} OPERA
+ * @property {Number} EDGE
+ * @property {Number} CHROMEEDGE
+ */
+ BROWSERS: {
+ UNKNOWN: 0,
+ IE: 1,
+ FIREFOX: 2,
+ SAFARI: 3,
+ CHROME: 4,
+ OPERA: 5,
+ EDGE: 6,
+ CHROMEEDGE: 7
+ },
+
+ /**
+ * An enumeration of when subpixel rounding should occur.
+ * @static
+ * @type {Object}
+ * @property {Number} NEVER Never apply subpixel rounding for transparency.
+ * @property {Number} ONLY_AT_REST Do not apply subpixel rounding for transparency during animation (panning, zoom, rotation) and apply it once animation is over.
+ * @property {Number} ALWAYS Apply subpixel rounding for transparency during animation and when animation is over.
+ */
+ SUBPIXEL_ROUNDING_OCCURRENCES: {
+ NEVER: 0,
+ ONLY_AT_REST: 1,
+ ALWAYS: 2
+ },
+
+ /**
+ * Keep track of which {@link Viewer}s have been created.
+ * - Key: {@link Element} to which a Viewer is attached.
+ * - Value: {@link Viewer} of the element defined by the key.
+ * @private
+ * @static
+ * @type {Object}
+ */
+ _viewers: new Map(),
+
+ /**
+ * Returns the {@link Viewer} attached to a given DOM element. If there is
+ * no viewer attached to the provided element, undefined is returned.
+ * @function
+ * @param {String|Element} element Accepts an id or element.
+ * @returns {Viewer} The viewer attached to the given element, or undefined.
+ */
+ getViewer: function(element) {
+ return $._viewers.get(this.getElement(element));
+ },
+
+ /**
+ * Returns a DOM Element for the given id or element.
+ * @function
+ * @param {String|Element} element Accepts an id or element.
+ * @returns {Element} The element with the given id, null, or the element itself.
+ */
+ getElement: function( element ) {
+ if ( typeof ( element ) === "string" ) {
+ element = document.getElementById( element );
+ }
+ return element;
+ },
+
+
+ /**
+ * Determines the position of the upper-left corner of the element.
+ * @function
+ * @param {Element|String} element - the element we want the position for.
+ * @returns {OpenSeadragon.Point} - the position of the upper left corner of the element.
+ */
+ getElementPosition: function( element ) {
+ let result = new $.Point();
+ let isFixed;
+ let offsetParent;
+
+ element = $.getElement( element );
+ isFixed = $.getElementStyle( element ).position === "fixed";
+ offsetParent = getOffsetParent( element, isFixed );
+
+ while ( offsetParent ) {
+
+ result.x += element.offsetLeft;
+ result.y += element.offsetTop;
+
+ if ( isFixed ) {
+ result = result.plus( $.getPageScroll() );
+ }
+
+ element = offsetParent;
+ isFixed = $.getElementStyle( element ).position === "fixed";
+ offsetParent = getOffsetParent( element, isFixed );
+ }
+
+ return result;
+ },
+
+
+ /**
+ * Determines the position of the upper-left corner of the element adjusted for current page and/or element scroll.
+ * @function
+ * @param {Element|String} element - the element we want the position for.
+ * @returns {OpenSeadragon.Point} - the position of the upper left corner of the element adjusted for current page and/or element scroll.
+ */
+ getElementOffset: function( element ) {
+ element = $.getElement( element );
+
+ const doc = element && element.ownerDocument;
+ let boundingRect = { top: 0, left: 0 };
+
+ if ( !doc ) {
+ return new $.Point();
+ }
+
+ const docElement = doc.documentElement;
+
+ if ( typeof element.getBoundingClientRect !== typeof undefined ) {
+ boundingRect = element.getBoundingClientRect();
+ }
+
+ const win = ( doc === doc.window ) ?
+ doc :
+ ( doc.nodeType === 9 ) ?
+ doc.defaultView || doc.parentWindow :
+ false;
+
+ return new $.Point(
+ boundingRect.left + ( win.pageXOffset || docElement.scrollLeft ) - ( docElement.clientLeft || 0 ),
+ boundingRect.top + ( win.pageYOffset || docElement.scrollTop ) - ( docElement.clientTop || 0 )
+ );
+ },
+
+
+ /**
+ * Determines the height and width of the given element.
+ * @function
+ * @param {Element|String} element
+ * @returns {OpenSeadragon.Point}
+ */
+ getElementSize: function( element ) {
+ element = $.getElement( element );
+
+ return new $.Point(
+ element.clientWidth,
+ element.clientHeight
+ );
+ },
+
+
+ /**
+ * Returns the CSSStyle object for the given element.
+ * @function
+ * @param {Element|String} element
+ * @returns {CSSStyle}
+ */
+ getElementStyle:
+ document.documentElement.currentStyle ?
+ function( element ) {
+ element = $.getElement( element );
+ return element.currentStyle;
+ } :
+ function( element ) {
+ element = $.getElement( element );
+ return window.getComputedStyle( element, "" );
+ },
+
+ /**
+ * Returns the property with the correct vendor prefix appended.
+ * @param {String} property the property name
+ * @returns {String} the property with the correct prefix or null if not
+ * supported.
+ */
+ getCssPropertyWithVendorPrefix: function(property) {
+ const memo = {};
+
+ $.getCssPropertyWithVendorPrefix = function(property) {
+ if (memo[property] !== undefined) {
+ return memo[property];
+ }
+ const style = document.createElement('div').style;
+ let result = null;
+ if (style[property] !== undefined) {
+ result = property;
+ } else {
+ const prefixes = ['Webkit', 'Moz', 'MS', 'O',
+ 'webkit', 'moz', 'ms', 'o'];
+ const suffix = $.capitalizeFirstLetter(property);
+ for (let i = 0; i < prefixes.length; i++) {
+ const prop = prefixes[i] + suffix;
+ if (style[prop] !== undefined) {
+ result = prop;
+ break;
+ }
+ }
+ }
+ memo[property] = result;
+ return result;
+ };
+ return $.getCssPropertyWithVendorPrefix(property);
+ },
+
+ /**
+ * Capitalizes the first letter of a string
+ * @param {String} string
+ * @returns {String} The string with the first letter capitalized
+ */
+ capitalizeFirstLetter: function(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ },
+
+ /**
+ * Compute the modulo of a number but makes sure to always return
+ * a positive value (also known as Euclidean modulo).
+ * @param {Number} number the number to compute the modulo of
+ * @param {Number} modulo the modulo
+ * @returns {Number} the result of the modulo of number
+ */
+ positiveModulo: function(number, modulo) {
+ let result = number % modulo;
+ if (result < 0) {
+ result += modulo;
+ }
+ return result;
+ },
+
+
+ /**
+ * Determines if a point is within the bounding rectangle of the given element (hit-test).
+ * @function
+ * @param {Element|String} element
+ * @param {OpenSeadragon.Point} point
+ * @returns {Boolean}
+ */
+ pointInElement: function( element, point ) {
+ element = $.getElement( element );
+ const offset = $.getElementOffset( element );
+ const size = $.getElementSize( element );
+ return point.x >= offset.x && point.x < offset.x + size.x && point.y < offset.y + size.y && point.y >= offset.y;
+ },
+
+
+ /**
+ * Gets the position of the mouse on the screen for a given event.
+ * @function
+ * @param {Event} [event]
+ * @returns {OpenSeadragon.Point}
+ */
+ getMousePosition: function( event ) {
+
+ if ( typeof ( event.pageX ) === "number" ) {
+ $.getMousePosition = function( event ){
+ const result = new $.Point();
+
+ result.x = event.pageX;
+ result.y = event.pageY;
+
+ return result;
+ };
+ } else if ( typeof ( event.clientX ) === "number" ) {
+ $.getMousePosition = function( event ){
+ const result = new $.Point();
+
+ result.x =
+ event.clientX +
+ document.body.scrollLeft +
+ document.documentElement.scrollLeft;
+ result.y =
+ event.clientY +
+ document.body.scrollTop +
+ document.documentElement.scrollTop;
+
+ return result;
+ };
+ } else {
+ throw new Error(
+ "Unknown event mouse position, no known technique."
+ );
+ }
+
+ return $.getMousePosition( event );
+ },
+
+
+ /**
+ * Determines the page's current scroll position.
+ * @function
+ * @returns {OpenSeadragon.Point}
+ */
+ getPageScroll: function() {
+ const docElement = document.documentElement || {};
+ const body = document.body || {};
+
+ if ( typeof ( window.pageXOffset ) === "number" ) {
+ $.getPageScroll = function(){
+ return new $.Point(
+ window.pageXOffset,
+ window.pageYOffset
+ );
+ };
+ } else if ( body.scrollLeft || body.scrollTop ) {
+ $.getPageScroll = function(){
+ return new $.Point(
+ document.body.scrollLeft,
+ document.body.scrollTop
+ );
+ };
+ } else if ( docElement.scrollLeft || docElement.scrollTop ) {
+ $.getPageScroll = function(){
+ return new $.Point(
+ document.documentElement.scrollLeft,
+ document.documentElement.scrollTop
+ );
+ };
+ } else {
+ // We can't reassign the function yet, as there was no scroll.
+ return new $.Point(0, 0);
+ }
+
+ return $.getPageScroll();
+ },
+
+ /**
+ * Set the page scroll position.
+ * @function
+ * @returns {OpenSeadragon.Point}
+ */
+ setPageScroll: function( scroll ) {
+ if ( typeof ( window.scrollTo ) !== "undefined" ) {
+ $.setPageScroll = function( scroll ) {
+ window.scrollTo( scroll.x, scroll.y );
+ };
+ } else {
+ const originalScroll = $.getPageScroll();
+ if ( originalScroll.x === scroll.x &&
+ originalScroll.y === scroll.y ) {
+ // We are already correctly positioned and there
+ // is no way to detect the correct method.
+ return;
+ }
+
+ document.body.scrollLeft = scroll.x;
+ document.body.scrollTop = scroll.y;
+ let currentScroll = $.getPageScroll();
+ if ( currentScroll.x !== originalScroll.x &&
+ currentScroll.y !== originalScroll.y ) {
+ $.setPageScroll = function( scroll ) {
+ document.body.scrollLeft = scroll.x;
+ document.body.scrollTop = scroll.y;
+ };
+ return;
+ }
+
+ document.documentElement.scrollLeft = scroll.x;
+ document.documentElement.scrollTop = scroll.y;
+ currentScroll = $.getPageScroll();
+ if ( currentScroll.x !== originalScroll.x &&
+ currentScroll.y !== originalScroll.y ) {
+ $.setPageScroll = function( scroll ) {
+ document.documentElement.scrollLeft = scroll.x;
+ document.documentElement.scrollTop = scroll.y;
+ };
+ return;
+ }
+
+ // We can't find anything working, so we do nothing.
+ $.setPageScroll = function( scroll ) {
+ };
+ }
+
+ $.setPageScroll( scroll );
+ },
+
+ /**
+ * Determines the size of the browsers window.
+ * @function
+ * @returns {OpenSeadragon.Point}
+ */
+ getWindowSize: function() {
+ const docElement = document.documentElement || {};
+ const body = document.body || {};
+
+ if ( typeof ( window.innerWidth ) === 'number' ) {
+ $.getWindowSize = function(){
+ return new $.Point(
+ window.innerWidth,
+ window.innerHeight
+ );
+ };
+ } else if ( docElement.clientWidth || docElement.clientHeight ) {
+ $.getWindowSize = function(){
+ return new $.Point(
+ document.documentElement.clientWidth,
+ document.documentElement.clientHeight
+ );
+ };
+ } else if ( body.clientWidth || body.clientHeight ) {
+ $.getWindowSize = function(){
+ return new $.Point(
+ document.body.clientWidth,
+ document.body.clientHeight
+ );
+ };
+ } else {
+ throw new Error("Unknown window size, no known technique.");
+ }
+
+ return $.getWindowSize();
+ },
+
+
+ /**
+ * Wraps the given element in a nest of divs so that the element can
+ * be easily centered using CSS tables
+ * @function
+ * @param {Element|String} element
+ * @returns {Element} outermost wrapper element
+ */
+ makeCenteredNode: function( element ) {
+ // Convert a possible ID to an actual HTMLElement
+ element = $.getElement( element );
+
+ /*
+ CSS tables require you to have a display:table/row/cell hierarchy so we need to create
+ three nested wrapper divs:
+ */
+
+ const wrappers = [
+ $.makeNeutralElement( 'div' ),
+ $.makeNeutralElement( 'div' ),
+ $.makeNeutralElement( 'div' )
+ ];
+
+ // It feels like we should be able to pass style dicts to makeNeutralElement:
+ $.extend(wrappers[0].style, {
+ display: "table",
+ height: "100%",
+ width: "100%"
+ });
+
+ $.extend(wrappers[1].style, {
+ display: "table-row"
+ });
+
+ $.extend(wrappers[2].style, {
+ display: "table-cell",
+ verticalAlign: "middle",
+ textAlign: "center"
+ });
+
+ wrappers[0].appendChild(wrappers[1]);
+ wrappers[1].appendChild(wrappers[2]);
+ wrappers[2].appendChild(element);
+
+ return wrappers[0];
+ },
+
+ /**
+ * Log trace information from the system. Useful for logging and debugging
+ * async events. Calls to this function SHOULD NOT BE present in the release.
+ * (or at least used only in debug mode).
+ * @param {OpenSeadragon.Tile|OpenSeadragon.CacheRecord|string} tile message to log or tile to inspect
+ * @param {boolean} stacktrace if true log the stacktrace
+ */
+ trace: function(tile, stacktrace = false) {
+ this.__traceLogs = [];
+ setInterval(() => {
+ if (!this.__traceLogs.length) {
+ return;
+ }
+ console.log(this.__traceLogs.join('\n'));
+ this.__traceLogs = [];
+ }, 2000);
+ this.trace = function (tile, stacktrace = false) {
+ if (typeof tile === 'string') {
+ this.__traceLogs.push(tile);
+ if (stacktrace) {
+ this.__traceLogs.push(...new Error().stack.split('\n').slice(1));
+ }
+ return;
+ }
+ if (tile instanceof OpenSeadragon.Tile) {
+ tile = tile.getCache(tile.originalCacheKey);
+ }
+ const cacheTile = tile._tiles[0];
+ this.__traceLogs.push(`Cache ${cacheTile.toString()} loaded ${cacheTile.loaded} loading ${cacheTile.loading} cacheCount ${Object.keys(cacheTile._caches).length} - CACHE ${tile.__invStamp}`);
+ if (stacktrace) {
+ this.__traceLogs.push(...new Error().stack.split('\n').slice(1));
+ }
+ };
+ this.trace(tile, stacktrace);
+ },
+
+
+ /**
+ * Creates an easily positionable element of the given type that therefor
+ * serves as an excellent container element.
+ * @function
+ * @param {String} tagName
+ * @returns {Element}
+ */
+ makeNeutralElement: function( tagName ) {
+ const element = document.createElement( tagName );
+ const style = element.style;
+
+ style.background = "transparent none";
+ style.border = "none";
+ style.margin = "0px";
+ style.padding = "0px";
+ style.position = "static";
+
+ return element;
+ },
+
+
+ /**
+ * Returns the current milliseconds, using Date.now() if available
+ * @function
+ */
+ now: function( ) {
+ if (Date.now) {
+ $.now = Date.now;
+ } else {
+ $.now = function() {
+ return new Date().getTime();
+ };
+ }
+
+ return $.now();
+ },
+
+
+ /**
+ * Ensures an image is loaded correctly to support alpha transparency.
+ * @function
+ * @param {String} src
+ * @returns {Element}
+ */
+ makeTransparentImage: function( src ) {
+ const img = $.makeNeutralElement( "img" );
+
+ img.src = src;
+
+ return img;
+ },
+
+
+ /**
+ * Sets the opacity of the specified element.
+ * @function
+ * @param {Element|String} element
+ * @param {Number} opacity
+ * @param {Boolean} [usesAlpha]
+ */
+ setElementOpacity: function( element, opacity, usesAlpha ) {
+
+ let ieOpacity;
+ let ieFilter;
+
+ element = $.getElement( element );
+
+ if ( usesAlpha && !$.Browser.alpha ) {
+ opacity = Math.round( opacity );
+ }
+
+ if ( $.Browser.opacity ) {
+ element.style.opacity = opacity < 1 ? opacity : "";
+ } else {
+ if ( opacity < 1 ) {
+ ieOpacity = Math.round( 100 * opacity );
+ ieFilter = "alpha(opacity=" + ieOpacity + ")";
+ element.style.filter = ieFilter;
+ } else {
+ element.style.filter = "";
+ }
+ }
+ },
+
+
+ /**
+ * Sets the specified element's touch-action style attribute to 'none'.
+ * @function
+ * @param {Element|String} element
+ */
+ setElementTouchActionNone: function( element ) {
+ element = $.getElement( element );
+ if ( typeof element.style.touchAction !== 'undefined' ) {
+ element.style.touchAction = 'none';
+ } else if ( typeof element.style.msTouchAction !== 'undefined' ) {
+ element.style.msTouchAction = 'none';
+ }
+ },
+
+
+ /**
+ * Sets the specified element's pointer-events style attribute to the passed value.
+ * @function
+ * @param {Element|String} element
+ * @param {String} value
+ */
+ setElementPointerEvents: function( element, value ) {
+ element = $.getElement( element );
+ if (typeof element.style !== 'undefined' && typeof element.style.pointerEvents !== 'undefined' ) {
+ element.style.pointerEvents = value;
+ }
+ },
+
+
+ /**
+ * Sets the specified element's pointer-events style attribute to 'none'.
+ * @function
+ * @param {Element|String} element
+ */
+ setElementPointerEventsNone: function( element ) {
+ $.setElementPointerEvents( element, 'none' );
+ },
+
+
+ /**
+ * Add the specified CSS class to the element if not present.
+ * @function
+ * @param {Element|String} element
+ * @param {String} className
+ */
+ addClass: function( element, className ) {
+ element = $.getElement( element );
+
+ if (!element.className) {
+ element.className = className;
+ } else if ( ( ' ' + element.className + ' ' ).
+ indexOf( ' ' + className + ' ' ) === -1 ) {
+ element.className += ' ' + className;
+ }
+ },
+
+ /**
+ * Find the first index at which an element is found in an array or -1
+ * if not present.
+ *
+ * Code taken and adapted from
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Compatibility
+ *
+ * @function
+ * @param {Array} array The array from which to find the element
+ * @param {Object} searchElement The element to find
+ * @param {Number} [fromIndex=0] Index to start research.
+ * @returns {Number} The index of the element in the array.
+ */
+ indexOf: function( array, searchElement, fromIndex ) {
+ if ( Array.prototype.indexOf ) {
+ this.indexOf = function( array, searchElement, fromIndex ) {
+ return array.indexOf( searchElement, fromIndex );
+ };
+ } else {
+ this.indexOf = function( array, searchElement, fromIndex ) {
+ let pivot = ( fromIndex ) ? fromIndex : 0;
+ if ( !array ) {
+ throw new TypeError( );
+ }
+
+ const length = array.length;
+ if ( length === 0 || pivot >= length ) {
+ return -1;
+ }
+
+ if ( pivot < 0 ) {
+ pivot = length - Math.abs( pivot );
+ }
+
+ for ( let i = pivot; i < length; i++ ) {
+ if ( array[i] === searchElement ) {
+ return i;
+ }
+ }
+ return -1;
+ };
+ }
+ return this.indexOf( array, searchElement, fromIndex );
+ },
+
+ /**
+ * Remove the specified CSS class from the element.
+ * @function
+ * @param {Element|String} element
+ * @param {String} className
+ */
+ removeClass: function( element, className ) {
+ const newClasses = [];
+
+ element = $.getElement( element );
+ const oldClasses = element.className.split( /\s+/ );
+ for ( let i = 0; i < oldClasses.length; i++ ) {
+ if ( oldClasses[ i ] && oldClasses[ i ] !== className ) {
+ newClasses.push( oldClasses[ i ] );
+ }
+ }
+ element.className = newClasses.join(' ');
+ },
+
+ /**
+ * Convert passed addEventListener() options to boolean or options object,
+ * depending on browser support.
+ * @function
+ * @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object
+ * @param {Boolean} [options.capture]
+ * @param {Boolean} [options.passive]
+ * @param {Boolean} [options.once]
+ * @returns {String} The protocol (http:, https:, file:, ftp: ...)
+ */
+ normalizeEventListenerOptions: function (options) {
+ let opts;
+ if ( typeof options !== 'undefined' ) {
+ if ( typeof options === 'boolean' ) {
+ // Legacy Boolean useCapture
+ opts = $.supportsEventListenerOptions ? { capture: options } : options;
+ } else {
+ // Options object
+ opts = $.supportsEventListenerOptions ? options :
+ ( ( typeof options.capture !== 'undefined' ) ? options.capture : false );
+ }
+ } else {
+ // No options specified - Legacy optional useCapture argument
+ // (for IE, first supported on version 9, so we'll pass a Boolean)
+ opts = $.supportsEventListenerOptions ? { capture: false } : false;
+ }
+ return opts;
+ },
+
+ /**
+ * Adds an event listener for the given element, eventName and handler.
+ * @function
+ * @param {Element|String} element
+ * @param {String} eventName
+ * @param {Function} handler
+ * @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object
+ * @param {Boolean} [options.capture]
+ * @param {Boolean} [options.passive]
+ * @param {Boolean} [options.once]
+ */
+ addEvent: (function () {
+ if ( $.supportsAddEventListener ) {
+ return function ( element, eventName, handler, options ) {
+ options = $.normalizeEventListenerOptions(options);
+ element = $.getElement( element );
+ element.addEventListener( eventName, handler, options );
+ };
+ } else if ( document.documentElement.attachEvent && document.attachEvent ) {
+ return function ( element, eventName, handler ) {
+ element = $.getElement( element );
+ element.attachEvent( 'on' + eventName, handler );
+ };
+ } else {
+ throw new Error( "No known event model." );
+ }
+ }()),
+
+
+ /**
+ * Remove a given event listener for the given element, event type and
+ * handler.
+ * @function
+ * @param {Element|String} element
+ * @param {String} eventName
+ * @param {Function} handler
+ * @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object
+ * @param {Boolean} [options.capture]
+ */
+ removeEvent: (function () {
+ if ( $.supportsRemoveEventListener ) {
+ return function ( element, eventName, handler, options ) {
+ options = $.normalizeEventListenerOptions(options);
+ element = $.getElement( element );
+ element.removeEventListener( eventName, handler, options );
+ };
+ } else if ( document.documentElement.detachEvent && document.detachEvent ) {
+ return function( element, eventName, handler ) {
+ element = $.getElement( element );
+ element.detachEvent( 'on' + eventName, handler );
+ };
+ } else {
+ throw new Error( "No known event model." );
+ }
+ }()),
+
+
+ /**
+ * Cancels the default browser behavior had the event propagated all
+ * the way up the DOM to the window object.
+ * @function
+ * @param {Event} [event]
+ */
+ cancelEvent: function( event ) {
+ event.preventDefault();
+ },
+
+
+ /**
+ * Returns true if {@link OpenSeadragon.cancelEvent|cancelEvent} has been called on
+ * the event, otherwise returns false.
+ * @function
+ * @param {Event} [event]
+ */
+ eventIsCanceled: function( event ) {
+ return event.defaultPrevented;
+ },
+
+
+ /**
+ * Stops the propagation of the event through the DOM in the capturing and bubbling phases.
+ * @function
+ * @param {Event} [event]
+ */
+ stopEvent: function( event ) {
+ event.stopPropagation();
+ },
+
+
+ /**
+ * Retrieves the value of a url parameter from the window.location string.
+ * @function
+ * @param {String} key
+ * @returns {String} The value of the url parameter or null if no param matches.
+ */
+ getUrlParameter: function( key ) {
+ // eslint-disable-next-line no-use-before-define
+ const value = URLPARAMS[ key ];
+ return value ? value : null;
+ },
+
+ /**
+ * Retrieves the protocol used by the url. The url can either be absolute
+ * or relative.
+ * @function
+ * @private
+ * @param {String} url The url to retrieve the protocol from.
+ * @returns {String} The protocol (http:, https:, file:, ftp: ...)
+ */
+ getUrlProtocol: function( url ) {
+ const match = url.match(/^([a-z]+:)\/\//i);
+ if ( match === null ) {
+ // Relative URL, retrive the protocol from window.location
+ return window.location.protocol;
+ }
+ return match[1].toLowerCase();
+ },
+
+ /**
+ * Create an XHR object
+ * @private
+ * @param {type} [local] Deprecated. Ignored (IE/ActiveXObject file protocol no longer supported).
+ * @returns {XMLHttpRequest}
+ */
+ createAjaxRequest: function() {
+ if ( window.XMLHttpRequest ) {
+ $.createAjaxRequest = function() {
+ return new XMLHttpRequest();
+ };
+ return new XMLHttpRequest();
+ } else {
+ throw new Error( "Browser doesn't support XMLHttpRequest." );
+ }
+ },
+
+ /**
+ * Makes an AJAX request.
+ * @param {String} url - the url to request
+ * @param {Function} onSuccess
+ * @param {Function} onError
+ * @throws {Error}
+ * @returns {XMLHttpRequest}
+ * @deprecated deprecated way of calling this function
+ *//**
+ * Makes an AJAX request.
+ * @param {Object} options
+ * @param {String} options.url - the url to request
+ * @param {Function} options.success - a function to call on a successful response
+ * @param {Function} options.error - a function to call on when an error occurs
+ * @param {Object} options.headers - headers to add to the AJAX request
+ * @param {String} options.responseType - the response type of the AJAX request
+ * @param {String} options.postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
+ * see TileSource::getTilePostData), GET method used if null
+ * @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials
+ * @throws {Error}
+ * @returns {XMLHttpRequest}
+ */
+ makeAjaxRequest: function( url, onSuccess, onError ) {
+ let withCredentials;
+ let headers;
+ let responseType;
+ let postData;
+
+ // Note that our preferred API is that you pass in a single object; the named
+ // arguments are for legacy support.
+ if( $.isPlainObject( url ) ){
+ onSuccess = url.success;
+ onError = url.error;
+ withCredentials = url.withCredentials;
+ headers = url.headers;
+ responseType = url.responseType || null;
+ postData = url.postData || null;
+ url = url.url;
+ } else {
+ $.console.warn("OpenSeadragon.makeAjaxRequest() deprecated usage!");
+ }
+
+ const protocol = $.getUrlProtocol( url );
+ const request = $.createAjaxRequest();
+
+ if ( !$.isFunction( onSuccess ) ) {
+ throw new Error( "makeAjaxRequest requires a success callback" );
+ }
+
+ request.onreadystatechange = function() {
+ // 4 = DONE (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#Properties)
+ if ( request.readyState === 4 ) {
+ request.onreadystatechange = function(){};
+
+ // With protocols other than http/https, a successful request status is in
+ // the 200's on Firefox and 0 on other browsers
+ if ( (request.status >= 200 && request.status < 300) ||
+ ( request.status === 0 &&
+ protocol !== "http:" &&
+ protocol !== "https:" )) {
+ onSuccess( request );
+ } else {
+ if ( $.isFunction( onError ) ) {
+ onError( request );
+ } else {
+ $.console.error( "AJAX request returned %d: %s", request.status, url );
+ }
+ }
+ }
+ };
+
+ const method = postData ? "POST" : "GET";
+ try {
+ request.open( method, url, true );
+
+ if (responseType) {
+ request.responseType = responseType;
+ }
+
+ if (headers) {
+ for (const headerName in headers) {
+ if (Object.prototype.hasOwnProperty.call(headers, headerName) && headers[headerName]) {
+ request.setRequestHeader(headerName, headers[headerName]);
+ }
+ }
+ }
+
+ if (withCredentials) {
+ request.withCredentials = true;
+ }
+
+ request.send(postData);
+ } catch (e) {
+ $.console.error( "%s while making AJAX request: %s", e.name, e.message );
+
+ request.onreadystatechange = function(){};
+
+ if ( $.isFunction( onError ) ) {
+ onError( request, e );
+ }
+ }
+
+ return request;
+ },
+
+ /**
+ * Taken from jQuery 1.6.1
+ * @function
+ * @param {Object} options
+ * @param {String} options.url
+ * @param {Function} options.callback
+ * @param {String} [options.param='callback'] The name of the url parameter
+ * to request the jsonp provider with.
+ * @param {String} [options.callbackName=] The name of the callback to
+ * request the jsonp provider with.
+ */
+ jsonp: function( options ){
+ let script;
+ let url = options.url;
+ const head = document.head ||
+ document.getElementsByTagName( "head" )[ 0 ] ||
+ document.documentElement;
+ const jsonpCallback = options.callbackName || 'openseadragon' + $.now();
+ const previous = window[ jsonpCallback ];
+ const replace = "$1" + jsonpCallback + "$2";
+ const callbackParam = options.param || 'callback';
+ const callback = options.callback;
+
+ url = url.replace( /(=)\?(&|$)|\?\?/i, replace );
+ // Add callback manually
+ url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback;
+
+ // Install callback
+ window[ jsonpCallback ] = function( response ) {
+ if ( !previous ){
+ try{
+ delete window[ jsonpCallback ];
+ }catch(e){
+ //swallow
+ }
+ } else {
+ window[ jsonpCallback ] = previous;
+ }
+ if( callback && $.isFunction( callback ) ){
+ callback( response );
+ }
+ };
+
+ script = document.createElement( "script" );
+
+ //TODO: having an issue with async info requests
+ if( undefined !== options.async || false !== options.async ){
+ script.async = "async";
+ }
+
+ if ( options.scriptCharset ) {
+ script.charset = options.scriptCharset;
+ }
+
+ script.src = url;
+
+ // Attach handlers for all browsers
+ script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+ if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+ // Handle memory leak in IE
+ script.onload = script.onreadystatechange = null;
+
+ // Remove the script
+ if ( head && script.parentNode ) {
+ head.removeChild( script );
+ }
+
+ // Dereference the script
+ script = undefined;
+ }
+ };
+ // Use insertBefore instead of appendChild to circumvent an IE6 bug.
+ // This arises when a base node is used (#2709 and #4378).
+ head.insertBefore( script, head.firstChild );
+
+ },
+
+
+ /**
+ * Fully deprecated. Will throw an error.
+ * @function
+ * @deprecated use {@link OpenSeadragon.Viewer#open}
+ */
+ createFromDZI: function() {
+ throw "OpenSeadragon.createFromDZI is deprecated, use Viewer.open.";
+ },
+
+ /**
+ * Parses an XML string into a DOM Document.
+ * @function
+ * @param {String} string
+ * @returns {Document}
+ */
+ parseXml: function( string ) {
+ if ( window.DOMParser ) {
+
+ $.parseXml = function( string ) {
+ let xmlDoc = null;
+
+ const parser = new DOMParser();
+ xmlDoc = parser.parseFromString( string, "text/xml" );
+ return xmlDoc;
+ };
+
+ } else {
+ throw new Error( "Browser doesn't support XML DOM." );
+ }
+
+ return $.parseXml( string );
+ },
+
+ /**
+ * Parses a JSON string into a Javascript object.
+ * @function
+ * @param {String} string
+ * @returns {Object}
+ */
+ parseJSON: function(string) {
+ $.parseJSON = window.JSON.parse;
+ return $.parseJSON(string);
+ },
+
+ /**
+ * Reports whether the image format is supported for tiling in this
+ * version.
+ * @function
+ * @param {String} [extension]
+ * @returns {Boolean}
+ */
+ imageFormatSupported: function( extension ) {
+ extension = extension ? extension : "";
+ // eslint-disable-next-line no-use-before-define
+ return !!FILEFORMATS[ extension.toLowerCase() ];
+ },
+
+ /**
+ * Updates supported image formats with user-specified values.
+ * Preexisting formats that are not being updated are left unchanged.
+ * By default, the defined formats are
+ *
+ * @function
+ * @example
+ * // sets bmp as supported and png as unsupported
+ * setImageFormatsSupported({bmp: true, png: false});
+ * @param {Object} formats An object containing format extensions as
+ * keys and booleans as values.
+ */
+ setImageFormatsSupported: function(formats) {
+ //TODO: how to deal with this within the data pipeline?
+ // $.console.warn("setImageFormatsSupported method is deprecated. You should check that" +
+ // " the system supports your TileSources by implementing corresponding data type converters.");
+
+ // eslint-disable-next-line no-use-before-define
+ $.extend(FILEFORMATS, formats);
+ },
+ });
+
+
+ //TODO: $.console is often used inside a try/catch block which generally
+ // prevents allowings errors to occur with detection until a debugger
+ // is attached. Although I've been guilty of the same anti-pattern
+ // I eventually was convinced that errors should naturally propagate in
+ // all but the most special cases.
+ /**
+ * A convenient alias for console when available, and a simple null
+ * function when console is unavailable.
+ * @static
+ * @private
+ */
+ const nullfunction = function( msg ){
+ //document.location.hash = msg;
+ };
+
+ $.console = window.console || {
+ log: nullfunction,
+ debug: nullfunction,
+ info: nullfunction,
+ warn: nullfunction,
+ error: nullfunction,
+ assert: nullfunction
+ };
+
+
+ /**
+ * The current browser vendor, version, and related information regarding detected features.
+ * @member {Object} Browser
+ * @memberof OpenSeadragon
+ * @static
+ * @type {Object}
+ * @property {OpenSeadragon.BROWSERS} vendor - One of the {@link OpenSeadragon.BROWSERS} enumeration values.
+ * @property {Number} version
+ * @property {Boolean} alpha - Does the browser support image alpha transparency.
+ */
+ $.Browser = {
+ vendor: $.BROWSERS.UNKNOWN,
+ version: 0,
+ alpha: true
+ };
+
+
+ const FILEFORMATS = {
+ avif: true,
+ bmp: false,
+ jpeg: true,
+ jpg: true,
+ png: true,
+ tif: false,
+ wdp: false,
+ webp: true
+ };
+ const URLPARAMS = {};
+
+ (function() {
+ //A small auto-executing routine to determine the browser vendor,
+ //version and supporting feature sets.
+ const ver = navigator.appVersion;
+ const ua = navigator.userAgent;
+ let regex;
+
+ //console.error( 'appName: ' + navigator.appName );
+ //console.error( 'appVersion: ' + navigator.appVersion );
+ //console.error( 'userAgent: ' + navigator.userAgent );
+
+ //TODO navigator.appName is deprecated. Should be 'Netscape' for all browsers
+ // but could be dropped at any time
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/appName
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
+ switch( navigator.appName ){
+ case "Microsoft Internet Explorer":
+ if( !!window.attachEvent &&
+ !!window.ActiveXObject ) {
+
+ $.Browser.vendor = $.BROWSERS.IE;
+ $.Browser.version = parseFloat(
+ ua.substring(
+ ua.indexOf( "MSIE" ) + 5,
+ ua.indexOf( ";", ua.indexOf( "MSIE" ) ) )
+ );
+ }
+ break;
+ case "Netscape":
+ if (window.addEventListener) {
+ if ( ua.indexOf( "Edge" ) >= 0 ) {
+ $.Browser.vendor = $.BROWSERS.EDGE;
+ $.Browser.version = parseFloat(
+ ua.substring( ua.indexOf( "Edge" ) + 5 )
+ );
+ } else if ( ua.indexOf( "Edg" ) >= 0 ) {
+ $.Browser.vendor = $.BROWSERS.CHROMEEDGE;
+ $.Browser.version = parseFloat(
+ ua.substring( ua.indexOf( "Edg" ) + 4 )
+ );
+ } else if ( ua.indexOf( "Firefox" ) >= 0 ) {
+ $.Browser.vendor = $.BROWSERS.FIREFOX;
+ $.Browser.version = parseFloat(
+ ua.substring( ua.indexOf( "Firefox" ) + 8 )
+ );
+ } else if ( ua.indexOf( "Safari" ) >= 0 ) {
+ $.Browser.vendor = ua.indexOf( "Chrome" ) >= 0 ?
+ $.BROWSERS.CHROME :
+ $.BROWSERS.SAFARI;
+ $.Browser.version = parseFloat(
+ ua.substring(
+ ua.substring( 0, ua.indexOf( "Safari" ) ).lastIndexOf( "/" ) + 1,
+ ua.indexOf( "Safari" )
+ )
+ );
+ } else {
+ regex = new RegExp( "Trident/.*rv:([0-9]{1,}[.0-9]{0,})");
+ if ( regex.exec( ua ) !== null ) {
+ $.Browser.vendor = $.BROWSERS.IE;
+ $.Browser.version = parseFloat( RegExp.$1 );
+ }
+ }
+ }
+ break;
+ case "Opera":
+ $.Browser.vendor = $.BROWSERS.OPERA;
+ $.Browser.version = parseFloat( ver );
+ break;
+ }
+
+ // ignore '?' portion of query string
+ const query = window.location.search.substring( 1 );
+ const parts = query.split('&');
+
+ for ( let i = 0; i < parts.length; i++ ) {
+ const part = parts[ i ];
+ const sep = part.indexOf( '=' );
+
+ if ( sep > 0 ) {
+ const key = part.substring( 0, sep );
+ const value = part.substring( sep + 1 );
+ try {
+ URLPARAMS[ key ] = decodeURIComponent( value );
+ } catch (e) {
+ $.console.error( "Ignoring malformed URL parameter: %s=%s", key, value );
+ }
+ }
+ }
+
+ //determine if this browser supports image alpha transparency
+ $.Browser.alpha = !(
+ $.Browser.vendor === $.BROWSERS.CHROME && $.Browser.version < 2
+ );
+
+ //determine if this browser supports element.style.opacity
+ $.Browser.opacity = true;
+
+ if ( $.Browser.vendor === $.BROWSERS.IE ) {
+ $.console.error('Internet Explorer is not supported by OpenSeadragon');
+ }
+ })();
+
+
+ // Adding support for HTML5's requestAnimationFrame as suggested by acdha.
+ // Implementation taken from matt synder's post here:
+ // http://mattsnider.com/cross-browser-and-legacy-supported-requestframeanimation/
+ (function( w ) {
+
+ // most browsers have an implementation
+ const requestAnimationFrame = w.requestAnimationFrame ||
+ w.mozRequestAnimationFrame ||
+ w.webkitRequestAnimationFrame ||
+ w.msRequestAnimationFrame;
+
+ const cancelAnimationFrame = w.cancelAnimationFrame ||
+ w.mozCancelAnimationFrame ||
+ w.webkitCancelAnimationFrame ||
+ w.msCancelAnimationFrame;
+
+ // polyfill, when necessary
+ if ( requestAnimationFrame && cancelAnimationFrame ) {
+ // We can't assign these window methods directly to $ because they
+ // expect their "this" to be "window", so we call them in wrappers.
+ $.requestAnimationFrame = function(){
+ return requestAnimationFrame.apply( w, arguments );
+ };
+ $.cancelAnimationFrame = function(){
+ return cancelAnimationFrame.apply( w, arguments );
+ };
+ } else {
+ let aAnimQueue = [];
+ let processing = [];
+ let iIntervalId;
+ let iRequestId = 0;
+
+ // create a mock requestAnimationFrame function
+ $.requestAnimationFrame = function( callback ) {
+ aAnimQueue.push( [ ++iRequestId, callback ] );
+
+ if ( !iIntervalId ) {
+ iIntervalId = setInterval( function() {
+ if ( aAnimQueue.length ) {
+ const time = $.now();
+ // Process all of the currently outstanding frame
+ // requests, but none that get added during the
+ // processing.
+ // Swap the arrays so we don't have to create a new
+ // array every frame.
+ const temp = processing;
+ processing = aAnimQueue;
+ aAnimQueue = temp;
+ while ( processing.length ) {
+ processing.shift()[ 1 ]( time );
+ }
+ } else {
+ // don't continue the interval, if unnecessary
+ clearInterval( iIntervalId );
+ iIntervalId = undefined;
+ }
+ }, 1000 / 50); // estimating support for 50 frames per second
+ }
+
+ return iRequestId;
+ };
+
+ // create a mock cancelAnimationFrame function
+ $.cancelAnimationFrame = function( requestId ) {
+ // find the request ID and remove it
+ let i, j;
+ for ( i = 0, j = aAnimQueue.length; i < j; i += 1 ) {
+ if ( aAnimQueue[ i ][ 0 ] === requestId ) {
+ aAnimQueue.splice( i, 1 );
+ return;
+ }
+ }
+
+ // If it's not in the queue, it may be in the set we're currently
+ // processing (if cancelAnimationFrame is called from within a
+ // requestAnimationFrame callback).
+ for ( i = 0, j = processing.length; i < j; i += 1 ) {
+ if ( processing[ i ][ 0 ] === requestId ) {
+ processing.splice( i, 1 );
+ return;
+ }
+ }
+ };
+ }
+ })( window );
+
+ /**
+ * @private
+ * @inner
+ * @function
+ * @param {Element} element
+ * @param {Boolean} [isFixed]
+ * @returns {Element}
+ */
+ function getOffsetParent( element, isFixed ) {
+ if ( isFixed && element !== document.body ) {
+ return document.body;
+ } else {
+ return element.offsetParent;
+ }
+ }
+
+ /**
+ * @template T
+ * @typedef {function(): OpenSeadragon.Promise} AsyncNullaryFunction
+ * Represents an asynchronous function that takes no arguments and returns a promise of type T.
+ */
+
+ /**
+ * @template T, A
+ * @typedef {function(A): OpenSeadragon.Promise} AsyncUnaryFunction
+ * Represents an asynchronous function that:
+ * @param {A} arg - The single argument of type A.
+ * @returns {OpenSeadragon.Promise} A promise that resolves to a value of type T.
+ */
+
+ /**
+ * @template T, A, B
+ * @typedef {function(A, B): OpenSeadragon.Promise} AsyncBinaryFunction
+ * Represents an asynchronous function that:
+ * @param {A} arg1 - The first argument of type A.
+ * @param {B} arg2 - The second argument of type B.
+ * @returns {OpenSeadragon.Promise} A promise that resolves to a value of type T.
+ */
+
+ /**
+ * Promise proxy in OpenSeadragon, enables $.supportsAsync feature.
+ * This proxy is also necessary because OperaMini does not implement Promises (checks fail).
+ * @type {PromiseConstructor}
+ */
+ $.Promise = window["Promise"] && $.supportsAsync ? window["Promise"] : class {
+ constructor(handler) {
+ this._error = false;
+ this.__value = undefined;
+
+ try {
+ // Make sure to unwrap all nested promises!
+ handler(
+ (value) => {
+ while (value instanceof $.Promise) {
+ value = value._value;
+ }
+ this._value = value;
+ },
+ (error) => {
+ while (error instanceof $.Promise) {
+ error = error._value;
+ }
+ this._value = error;
+ this._error = true;
+ }
+ );
+ } catch (e) {
+ this._value = e;
+ this._error = true;
+ }
+ }
+
+ then(handler) {
+ if (!this._error) {
+ try {
+ this._value = handler(this._value);
+ } catch (e) {
+ this._value = e;
+ this._error = true;
+ }
+ }
+ return this;
+ }
+
+ catch(handler) {
+ if (this._error) {
+ try {
+ this._value = handler(this._value);
+ this._error = false;
+ } catch (e) {
+ this._value = e;
+ this._error = true;
+ }
+ }
+ return this;
+ }
+
+ get _value() {
+ return this.__value;
+ }
+ set _value(val) {
+ if (val && val.constructor === this.constructor) {
+ val = val._value; //unwrap
+ }
+ this.__value = val;
+ }
+
+ static resolve(value) {
+ return new this((resolve) => resolve(value));
+ }
+
+ static reject(error) {
+ return new this((_, reject) => reject(error));
+ }
+
+ static all(functions) {
+ return new this((resolve) => {
+ // no async support, just execute them
+ return resolve(functions.map(fn => fn()));
+ });
+ }
+
+ static race(functions) {
+ if (functions.length < 1) {
+ return this.resolve();
+ }
+ // no async support, just execute the first
+ return new this((resolve) => {
+ return resolve(functions[0]());
+ });
+ }
+ };
+}(OpenSeadragon));
+
+
+// Universal Module Definition, supports CommonJS, AMD and simple script tag
+(function (root, $) {
+ if (typeof define === 'function' && define.amd) {
+ // expose as amd module
+ define([], function () {
+ return $;
+ });
+ } else if (typeof module === 'object' && module.exports) {
+ // expose as commonjs module
+ module.exports = $;
+ } else {
+ if (!root) {
+ root = typeof window === 'object' && window;
+ if (!root) {
+ $.console.error("OpenSeadragon must run in browser environment!");
+ }
+ }
+ // expose as window.OpenSeadragon
+ root.OpenSeadragon = $;
+ }
+}(this, OpenSeadragon));
+
+/* eslint-disable one-var-declaration-per-line */
+
+/*
+ * OpenSeadragon - Mat3
+ *
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+
+/*
+ * Portions of this source file are taken from WegGL Fundamentals:
+ *
+ * Copyright 2012, Gregg Tavares.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Gregg Tavares. nor the names of his
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+
+
+
+(function( $ ){
+
+// Modified from https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
+
+/**
+ *
+ *
+ * @class Mat3
+ * @classdesc A left-to-right matrix representation, useful for affine transforms for
+ * positioning tiles for drawing
+ *
+ * @memberof OpenSeadragon
+ *
+ * @param {Array} [values] - Initial values for the matrix
+ *
+ **/
+class Mat3{
+ constructor(values){
+ if(!values) {
+ values = [
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0
+ ];
+ }
+ this.values = values;
+ }
+
+ /**
+ * @function makeIdentity
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @returns {OpenSeadragon.Mat3} an identity matrix
+ */
+ static makeIdentity(){
+ return new Mat3([
+ 1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 1
+ ]);
+ }
+
+ /**
+ * @function makeTranslation
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @param {Number} tx The x value of the translation
+ * @param {Number} ty The y value of the translation
+ * @returns {OpenSeadragon.Mat3} A translation matrix
+ */
+ static makeTranslation(tx, ty) {
+ return new Mat3([
+ 1, 0, 0,
+ 0, 1, 0,
+ tx, ty, 1,
+ ]);
+ }
+
+ /**
+ * @function makeRotation
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @param {Number} angleInRadians The desired rotation angle, in radians
+ * @returns {OpenSeadragon.Mat3} A rotation matrix
+ */
+ static makeRotation(angleInRadians) {
+ const c = Math.cos(angleInRadians);
+ const s = Math.sin(angleInRadians);
+ return new Mat3([
+ c, -s, 0,
+ s, c, 0,
+ 0, 0, 1,
+ ]);
+ }
+
+ /**
+ * @function makeScaling
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @param {Number} sx The x value of the scaling
+ * @param {Number} sy The y value of the scaling
+ * @returns {OpenSeadragon.Mat3} A scaling matrix
+ */
+ static makeScaling(sx, sy) {
+ return new Mat3([
+ sx, 0, 0,
+ 0, sy, 0,
+ 0, 0, 1,
+ ]);
+ }
+
+ /**
+ * @alias multiply
+ * @memberof! OpenSeadragon.Mat3
+ * @param {OpenSeadragon.Mat3} other the matrix to multiply with
+ * @returns {OpenSeadragon.Mat3} The result of matrix multiplication
+ */
+ multiply(other) {
+ let a = this.values;
+ let b = other.values;
+
+ const a00 = a[0 * 3 + 0], a01 = a[0 * 3 + 1], a02 = a[0 * 3 + 2];
+ const a10 = a[1 * 3 + 0], a11 = a[1 * 3 + 1], a12 = a[1 * 3 + 2];
+ const a20 = a[2 * 3 + 0], a21 = a[2 * 3 + 1], a22 = a[2 * 3 + 2];
+ const b00 = b[0 * 3 + 0], b01 = b[0 * 3 + 1], b02 = b[0 * 3 + 2];
+ const b10 = b[1 * 3 + 0], b11 = b[1 * 3 + 1], b12 = b[1 * 3 + 2];
+ const b20 = b[2 * 3 + 0], b21 = b[2 * 3 + 1], b22 = b[2 * 3 + 2];
+
+ return new Mat3([
+ b00 * a00 + b01 * a10 + b02 * a20,
+ b00 * a01 + b01 * a11 + b02 * a21,
+ b00 * a02 + b01 * a12 + b02 * a22,
+ b10 * a00 + b11 * a10 + b12 * a20,
+ b10 * a01 + b11 * a11 + b12 * a21,
+ b10 * a02 + b11 * a12 + b12 * a22,
+ b20 * a00 + b21 * a10 + b22 * a20,
+ b20 * a01 + b21 * a11 + b22 * a21,
+ b20 * a02 + b21 * a12 + b22 * a22,
+ ]);
+ }
+
+ /**
+ * Sets the values of the matrix.
+ * @param a00 top left
+ * @param a01 top middle
+ * @param a02 top right
+ * @param a10 middle left
+ * @param a11 middle middle
+ * @param a12 middle right
+ * @param a20 bottom left
+ * @param a21 bottom middle
+ * @param a22 bottom right
+ */
+ setValues(a00, a01, a02,
+ a10, a11, a12,
+ a20, a21, a22) {
+ this.values[0] = a00;
+ this.values[1] = a01;
+ this.values[2] = a02;
+ this.values[3] = a10;
+ this.values[4] = a11;
+ this.values[5] = a12;
+ this.values[6] = a20;
+ this.values[7] = a21;
+ this.values[8] = a22;
+ }
+
+ /**
+ * Scaling & translation only changes certain values, no need to compute full matrix multiplication.
+ * @memberof OpenSeadragon.Mat3
+ * @returns {OpenSeadragon.Mat3} The result of matrix multiplication
+ */
+ scaleAndTranslate(sx, sy, tx, ty) {
+ const a = this.values;
+ const a00 = a[0];
+ const a01 = a[1];
+ const a02 = a[2];
+ const a10 = a[3];
+ const a11 = a[4];
+ const a12 = a[5];
+ return new Mat3([
+ sx * a00,
+ sx * a01,
+ sx * a02,
+ sy * a10,
+ sy * a11,
+ sy * a12,
+ tx * a00 + ty * a10,
+ tx * a01 + ty * a11,
+ tx * a02 + ty * a12,
+ ]);
+ }
+
+ /**
+ * Scaling & translation only changes certain values, no need to compute full matrix multiplication.
+ * Optimization: in case the original matrix can be thrown away, optimize instead by computing in-place.
+ * @memberof OpenSeadragon.Mat3
+ */
+ scaleAndTranslateSelf(sx, sy, tx, ty) {
+ const a = this.values;
+
+ const m00 = a[0], m01 = a[1], m02 = a[2];
+ const m10 = a[3], m11 = a[4], m12 = a[5];
+
+ a[0] = sx * m00;
+ a[1] = sx * m01;
+ a[2] = sx * m02;
+
+ a[3] = sy * m10;
+ a[4] = sy * m11;
+ a[5] = sy * m12;
+
+ a[6] = tx * m00 + ty * m10 + a[6];
+ a[7] = tx * m01 + ty * m11 + a[7];
+ a[8] = tx * m02 + ty * m12 + a[8];
+ }
+
+ /**
+ * Move and translate another matrix by self. 'this' matrix must be scale & translate matrix.
+ * Optimization: in case the original matrix can be thrown away, optimize instead by computing in-place.
+ * Used for optimization: we have
+ * A) THIS matrix, carrying scale and translation,
+ * B) OTHER general matrix to scale and translate.
+ * Since THIS matrix is unique per tile, we can optimize the operation by:
+ * - move & scale OTHER by THIS, and
+ * - store the result to THIS, since we don't need to keep the scaling and translation, but
+ * we need to keep the original OTHER matrix (for each tile within tiled image).
+ * @param {OpenSeadragon.Mat3} other the matrix to scale and translate by this matrix and accept values from
+ * @memberof OpenSeadragon.Mat3
+ */
+ scaleAndTranslateOtherSetSelf(other) {
+ const a = other.values;
+ const out = this.values;
+
+ // Read scale and translation values from 'this'
+ const sx = out[0]; // scale X (this[0])
+ const sy = out[4]; // scale Y (this[4])
+ const tx = out[6]; // translate X
+ const ty = out[7]; // translate Y
+
+ // Compute result = this * other, store into this.values (in-place)
+ out[0] = sx * a[0];
+ out[1] = sx * a[1];
+ out[2] = sx * a[2];
+
+ out[3] = sy * a[3];
+ out[4] = sy * a[4];
+ out[5] = sy * a[5];
+
+ out[6] = tx * a[0] + ty * a[3] + a[6];
+ out[7] = tx * a[1] + ty * a[4] + a[7];
+ out[8] = tx * a[2] + ty * a[5] + a[8];
+ }
+}
+
+
+$.Mat3 = Mat3;
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - full-screen support functions
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function( $ ) {
+ /**
+ * Determine native full screen support we can get from the browser.
+ * @member fullScreenApi
+ * @memberof OpenSeadragon
+ * @type {object}
+ * @property {Boolean} supportsFullScreen Return true if full screen API is supported.
+ * @property {Function} isFullScreen Return true if currently in full screen mode.
+ * @property {Function} getFullScreenElement Return the element currently in full screen mode.
+ * @property {Function} requestFullScreen Make a request to go in full screen mode.
+ * @property {Function} exitFullScreen Make a request to exit full screen mode.
+ * @property {Function} cancelFullScreen Deprecated, use exitFullScreen instead.
+ * @property {String} fullScreenEventName Event fired when the full screen mode change.
+ * @property {String} fullScreenErrorEventName Event fired when a request to go
+ * in full screen mode failed.
+ */
+ const fullScreenApi = {
+ supportsFullScreen: false,
+ isFullScreen: function() { return false; },
+ getFullScreenElement: function() { return null; },
+ requestFullScreen: function() {},
+ exitFullScreen: function() {},
+ cancelFullScreen: function() {},
+ fullScreenEventName: '',
+ fullScreenErrorEventName: ''
+ };
+
+ // check for native support
+ if ( document.exitFullscreen ) {
+ // W3C standard
+ fullScreenApi.supportsFullScreen = true;
+ fullScreenApi.getFullScreenElement = function() {
+ return document.fullscreenElement;
+ };
+ fullScreenApi.requestFullScreen = function( element ) {
+ return element.requestFullscreen().catch(function (msg) {
+ $.console.error('Fullscreen request failed: ', msg);
+ });
+ };
+ fullScreenApi.exitFullScreen = function() {
+ document.exitFullscreen().catch(function (msg) {
+ $.console.error('Error while exiting fullscreen: ', msg);
+ });
+ };
+ fullScreenApi.fullScreenEventName = "fullscreenchange";
+ fullScreenApi.fullScreenErrorEventName = "fullscreenerror";
+ } else if ( document.msExitFullscreen ) {
+ // IE 11
+ fullScreenApi.supportsFullScreen = true;
+ fullScreenApi.getFullScreenElement = function() {
+ return document.msFullscreenElement;
+ };
+ fullScreenApi.requestFullScreen = function( element ) {
+ return element.msRequestFullscreen();
+ };
+ fullScreenApi.exitFullScreen = function() {
+ document.msExitFullscreen();
+ };
+ fullScreenApi.fullScreenEventName = "MSFullscreenChange";
+ fullScreenApi.fullScreenErrorEventName = "MSFullscreenError";
+ } else if ( document.webkitExitFullscreen ) {
+ // Recent webkit
+ fullScreenApi.supportsFullScreen = true;
+ fullScreenApi.getFullScreenElement = function() {
+ return document.webkitFullscreenElement;
+ };
+ fullScreenApi.requestFullScreen = function( element ) {
+ return element.webkitRequestFullscreen();
+ };
+ fullScreenApi.exitFullScreen = function() {
+ document.webkitExitFullscreen();
+ };
+ fullScreenApi.fullScreenEventName = "webkitfullscreenchange";
+ fullScreenApi.fullScreenErrorEventName = "webkitfullscreenerror";
+ } else if ( document.webkitCancelFullScreen ) {
+ // Old webkit
+ fullScreenApi.supportsFullScreen = true;
+ fullScreenApi.getFullScreenElement = function() {
+ return document.webkitCurrentFullScreenElement;
+ };
+ fullScreenApi.requestFullScreen = function( element ) {
+ return element.webkitRequestFullScreen();
+ };
+ fullScreenApi.exitFullScreen = function() {
+ document.webkitCancelFullScreen();
+ };
+ fullScreenApi.fullScreenEventName = "webkitfullscreenchange";
+ fullScreenApi.fullScreenErrorEventName = "webkitfullscreenerror";
+ } else if ( document.mozCancelFullScreen ) {
+ // Firefox
+ fullScreenApi.supportsFullScreen = true;
+ fullScreenApi.getFullScreenElement = function() {
+ return document.mozFullScreenElement;
+ };
+ fullScreenApi.requestFullScreen = function( element ) {
+ return element.mozRequestFullScreen();
+ };
+ fullScreenApi.exitFullScreen = function() {
+ document.mozCancelFullScreen();
+ };
+ fullScreenApi.fullScreenEventName = "mozfullscreenchange";
+ fullScreenApi.fullScreenErrorEventName = "mozfullscreenerror";
+ }
+ fullScreenApi.isFullScreen = function() {
+ return fullScreenApi.getFullScreenElement() !== null;
+ };
+ fullScreenApi.cancelFullScreen = function() {
+ $.console.error("cancelFullScreen is deprecated. Use exitFullScreen instead.");
+ fullScreenApi.exitFullScreen();
+ };
+
+ // export api
+ $.extend( $, fullScreenApi );
+
+})( OpenSeadragon );
+
+/*
+ * OpenSeadragon - EventSource
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function($){
+
+/**
+ * @typedef {Object} OpenSeadragon.Event
+ * @memberof OpenSeadragon
+ * @property {boolean|function} [stopPropagation=undefined] - If set to true or the functional predicate returns true,
+ * the event exits after handling the current call.
+ */
+
+/**
+ * Event handler method signature used by all OpenSeadragon events.
+ *
+ * @typedef {function(OpenSeadragon.Event): void} OpenSeadragon.EventHandler
+ * @memberof OpenSeadragon
+ * @param {OpenSeadragon.Event} event - The event object containing event-specific properties.
+ * @returns {void} This handler does not return a value.
+ */
+
+/**
+ * Event handler method signature used by all OpenSeadragon events.
+ *
+ * @typedef {function(OpenSeadragon.Event): Promise} OpenSeadragon.AsyncEventHandler
+ * @memberof OpenSeadragon
+ * @param {OpenSeadragon.Event} event - The event object containing event-specific properties.
+ * @returns {Promise} This handler does not return a value.
+ */
+
+
+/**
+ * @class EventSource
+ * @classdesc For use by classes which want to support custom, non-browser events.
+ *
+ * @memberof OpenSeadragon
+ */
+$.EventSource = function() {
+ this.events = {};
+ this._rejectedEventList = {};
+};
+
+/** @lends OpenSeadragon.EventSource.prototype */
+$.EventSource.prototype = {
+
+ /**
+ * Add an event handler to be triggered only once (or a given number of times)
+ * for a given event. It is not removable with removeHandler().
+ * @function
+ * @param {String} eventName - Name of event to register.
+ * @param {OpenSeadragon.EventHandler|OpenSeadragon.AsyncEventHandler} handler - Function to call when event
+ * is triggered.
+ * @param {Object} [userData=null] - Arbitrary object to be passed unchanged
+ * to the handler.
+ * @param {Number} [times=1] - The number of times to handle the event
+ * before removing it.
+ * @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
+ * @returns {Boolean} - True if the handler was added, false if it was rejected
+ */
+ addOnceHandler: function(eventName, handler, userData, times, priority) {
+ const self = this;
+ times = times || 1;
+ let count = 0;
+ const onceHandler = function(event) {
+ count++;
+ if (count === times) {
+ self.removeHandler(eventName, onceHandler);
+ }
+ return handler(event);
+ };
+ return this.addHandler(eventName, onceHandler, userData, priority);
+ },
+
+ /**
+ * Add an event handler for a given event.
+ * @function
+ * @param {String} eventName - Name of event to register.
+ * @param {OpenSeadragon.EventHandler|OpenSeadragon.AsyncEventHandler} handler - Function to call when event is triggered.
+ * @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler.
+ * @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
+ * @returns {Boolean} - True if the handler was added, false if it was rejected
+ */
+ addHandler: function ( eventName, handler, userData, priority ) {
+
+ if(Object.prototype.hasOwnProperty.call(this._rejectedEventList, eventName)){
+ $.console.error(`Error adding handler for ${eventName}. ${this._rejectedEventList[eventName]}`);
+ return false;
+ }
+
+ let events = this.events[ eventName ];
+ if ( !events ) {
+ this.events[ eventName ] = events = [];
+ }
+ if ( handler && $.isFunction( handler ) ) {
+ let index = events.length,
+ event = { handler: handler, userData: userData || null, priority: priority || 0 };
+ events[ index ] = event;
+ while ( index > 0 && events[ index - 1 ].priority < events[ index ].priority ) {
+ events[ index ] = events[ index - 1 ];
+ events[ index - 1 ] = event;
+ index--;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Remove a specific event handler for a given event.
+ * @function
+ * @param {String} eventName - Name of event for which the handler is to be removed.
+ * @param {OpenSeadragon.EventHandler|OpenSeadragon.AsyncEventHandler} handler - Function to be removed.
+ */
+ removeHandler: function ( eventName, handler ) {
+ const events = this.events[ eventName ];
+ const handlers = [];
+ if ( !events ) {
+ return;
+ }
+ if ( $.isArray( events ) ) {
+ for ( let i = 0; i < events.length; i++ ) {
+ if ( events[i].handler !== handler ) {
+ handlers.push( events[ i ] );
+ }
+ }
+ this.events[ eventName ] = handlers;
+ }
+ },
+
+ /**
+ * Get the amount of handlers registered for a given event.
+ * @param {String} eventName - Name of event to inspect.
+ * @returns {number} amount of events
+ */
+ numberOfHandlers: function (eventName) {
+ const events = this.events[ eventName ];
+ if ( !events ) {
+ return 0;
+ }
+ return events.length;
+ },
+
+ /**
+ * Remove all event handlers for a given event type. If no type is given all
+ * event handlers for every event type are removed.
+ * @function
+ * @param {String} [eventName] - Name of event for which all handlers are to be removed.
+ */
+ removeAllHandlers: function( eventName ) {
+ if ( eventName ){
+ this.events[ eventName ] = [];
+ } else{
+ for ( let eventType in this.events ) {
+ this.events[ eventType ] = [];
+ }
+ }
+ },
+
+ /**
+ * Get a function which iterates the list of all handlers registered for a given event, calling the handler for each.
+ * @function
+ * @param {String} eventName - Name of event to get handlers for.
+ */
+ getHandler: function ( eventName) {
+ let events = this.events[ eventName ];
+ if ( !events || !events.length ) {
+ return null;
+ }
+ events = events.length === 1 ?
+ [ events[ 0 ] ] :
+ Array.apply( null, events );
+ return function ( source, args ) {
+ let length = events.length;
+ for ( let i = 0; i < length; i++ ) {
+ if ( events[ i ] ) {
+ args.eventSource = source;
+ args.userData = events[ i ].userData;
+ events[ i ].handler( args );
+
+ if (args.stopPropagation && (typeof args.stopPropagation !== "function" || args.stopPropagation() === true)) {
+ break;
+ }
+ }
+ }
+ };
+ },
+
+ /**
+ * Get a function which iterates the list of all handlers registered for a given event,
+ * calling the handler for each and awaiting async ones.
+ * @function
+ * @param {String} eventName - Name of event to get handlers for.
+ * @param {any} bindTarget - Bound target to return with the promise on finish
+ */
+ getAwaitingHandler: function ( eventName, bindTarget ) {
+ let events = this.events[ eventName ];
+ if ( !events || !events.length ) {
+ return null;
+ }
+ events = events.length === 1 ?
+ [ events[ 0 ] ] :
+ Array.apply( null, events );
+
+ return function ( source, args ) {
+ // We return a promise that gets resolved after all the events finish.
+ // Returning loop result is not correct, loop promises chain dynamically
+ // and outer code could process finishing logics in the middle of event loop.
+ return new $.Promise((resolve, reject) => {
+ const length = events.length;
+ function loop(index) {
+ if ( index >= length || !events[ index ] ) {
+ resolve(bindTarget);
+ return null;
+ }
+ args.eventSource = source;
+ args.userData = events[ index ].userData;
+ let result;
+ try {
+ result = events[ index ].handler( args );
+ } catch (e) {
+ return reject(e);
+ }
+ result = (!result || $.type(result) !== "promise") ? $.Promise.resolve() : result;
+ return result.then(() => {
+ if (!args.stopPropagation || (typeof args.stopPropagation === "function" && args.stopPropagation() === false)) {
+ return loop(index + 1);
+ }
+ return loop(length);
+ });
+ }
+ loop(0).catch(reject);
+ });
+ };
+ },
+
+ /**
+ * Trigger an event, optionally passing additional information. Does not await async handlers, i.e.
+ * OpenSeadragon.AsyncEventHandler.
+ * @function
+ * @param {String} eventName - Name of event to register.
+ * @param {Object|undefined} eventArgs - Event-specific data.
+ * @returns {Boolean} True if the event was fired, false if it was rejected because of rejectEventHandler(eventName)
+ */
+ raiseEvent: function( eventName, eventArgs ) {
+ //uncomment if you want to get a log of all events
+ //$.console.log( "Event fired:", eventName );
+
+ if(Object.prototype.hasOwnProperty.call(this._rejectedEventList, eventName)){
+ $.console.error(`Error adding handler for ${eventName}. ${this._rejectedEventList[eventName]}`);
+ return false;
+ }
+
+ const handler = this.getHandler( eventName );
+ if ( handler ) {
+ handler( this, eventArgs || {} );
+ }
+ return true;
+ },
+
+ /**
+ * Trigger an event, optionally passing additional information.
+ * This events awaits every asynchronous or promise-returning function, i.e.
+ * OpenSeadragon.AsyncEventHandler.
+ * @param {String} eventName - Name of event to register.
+ * @param {Object|undefined} eventArgs - Event-specific data.
+ * @param {?} [bindTarget = null] - Promise-resolved value on the event finish
+ * @return {OpenSeadragon.Promise|undefined} - Promise resolved upon the event completion.
+ */
+ raiseEventAwaiting: function ( eventName, eventArgs, bindTarget = null ) {
+ //uncomment if you want to get a log of all events
+ //$.console.log( "Awaiting event fired:", eventName );
+
+ const awaitingHandler = this.getAwaitingHandler(eventName, bindTarget);
+ if (awaitingHandler) {
+ return awaitingHandler(this, eventArgs || {});
+ }
+ return $.Promise.resolve(bindTarget);
+ },
+
+ /**
+ * Set an event name as being disabled, and provide an optional error message
+ * to be printed to the console
+ * @param {String} eventName - Name of the event
+ * @param {String} [errorMessage] - Optional string to print to the console
+ * @private
+ */
+ rejectEventHandler(eventName, errorMessage = ''){
+ this._rejectedEventList[eventName] = errorMessage;
+ },
+
+ /**
+ * Explicitly allow an event handler to be added for this event type, undoing
+ * the effects of rejectEventHandler
+ * @param {String} eventName - Name of the event
+ * @private
+ */
+ allowEventHandler(eventName){
+ delete this._rejectedEventList[eventName];
+ }
+};
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - MouseTracker
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function ( $ ) {
+
+ // All MouseTracker instances
+ const MOUSETRACKERS = [];
+
+ // dictionary from hash to private properties
+ const THIS = {};
+
+
+ /**
+ * @class MouseTracker
+ * @classdesc Provides simplified handling of common pointer device (mouse, touch, pen, etc.) gestures
+ * and keyboard events on a specified element.
+ * @memberof OpenSeadragon
+ * @param {Object} options
+ * Allows configurable properties to be entirely specified by passing
+ * an options object to the constructor. The constructor also supports
+ * the original positional arguments 'element', 'clickTimeThreshold',
+ * and 'clickDistThreshold' in that order.
+ * @param {Element|String} options.element
+ * A reference to an element or an element id for which the pointer/key
+ * events will be monitored.
+ * @param {Boolean} [options.startDisabled=false]
+ * If true, event tracking on the element will not start until
+ * {@link OpenSeadragon.MouseTracker.setTracking|setTracking} is called.
+ * @param {Number} [options.clickTimeThreshold=300]
+ * The number of milliseconds within which a pointer down-up event combination
+ * will be treated as a click gesture.
+ * @param {Number} [options.clickDistThreshold=5]
+ * The maximum distance allowed between a pointer down event and a pointer up event
+ * to be treated as a click gesture.
+ * @param {Number} [options.dblClickTimeThreshold=300]
+ * The number of milliseconds within which two pointer down-up event combinations
+ * will be treated as a double-click gesture.
+ * @param {Number} [options.dblClickDistThreshold=20]
+ * The maximum distance allowed between two pointer click events
+ * to be treated as a click gesture.
+ * @param {Number} [options.stopDelay=50]
+ * The number of milliseconds without pointer move before the stop
+ * event is fired.
+ * @param {OpenSeadragon.EventHandler} [options.preProcessEventHandler=null]
+ * An optional handler for controlling DOM event propagation and processing.
+ * @param {OpenSeadragon.EventHandler} [options.contextMenuHandler=null]
+ * An optional handler for contextmenu.
+ * @param {OpenSeadragon.EventHandler} [options.enterHandler=null]
+ * An optional handler for pointer enter.
+ * @param {OpenSeadragon.EventHandler} [options.leaveHandler=null]
+ * An optional handler for pointer leave.
+ * @param {OpenSeadragon.EventHandler} [options.exitHandler=null]
+ * An optional handler for pointer leave. Deprecated. Use leaveHandler instead.
+ * @param {OpenSeadragon.EventHandler} [options.overHandler=null]
+ * An optional handler for pointer over.
+ * @param {OpenSeadragon.EventHandler} [options.outHandler=null]
+ * An optional handler for pointer out.
+ * @param {OpenSeadragon.EventHandler} [options.pressHandler=null]
+ * An optional handler for pointer press.
+ * @param {OpenSeadragon.EventHandler} [options.nonPrimaryPressHandler=null]
+ * An optional handler for pointer non-primary button press.
+ * @param {OpenSeadragon.EventHandler} [options.releaseHandler=null]
+ * An optional handler for pointer release.
+ * @param {OpenSeadragon.EventHandler} [options.nonPrimaryReleaseHandler=null]
+ * An optional handler for pointer non-primary button release.
+ * @param {OpenSeadragon.EventHandler} [options.moveHandler=null]
+ * An optional handler for pointer move.
+ * @param {OpenSeadragon.EventHandler} [options.scrollHandler=null]
+ * An optional handler for mouse wheel scroll.
+ * @param {OpenSeadragon.EventHandler} [options.clickHandler=null]
+ * An optional handler for pointer click.
+ * @param {OpenSeadragon.EventHandler} [options.dblClickHandler=null]
+ * An optional handler for pointer double-click.
+ * @param {OpenSeadragon.EventHandler} [options.dragHandler=null]
+ * An optional handler for the drag gesture.
+ * @param {OpenSeadragon.EventHandler} [options.dragEndHandler=null]
+ * An optional handler for after a drag gesture.
+ * @param {OpenSeadragon.EventHandler} [options.pinchHandler=null]
+ * An optional handler for the pinch gesture.
+ * @param {OpenSeadragon.EventHandler} [options.keyDownHandler=null]
+ * An optional handler for keydown.
+ * @param {OpenSeadragon.EventHandler} [options.keyUpHandler=null]
+ * An optional handler for keyup.
+ * @param {OpenSeadragon.EventHandler} [options.keyHandler=null]
+ * An optional handler for keypress.
+ * @param {OpenSeadragon.EventHandler} [options.focusHandler=null]
+ * An optional handler for focus.
+ * @param {OpenSeadragon.EventHandler} [options.blurHandler=null]
+ * An optional handler for blur.
+ * @param {Object} [options.userData=null]
+ * Arbitrary object to be passed unchanged to any attached handler methods.
+ */
+ $.MouseTracker = function ( options ) {
+
+ MOUSETRACKERS.push( this );
+
+ const args = arguments;
+
+ if ( !$.isPlainObject( options ) ) {
+ options = {
+ element: args[ 0 ],
+ clickTimeThreshold: args[ 1 ],
+ clickDistThreshold: args[ 2 ]
+ };
+ }
+
+ this.hash = uniqueHash(); // An unique hash for this tracker.
+ /**
+ * The element for which pointer events are being monitored.
+ * @member {Element} element
+ * @memberof OpenSeadragon.MouseTracker#
+ */
+ this.element = $.getElement( options.element );
+ /**
+ * The number of milliseconds within which a pointer down-up event combination
+ * will be treated as a click gesture.
+ * @member {Number} clickTimeThreshold
+ * @memberof OpenSeadragon.MouseTracker#
+ */
+ this.clickTimeThreshold = options.clickTimeThreshold || $.DEFAULT_SETTINGS.clickTimeThreshold;
+ /**
+ * The maximum distance allowed between a pointer down event and a pointer up event
+ * to be treated as a click gesture.
+ * @member {Number} clickDistThreshold
+ * @memberof OpenSeadragon.MouseTracker#
+ */
+ this.clickDistThreshold = options.clickDistThreshold || $.DEFAULT_SETTINGS.clickDistThreshold;
+ /**
+ * The number of milliseconds within which two pointer down-up event combinations
+ * will be treated as a double-click gesture.
+ * @member {Number} dblClickTimeThreshold
+ * @memberof OpenSeadragon.MouseTracker#
+ */
+ this.dblClickTimeThreshold = options.dblClickTimeThreshold || $.DEFAULT_SETTINGS.dblClickTimeThreshold;
+ /**
+ * The maximum distance allowed between two pointer click events
+ * to be treated as a double-click gesture.
+ * @member {Number} dblClickDistThreshold
+ * @memberof OpenSeadragon.MouseTracker#
+ */
+ this.dblClickDistThreshold = options.dblClickDistThreshold || $.DEFAULT_SETTINGS.dblClickDistThreshold;
+ /*eslint-disable no-multi-spaces*/
+ this.userData = options.userData || null;
+ this.stopDelay = options.stopDelay || 50;
+
+ this.preProcessEventHandler = options.preProcessEventHandler || null;
+ this.contextMenuHandler = options.contextMenuHandler || null;
+ this.enterHandler = options.enterHandler || null;
+ this.leaveHandler = options.leaveHandler || null;
+ this.exitHandler = options.exitHandler || null; // Deprecated v2.5.0
+ this.overHandler = options.overHandler || null;
+ this.outHandler = options.outHandler || null;
+ this.pressHandler = options.pressHandler || null;
+ this.nonPrimaryPressHandler = options.nonPrimaryPressHandler || null;
+ this.releaseHandler = options.releaseHandler || null;
+ this.nonPrimaryReleaseHandler = options.nonPrimaryReleaseHandler || null;
+ this.moveHandler = options.moveHandler || null;
+ this.scrollHandler = options.scrollHandler || null;
+ this.clickHandler = options.clickHandler || null;
+ this.dblClickHandler = options.dblClickHandler || null;
+ this.dragHandler = options.dragHandler || null;
+ this.dragEndHandler = options.dragEndHandler || null;
+ this.pinchHandler = options.pinchHandler || null;
+ this.stopHandler = options.stopHandler || null;
+ this.keyDownHandler = options.keyDownHandler || null;
+ this.keyUpHandler = options.keyUpHandler || null;
+ this.keyHandler = options.keyHandler || null;
+ this.focusHandler = options.focusHandler || null;
+ this.blurHandler = options.blurHandler || null;
+ /*eslint-enable no-multi-spaces*/
+
+ //Store private properties in a scope sealed hash map
+ const _this = this;
+
+ /**
+ * @private
+ * @property {Boolean} tracking
+ * Are we currently tracking pointer events for this element.
+ */
+ THIS[ this.hash ] = {
+ click: function ( event ) { onClick( _this, event ); },
+ dblclick: function ( event ) { onDblClick( _this, event ); },
+ keydown: function ( event ) { onKeyDown( _this, event ); },
+ keyup: function ( event ) { onKeyUp( _this, event ); },
+ keypress: function ( event ) { onKeyPress( _this, event ); },
+ focus: function ( event ) { onFocus( _this, event ); },
+ blur: function ( event ) { onBlur( _this, event ); },
+ contextmenu: function ( event ) { onContextMenu( _this, event ); },
+
+ wheel: function ( event ) { onWheel( _this, event ); },
+ mousewheel: function ( event ) { onMouseWheel( _this, event ); },
+ DOMMouseScroll: function ( event ) { onMouseWheel( _this, event ); },
+ MozMousePixelScroll: function ( event ) { onMouseWheel( _this, event ); },
+
+ losecapture: function ( event ) { onLoseCapture( _this, event ); },
+
+ mouseenter: function ( event ) { onPointerEnter( _this, event ); },
+ mouseleave: function ( event ) { onPointerLeave( _this, event ); },
+ mouseover: function ( event ) { onPointerOver( _this, event ); },
+ mouseout: function ( event ) { onPointerOut( _this, event ); },
+ mousedown: function ( event ) { onPointerDown( _this, event ); },
+ mouseup: function ( event ) { onPointerUp( _this, event ); },
+ mousemove: function ( event ) { onPointerMove( _this, event ); },
+
+ touchstart: function ( event ) { onTouchStart( _this, event ); },
+ touchend: function ( event ) { onTouchEnd( _this, event ); },
+ touchmove: function ( event ) { onTouchMove( _this, event ); },
+ touchcancel: function ( event ) { onTouchCancel( _this, event ); },
+
+ gesturestart: function ( event ) { onGestureStart( _this, event ); }, // Safari/Safari iOS
+ gesturechange: function ( event ) { onGestureChange( _this, event ); }, // Safari/Safari iOS
+
+ gotpointercapture: function ( event ) { onGotPointerCapture( _this, event ); },
+ lostpointercapture: function ( event ) { onLostPointerCapture( _this, event ); },
+ pointerenter: function ( event ) { onPointerEnter( _this, event ); },
+ pointerleave: function ( event ) { onPointerLeave( _this, event ); },
+ pointerover: function ( event ) { onPointerOver( _this, event ); },
+ pointerout: function ( event ) { onPointerOut( _this, event ); },
+ pointerdown: function ( event ) { onPointerDown( _this, event ); },
+ pointerup: function ( event ) { onPointerUp( _this, event ); },
+ pointermove: function ( event ) { onPointerMove( _this, event ); },
+ pointercancel: function ( event ) { onPointerCancel( _this, event ); },
+ pointerupcaptured: function ( event ) { onPointerUpCaptured( _this, event ); },
+ pointermovecaptured: function ( event ) { onPointerMoveCaptured( _this, event ); },
+
+ tracking: false,
+
+ // Active pointers lists. Array of GesturePointList objects, one for each pointer device type.
+ // GesturePointList objects are added each time a pointer is tracked by a new pointer device type (see getActivePointersListByType()).
+ // Active pointers are any pointer being tracked for this element which are in the hit-test area
+ // of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
+ activePointersLists: [],
+
+ // Tracking for double-click gesture
+ lastClickPos: null,
+ dblClickTimeOut: null,
+
+ // Tracking for pinch gesture
+ pinchGPoints: [],
+ lastPinchDist: 0,
+ currentPinchDist: 0,
+ lastPinchCenter: null,
+ currentPinchCenter: null,
+
+ // Tracking for drag
+ sentDragEvent: false
+ };
+
+ if ( $.MouseTracker.havePointerEvents ) {
+ $.setElementPointerEvents( this.element, 'auto' );
+ }
+
+ if (this.exitHandler) {
+ $.console.error("MouseTracker.exitHandler is deprecated. Use MouseTracker.leaveHandler instead.");
+ }
+
+ if ( !options.startDisabled ) {
+ this.setTracking( true );
+ }
+ };
+
+ /** @lends OpenSeadragon.MouseTracker.prototype */
+ $.MouseTracker.prototype = {
+
+ /**
+ * Clean up any events or objects created by the tracker.
+ * @function
+ */
+ destroy: function () {
+ stopTracking( this );
+ this.element = null;
+
+ for ( let i = 0; i < MOUSETRACKERS.length; i++ ) {
+ if ( MOUSETRACKERS[ i ] === this ) {
+ MOUSETRACKERS.splice( i, 1 );
+ break;
+ }
+ }
+
+ THIS[ this.hash ] = null;
+ delete THIS[ this.hash ];
+ },
+
+ /**
+ * Are we currently tracking events on this element.
+ * @deprecated Just use this.tracking
+ * @function
+ * @returns {Boolean} Are we currently tracking events on this element.
+ */
+ isTracking: function () {
+ return THIS[ this.hash ].tracking;
+ },
+
+ /**
+ * Enable or disable whether or not we are tracking events on this element.
+ * @function
+ * @param {Boolean} track True to start tracking, false to stop tracking.
+ * @returns {OpenSeadragon.MouseTracker} Chainable.
+ */
+ setTracking: function ( track ) {
+ if ( track ) {
+ startTracking( this );
+ } else {
+ stopTracking( this );
+ }
+ //chain
+ return this;
+ },
+
+ /**
+ * Returns the {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} for the given pointer device type,
+ * creating and caching a new {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} if one doesn't already exist for the type.
+ * @function
+ * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc.
+ * @returns {OpenSeadragon.MouseTracker.GesturePointList}
+ */
+ getActivePointersListByType: function ( type ) {
+ const delegate = THIS[ this.hash ];
+ const len = delegate ? delegate.activePointersLists.length : 0;
+ let list;
+
+ for ( let i = 0; i < len; i++ ) {
+ if ( delegate.activePointersLists[ i ].type === type ) {
+ return delegate.activePointersLists[ i ];
+ }
+ }
+
+ list = new $.MouseTracker.GesturePointList( type );
+ if(delegate){
+ delegate.activePointersLists.push( list );
+ }
+ return list;
+ },
+
+ /**
+ * Returns the total number of pointers currently active on the tracked element.
+ * @function
+ * @returns {Number}
+ */
+ getActivePointerCount: function () {
+ const delegate = THIS[ this.hash ];
+ const len = delegate.activePointersLists.length;
+ let count = 0;
+
+ for ( let i = 0; i < len; i++ ) {
+ count += delegate.activePointersLists[ i ].getLength();
+ }
+
+ return count;
+ },
+
+ /**
+ * Do we currently have any assigned gesture handlers.
+ * @returns {Boolean} Do we currently have any assigned gesture handlers.
+ */
+ get hasGestureHandlers() {
+ return !!(this.pressHandler ||
+ this.nonPrimaryPressHandler ||
+ this.releaseHandler ||
+ this.nonPrimaryReleaseHandler ||
+ this.clickHandler ||
+ this.dblClickHandler ||
+ this.dragHandler ||
+ this.dragEndHandler ||
+ this.pinchHandler);
+ },
+
+ /**
+ * Do we currently have a scroll handler.
+ * @returns {Boolean} Do we currently have a scroll handler.
+ */
+ get hasScrollHandler() {
+ return !!this.scrollHandler;
+ },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ */
+ preProcessEventHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Boolean} event.preventDefault
+ * Set to true to prevent the default user-agent's handling of the contextmenu event.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ contextMenuHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Number} event.pointers
+ * Number of pointers (all types) active in the tracked element.
+ * @param {Boolean} event.insideElementPressed
+ * True if the left mouse button is currently being pressed and was
+ * initiated inside the tracked element, otherwise false.
+ * @param {Boolean} event.buttonDownAny
+ * Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ enterHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @since v2.5.0
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Number} event.pointers
+ * Number of pointers (all types) active in the tracked element.
+ * @param {Boolean} event.insideElementPressed
+ * True if the left mouse button is currently being pressed and was
+ * initiated inside the tracked element, otherwise false.
+ * @param {Boolean} event.buttonDownAny
+ * Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ leaveHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @deprecated v2.5.0 Use leaveHandler instead
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Number} event.pointers
+ * Number of pointers (all types) active in the tracked element.
+ * @param {Boolean} event.insideElementPressed
+ * True if the left mouse button is currently being pressed and was
+ * initiated inside the tracked element, otherwise false.
+ * @param {Boolean} event.buttonDownAny
+ * Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ exitHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @since v2.5.0
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Number} event.pointers
+ * Number of pointers (all types) active in the tracked element.
+ * @param {Boolean} event.insideElementPressed
+ * True if the left mouse button is currently being pressed and was
+ * initiated inside the tracked element, otherwise false.
+ * @param {Boolean} event.buttonDownAny
+ * Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ overHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @since v2.5.0
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Number} event.pointers
+ * Number of pointers (all types) active in the tracked element.
+ * @param {Boolean} event.insideElementPressed
+ * True if the left mouse button is currently being pressed and was
+ * initiated inside the tracked element, otherwise false.
+ * @param {Boolean} event.buttonDownAny
+ * Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ outHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ pressHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.button
+ * Button which caused the event.
+ * -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ nonPrimaryPressHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Boolean} event.insideElementPressed
+ * True if the left mouse button is currently being pressed and was
+ * initiated inside the tracked element, otherwise false.
+ * @param {Boolean} event.insideElementReleased
+ * True if the cursor inside the tracked element when the button was released.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ releaseHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.button
+ * Button which caused the event.
+ * -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ nonPrimaryReleaseHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ moveHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.scroll
+ * The scroll delta for the event.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. Touch devices no longer generate scroll event.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Boolean} event.preventDefault
+ * Set to true to prevent the default user-agent's handling of the wheel event.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ scrollHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Boolean} event.quick
+ * True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for ignoring drag events.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Element} event.originalTarget
+ * The DOM element clicked on.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ clickHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ dblClickHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {OpenSeadragon.Point} event.delta
+ * The x,y components of the difference between the current position and the last drag event position. Useful for ignoring or weighting the events.
+ * @param {Number} event.speed
+ * Current computed speed, in pixels per second.
+ * @param {Number} event.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.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ dragHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.speed
+ * Speed at the end of a drag gesture, in pixels per second.
+ * @param {Number} event.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.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ dragEndHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {Array.} event.gesturePoints
+ * Gesture points associated with the gesture. Velocity data can be found here.
+ * @param {OpenSeadragon.Point} event.lastCenter
+ * The previous center point of the two pinch contact points relative to the tracked element.
+ * @param {OpenSeadragon.Point} event.center
+ * The center point of the two pinch contact points relative to the tracked element.
+ * @param {Number} event.lastDistance
+ * The previous distance between the two pinch contact points in CSS pixels.
+ * @param {Number} event.distance
+ * The distance between the two pinch contact points in CSS pixels.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ pinchHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {String} event.pointerType
+ * "mouse", "touch", "pen", etc.
+ * @param {OpenSeadragon.Point} event.position
+ * The position of the event relative to the tracked element.
+ * @param {Number} event.buttons
+ * Current buttons pressed.
+ * 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.
+ * @param {Boolean} event.isTouchEvent
+ * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ stopHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {Number} event.keyCode
+ * The key code that was pressed.
+ * @param {Boolean} event.ctrl
+ * True if the ctrl key was pressed during this event.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.alt
+ * True if the alt key was pressed during this event.
+ * @param {Boolean} event.meta
+ * True if the meta key was pressed during this event.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Boolean} event.preventDefault
+ * Set to true to prevent the default user-agent's handling of the keydown event.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ keyDownHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {Number} event.keyCode
+ * The key code that was pressed.
+ * @param {Boolean} event.ctrl
+ * True if the ctrl key was pressed during this event.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.alt
+ * True if the alt key was pressed during this event.
+ * @param {Boolean} event.meta
+ * True if the meta key was pressed during this event.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Boolean} event.preventDefault
+ * Set to true to prevent the default user-agent's handling of the keyup event.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ keyUpHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {Number} event.keyCode
+ * The key code that was pressed.
+ * @param {Boolean} event.ctrl
+ * True if the ctrl key was pressed during this event.
+ * @param {Boolean} event.shift
+ * True if the shift key was pressed during this event.
+ * @param {Boolean} event.alt
+ * True if the alt key was pressed during this event.
+ * @param {Boolean} event.meta
+ * True if the meta key was pressed during this event.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Boolean} event.preventDefault
+ * Set to true to prevent the default user-agent's handling of the keypress event.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ keyHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ focusHandler: function () { },
+
+ /**
+ * Implement or assign implementation to these handlers during or after
+ * calling the constructor.
+ * @function
+ * @param {Object} event
+ * @param {OpenSeadragon.MouseTracker} event.eventSource
+ * A reference to the tracker instance.
+ * @param {Object} event.originalEvent
+ * The original event object.
+ * @param {Object} event.userData
+ * Arbitrary user-defined object.
+ */
+ blurHandler: function () { }
+ };
+
+ // https://github.com/openseadragon/openseadragon/pull/790
+ /**
+ * True if inside an iframe, otherwise false.
+ * @member {Boolean} isInIframe
+ * @private
+ * @inner
+ */
+ const isInIframe = (function() {
+ try {
+ return window.self !== window.top;
+ } catch (e) {
+ return true;
+ }
+ })();
+
+ // https://github.com/openseadragon/openseadragon/pull/790
+ /**
+ * @function
+ * @private
+ * @inner
+ * @returns {Boolean} True if the target supports DOM Level 2 event subscription methods, otherwise false.
+ */
+ function canAccessEvents (target) {
+ try {
+ return target.addEventListener && target.removeEventListener;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * Provides continuous computation of velocity (speed and direction) of active pointers.
+ * This is a singleton, used by all MouseTracker instances, as it is unlikely there will ever be more than
+ * two active gesture pointers at a time.
+ *
+ * @private
+ * @member gesturePointVelocityTracker
+ * @memberof OpenSeadragon.MouseTracker
+ */
+ $.MouseTracker.gesturePointVelocityTracker = (function () {
+ const trackerPoints = [];
+ let intervalId = 0;
+ let lastTime = 0;
+
+ // Generates a unique identifier for a tracked gesture point
+ const _generateGuid = function ( tracker, gPoint ) {
+ return tracker.hash.toString() + gPoint.type + gPoint.id.toString();
+ };
+
+ // Interval timer callback. Computes velocity for all tracked gesture points.
+ const _doTracking = function () {
+ const len = trackerPoints.length;
+ const now = $.now();
+ let distance;
+ let speed;
+
+ const elapsedTime = now - lastTime;
+ lastTime = now;
+
+ for ( let i = 0; i < len; i++ ) {
+ const trackPoint = trackerPoints[ i ];
+ const gPoint = trackPoint.gPoint;
+ // Math.atan2 gives us just what we need for a velocity vector, as we can simply
+ // use cos()/sin() to extract the x/y velocity components.
+ gPoint.direction = Math.atan2( gPoint.currentPos.y - trackPoint.lastPos.y, gPoint.currentPos.x - trackPoint.lastPos.x );
+ // speed = distance / elapsed time
+ distance = trackPoint.lastPos.distanceTo( gPoint.currentPos );
+ trackPoint.lastPos = gPoint.currentPos;
+ speed = 1000 * distance / ( elapsedTime + 1 );
+ // Simple biased average, favors the most recent speed computation. Smooths out erratic gestures a bit.
+ gPoint.speed = 0.75 * speed + 0.25 * gPoint.speed;
+ }
+ };
+
+ // Public. Add a gesture point to be tracked
+ const addPoint = function ( tracker, gPoint ) {
+ const guid = _generateGuid( tracker, gPoint );
+
+ trackerPoints.push(
+ {
+ guid: guid,
+ gPoint: gPoint,
+ lastPos: gPoint.currentPos
+ } );
+
+ // Only fire up the interval timer when there's gesture pointers to track
+ if ( trackerPoints.length === 1 ) {
+ lastTime = $.now();
+ intervalId = window.setInterval( _doTracking, 50 );
+ }
+ };
+
+ // Public. Stop tracking a gesture point
+ const removePoint = function ( tracker, gPoint ) {
+ const guid = _generateGuid( tracker, gPoint );
+ let len = trackerPoints.length;
+
+ for ( let i = 0; i < len; i++ ) {
+ if ( trackerPoints[ i ].guid === guid ) {
+ trackerPoints.splice( i, 1 );
+ // Only run the interval timer if theres gesture pointers to track
+ len--;
+ if ( len === 0 ) {
+ window.clearInterval( intervalId );
+ }
+ break;
+ }
+ }
+ };
+
+ return {
+ addPoint: addPoint,
+ removePoint: removePoint
+ };
+ } )();
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Pointer event model and feature detection
+///////////////////////////////////////////////////////////////////////////////
+
+ $.MouseTracker.captureElement = document;
+
+ /**
+ * Detect available mouse wheel event name.
+ */
+ $.MouseTracker.wheelEventName = ( 'onwheel' in document.createElement( 'div' ) ) ? 'wheel' : // Modern browsers support 'wheel'
+ document.onmousewheel !== undefined ? 'mousewheel' : // Webkit (and unsupported IE) support at least 'mousewheel'
+ 'DOMMouseScroll'; // Assume old Firefox (deprecated)
+
+ /**
+ * Detect browser pointer device event model(s) and build appropriate list of events to subscribe to.
+ */
+ $.MouseTracker.subscribeEvents = [ "click", "dblclick", "keydown", "keyup", "keypress", "focus", "blur", "contextmenu", $.MouseTracker.wheelEventName ];
+
+ if( $.MouseTracker.wheelEventName === "DOMMouseScroll" ) {
+ // Older Firefox
+ $.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" );
+ }
+
+ if ( window.PointerEvent ) {
+ // W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents)
+ $.MouseTracker.havePointerEvents = true;
+ $.MouseTracker.subscribeEvents.push( "pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel" );
+ // Pointer events capture support
+ $.MouseTracker.havePointerCapture = (function () {
+ const divElement = document.createElement( 'div' );
+ return $.isFunction( divElement.setPointerCapture ) && $.isFunction( divElement.releasePointerCapture );
+ }());
+ if ( $.MouseTracker.havePointerCapture ) {
+ $.MouseTracker.subscribeEvents.push( "gotpointercapture", "lostpointercapture" );
+ }
+ } else {
+ // Legacy W3C mouse events
+ $.MouseTracker.havePointerEvents = false;
+ $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave", "mouseover", "mouseout", "mousedown", "mouseup", "mousemove" );
+ $.MouseTracker.mousePointerId = "legacy-mouse";
+ // Legacy mouse events capture support (IE/Firefox only?)
+ $.MouseTracker.havePointerCapture = (function () {
+ const divElement = document.createElement( 'div' );
+ return $.isFunction( divElement.setCapture ) && $.isFunction( divElement.releaseCapture );
+ }());
+ if ( $.MouseTracker.havePointerCapture ) {
+ $.MouseTracker.subscribeEvents.push( "losecapture" );
+ }
+ // Legacy touch events
+ if ( 'ontouchstart' in window ) {
+ // iOS, Android, and other W3c Touch Event implementations
+ // (see http://www.w3.org/TR/touch-events/)
+ // (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
+ // (see https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
+ $.MouseTracker.subscribeEvents.push( "touchstart", "touchend", "touchmove", "touchcancel" );
+ }
+ if ( 'ongesturestart' in window ) {
+ // iOS (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
+ // Subscribe to these to prevent default gesture handling
+ $.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" );
+ }
+ }
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Classes and typedefs
+///////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Used for the processing/disposition of DOM events (propagation, default handling, capture, etc.)
+ *
+ * @typedef {Object} EventProcessInfo
+ * @memberof OpenSeadragon.MouseTracker
+ * @since v2.5.0
+ *
+ * @property {OpenSeadragon.MouseTracker} eventSource
+ * A reference to the tracker instance.
+ * @property {Object} originalEvent
+ * The original DOM event object.
+ * @property {Number} eventPhase
+ * 0 == NONE, 1 == CAPTURING_PHASE, 2 == AT_TARGET, 3 == BUBBLING_PHASE.
+ * @property {String} eventType
+ * "keydown", "keyup", "keypress", "focus", "blur", "contextmenu", "gotpointercapture", "lostpointercapture", "pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel", "wheel", "click", "dblclick".
+ * @property {String} pointerType
+ * "mouse", "touch", "pen", etc.
+ * @property {Boolean} isEmulated
+ * True if this is an emulated event. If true, originalEvent is either the event that caused
+ * the emulated event, a synthetic event object created with values from the actual DOM event,
+ * or null if no DOM event applies. Emulated events can occur on eventType "wheel" on legacy mouse-scroll
+ * event emitting user agents.
+ * @property {Boolean} isStoppable
+ * True if propagation of the event (e.g. bubbling) can be stopped with stopPropagation/stopImmediatePropagation.
+ * @property {Boolean} isCancelable
+ * True if the event's default handling by the browser can be prevented with preventDefault.
+ * @property {Boolean} defaultPrevented
+ * True if the event's default handling has already been prevented by a descendent element.
+ * @property {Boolean} preventDefault
+ * Set to true to prevent the event's default handling by the browser.
+ * @property {Boolean} preventGesture
+ * Set to true to prevent this MouseTracker from generating a gesture from the event.
+ * Valid on eventType "pointerdown".
+ * @property {Boolean} stopPropagation
+ * Set to true prevent the event from propagating to ancestor/descendent elements on capture/bubble phase.
+ * @property {Boolean} shouldCapture
+ * (Internal Use) Set to true if the pointer should be captured (events (re)targeted to tracker element).
+ * @property {Boolean} shouldReleaseCapture
+ * (Internal Use) Set to true if the captured pointer should be released.
+ * @property {Object} userData
+ * Arbitrary user-defined object.
+ */
+
+
+ /**
+ * Represents a point of contact on the screen made by a mouse cursor, pen, touch, or other pointer device.
+ *
+ * @typedef {Object} GesturePoint
+ * @memberof OpenSeadragon.MouseTracker
+ *
+ * @property {Number} id
+ * Identifier unique from all other active GesturePoints for a given pointer device.
+ * @property {String} type
+ * The pointer device type: "mouse", "touch", "pen", etc.
+ * @property {Boolean} captured
+ * True if events for the gesture point are captured to the tracked element.
+ * @property {Boolean} isPrimary
+ * 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.
+ * @property {Boolean} insideElementPressed
+ * True if button pressed or contact point initiated inside the screen area of the tracked element.
+ * @property {Boolean} insideElement
+ * True if pointer or contact point is currently inside the bounds of the tracked element.
+ * @property {Number} speed
+ * Current computed speed, in pixels per second.
+ * @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.
+ * @property {OpenSeadragon.Point} contactPos
+ * The initial pointer contact position, relative to the page including any scrolling. Only valid if the pointer has contact (pressed, touch contact, pen contact).
+ * @property {Number} contactTime
+ * The initial pointer contact time, in milliseconds. Only valid if the pointer has contact (pressed, touch contact, pen contact).
+ * @property {OpenSeadragon.Point} lastPos
+ * The last pointer position, relative to the page including any scrolling.
+ * @property {Number} lastTime
+ * The last pointer contact time, in milliseconds.
+ * @property {OpenSeadragon.Point} currentPos
+ * The current pointer position, relative to the page including any scrolling.
+ * @property {Number} currentTime
+ * The current pointer contact time, in milliseconds.
+ */
+
+
+ /**
+ * @class GesturePointList
+ * @classdesc Provides an abstraction for a set of active {@link OpenSeadragon.MouseTracker.GesturePoint|GesturePoint} objects for a given pointer device type.
+ * Active pointers are any pointer being tracked for this element which are in the hit-test area
+ * of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
+ * @memberof OpenSeadragon.MouseTracker
+ * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc.
+ */
+ $.MouseTracker.GesturePointList = function ( type ) {
+ this._gPoints = [];
+ /**
+ * The pointer device type: "mouse", "touch", "pen", etc.
+ * @member {String} type
+ * @memberof OpenSeadragon.MouseTracker.GesturePointList#
+ */
+ this.type = type;
+ /**
+ * Current buttons pressed for the device.
+ * 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.
+ * @member {Number} buttons
+ * @memberof OpenSeadragon.MouseTracker.GesturePointList#
+ */
+ this.buttons = 0;
+ /**
+ * Current number of contact points (touch points, mouse down, etc.) for the device.
+ * @member {Number} contacts
+ * @memberof OpenSeadragon.MouseTracker.GesturePointList#
+ */
+ this.contacts = 0;
+ /**
+ * Current number of clicks for the device. Used for multiple click gesture tracking.
+ * @member {Number} clicks
+ * @memberof OpenSeadragon.MouseTracker.GesturePointList#
+ */
+ this.clicks = 0;
+ /**
+ * Current number of captured pointers for the device.
+ * @member {Number} captureCount
+ * @memberof OpenSeadragon.MouseTracker.GesturePointList#
+ */
+ this.captureCount = 0;
+ };
+
+ /** @lends OpenSeadragon.MouseTracker.GesturePointList.prototype */
+ $.MouseTracker.GesturePointList.prototype = {
+ /**
+ * @function
+ * @returns {Number} Number of gesture points in the list.
+ */
+ getLength: function () {
+ return this._gPoints.length;
+ },
+ /**
+ * @function
+ * @returns {Array.} The list of gesture points in the list as an array (read-only).
+ */
+ asArray: function () {
+ return this._gPoints;
+ },
+ /**
+ * @function
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gesturePoint - A gesture point to add to the list.
+ * @returns {Number} Number of gesture points in the list.
+ */
+ add: function ( gp ) {
+ return this._gPoints.push( gp );
+ },
+ /**
+ * @function
+ * @param {Number} id - The id of the gesture point to remove from the list.
+ * @returns {Number} Number of gesture points in the list.
+ */
+ removeById: function ( id ) {
+ const len = this._gPoints.length;
+ for ( let i = 0; i < len; i++ ) {
+ if ( this._gPoints[ i ].id === id ) {
+ this._gPoints.splice( i, 1 );
+ break;
+ }
+ }
+ return this._gPoints.length;
+ },
+ /**
+ * @function
+ * @param {Number} index - The index of the gesture point to retrieve from the list.
+ * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point at the given index, or null if not found.
+ */
+ getByIndex: function ( index ) {
+ if ( index < this._gPoints.length) {
+ return this._gPoints[ index ];
+ }
+
+ return null;
+ },
+ /**
+ * @function
+ * @param {Number} id - The id of the gesture point to retrieve from the list.
+ * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point with the given id, or null if not found.
+ */
+ getById: function ( id ) {
+ const len = this._gPoints.length;
+ for ( let i = 0; i < len; i++ ) {
+ if ( this._gPoints[ i ].id === id ) {
+ return this._gPoints[ i ];
+ }
+ }
+ return null;
+ },
+ /**
+ * @function
+ * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The primary gesture point in the list, or null if not found.
+ */
+ getPrimary: function ( id ) {
+ const len = this._gPoints.length;
+ for ( let i = 0; i < len; i++ ) {
+ if ( this._gPoints[ i ].isPrimary ) {
+ return this._gPoints[ i ];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Increment this pointer list's contact count.
+ * It will evaluate whether this pointer type is allowed to have multiple contacts.
+ * @function
+ */
+ addContact: function() {
+ ++this.contacts;
+
+ if (this.contacts > 1 && (this.type === "mouse" || this.type === "pen")) {
+ $.console.warn('GesturePointList.addContact() Implausible contacts value');
+ this.contacts = 1;
+ }
+ },
+
+ /**
+ * Decrement this pointer list's contact count.
+ * It will make sure the count does not go below 0.
+ * @function
+ */
+ removeContact: function() {
+ --this.contacts;
+
+ if (this.contacts < 0) {
+ this.contacts = 0;
+ }
+ }
+ };
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Utility functions
+///////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Removes all tracked pointers.
+ * @private
+ * @inner
+ */
+ function clearTrackedPointers( tracker ) {
+ const delegate = THIS[ tracker.hash ];
+ const pointerListCount = delegate.activePointersLists.length;
+
+ for ( let i = 0; i < pointerListCount; i++ ) {
+ const pointsList = delegate.activePointersLists[ i ];
+
+ if ( pointsList.getLength() > 0 ) {
+ // Make an array containing references to the gPoints in the pointer list
+ // (because calls to stopTrackingPointer() are going to modify the pointer list)
+ const gPointsToRemove = [];
+ const gPoints = pointsList.asArray();
+ for ( let j = 0; j < gPoints.length; j++ ) {
+ gPointsToRemove.push( gPoints[ j ] );
+ }
+
+ // Release and remove all gPoints from the pointer list
+ for ( let j = 0; j < gPointsToRemove.length; j++ ) {
+ stopTrackingPointer( tracker, pointsList, gPointsToRemove[ j ] );
+ }
+ }
+ }
+
+ for ( let i = 0; i < pointerListCount; i++ ) {
+ delegate.activePointersLists.pop();
+ }
+
+ delegate.sentDragEvent = false;
+ }
+
+ /**
+ * Starts tracking pointer events on the tracked element.
+ * @private
+ * @inner
+ */
+ function startTracking( tracker ) {
+ const delegate = THIS[ tracker.hash ];
+
+ if ( !delegate.tracking ) {
+ for ( let i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) {
+ const event = $.MouseTracker.subscribeEvents[ i ];
+ $.addEvent(
+ tracker.element,
+ event,
+ delegate[ event ],
+ event === $.MouseTracker.wheelEventName ? { passive: false, capture: false } : false
+ );
+ }
+
+ clearTrackedPointers( tracker );
+
+ delegate.tracking = true;
+ }
+ }
+
+ /**
+ * Stops tracking pointer events on the tracked element.
+ * @private
+ * @inner
+ */
+ function stopTracking( tracker ) {
+ const delegate = THIS[ tracker.hash ];
+
+ if ( delegate.tracking ) {
+ for ( let i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) {
+ const event = $.MouseTracker.subscribeEvents[ i ];
+ $.removeEvent(
+ tracker.element,
+ event,
+ delegate[ event ],
+ false
+ );
+ }
+
+ clearTrackedPointers( tracker );
+
+ delegate.tracking = false;
+ }
+ }
+
+ /**
+ * @private
+ * @inner
+ */
+ function getCaptureEventParams( tracker, pointerType ) {
+ const delegate = THIS[ tracker.hash ];
+
+ if ( pointerType === 'pointerevent' ) {
+ return {
+ upName: 'pointerup',
+ upHandler: delegate.pointerupcaptured,
+ moveName: 'pointermove',
+ moveHandler: delegate.pointermovecaptured
+ };
+ } else if ( pointerType === 'mouse' ) {
+ return {
+ upName: 'pointerup',
+ upHandler: delegate.pointerupcaptured,
+ moveName: 'pointermove',
+ moveHandler: delegate.pointermovecaptured
+ };
+ } else if ( pointerType === 'touch' ) {
+ return {
+ upName: 'touchend',
+ upHandler: delegate.touchendcaptured,
+ moveName: 'touchmove',
+ moveHandler: delegate.touchmovecaptured
+ };
+ } else {
+ throw new Error( "MouseTracker.getCaptureEventParams: Unknown pointer type." );
+ }
+ }
+
+ /**
+ * Begin capturing pointer events to the tracked element.
+ * @private
+ * @inner
+ */
+ function capturePointer( tracker, gPoint ) {
+ if ( $.MouseTracker.havePointerCapture ) {
+ if ( $.MouseTracker.havePointerEvents ) {
+ // Can throw NotFoundError (InvalidPointerId Firefox < 82)
+ // (should never happen so we'll log a warning)
+ try {
+ tracker.element.setPointerCapture( gPoint.id );
+ //$.console.log('element.setPointerCapture() called');
+ } catch ( e ) {
+ $.console.warn('setPointerCapture() called on invalid pointer ID');
+ return;
+ }
+ } else {
+ tracker.element.setCapture( true );
+ //$.console.log('element.setCapture() called');
+ }
+ } else {
+ // Emulate mouse capture by hanging listeners on the document object.
+ // (Note we listen on the capture phase so the captured handlers will get called first)
+ // eslint-disable-next-line no-use-before-define
+ //$.console.log('Emulated mouse capture set');
+ const eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : gPoint.type );
+ // https://github.com/openseadragon/openseadragon/pull/790
+ if (isInIframe && canAccessEvents(window.top)) {
+ $.addEvent(
+ window.top,
+ eventParams.upName,
+ eventParams.upHandler,
+ true
+ );
+ }
+ $.addEvent(
+ $.MouseTracker.captureElement,
+ eventParams.upName,
+ eventParams.upHandler,
+ true
+ );
+ $.addEvent(
+ $.MouseTracker.captureElement,
+ eventParams.moveName,
+ eventParams.moveHandler,
+ true
+ );
+ }
+
+ updatePointerCaptured( tracker, gPoint, true );
+ }
+
+
+ /**
+ * Stop capturing pointer events to the tracked element.
+ * @private
+ * @inner
+ */
+ function releasePointer( tracker, gPoint ) {
+ if ( $.MouseTracker.havePointerCapture ) {
+ if ( $.MouseTracker.havePointerEvents ) {
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+ const cachedGPoint = pointsList.getById( gPoint.id );
+ if ( !cachedGPoint || !cachedGPoint.captured ) {
+ return;
+ }
+ // Can throw NotFoundError (InvalidPointerId Firefox < 82)
+ // (should never happen, but it does on Firefox 79 touch so we won't log a warning)
+ try {
+ tracker.element.releasePointerCapture( gPoint.id );
+ //$.console.log('element.releasePointerCapture() called');
+ } catch ( e ) {
+ //$.console.warn('releasePointerCapture() called on invalid pointer ID');
+ }
+ } else {
+ tracker.element.releaseCapture();
+ //$.console.log('element.releaseCapture() called');
+ }
+ } else {
+ // Emulate mouse capture by hanging listeners on the document object.
+ // (Note we listen on the capture phase so the captured handlers will get called first)
+ //$.console.log('Emulated mouse capture release');
+ const eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : gPoint.type );
+ // https://github.com/openseadragon/openseadragon/pull/790
+ if (isInIframe && canAccessEvents(window.top)) {
+ $.removeEvent(
+ window.top,
+ eventParams.upName,
+ eventParams.upHandler,
+ true
+ );
+ }
+ $.removeEvent(
+ $.MouseTracker.captureElement,
+ eventParams.moveName,
+ eventParams.moveHandler,
+ true
+ );
+ $.removeEvent(
+ $.MouseTracker.captureElement,
+ eventParams.upName,
+ eventParams.upHandler,
+ true
+ );
+ }
+
+ updatePointerCaptured( tracker, gPoint, false );
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ * @private
+ * @inner
+ */
+ function getPointerId( event ) {
+ return ( $.MouseTracker.havePointerEvents ) ? event.pointerId : $.MouseTracker.mousePointerId;
+ }
+
+
+ /**
+ * Gets a W3C Pointer Events model compatible pointer type string from a DOM pointer event.
+ *
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ * @private
+ * @inner
+ */
+ function getPointerType( event ) {
+ return $.MouseTracker.havePointerEvents && event.pointerType ? event.pointerType : 'mouse';
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ * @private
+ * @inner
+ */
+ function getIsPrimary( event ) {
+ return ( $.MouseTracker.havePointerEvents ) ? event.isPrimary : true;
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function getMouseAbsolute( event ) {
+ return $.getMousePosition( event );
+ }
+
+ /**
+ * @private
+ * @inner
+ */
+ function getMouseRelative( event, element ) {
+ return getPointRelativeToAbsolute( getMouseAbsolute( event ), element );
+ }
+
+ /**
+ * @private
+ * @inner
+ */
+ function getPointRelativeToAbsolute( point, element ) {
+ const offset = $.getElementOffset( element );
+ return point.minus( offset );
+ }
+
+ /**
+ * @private
+ * @inner
+ */
+ function getCenterPoint( point1, point2 ) {
+ return new $.Point( ( point1.x + point2.x ) / 2, ( point1.y + point2.y ) / 2 );
+ }
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Device-specific DOM event handlers
+///////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @private
+ * @inner
+ */
+ function onClick( tracker, event ) {
+ //$.console.log('click ' + (tracker.userData ? tracker.userData.toString() : ''));
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'click',
+ pointerType: 'mouse',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onDblClick( tracker, event ) {
+ //$.console.log('dblclick ' + (tracker.userData ? tracker.userData.toString() : ''));
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'dblclick',
+ pointerType: 'mouse',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onKeyDown( tracker, event ) {
+ //$.console.log( "keydown %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
+ let eventArgs = null;
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'keydown',
+ pointerType: '',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( tracker.keyDownHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ eventArgs = {
+ eventSource: tracker,
+ keyCode: event.keyCode ? event.keyCode : event.charCode,
+ ctrl: event.ctrlKey,
+ shift: event.shiftKey,
+ alt: event.altKey,
+ meta: event.metaKey,
+ originalEvent: event,
+ preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
+ userData: tracker.userData
+ };
+
+ tracker.keyDownHandler( eventArgs );
+ }
+
+ if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onKeyUp( tracker, event ) {
+ //$.console.log( "keyup %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
+
+ let eventArgs = null;
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'keyup',
+ pointerType: '',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( tracker.keyUpHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ eventArgs = {
+ eventSource: tracker,
+ keyCode: event.keyCode ? event.keyCode : event.charCode,
+ ctrl: event.ctrlKey,
+ shift: event.shiftKey,
+ alt: event.altKey,
+ meta: event.metaKey,
+ originalEvent: event,
+ preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
+ userData: tracker.userData
+ };
+
+ tracker.keyUpHandler( eventArgs );
+ }
+
+ if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onKeyPress( tracker, event ) {
+ //$.console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
+
+ let eventArgs = null;
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'keypress',
+ pointerType: '',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( tracker.keyHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ eventArgs = {
+ eventSource: tracker,
+ keyCode: event.keyCode ? event.keyCode : event.charCode,
+ ctrl: event.ctrlKey,
+ shift: event.shiftKey,
+ alt: event.altKey,
+ meta: event.metaKey,
+ originalEvent: event,
+ preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
+ userData: tracker.userData
+ };
+
+ tracker.keyHandler( eventArgs );
+ }
+
+ if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onFocus( tracker, event ) {
+ //$.console.log('focus ' + (tracker.userData ? tracker.userData.toString() : ''));
+
+ // focus doesn't bubble and is not cancelable, but we call
+ // preProcessEvent() so it's dispatched to preProcessEventHandler
+ // if necessary
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'focus',
+ pointerType: '',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( tracker.focusHandler && !eventInfo.preventGesture ) {
+ tracker.focusHandler(
+ {
+ eventSource: tracker,
+ originalEvent: event,
+ userData: tracker.userData
+ }
+ );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onBlur( tracker, event ) {
+ //$.console.log('blur ' + (tracker.userData ? tracker.userData.toString() : ''));
+
+ // blur doesn't bubble and is not cancelable, but we call
+ // preProcessEvent() so it's dispatched to preProcessEventHandler
+ // if necessary
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'blur',
+ pointerType: '',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( tracker.blurHandler && !eventInfo.preventGesture ) {
+ tracker.blurHandler(
+ {
+ eventSource: tracker,
+ originalEvent: event,
+ userData: tracker.userData
+ }
+ );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onContextMenu( tracker, event ) {
+ //$.console.log('contextmenu ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ let eventArgs = null;
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'contextmenu',
+ pointerType: 'mouse',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ // ContextMenu
+ if ( tracker.contextMenuHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ eventArgs = {
+ eventSource: tracker,
+ position: getPointRelativeToAbsolute( getMouseAbsolute( event ), tracker.element ),
+ originalEvent: eventInfo.originalEvent,
+ preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
+ userData: tracker.userData
+ };
+
+ tracker.contextMenuHandler( eventArgs );
+ }
+
+ if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * Handler for 'wheel' events
+ *
+ * @private
+ * @inner
+ */
+ function onWheel( tracker, event ) {
+ handleWheelEvent( tracker, event, event );
+ }
+
+
+ /**
+ * Handler for 'mousewheel', 'DOMMouseScroll', and 'MozMousePixelScroll' events
+ *
+ * @private
+ * @inner
+ */
+ function onMouseWheel( tracker, event ) {
+ // Simulate a 'wheel' event
+ const simulatedEvent = {
+ target: event.target || event.srcElement,
+ type: "wheel",
+ shiftKey: event.shiftKey || false,
+ clientX: event.clientX,
+ clientY: event.clientY,
+ pageX: event.pageX ? event.pageX : event.clientX,
+ pageY: event.pageY ? event.pageY : event.clientY,
+ deltaMode: event.type === "MozMousePixelScroll" ? 0 : 1, // 0=pixel, 1=line, 2=page
+ deltaX: 0,
+ deltaZ: 0
+ };
+
+ // Calculate deltaY
+ if ( $.MouseTracker.wheelEventName === "mousewheel" ) {
+ simulatedEvent.deltaY = -event.wheelDelta / $.DEFAULT_SETTINGS.pixelsPerWheelLine;
+ } else {
+ simulatedEvent.deltaY = event.detail;
+ }
+
+ handleWheelEvent( tracker, simulatedEvent, event );
+ }
+
+
+ /**
+ * Handles 'wheel' events.
+ * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()).
+ *
+ * @private
+ * @inner
+ */
+ function handleWheelEvent( tracker, event, originalEvent ) {
+ let nDelta = 0;
+ let eventInfo;
+
+ let eventArgs = null;
+
+ // The nDelta variable is gated to provide smooth z-index scrolling
+ // since the mouse wheel allows for substantial deltas meant for rapid
+ // y-index scrolling.
+ // event.deltaMode: 0=pixel, 1=line, 2=page
+ // TODO: Deltas in pixel mode should be accumulated then a scroll value computed after $.DEFAULT_SETTINGS.pixelsPerWheelLine threshold reached
+ nDelta = event.deltaY ? (event.deltaY < 0 ? 1 : -1) : 0;
+
+ eventInfo = {
+ originalEvent: event,
+ eventType: 'wheel',
+ pointerType: 'mouse',
+ isEmulated: event !== originalEvent
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( tracker.scrollHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ eventArgs = {
+ eventSource: tracker,
+ pointerType: 'mouse',
+ position: getMouseRelative( event, tracker.element ),
+ scroll: nDelta,
+ shift: event.shiftKey,
+ isTouchEvent: false,
+ originalEvent: originalEvent,
+ preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
+ userData: tracker.userData
+ };
+
+
+ tracker.scrollHandler( eventArgs );
+ }
+
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( originalEvent );
+ }
+ if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
+ $.cancelEvent( originalEvent );
+ }
+}
+
+
+ /**
+ * TODO Never actually seen this event fired, and documentation is tough to find
+ * @private
+ * @inner
+ */
+ function onLoseCapture( tracker, event ) {
+ //$.console.log('losecapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const gPoint = {
+ id: $.MouseTracker.mousePointerId,
+ type: 'mouse'
+ };
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'lostpointercapture',
+ pointerType: 'mouse',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( event.target === tracker.element ) {
+ updatePointerCaptured( tracker, gPoint, false );
+ }
+
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onTouchStart( tracker, event ) {
+ const touchCount = event.changedTouches.length;
+ const pointsList = tracker.getActivePointersListByType( 'touch' );
+
+ const time = $.now();
+
+ //$.console.log('touchstart ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ if ( pointsList.getLength() > event.touches.length - touchCount ) {
+ $.console.warn('Tracked touch contact count doesn\'t match event.touches.length');
+ }
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerdown',
+ pointerType: 'touch',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ for ( let i = 0; i < touchCount; i++ ) {
+ const gPoint = {
+ id: event.changedTouches[ i ].identifier,
+ type: 'touch',
+ // Simulate isPrimary
+ isPrimary: pointsList.getLength() === 0,
+ currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
+ currentTime: time
+ };
+
+ // simulate touchenter on our tracked element
+ updatePointerEnter( tracker, eventInfo, gPoint );
+
+ updatePointerDown( tracker, eventInfo, gPoint, 0 );
+
+ updatePointerCaptured( tracker, gPoint, true );
+ }
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onTouchEnd( tracker, event ) {
+ const touchCount = event.changedTouches.length;
+ const time = $.now();
+
+ //$.console.log('touchend ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerup',
+ pointerType: 'touch',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ for ( let i = 0; i < touchCount; i++ ) {
+ const gPoint = {
+ id: event.changedTouches[ i ].identifier,
+ type: 'touch',
+ currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
+ currentTime: time
+ };
+
+ updatePointerUp( tracker, eventInfo, gPoint, 0 );
+
+ updatePointerCaptured( tracker, gPoint, false );
+
+ // simulate touchleave on our tracked element
+ updatePointerLeave( tracker, eventInfo, gPoint );
+ }
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onTouchMove( tracker, event ) {
+ const touchCount = event.changedTouches.length;
+ const time = $.now();
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointermove',
+ pointerType: 'touch',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ for ( let i = 0; i < touchCount; i++ ) {
+ const gPoint = {
+ id: event.changedTouches[ i ].identifier,
+ type: 'touch',
+ currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
+ currentTime: time
+ };
+
+ updatePointerMove( tracker, eventInfo, gPoint );
+ }
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onTouchCancel( tracker, event ) {
+ const touchCount = event.changedTouches.length;
+ //$.console.log('touchcancel ' + (tracker.userData ? tracker.userData.toString() : ''));
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointercancel',
+ pointerType: 'touch',
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ for ( let i = 0; i < touchCount; i++ ) {
+ const gPoint = {
+ id: event.changedTouches[ i ].identifier,
+ type: 'touch'
+ };
+
+ //TODO need to only do this if our element is target?
+ updatePointerCancel( tracker, eventInfo, gPoint );
+ }
+
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onGestureStart( tracker, event ) {
+ if ( !$.eventIsCanceled( event ) ) {
+ event.preventDefault();
+ }
+ return false;
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onGestureChange( tracker, event ) {
+ if ( !$.eventIsCanceled( event ) ) {
+ event.preventDefault();
+ }
+ return false;
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onGotPointerCapture( tracker, event ) {
+ //$.console.log('gotpointercapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'gotpointercapture',
+ pointerType: getPointerType( event ),
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( event.target === tracker.element ) {
+ //$.console.log('gotpointercapture ' + (tracker.userData ? tracker.userData.toString() : ''));
+ updatePointerCaptured( tracker, {
+ id: event.pointerId,
+ type: getPointerType( event )
+ }, true );
+ }
+
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onLostPointerCapture( tracker, event ) {
+ //$.console.log('lostpointercapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'lostpointercapture',
+ pointerType: getPointerType( event ),
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ if ( event.target === tracker.element ) {
+ //$.console.log('lostpointercapture ' + (tracker.userData ? tracker.userData.toString() : ''));
+ updatePointerCaptured( tracker, {
+ id: event.pointerId,
+ type: getPointerType( event )
+ }, false );
+ }
+
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function onPointerEnter( tracker, event ) {
+ //$.console.log('pointerenter ' + (tracker.userData ? tracker.userData.toString() : ''));
+
+ const gPoint = {
+ id: getPointerId( event ),
+ type: getPointerType( event ),
+ isPrimary: getIsPrimary( event ),
+ currentPos: getMouseAbsolute( event ),
+ currentTime: $.now()
+ };
+
+ // pointerenter doesn't bubble and is not cancelable, but we call
+ // preProcessEvent() so it's dispatched to preProcessEventHandler
+ // if necessary
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerenter',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ updatePointerEnter( tracker, eventInfo, gPoint );
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function onPointerLeave( tracker, event ) {
+ //$.console.log('pointerleave ' + (tracker.userData ? tracker.userData.toString() : ''));
+
+ const gPoint = {
+ id: getPointerId( event ),
+ type: getPointerType( event ),
+ isPrimary: getIsPrimary( event ),
+ currentPos: getMouseAbsolute( event ),
+ currentTime: $.now()
+ };
+
+ // pointerleave doesn't bubble and is not cancelable, but we call
+ // preProcessEvent() so it's dispatched to preProcessEventHandler
+ // if necessary
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerleave',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ updatePointerLeave( tracker, eventInfo, gPoint );
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function onPointerOver( tracker, event ) {
+ //$.console.log('pointerover ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const gPoint = {
+ id: getPointerId( event ),
+ type: getPointerType( event ),
+ isPrimary: getIsPrimary( event ),
+ currentPos: getMouseAbsolute( event ),
+ currentTime: $.now()
+ };
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerover',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ updatePointerOver( tracker, eventInfo, gPoint );
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function onPointerOut( tracker, event ) {
+ //$.console.log('pointerout ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const gPoint = {
+ id: getPointerId( event ),
+ type: getPointerType( event ),
+ isPrimary: getIsPrimary( event ),
+ currentPos: getMouseAbsolute( event ),
+ currentTime: $.now()
+ };
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerout',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ updatePointerOut( tracker, eventInfo, gPoint );
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function onPointerDown( tracker, event ) {
+ const gPoint = {
+ id: getPointerId( event ),
+ type: getPointerType( event ),
+ isPrimary: getIsPrimary( event ),
+ currentPos: getMouseAbsolute( event ),
+ currentTime: $.now()
+ };
+
+ // Most browsers implicitly capture touch pointer events
+ // Note no IE versions (unsupported) have element.hasPointerCapture() so
+ // no implicit pointer capture possible
+ // var implicitlyCaptured = ($.MouseTracker.havePointerEvents &&
+ // event.target.hasPointerCapture &&
+ // $.Browser.vendor !== $.BROWSERS.IE) ?
+ // event.target.hasPointerCapture(event.pointerId) : false;
+ const implicitlyCaptured = $.MouseTracker.havePointerEvents &&
+ gPoint.type === 'touch';
+
+ //$.console.log('pointerdown ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerdown',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ updatePointerDown( tracker, eventInfo, gPoint, event.button );
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ if ( eventInfo.shouldCapture ) {
+ if ( implicitlyCaptured ) {
+ updatePointerCaptured( tracker, gPoint, true );
+ } else {
+ capturePointer( tracker, gPoint );
+ }
+ }
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function onPointerUp( tracker, event ) {
+ handlePointerUp( tracker, event );
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * This handler is attached to the window object (on the capture phase) to emulate mouse capture.
+ * onPointerUp is still attached to the tracked element, so stop propagation to avoid processing twice.
+ *
+ * @private
+ * @inner
+ */
+ function onPointerUpCaptured( tracker, event ) {
+ const pointsList = tracker.getActivePointersListByType( getPointerType( event ) );
+ if ( pointsList.getById( event.pointerId ) ) {
+ handlePointerUp( tracker, event );
+ }
+ $.stopEvent( event );
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function handlePointerUp( tracker, event ) {
+ //$.console.log('pointerup ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const gPoint = {
+ id: getPointerId( event ),
+ type: getPointerType( event ),
+ isPrimary: getIsPrimary( event ),
+ currentPos: getMouseAbsolute( event ),
+ currentTime: $.now()
+ };
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointerup',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ updatePointerUp( tracker, eventInfo, gPoint, event.button );
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+
+ // Per spec, pointerup events are supposed to release capture. Not all browser
+ // versions have adhered to the spec, and there's no harm in releasing
+ // explicitly
+ if ( eventInfo.shouldReleaseCapture ) {
+ if ( event.target === tracker.element ) {
+ releasePointer( tracker, gPoint );
+ } else {
+ updatePointerCaptured( tracker, gPoint, false );
+ }
+ }
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function onPointerMove( tracker, event ) {
+ handlePointerMove( tracker, event );
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * This handler is attached to the window object (on the capture phase) to emulate mouse capture.
+ * onPointerMove is still attached to the tracked element, so stop propagation to avoid processing twice.
+ *
+ * @private
+ * @inner
+ */
+ function onPointerMoveCaptured( tracker, event ) {
+ const pointsList = tracker.getActivePointersListByType( getPointerType( event ) );
+ if ( pointsList.getById( event.pointerId ) ) {
+ handlePointerMove( tracker, event );
+ }
+ $.stopEvent( event );
+ }
+
+
+ /**
+ * Note: Called for both pointer events and legacy mouse events
+ * ($.MouseTracker.havePointerEvents determines which)
+ *
+ * @private
+ * @inner
+ */
+ function handlePointerMove( tracker, event ) {
+ // Pointer changed coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height)
+
+ const gPoint = {
+ id: getPointerId( event ),
+ type: getPointerType( event ),
+ isPrimary: getIsPrimary( event ),
+ currentPos: getMouseAbsolute( event ),
+ currentTime: $.now()
+ };
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointermove',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ updatePointerMove( tracker, eventInfo, gPoint );
+
+ if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
+ $.cancelEvent( event );
+ }
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function onPointerCancel( tracker, event ) {
+ //$.console.log('pointercancel ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
+
+ const gPoint = {
+ id: event.pointerId,
+ type: getPointerType( event )
+ };
+
+ const eventInfo = {
+ originalEvent: event,
+ eventType: 'pointercancel',
+ pointerType: gPoint.type,
+ isEmulated: false
+ };
+ preProcessEvent( tracker, eventInfo );
+
+ //TODO need to only do this if our element is target?
+ updatePointerCancel( tracker, eventInfo, gPoint );
+
+ if ( eventInfo.stopPropagation ) {
+ $.stopEvent( event );
+ }
+ }
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Device-agnostic DOM event handlers
+///////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList
+ * The GesturePointList to track the pointer in.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture point to track.
+ * @returns {Number} Number of gesture points in pointsList.
+ */
+ function startTrackingPointer( pointsList, gPoint ) {
+ //$.console.log('startTrackingPointer *** ' + pointsList.type + ' ' + gPoint.id.toString());
+ gPoint.speed = 0;
+ gPoint.direction = 0;
+ gPoint.contactPos = gPoint.currentPos;
+ gPoint.contactTime = gPoint.currentTime;
+ gPoint.lastPos = gPoint.currentPos;
+ gPoint.lastTime = gPoint.currentTime;
+
+ return pointsList.add( gPoint );
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList
+ * The GesturePointList to stop tracking the pointer on.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture point to stop tracking.
+ * @returns {Number} Number of gesture points in pointsList.
+ */
+ function stopTrackingPointer( tracker, pointsList, gPoint ) {
+ //$.console.log('stopTrackingPointer *** ' + pointsList.type + ' ' + gPoint.id.toString());
+ let listLength;
+ const trackedGPoint = pointsList.getById( gPoint.id );
+
+ if ( trackedGPoint ) {
+ if ( trackedGPoint.captured ) {
+ $.console.warn('stopTrackingPointer() called on captured pointer');
+ releasePointer( tracker, trackedGPoint );
+ }
+
+ // If child element relinquishes capture to a parent we may get here
+ // from a pointerleave event while a pointerup event will never be received.
+ // In that case, we'll clean up the contact count
+ pointsList.removeContact();
+
+ listLength = pointsList.removeById( gPoint.id );
+ } else {
+ listLength = pointsList.getLength();
+ }
+
+ return listLength;
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ */
+ function getEventProcessDefaults( tracker, eventInfo ) {
+ switch ( eventInfo.eventType ) {
+ case 'pointermove':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = true;
+ eventInfo.preventDefault = false;
+ eventInfo.preventGesture = !tracker.hasGestureHandlers;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'pointerover':
+ case 'pointerout':
+ case 'contextmenu':
+ case 'keydown':
+ case 'keyup':
+ case 'keypress':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = true;
+ eventInfo.preventDefault = false; // onContextMenu(), onKeyDown(), onKeyUp(), onKeyPress() may set true
+ eventInfo.preventGesture = false;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'pointerdown':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = true;
+ eventInfo.preventDefault = false; // updatePointerDown() may set true (tracker.hasGestureHandlers)
+ eventInfo.preventGesture = !tracker.hasGestureHandlers;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'pointerup':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = true;
+ eventInfo.preventDefault = false;
+ eventInfo.preventGesture = !tracker.hasGestureHandlers;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'wheel':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = true;
+ eventInfo.preventDefault = false; // handleWheelEvent() may set true
+ eventInfo.preventGesture = !tracker.hasScrollHandler;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'gotpointercapture':
+ case 'lostpointercapture':
+ case 'pointercancel':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = false;
+ eventInfo.preventDefault = false;
+ eventInfo.preventGesture = false;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'click':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = true;
+ eventInfo.preventDefault = !!tracker.clickHandler;
+ eventInfo.preventGesture = false;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'dblclick':
+ eventInfo.isStoppable = true;
+ eventInfo.isCancelable = true;
+ eventInfo.preventDefault = !!tracker.dblClickHandler;
+ eventInfo.preventGesture = false;
+ eventInfo.stopPropagation = false;
+ break;
+ case 'focus':
+ case 'blur':
+ case 'pointerenter':
+ case 'pointerleave':
+ default:
+ eventInfo.isStoppable = false;
+ eventInfo.isCancelable = false;
+ eventInfo.preventDefault = false;
+ eventInfo.preventGesture = false;
+ eventInfo.stopPropagation = false;
+ break;
+ }
+ }
+
+
+ /**
+ * Sets up for and calls preProcessEventHandler. Call with the following parameters -
+ * this function will fill in the rest of the preProcessEventHandler event object
+ * properties
+ *
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * @param {Object} eventInfo.originalEvent
+ * @param {String} eventInfo.eventType
+ * @param {String} eventInfo.pointerType
+ * @param {Boolean} eventInfo.isEmulated
+ */
+ function preProcessEvent( tracker, eventInfo ) {
+ eventInfo.eventSource = tracker;
+ eventInfo.eventPhase = eventInfo.originalEvent ?
+ ((typeof eventInfo.originalEvent.eventPhase !== 'undefined') ?
+ eventInfo.originalEvent.eventPhase : 0) : 0;
+ eventInfo.defaultPrevented = $.eventIsCanceled( eventInfo.originalEvent );
+ eventInfo.shouldCapture = false;
+ eventInfo.shouldReleaseCapture = false;
+ eventInfo.userData = tracker.userData;
+
+ getEventProcessDefaults( tracker, eventInfo );
+
+ if ( tracker.preProcessEventHandler ) {
+ tracker.preProcessEventHandler( eventInfo );
+ }
+ }
+
+
+ /**
+ * Sets or resets the captured property on the tracked pointer matching the passed gPoint's id/type
+ *
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {Object} gPoint
+ * An object with id and type properties describing the pointer to update.
+ * @param {Boolean} isCaptured
+ * Value to set the captured property to.
+ */
+ function updatePointerCaptured( tracker, gPoint, isCaptured ) {
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+ const updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ if ( isCaptured && !updateGPoint.captured ) {
+ updateGPoint.captured = true;
+ pointsList.captureCount++;
+ } else if ( !isCaptured && updateGPoint.captured ) {
+ updateGPoint.captured = false;
+ pointsList.captureCount--;
+ if ( pointsList.captureCount < 0 ) {
+ pointsList.captureCount = 0;
+ $.console.warn('updatePointerCaptured() - pointsList.captureCount went negative');
+ }
+ }
+ } else {
+ $.console.warn('updatePointerCaptured() called on untracked pointer');
+ }
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture point associated with the event.
+ */
+ function updatePointerEnter( tracker, eventInfo, gPoint ) {
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+ const updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ // Already tracking the pointer...update it
+ updateGPoint.insideElement = true;
+ updateGPoint.lastPos = updateGPoint.currentPos;
+ updateGPoint.lastTime = updateGPoint.currentTime;
+ updateGPoint.currentPos = gPoint.currentPos;
+ updateGPoint.currentTime = gPoint.currentTime;
+
+ gPoint = updateGPoint;
+ } else {
+ // Initialize for tracking and add to the tracking list
+ gPoint.captured = false; // Handled by updatePointerCaptured()
+ gPoint.insideElementPressed = false;
+ gPoint.insideElement = true;
+ startTrackingPointer( pointsList, gPoint );
+ }
+
+ // Enter (doesn't bubble and not cancelable)
+ if ( tracker.enterHandler ) {
+ tracker.enterHandler(
+ {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
+ buttons: pointsList.buttons,
+ pointers: tracker.getActivePointerCount(),
+ insideElementPressed: gPoint.insideElementPressed,
+ buttonDownAny: pointsList.buttons !== 0,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture point associated with the event.
+ */
+ function updatePointerLeave( tracker, eventInfo, gPoint ) {
+ const pointsList = tracker.getActivePointersListByType(gPoint.type);
+ const updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ // Already tracking the pointer. If captured then update it, else stop tracking it
+ if ( updateGPoint.captured ) {
+ updateGPoint.insideElement = false;
+ updateGPoint.lastPos = updateGPoint.currentPos;
+ updateGPoint.lastTime = updateGPoint.currentTime;
+ updateGPoint.currentPos = gPoint.currentPos;
+ updateGPoint.currentTime = gPoint.currentTime;
+ } else {
+ stopTrackingPointer( tracker, pointsList, updateGPoint );
+ }
+
+ gPoint = updateGPoint;
+ } else {
+ gPoint.captured = false; // Handled by updatePointerCaptured()
+ gPoint.insideElementPressed = false;
+ }
+
+ // Leave (doesn't bubble and not cancelable)
+ // Note: exitHandler is deprecated (v2.5.0), replaced by leaveHandler
+ if ( tracker.leaveHandler || tracker.exitHandler ) {
+ const dispatchEventObj = {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ // GitHub PR: https://github.com/openseadragon/openseadragon/pull/1754 (gPoint.currentPos && )
+ position: gPoint.currentPos && getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
+ buttons: pointsList.buttons,
+ pointers: tracker.getActivePointerCount(),
+ insideElementPressed: gPoint.insideElementPressed,
+ buttonDownAny: pointsList.buttons !== 0,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ };
+
+ if ( tracker.leaveHandler ) {
+ tracker.leaveHandler( dispatchEventObj );
+ }
+ // Deprecated
+ if ( tracker.exitHandler ) {
+ tracker.exitHandler( dispatchEventObj );
+ }
+ }
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture point associated with the event.
+ */
+ function updatePointerOver( tracker, eventInfo, gPoint ) {
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+ const updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ gPoint = updateGPoint;
+ } else {
+ gPoint.captured = false;
+ gPoint.insideElementPressed = false;
+ //gPoint.insideElement = true; // Tracked by updatePointerEnter
+ }
+
+ if ( tracker.overHandler ) {
+ // Over
+ tracker.overHandler(
+ {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
+ buttons: pointsList.buttons,
+ pointers: tracker.getActivePointerCount(),
+ insideElementPressed: gPoint.insideElementPressed,
+ buttonDownAny: pointsList.buttons !== 0,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+ }
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture point associated with the event.
+ */
+ function updatePointerOut( tracker, eventInfo, gPoint ) {
+ const pointsList = tracker.getActivePointersListByType(gPoint.type);
+ const updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ gPoint = updateGPoint;
+ } else {
+ gPoint.captured = false;
+ gPoint.insideElementPressed = false;
+ //gPoint.insideElement = true; // Tracked by updatePointerEnter
+ }
+
+ if ( tracker.outHandler ) {
+ // Out
+ tracker.outHandler( {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ position: gPoint.currentPos && getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
+ buttons: pointsList.buttons,
+ pointers: tracker.getActivePointerCount(),
+ insideElementPressed: gPoint.insideElementPressed,
+ buttonDownAny: pointsList.buttons !== 0,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ } );
+ }
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture point associated with the event.
+ * @param {Number} buttonChanged
+ * The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
+ * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
+ * only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
+ */
+ function updatePointerDown( tracker, eventInfo, gPoint, buttonChanged ) {
+ const delegate = THIS[ tracker.hash ];
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+
+ if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) {
+ pointsList.buttons = eventInfo.originalEvent.buttons;
+ } else {
+ if ( buttonChanged === 0 ) {
+ // Primary
+ pointsList.buttons |= 1;
+ } else if ( buttonChanged === 1 ) {
+ // Aux
+ pointsList.buttons |= 4;
+ } else if ( buttonChanged === 2 ) {
+ // Secondary
+ pointsList.buttons |= 2;
+ } else if ( buttonChanged === 3 ) {
+ // X1 (Back)
+ pointsList.buttons |= 8;
+ } else if ( buttonChanged === 4 ) {
+ // X2 (Forward)
+ pointsList.buttons |= 16;
+ } else if ( buttonChanged === 5 ) {
+ // Pen Eraser
+ pointsList.buttons |= 32;
+ }
+ }
+
+ // Only capture and track primary button, pen, and touch contacts
+ if ( buttonChanged !== 0 ) {
+ eventInfo.shouldCapture = false;
+ eventInfo.shouldReleaseCapture = false;
+
+ // Aux Press
+ if ( tracker.nonPrimaryPressHandler &&
+ !eventInfo.preventGesture &&
+ !eventInfo.defaultPrevented ) {
+ eventInfo.preventDefault = true;
+
+ tracker.nonPrimaryPressHandler(
+ {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
+ button: buttonChanged,
+ buttons: pointsList.buttons,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+
+ return;
+ }
+
+ const updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ // Already tracking the pointer...update it
+ //updateGPoint.captured = true; // Handled by updatePointerCaptured()
+ updateGPoint.insideElementPressed = true;
+ updateGPoint.insideElement = true;
+ updateGPoint.originalTarget = eventInfo.originalEvent.target;
+ updateGPoint.contactPos = gPoint.currentPos;
+ updateGPoint.contactTime = gPoint.currentTime;
+ updateGPoint.lastPos = updateGPoint.currentPos;
+ updateGPoint.lastTime = updateGPoint.currentTime;
+ updateGPoint.currentPos = gPoint.currentPos;
+ updateGPoint.currentTime = gPoint.currentTime;
+
+ gPoint = updateGPoint;
+ } else {
+ // Initialize for tracking and add to the tracking list (no pointerenter event occurred before this)
+ // NOTE: pointerdown event on untracked pointer
+ gPoint.captured = false; // Handled by updatePointerCaptured()
+ gPoint.insideElementPressed = true;
+ gPoint.insideElement = true;
+ gPoint.originalTarget = eventInfo.originalEvent.target;
+ startTrackingPointer( pointsList, gPoint );
+ }
+
+ pointsList.addContact();
+ //$.console.log('contacts++ ', pointsList.contacts);
+
+ if ( !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ eventInfo.shouldCapture = true;
+ eventInfo.shouldReleaseCapture = false;
+ eventInfo.preventDefault = true;
+
+ if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) {
+ $.MouseTracker.gesturePointVelocityTracker.addPoint( tracker, gPoint );
+ }
+
+ if ( pointsList.contacts === 1 ) {
+ // Press
+ if ( tracker.pressHandler && !eventInfo.preventGesture ) {
+ tracker.pressHandler(
+ {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ position: getPointRelativeToAbsolute( gPoint.contactPos, tracker.element ),
+ buttons: pointsList.buttons,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+ } else if ( pointsList.contacts === 2 ) {
+ if ( tracker.pinchHandler && gPoint.type === 'touch' ) {
+ // Initialize for pinch
+ delegate.pinchGPoints = pointsList.asArray();
+ delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
+ delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
+ }
+ }
+ } else {
+ eventInfo.shouldCapture = false;
+ eventInfo.shouldReleaseCapture = false;
+ }
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture points associated with the event.
+ * @param {Number} buttonChanged
+ * The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
+ * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
+ * only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
+ */
+ function updatePointerUp( tracker, eventInfo, gPoint, buttonChanged ) {
+ const delegate = THIS[ tracker.hash ];
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+ let releasePoint;
+ let releaseTime;
+ let wasCaptured = false;
+ let quick;
+
+ if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) {
+ pointsList.buttons = eventInfo.originalEvent.buttons;
+ } else {
+ if ( buttonChanged === 0 ) {
+ // Primary
+ pointsList.buttons ^= ~1;
+ } else if ( buttonChanged === 1 ) {
+ // Aux
+ pointsList.buttons ^= ~4;
+ } else if ( buttonChanged === 2 ) {
+ // Secondary
+ pointsList.buttons ^= ~2;
+ } else if ( buttonChanged === 3 ) {
+ // X1 (Back)
+ pointsList.buttons ^= ~8;
+ } else if ( buttonChanged === 4 ) {
+ // X2 (Forward)
+ pointsList.buttons ^= ~16;
+ } else if ( buttonChanged === 5 ) {
+ // Pen Eraser
+ pointsList.buttons ^= ~32;
+ }
+ }
+
+ eventInfo.shouldCapture = false;
+
+ // Only capture and track primary button, pen, and touch contacts
+ if ( buttonChanged !== 0 ) {
+ eventInfo.shouldReleaseCapture = false;
+
+ // Aux Release
+ if ( tracker.nonPrimaryReleaseHandler &&
+ !eventInfo.preventGesture &&
+ !eventInfo.defaultPrevented ) {
+ eventInfo.preventDefault = true;
+
+ tracker.nonPrimaryReleaseHandler(
+ {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ position: getPointRelativeToAbsolute(gPoint.currentPos, tracker.element),
+ button: buttonChanged,
+ buttons: pointsList.buttons,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+
+ return;
+ }
+
+ let updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ pointsList.removeContact();
+ //$.console.log('contacts-- ', pointsList.contacts);
+
+ // Update the pointer, stop tracking it if not still in this element
+ if ( updateGPoint.captured ) {
+ //updateGPoint.captured = false; // Handled by updatePointerCaptured()
+ wasCaptured = true;
+ }
+ updateGPoint.lastPos = updateGPoint.currentPos;
+ updateGPoint.lastTime = updateGPoint.currentTime;
+ updateGPoint.currentPos = gPoint.currentPos;
+ updateGPoint.currentTime = gPoint.currentTime;
+ if ( !updateGPoint.insideElement ) {
+ stopTrackingPointer( tracker, pointsList, updateGPoint );
+ }
+
+ releasePoint = updateGPoint.currentPos;
+ releaseTime = updateGPoint.currentTime;
+ } else {
+ // NOTE: updatePointerUp(): pointerup on untracked gPoint
+ // ...we'll start to track pointer again
+ gPoint.captured = false; // Handled by updatePointerCaptured()
+ gPoint.insideElementPressed = false;
+ gPoint.insideElement = true;
+ startTrackingPointer( pointsList, gPoint );
+
+ updateGPoint = gPoint;
+ }
+
+ if ( !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ if ( wasCaptured ) {
+ // Pointer was activated in our element but could have been removed in any element since events are captured to our element
+
+ eventInfo.shouldReleaseCapture = true;
+ eventInfo.preventDefault = true;
+
+ if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) {
+ $.MouseTracker.gesturePointVelocityTracker.removePoint( tracker, updateGPoint );
+ }
+
+ if ( pointsList.contacts === 0 ) {
+
+ // Release (pressed in our element)
+ if ( tracker.releaseHandler && releasePoint ) {
+ tracker.releaseHandler(
+ {
+ eventSource: tracker,
+ pointerType: updateGPoint.type,
+ position: getPointRelativeToAbsolute( releasePoint, tracker.element ),
+ buttons: pointsList.buttons,
+ insideElementPressed: updateGPoint.insideElementPressed,
+ insideElementReleased: updateGPoint.insideElement,
+ isTouchEvent: updateGPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+
+ // Drag End
+ if ( tracker.dragEndHandler && delegate.sentDragEvent ) {
+ tracker.dragEndHandler(
+ {
+ eventSource: tracker,
+ pointerType: updateGPoint.type,
+ position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
+ speed: updateGPoint.speed,
+ direction: updateGPoint.direction,
+ shift: eventInfo.originalEvent.shiftKey,
+ isTouchEvent: updateGPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+
+ // We want to clear this flag regardless of whether we fired the dragEndHandler
+ delegate.sentDragEvent = false;
+
+ // Click / Double-Click
+ if ( ( tracker.clickHandler || tracker.dblClickHandler ) && updateGPoint.insideElement ) {
+ quick = releaseTime - updateGPoint.contactTime <= tracker.clickTimeThreshold &&
+ updateGPoint.contactPos.distanceTo( releasePoint ) <= tracker.clickDistThreshold;
+
+ // Click
+ if ( tracker.clickHandler ) {
+ tracker.clickHandler(
+ {
+ eventSource: tracker,
+ pointerType: updateGPoint.type,
+ position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
+ quick: quick,
+ shift: eventInfo.originalEvent.shiftKey,
+ isTouchEvent: updateGPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ originalTarget: updateGPoint.originalTarget,
+ userData: tracker.userData
+ }
+ );
+ }
+
+ // Double-Click
+ if ( tracker.dblClickHandler && quick ) {
+ pointsList.clicks++;
+ if ( pointsList.clicks === 1 ) {
+ delegate.lastClickPos = releasePoint;
+ /*jshint loopfunc:true*/
+ delegate.dblClickTimeOut = setTimeout( function() {
+ pointsList.clicks = 0;
+ }, tracker.dblClickTimeThreshold );
+ /*jshint loopfunc:false*/
+ } else if ( pointsList.clicks === 2 ) {
+ clearTimeout( delegate.dblClickTimeOut );
+ pointsList.clicks = 0;
+ if ( delegate.lastClickPos.distanceTo( releasePoint ) <= tracker.dblClickDistThreshold ) {
+ tracker.dblClickHandler(
+ {
+ eventSource: tracker,
+ pointerType: updateGPoint.type,
+ position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
+ shift: eventInfo.originalEvent.shiftKey,
+ isTouchEvent: updateGPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+ delegate.lastClickPos = null;
+ }
+ }
+ }
+ } else if ( pointsList.contacts === 2 ) {
+ if ( tracker.pinchHandler && updateGPoint.type === 'touch' ) {
+ // Reset for pinch
+ delegate.pinchGPoints = pointsList.asArray();
+ delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
+ delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
+ }
+ }
+ } else {
+ // Pointer was activated in another element but removed in our element
+
+ eventInfo.shouldReleaseCapture = false;
+
+ // Release (pressed in another element)
+ if ( tracker.releaseHandler && releasePoint ) {
+ tracker.releaseHandler(
+ {
+ eventSource: tracker,
+ pointerType: updateGPoint.type,
+ position: getPointRelativeToAbsolute( releasePoint, tracker.element ),
+ buttons: pointsList.buttons,
+ insideElementPressed: updateGPoint.insideElementPressed,
+ insideElementReleased: updateGPoint.insideElement,
+ isTouchEvent: updateGPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ eventInfo.preventDefault = true;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Call when pointer(s) change coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height)
+ *
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture points associated with the event.
+ */
+ function updatePointerMove( tracker, eventInfo, gPoint ) {
+ const delegate = THIS[ tracker.hash ];
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+ let delta;
+
+ if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) {
+ pointsList.buttons = eventInfo.originalEvent.buttons;
+ }
+
+ let updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ // Already tracking the pointer...update it
+ updateGPoint.lastPos = updateGPoint.currentPos;
+ updateGPoint.lastTime = updateGPoint.currentTime;
+ updateGPoint.currentPos = gPoint.currentPos;
+ updateGPoint.currentTime = gPoint.currentTime;
+ } else {
+ // Should never get here, but due to user agent bugs (e.g. legacy touch) it sometimes happens
+ return;
+ }
+
+ eventInfo.shouldCapture = false;
+ eventInfo.shouldReleaseCapture = false;
+
+ // Stop (mouse only)
+ if ( tracker.stopHandler && gPoint.type === 'mouse' ) {
+ clearTimeout( tracker.stopTimeOut );
+ tracker.stopTimeOut = setTimeout( function() {
+ handlePointerStop( tracker, eventInfo.originalEvent, gPoint.type );
+ }, tracker.stopDelay );
+ }
+
+ if ( pointsList.contacts === 0 ) {
+ // Move (no contacts: hovering mouse or other hover-capable device)
+ if ( tracker.moveHandler ) {
+ tracker.moveHandler(
+ {
+ eventSource: tracker,
+ pointerType: gPoint.type,
+ position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
+ buttons: pointsList.buttons,
+ isTouchEvent: gPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+ } else if ( pointsList.contacts === 1 ) {
+ // Move (1 contact)
+ if ( tracker.moveHandler ) {
+ updateGPoint = pointsList.asArray()[ 0 ];
+ tracker.moveHandler(
+ {
+ eventSource: tracker,
+ pointerType: updateGPoint.type,
+ position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
+ buttons: pointsList.buttons,
+ isTouchEvent: updateGPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+
+ // Drag
+ if ( tracker.dragHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ updateGPoint = pointsList.asArray()[ 0 ];
+ delta = updateGPoint.currentPos.minus( updateGPoint.lastPos );
+ tracker.dragHandler(
+ {
+ eventSource: tracker,
+ pointerType: updateGPoint.type,
+ position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
+ buttons: pointsList.buttons,
+ delta: delta,
+ speed: updateGPoint.speed,
+ direction: updateGPoint.direction,
+ shift: eventInfo.originalEvent.shiftKey,
+ isTouchEvent: updateGPoint.type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ eventInfo.preventDefault = true;
+ delegate.sentDragEvent = true;
+ }
+ } else if ( pointsList.contacts === 2 ) {
+ // Move (2 contacts, use center)
+ if ( tracker.moveHandler ) {
+ const gPointArray = pointsList.asArray();
+ tracker.moveHandler(
+ {
+ eventSource: tracker,
+ pointerType: gPointArray[ 0 ].type,
+ position: getPointRelativeToAbsolute( getCenterPoint( gPointArray[ 0 ].currentPos, gPointArray[ 1 ].currentPos ), tracker.element ),
+ buttons: pointsList.buttons,
+ isTouchEvent: gPointArray[ 0 ].type === 'touch',
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ }
+
+ // Pinch
+ if ( tracker.pinchHandler && gPoint.type === 'touch' &&
+ !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
+ delta = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
+ if ( delta !== delegate.currentPinchDist ) {
+ delegate.lastPinchDist = delegate.currentPinchDist;
+ delegate.currentPinchDist = delta;
+ delegate.lastPinchCenter = delegate.currentPinchCenter;
+ delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
+ tracker.pinchHandler(
+ {
+ eventSource: tracker,
+ pointerType: 'touch',
+ gesturePoints: delegate.pinchGPoints,
+ lastCenter: getPointRelativeToAbsolute( delegate.lastPinchCenter, tracker.element ),
+ center: getPointRelativeToAbsolute( delegate.currentPinchCenter, tracker.element ),
+ lastDistance: delegate.lastPinchDist,
+ distance: delegate.currentPinchDist,
+ shift: eventInfo.originalEvent.shiftKey,
+ originalEvent: eventInfo.originalEvent,
+ userData: tracker.userData
+ }
+ );
+ eventInfo.preventDefault = true;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ * @param {OpenSeadragon.MouseTracker} tracker
+ * A reference to the MouseTracker instance.
+ * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
+ * Processing info for originating DOM event.
+ * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
+ * Gesture points associated with the event.
+ */
+ function updatePointerCancel( tracker, eventInfo, gPoint ) {
+ const pointsList = tracker.getActivePointersListByType( gPoint.type );
+ const updateGPoint = pointsList.getById( gPoint.id );
+
+ if ( updateGPoint ) {
+ stopTrackingPointer( tracker, pointsList, updateGPoint );
+ }
+ }
+
+
+ /**
+ * @private
+ * @inner
+ */
+ function handlePointerStop( tracker, originalMoveEvent, pointerType ) {
+ if ( tracker.stopHandler ) {
+ tracker.stopHandler( {
+ eventSource: tracker,
+ pointerType: pointerType,
+ position: getMouseRelative( originalMoveEvent, tracker.element ),
+ buttons: tracker.getActivePointersListByType( pointerType ).buttons,
+ isTouchEvent: pointerType === 'touch',
+ originalEvent: originalMoveEvent,
+ userData: tracker.userData
+ } );
+ }
+ }
+
+
+ /**
+ * @function
+ * @private
+ * @inner
+ */
+ function uniqueHash( ) {
+ let uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
+ while (uniqueId in THIS) {
+ // rehash when not unique
+ uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
+ }
+ return uniqueId;
+ }
+
+}(OpenSeadragon));
+
+/*
+ * OpenSeadragon - Control
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function( $ ){
+
+/**
+ * An enumeration of supported locations where controls can be anchored.
+ * The anchoring is always relative to the container.
+ * @member ControlAnchor
+ * @memberof OpenSeadragon
+ * @static
+ * @type {Object}
+ * @property {Number} NONE
+ * @property {Number} TOP_LEFT
+ * @property {Number} TOP_RIGHT
+ * @property {Number} BOTTOM_LEFT
+ * @property {Number} BOTTOM_RIGHT
+ * @property {Number} ABSOLUTE
+ */
+$.ControlAnchor = {
+ NONE: 0,
+ TOP_LEFT: 1,
+ TOP_RIGHT: 2,
+ BOTTOM_RIGHT: 3,
+ BOTTOM_LEFT: 4,
+ ABSOLUTE: 5
+};
+
+/**
+ * @class Control
+ * @classdesc A Control represents any interface element which is meant to allow the user
+ * to interact with the zoomable interface. Any control can be anchored to any
+ * element.
+ *
+ * @memberof OpenSeadragon
+ * @param {Element} element - the control element to be anchored in the container.
+ * @param {Object } options - All required and optional settings for configuring a control element.
+ * @param {OpenSeadragon.ControlAnchor} [options.anchor=OpenSeadragon.ControlAnchor.NONE] - the position of the control
+ * relative to the container.
+ * @param {Boolean} [options.attachToViewer=true] - Whether the control should be added directly to the viewer, or
+ * directly to the container
+ * @param {Boolean} [options.autoFade=true] - Whether the control should have the autofade behavior
+ * @param {Element} container - the element to control will be anchored too.
+ */
+$.Control = function ( element, options, container ) {
+
+ const parent = element.parentNode;
+ if (typeof options === 'number')
+ {
+ $.console.error("Passing an anchor directly into the OpenSeadragon.Control constructor is deprecated; " +
+ "please use an options object instead. " +
+ "Support for this deprecated variant is scheduled for removal in December 2013");
+ options = {anchor: options};
+ }
+ options.attachToViewer = (typeof options.attachToViewer === 'undefined') ? true : options.attachToViewer;
+ /**
+ * True if the control should have autofade behavior.
+ * @member {Boolean} autoFade
+ * @memberof OpenSeadragon.Control#
+ */
+ this.autoFade = (typeof options.autoFade === 'undefined') ? true : options.autoFade;
+ /**
+ * The element providing the user interface with some type of control (e.g. a zoom-in button).
+ * @member {Element} element
+ * @memberof OpenSeadragon.Control#
+ */
+ this.element = element;
+ /**
+ * The position of the Control relative to its container.
+ * @member {OpenSeadragon.ControlAnchor} anchor
+ * @memberof OpenSeadragon.Control#
+ */
+ this.anchor = options.anchor;
+ /**
+ * The Control's containing element.
+ * @member {Element} container
+ * @memberof OpenSeadragon.Control#
+ */
+ this.container = container;
+ /**
+ * A neutral element surrounding the control element.
+ * @member {Element} wrapper
+ * @memberof OpenSeadragon.Control#
+ */
+ if ( this.anchor === $.ControlAnchor.ABSOLUTE ) {
+ this.wrapper = $.makeNeutralElement( "div" );
+ this.wrapper.style.position = "absolute";
+ this.wrapper.style.top = typeof (options.top) === "number" ? (options.top + 'px') : options.top;
+ this.wrapper.style.left = typeof (options.left) === "number" ? (options.left + 'px') : options.left;
+ this.wrapper.style.height = typeof (options.height) === "number" ? (options.height + 'px') : options.height;
+ this.wrapper.style.width = typeof (options.width) === "number" ? (options.width + 'px') : options.width;
+ this.wrapper.style.margin = "0px";
+ this.wrapper.style.padding = "0px";
+
+ this.element.style.position = "relative";
+ this.element.style.top = "0px";
+ this.element.style.left = "0px";
+ this.element.style.height = "100%";
+ this.element.style.width = "100%";
+ } else {
+ this.wrapper = $.makeNeutralElement( "div" );
+ this.wrapper.style.display = "inline-block";
+ if ( this.anchor === $.ControlAnchor.NONE ) {
+ // IE6 fix
+ this.wrapper.style.width = this.wrapper.style.height = "100%";
+ }
+ }
+ this.wrapper.appendChild( this.element );
+
+ if (options.attachToViewer ) {
+ if ( this.anchor === $.ControlAnchor.TOP_RIGHT ||
+ this.anchor === $.ControlAnchor.BOTTOM_RIGHT ) {
+ this.container.insertBefore(
+ this.wrapper,
+ this.container.firstChild
+ );
+ } else {
+ this.container.appendChild( this.wrapper );
+ }
+ } else {
+ parent.appendChild( this.wrapper );
+ }
+
+};
+
+/** @lends OpenSeadragon.Control.prototype */
+$.Control.prototype = {
+
+ /**
+ * Removes the control from the container.
+ * @function
+ */
+ destroy: function() {
+ this.wrapper.removeChild( this.element );
+ if (this.anchor !== $.ControlAnchor.NONE) {
+ this.container.removeChild(this.wrapper);
+ }
+ },
+
+ /**
+ * Determines if the control is currently visible.
+ * @function
+ * @returns {Boolean} true if currently visible, false otherwise.
+ */
+ isVisible: function() {
+ return this.wrapper.style.display !== "none";
+ },
+
+ /**
+ * Toggles the visibility of the control.
+ * @function
+ * @param {Boolean} visible - true to make visible, false to hide.
+ */
+ setVisible: function( visible ) {
+ this.wrapper.style.display = visible ?
+ ( this.anchor === $.ControlAnchor.ABSOLUTE ? 'block' : 'inline-block' ) :
+ "none";
+ },
+
+ /**
+ * Sets the opacity level for the control.
+ * @function
+ * @param {Number} opactiy - a value between 1 and 0 inclusively.
+ */
+ setOpacity: function( opacity ) {
+ $.setElementOpacity( this.wrapper, opacity, true );
+ }
+};
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - ControlDock
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function( $ ){
+ /**
+ * @class ControlDock
+ * @classdesc Provides a container element (a <form> element) with support for the layout of control elements.
+ *
+ * @memberof OpenSeadragon
+ */
+ $.ControlDock = function( options ){
+ const layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'];
+
+ $.extend( true, this, {
+ id: 'controldock-' + $.now() + '-' + Math.floor(Math.random() * 1000000),
+ container: $.makeNeutralElement( 'div' ),
+ controls: []
+ }, options );
+
+ // Disable the form's submit; otherwise button clicks and return keys
+ // can trigger it.
+ this.container.onsubmit = function() {
+ return false;
+ };
+
+ if( this.element ){
+ this.element = $.getElement( this.element );
+ this.element.appendChild( this.container );
+ if( $.getElementStyle(this.element).position === 'static' ){
+ this.element.style.position = 'relative';
+ }
+ this.container.style.width = '100%';
+ this.container.style.height = '100%';
+ }
+
+ for( let i = 0; i < layouts.length; i++ ){
+ let layout = layouts[ i ];
+ this.controls[ layout ] = $.makeNeutralElement( "div" );
+ this.controls[ layout ].style.position = 'absolute';
+ if ( layout.match( 'left' ) ){
+ this.controls[ layout ].style.left = '0px';
+ }
+ if ( layout.match( 'right' ) ){
+ this.controls[ layout ].style.right = '0px';
+ }
+ if ( layout.match( 'top' ) ){
+ this.controls[ layout ].style.top = '0px';
+ }
+ if ( layout.match( 'bottom' ) ){
+ this.controls[ layout ].style.bottom = '0px';
+ }
+ }
+
+ this.container.appendChild( this.controls.topleft );
+ this.container.appendChild( this.controls.topright );
+ this.container.appendChild( this.controls.bottomright );
+ this.container.appendChild( this.controls.bottomleft );
+ };
+
+ /** @lends OpenSeadragon.ControlDock.prototype */
+ $.ControlDock.prototype = {
+
+ /**
+ * @function
+ */
+ addControl: function ( element, controlOptions ) {
+ element = $.getElement( element );
+ let div = null;
+
+ if ( getControlIndex( this, element ) >= 0 ) {
+ return; // they're trying to add a duplicate control
+ }
+
+ switch ( controlOptions.anchor ) {
+ case $.ControlAnchor.TOP_RIGHT:
+ div = this.controls.topright;
+ element.style.position = "relative";
+ element.style.paddingRight = "0px";
+ element.style.paddingTop = "0px";
+ break;
+ case $.ControlAnchor.BOTTOM_RIGHT:
+ div = this.controls.bottomright;
+ element.style.position = "relative";
+ element.style.paddingRight = "0px";
+ element.style.paddingBottom = "0px";
+ break;
+ case $.ControlAnchor.BOTTOM_LEFT:
+ div = this.controls.bottomleft;
+ element.style.position = "relative";
+ element.style.paddingLeft = "0px";
+ element.style.paddingBottom = "0px";
+ break;
+ case $.ControlAnchor.TOP_LEFT:
+ div = this.controls.topleft;
+ element.style.position = "relative";
+ element.style.paddingLeft = "0px";
+ element.style.paddingTop = "0px";
+ break;
+ case $.ControlAnchor.ABSOLUTE:
+ div = this.container;
+ element.style.margin = "0px";
+ element.style.padding = "0px";
+ break;
+ default:
+ case $.ControlAnchor.NONE:
+ div = this.container;
+ element.style.margin = "0px";
+ element.style.padding = "0px";
+ break;
+ }
+
+ this.controls.push(
+ new $.Control( element, controlOptions, div )
+ );
+ element.style.display = "inline-block";
+ },
+
+
+ /**
+ * @function
+ * @returns {OpenSeadragon.ControlDock} Chainable.
+ */
+ removeControl: function ( element ) {
+ element = $.getElement( element );
+ const i = getControlIndex( this, element );
+
+ if ( i >= 0 ) {
+ this.controls[ i ].destroy();
+ this.controls.splice( i, 1 );
+ }
+
+ return this;
+ },
+
+ /**
+ * @function
+ * @returns {OpenSeadragon.ControlDock} Chainable.
+ */
+ clearControls: function () {
+ while ( this.controls.length > 0 ) {
+ this.controls.pop().destroy();
+ }
+
+ return this;
+ },
+
+
+ /**
+ * @function
+ * @returns {Boolean}
+ */
+ areControlsEnabled: function () {
+ for ( let i = this.controls.length - 1; i >= 0; i-- ) {
+ if ( this.controls[ i ].isVisible() ) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+
+ /**
+ * @function
+ * @returns {OpenSeadragon.ControlDock} Chainable.
+ */
+ setControlsEnabled: function( enabled ) {
+ for (let i = this.controls.length - 1; i >= 0; i-- ) {
+ this.controls[ i ].setVisible( enabled );
+ }
+
+ return this;
+ }
+
+ };
+
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // Utility methods
+ ///////////////////////////////////////////////////////////////////////////////
+ function getControlIndex( dock, element ) {
+ const controls = dock.controls;
+
+ for (let i = controls.length - 1; i >= 0; i-- ) {
+ if ( controls[ i ].element === element ) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - Placement
+ *
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function($) {
+
+ /**
+ * An enumeration of positions to anchor an element.
+ * @member Placement
+ * @memberOf OpenSeadragon
+ * @static
+ * @readonly
+ * @property {OpenSeadragon.Placement} CENTER
+ * @property {OpenSeadragon.Placement} TOP_LEFT
+ * @property {OpenSeadragon.Placement} TOP
+ * @property {OpenSeadragon.Placement} TOP_RIGHT
+ * @property {OpenSeadragon.Placement} RIGHT
+ * @property {OpenSeadragon.Placement} BOTTOM_RIGHT
+ * @property {OpenSeadragon.Placement} BOTTOM
+ * @property {OpenSeadragon.Placement} BOTTOM_LEFT
+ * @property {OpenSeadragon.Placement} LEFT
+ */
+ $.Placement = $.freezeObject({
+ CENTER: 0,
+ TOP_LEFT: 1,
+ TOP: 2,
+ TOP_RIGHT: 3,
+ RIGHT: 4,
+ BOTTOM_RIGHT: 5,
+ BOTTOM: 6,
+ BOTTOM_LEFT: 7,
+ LEFT: 8,
+ properties: {
+ 0: {
+ isLeft: false,
+ isHorizontallyCentered: true,
+ isRight: false,
+ isTop: false,
+ isVerticallyCentered: true,
+ isBottom: false
+ },
+ 1: {
+ isLeft: true,
+ isHorizontallyCentered: false,
+ isRight: false,
+ isTop: true,
+ isVerticallyCentered: false,
+ isBottom: false
+ },
+ 2: {
+ isLeft: false,
+ isHorizontallyCentered: true,
+ isRight: false,
+ isTop: true,
+ isVerticallyCentered: false,
+ isBottom: false
+ },
+ 3: {
+ isLeft: false,
+ isHorizontallyCentered: false,
+ isRight: true,
+ isTop: true,
+ isVerticallyCentered: false,
+ isBottom: false
+ },
+ 4: {
+ isLeft: false,
+ isHorizontallyCentered: false,
+ isRight: true,
+ isTop: false,
+ isVerticallyCentered: true,
+ isBottom: false
+ },
+ 5: {
+ isLeft: false,
+ isHorizontallyCentered: false,
+ isRight: true,
+ isTop: false,
+ isVerticallyCentered: false,
+ isBottom: true
+ },
+ 6: {
+ isLeft: false,
+ isHorizontallyCentered: true,
+ isRight: false,
+ isTop: false,
+ isVerticallyCentered: false,
+ isBottom: true
+ },
+ 7: {
+ isLeft: true,
+ isHorizontallyCentered: false,
+ isRight: false,
+ isTop: false,
+ isVerticallyCentered: false,
+ isBottom: true
+ },
+ 8: {
+ isLeft: true,
+ isHorizontallyCentered: false,
+ isRight: false,
+ isTop: false,
+ isVerticallyCentered: true,
+ isBottom: false
+ }
+ }
+ });
+
+}(OpenSeadragon));
+
+/*
+ * OpenSeadragon - Viewer
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2025 OpenSeadragon contributors
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of CodePlex Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function( $ ){
+
+// dictionary from hash to private properties
+const THIS = {};
+let nextHash = 1;
+
+/**
+ *
+ * The main point of entry into creating a zoomable image on the page.
+ *
+ * We have provided an idiomatic javascript constructor which takes
+ * a single object, but still support the legacy positional arguments.
+ *
+ * The options below are given in order that they appeared in the constructor
+ * as arguments and we translate a positional call into an idiomatic call.
+ *
+ * To create a viewer, you can use either of this methods:
+ *
+ *
var viewer = new OpenSeadragon.Viewer(options);
+ *
var viewer = OpenSeadragon(options);
+ *
+ * @class Viewer
+ * @classdesc The main OpenSeadragon viewer class.
+ *
+ * @memberof OpenSeadragon
+ * @extends OpenSeadragon.EventSource
+ * @extends OpenSeadragon.ControlDock
+ * @param {OpenSeadragon.Options} options - Viewer options.
+ *
+ **/
+$.Viewer = function( options ) {
+
+ const args = arguments;
+ const _this = this;
+ let i;
+
+
+ //backward compatibility for positional args while preferring more
+ //idiomatic javascript options object as the only argument
+ if( !$.isPlainObject( options ) ){
+ options = {
+ id: args[ 0 ],
+ xmlPath: args.length > 1 ? args[ 1 ] : undefined,
+ prefixUrl: args.length > 2 ? args[ 2 ] : undefined,
+ controls: args.length > 3 ? args[ 3 ] : undefined,
+ overlays: args.length > 4 ? args[ 4 ] : undefined
+ };
+ }
+
+ //options.config and the general config argument are deprecated
+ //in favor of the more direct specification of optional settings
+ //being pass directly on the options object
+ if ( options.config ){
+ $.extend( true, options, options.config );
+ delete options.config;
+ }
+
+ // Move deprecated drawer options from the base options object into a sub-object
+ // This is an array to make it easy to add additional properties to convert to
+ // drawer options later if it makes sense to set at the drawer level rather than
+ // per tiled image (for example, subPixelRoundingForTransparency).
+ const drawerOptionList = [
+ 'useCanvas', // deprecated
+ ];
+ options.drawerOptions = Object.assign({},
+ drawerOptionList.reduce((drawerOptions, option) => {
+ drawerOptions[option] = options[option];
+ delete options[option];
+ return drawerOptions;
+ }, {}),
+ options.drawerOptions);
+
+ //Public properties
+ //Allow the options object to override global defaults
+ $.extend( true, this, {
+
+ //internal state and dom identifiers
+ id: options.id,
+ hash: options.hash || nextHash++,
+ /**
+ * Parent viewer reference. Base Viewer has null reference, child viewers (such as navigator
+ * or reference strip) must reference the parent viewer they were spawned from.
+ * @member {OpenSeadragon.Viewer} viewer
+ * @memberof OpenSeadragon.Viewer#
+ */
+ viewer: null,
+ /**
+ * Index for page to be shown first next time open() is called (only used in sequenceMode).
+ * @member {Number} initialPage
+ * @memberof OpenSeadragon.Viewer#
+ */
+ initialPage: 0,
+
+ //dom nodes
+ /**
+ * The parent element of this Viewer instance, passed in when the Viewer was created.
+ * @member {Element} element
+ * @memberof OpenSeadragon.Viewer#
+ */
+ element: null,
+ /**
+ * A <div> element (provided by {@link OpenSeadragon.ControlDock}), the base element of this Viewer instance.
+ * Child element of {@link OpenSeadragon.Viewer#element}.
+ * @member {Element} container
+ * @memberof OpenSeadragon.Viewer#
+ */
+ container: null,
+ /**
+ * A <div> element, the element where user-input events are handled for panning and zooming.
+ * Child element of {@link OpenSeadragon.Viewer#container},
+ * positioned on top of {@link OpenSeadragon.Viewer#keyboardCommandArea}.
+ * The parent of {@link OpenSeadragon.Drawer#canvas} instances.
+ * @member {Element} canvas
+ * @memberof OpenSeadragon.Viewer#
+ */
+ canvas: null,
+
+ // Overlays list. An overlay allows to add html on top of the viewer.
+ overlays: [],
+ // Container inside the canvas where overlays are drawn.
+ overlaysContainer: null,
+
+ //private state properties
+
+ // When we go full-screen we insert ourselves into the body and make
+ // everything else hidden. This is basically the same as
+ // `requestFullScreen` but works in all browsers: iPhone is known to not
+ // allow full-screen with the requestFullScreen API. This holds the
+ // children of the body and their display values, so we can undo our
+ // changes when we go out of full-screen
+ previousDisplayValuesOfBodyChildren: [],
+
+ //This was originally initialized in the constructor and so could never
+ //have anything in it. now it can because we allow it to be specified
+ //in the options and is only empty by default if not specified. Also
+ //this array was returned from get_controls which I find confusing
+ //since this object has a controls property which is treated in other
+ //functions like clearControls. I'm removing the accessors.
+ customControls: [],
+
+ //These are originally not part options but declared as members
+ //in initialize. It's still considered idiomatic to put them here
+ //source is here for backwards compatibility. It is not an official
+ //part of the API and should not be relied upon.
+ source: null,
+ /**
+ * Handles rendering of tiles in the viewer. Created for each TileSource opened.
+ * @member {OpenSeadragon.Drawer} drawer
+ * @memberof OpenSeadragon.Viewer#
+ */
+ drawer: null,
+ /**
+ * Resolved list of drawer type strings (after expanding 'auto', de-duplicating, and
+ * normalizing: constructors are replaced by their getType() result). Used to decide
+ * allowed fallbacks: WebGL drawer only falls back to canvas when the string 'canvas' is
+ * in this list (see per-tile and context-loss fallback). Normalized so includes('canvas')
+ * is reliable even when custom drawer constructors were passed in options.
+ * @member {string[]} drawerCandidates
+ * @memberof OpenSeadragon.Viewer#
+ */
+ drawerCandidates: null,
+ /**
+ * Keeps track of all of the tiled images in the scene.
+ * @member {OpenSeadragon.World} world
+ * @memberof OpenSeadragon.Viewer#
+ */
+ world: null,
+ /**
+ * Handles coordinate-related functionality - zoom, pan, rotation, etc. Created for each TileSource opened.
+ * @member {OpenSeadragon.Viewport} viewport
+ * @memberof OpenSeadragon.Viewer#
+ */
+ viewport: null,
+ /**
+ * @member {OpenSeadragon.Navigator} navigator
+ * @memberof OpenSeadragon.Viewer#
+ */
+ navigator: null,
+
+ //A collection viewport is a separate viewport used to provide
+ //simultaneous rendering of sets of tiles
+ collectionViewport: null,
+ collectionDrawer: null,
+
+ //UI image resources
+ //TODO: rename navImages to uiImages
+ navImages: null,
+
+ //interface button controls
+ buttonGroup: null,
+
+ //TODO: this is defunct so safely remove it
+ profiler: null
+
+ }, $.DEFAULT_SETTINGS, options );
+
+ if ( typeof ( this.hash) === "undefined" ) {
+ throw new Error("A hash must be defined, either by specifying options.id or options.hash.");
+ }
+ if ( typeof ( THIS[ this.hash ] ) !== "undefined" ) {
+ // We don't want to throw an error here, as the user might have discarded
+ // the previous viewer with the same hash and now want to recreate it.
+ $.console.warn("Hash " + this.hash + " has already been used.");
+ }
+
+ //Private state properties
+ THIS[ this.hash ] = {
+ fsBoundsDelta: new $.Point( 1, 1 ),
+ prevContainerSize: null,
+ animating: false,
+ forceRedraw: false,
+ needsResize: false,
+ forceResize: false,
+ mouseInside: false,
+ group: null,
+ // whether we should be continuously zooming
+ zooming: false,
+ // how much we should be continuously zooming by
+ zoomFactor: null,
+ lastZoomTime: null,
+ fullPage: false,
+ onfullscreenchange: null,
+ lastClickTime: null,
+ draggingToZoom: false,
+ };
+
+ this._sequenceIndex = 0;
+ this._firstOpen = true;
+ this._updateRequestId = null;
+ this._loadQueue = [];
+ this.currentOverlays = [];
+ this._updatePixelDensityRatioBind = null;
+
+ this._lastScrollTime = $.now(); // variable used to help normalize the scroll event speed of different devices
+
+ this._fullyLoaded = false; // variable used to track the viewer's aggregate loading state.
+
+ this._navActionFrames = {}; // tracks cumulative pan distance per key press
+ this._navActionVirtuallyHeld = {}; // marks keys virtually held after early release
+ this._minNavActionFrames = 10; // minimum pan distance per tap or key press
+
+ this._activeActions = { // variable to keep track of currently pressed action
+ // Basic arrow key panning (no modifiers)
+ panUp: false,
+ panDown: false,
+ panLeft: false,
+ panRight: false,
+
+ // Modifier-based actions
+ zoomIn: false, // Shift + Up
+ zoomOut: false // Shift + Down
+ };
+
+
+ //Inherit some behaviors and properties
+ $.EventSource.call( this );
+
+ this.addHandler( 'open-failed', function ( event ) {
+ const msg = $.getString( "Errors.OpenFailed", event.eventSource, event.message);
+ _this._showMessage( msg );
+ });
+
+ $.ControlDock.call( this, options );
+
+ //Deal with tile sources
+ if (this.xmlPath) {
+ //Deprecated option. Now it is preferred to use the tileSources option
+ this.tileSources = [ this.xmlPath ];
+ }
+
+ this.element = this.element || document.getElementById( this.id );
+ this.canvas = $.makeNeutralElement( "div" );
+ this.canvas.className = "openseadragon-canvas";
+
+ // Injecting mobile-only CSS to remove focus outline
+ if (!document.querySelector('style[data-openseadragon-mobile-css]')) {
+ const style = document.createElement('style');
+ style.setAttribute('data-openseadragon-mobile-css', 'true');
+ style.textContent =
+ '@media (hover: none) {' +
+ ' .openseadragon-canvas:focus {' +
+ ' outline: none !important;' +
+ ' }' +
+ '}';
+ document.head.appendChild(style);
+ }
+
+ (function( style ){
+ style.width = "100%";
+ style.height = "100%";
+ style.overflow = "hidden";
+ style.position = "absolute";
+ style.top = "0px";
+ style.left = "0px";
+ }(this.canvas.style));
+ $.setElementTouchActionNone( this.canvas );
+ if (options.tabIndex !== "") {
+ this.canvas.tabIndex = (options.tabIndex === undefined ? 0 : options.tabIndex);
+ }
+
+ //the container is created through applying the ControlDock constructor above
+ this.container.className = "openseadragon-container";
+ (function( style ){
+ style.width = "100%";
+ style.height = "100%";
+ style.position = "relative";
+ style.overflow = "hidden";
+ style.left = "0px";
+ style.top = "0px";
+ style.textAlign = "left"; // needed to protect against
+ }( this.container.style ));
+ $.setElementTouchActionNone( this.container );
+
+ this.container.insertBefore( this.canvas, this.container.firstChild );
+ this.element.appendChild( this.container );
+ //Used for toggling between fullscreen and default container size
+ //TODO: these can be closure private and shared across Viewer
+ // instances.
+ this.bodyWidth = document.body.style.width;
+ this.bodyHeight = document.body.style.height;
+ this.bodyOverflow = document.body.style.overflow;
+ this.docOverflow = document.documentElement.style.overflow;
+
+ this.innerTracker = new $.MouseTracker({
+ userData: 'Viewer.innerTracker',
+ element: this.canvas,
+ startDisabled: !this.mouseNavEnabled,
+ clickTimeThreshold: this.clickTimeThreshold,
+ clickDistThreshold: this.clickDistThreshold,
+ dblClickTimeThreshold: this.dblClickTimeThreshold,
+ dblClickDistThreshold: this.dblClickDistThreshold,
+ contextMenuHandler: $.delegate( this, onCanvasContextMenu ),
+ keyDownHandler: $.delegate( this, onCanvasKeyDown ),
+ keyUpHandler: $.delegate(this, onCanvasKeyUp),
+ keyHandler: $.delegate( this, onCanvasKeyPress ),
+ clickHandler: $.delegate( this, onCanvasClick ),
+ dblClickHandler: $.delegate( this, onCanvasDblClick ),
+ dragHandler: $.delegate( this, onCanvasDrag ),
+ dragEndHandler: $.delegate( this, onCanvasDragEnd ),
+ enterHandler: $.delegate( this, onCanvasEnter ),
+ leaveHandler: $.delegate( this, onCanvasLeave ),
+ pressHandler: $.delegate( this, onCanvasPress ),
+ releaseHandler: $.delegate( this, onCanvasRelease ),
+ nonPrimaryPressHandler: $.delegate( this, onCanvasNonPrimaryPress ),
+ nonPrimaryReleaseHandler: $.delegate( this, onCanvasNonPrimaryRelease ),
+ scrollHandler: $.delegate( this, onCanvasScroll ),
+ pinchHandler: $.delegate( this, onCanvasPinch ),
+ focusHandler: $.delegate( this, onCanvasFocus ),
+ blurHandler: $.delegate( this, onCanvasBlur ),
+ });
+
+ this.outerTracker = new $.MouseTracker({
+ userData: 'Viewer.outerTracker',
+ element: this.container,
+ startDisabled: !this.mouseNavEnabled,
+ clickTimeThreshold: this.clickTimeThreshold,
+ clickDistThreshold: this.clickDistThreshold,
+ dblClickTimeThreshold: this.dblClickTimeThreshold,
+ dblClickDistThreshold: this.dblClickDistThreshold,
+ enterHandler: $.delegate( this, onContainerEnter ),
+ leaveHandler: $.delegate( this, onContainerLeave )
+ });
+
+ if( this.toolbar ){
+ this.toolbar = new $.ControlDock({ element: this.toolbar });
+ }
+
+ this.bindStandardControls();
+
+ THIS[ this.hash ].prevContainerSize = _getSafeElemSize( this.container );
+
+ if(window.ResizeObserver){
+ this._autoResizePolling = false;
+ this._resizeObserver = new ResizeObserver(function(){
+ THIS[_this.hash].needsResize = true;
+ });
+
+ this._resizeObserver.observe(this.container, {});
+ } else {
+ this._autoResizePolling = true;
+ }
+
+ // Create the world
+ this.world = new $.World({
+ viewer: this
+ });
+
+ this.world.addHandler('add-item', function(event) {
+ // For backwards compatibility, we maintain the source property
+ _this.source = _this.world.getItemAt(0).source;
+
+ THIS[ _this.hash ].forceRedraw = true;
+
+ if (!_this._updateRequestId) {
+ _this._updateRequestId = scheduleUpdate( _this, updateMulti );
+ }
+
+ const tiledImage = event.item;
+ const fullyLoadedHandler = function() {
+ const newFullyLoaded = _this._areAllFullyLoaded();
+ if (newFullyLoaded !== _this._fullyLoaded) {
+ _this._fullyLoaded = newFullyLoaded;
+
+ /**
+ * Fired when the viewer's aggregate "fully loaded" state changes (when all
+ * TiledImages in the world have loaded tiles for the current view resolution).
+ *
+ * @event fully-loaded-change
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {Boolean} fullyLoaded - The new aggregate "fully loaded" value
+ * @property {OpenSeadragon.Viewer} eventSource - Reference to the Viewer instance
+ * @property {?Object} userData - Arbitrary subscriber-defined object
+ */
+ _this.raiseEvent('fully-loaded-change', {
+ fullyLoaded: newFullyLoaded
+ });
+ }
+ };
+ tiledImage._fullyLoadedHandlerForViewer = fullyLoadedHandler;
+ tiledImage.addHandler('fully-loaded-change', fullyLoadedHandler);
+ });
+
+ this.world.addHandler('remove-item', function(event) {
+ const tiledImage = event.item;
+
+ // SAFE cleanup with existence check
+ if (tiledImage._fullyLoadedHandlerForViewer) {
+ tiledImage.removeHandler('fully-loaded-change', tiledImage._fullyLoadedHandlerForViewer);
+ delete tiledImage._fullyLoadedHandlerForViewer; // Remove the reference
+ }
+
+ // For backwards compatibility, we maintain the source property
+ if (_this.world.getItemCount()) {
+ _this.source = _this.world.getItemAt(0).source;
+ } else {
+ _this.source = null;
+ }
+
+ THIS[ _this.hash ].forceRedraw = true;
+ });
+
+ this.world.addHandler('metrics-change', function(event) {
+ if (_this.viewport) {
+ _this.viewport._setContentBounds(_this.world.getHomeBounds(), _this.world.getContentFactor());
+ }
+ });
+
+ this.world.addHandler('item-index-change', function(event) {
+ // For backwards compatibility, we maintain the source property
+ _this.source = _this.world.getItemAt(0).source;
+ });
+
+ // Create the viewport
+ this.viewport = new $.Viewport({
+ containerSize: THIS[ this.hash ].prevContainerSize,
+ springStiffness: this.springStiffness,
+ animationTime: this.animationTime,
+ minZoomImageRatio: this.minZoomImageRatio,
+ maxZoomPixelRatio: this.maxZoomPixelRatio,
+ visibilityRatio: this.visibilityRatio,
+ wrapHorizontal: this.wrapHorizontal,
+ wrapVertical: this.wrapVertical,
+ defaultZoomLevel: this.defaultZoomLevel,
+ minZoomLevel: this.minZoomLevel,
+ maxZoomLevel: this.maxZoomLevel,
+ viewer: this,
+ degrees: this.degrees,
+ flipped: this.flipped,
+ overlayPreserveContentDirection: this.overlayPreserveContentDirection,
+ navigatorRotate: this.navigatorRotate,
+ homeFillsViewer: this.homeFillsViewer,
+ margins: this.viewportMargins,
+ silenceMultiImageWarnings: this.silenceMultiImageWarnings
+ });
+
+ this.viewport._setContentBounds(this.world.getHomeBounds(), this.world.getContentFactor());
+
+ // Create the image loader
+ this.imageLoader = new $.ImageLoader({
+ jobLimit: this.imageLoaderLimit,
+ timeout: options.timeout,
+ tileRetryMax: this.tileRetryMax,
+ tileRetryDelay: this.tileRetryDelay
+ });
+
+ // Create the tile cache
+ this.tileCache = new $.TileCache({
+ viewer: this,
+ maxImageCacheCount: this.maxImageCacheCount
+ });
+
+ //Create the drawer based on selected options
+ if (Object.prototype.hasOwnProperty.call(this.drawerOptions, 'useCanvas') ){
+ $.console.error('useCanvas is deprecated, use the "drawer" option to indicate preferred drawer(s)');
+
+ // for backwards compatibility, use HTMLDrawer if useCanvas is defined and is falsey
+ if (!this.drawerOptions.useCanvas){
+ this.drawer = $.HTMLDrawer;
+ }
+
+ delete this.drawerOptions.useCanvas;
+ }
+ let drawerCandidates = Array.isArray(this.drawer) ? this.drawer : [this.drawer];
+ if (drawerCandidates.length === 0){
+ // if an empty array was passed in, throw a warning and use the defaults
+ // note: if the drawer option is not specified, the defaults will already be set so this won't apply
+ drawerCandidates = [$.DEFAULT_SETTINGS.drawer].flat(); // ensure it is a list
+ $.console.warn('No valid drawers were selected. Using the default value.');
+ }
+
+ // 'auto' is expanded in the candidate list in a platform-dependent way: on iOS-like devices
+ // to ['canvas'] only, on other platforms to ['webgl', 'canvas'] so that if WebGL fails at
+ // creation, canvas is tried next. Same detection as getAutoDrawerCandidates() / determineDrawer('auto').
+ drawerCandidates = drawerCandidates.flatMap(
+ function(c) {
+ return c === 'auto' ? getAutoDrawerCandidates() : [c];
+ }
+ );
+ drawerCandidates = drawerCandidates.filter(
+ function(c, i, arr) {
+ return arr.indexOf(c) === i;
+ }
+ );
+ this.drawerCandidates = drawerCandidates.map(getDrawerTypeString).filter(Boolean);
+
+ this.drawer = null;
+ for (const drawerCandidate of drawerCandidates){
+ const success = this.requestDrawer(drawerCandidate, {mainDrawer: true, redrawImmediately: false});
+ if(success){
+ break;
+ }
+ }
+
+ if (!this.drawer){
+ $.console.error('No drawer could be created!');
+ throw('Error with creating the selected drawer(s)');
+ }
+
+ // Pass the imageSmoothingEnabled option along to the drawer
+ this.drawer.setImageSmoothingEnabled(this.imageSmoothingEnabled);
+
+ // Overlay container
+ this.overlaysContainer = $.makeNeutralElement( "div" );
+ this.canvas.appendChild( this.overlaysContainer );
+
+ // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons
+ if (!this.drawer.canRotate()) {
+ // Disable/remove the rotate left/right buttons since they aren't supported
+ if (this.rotateLeft) {
+ i = this.buttonGroup.buttons.indexOf(this.rotateLeft);
+ this.buttonGroup.buttons.splice(i, 1);
+ this.buttonGroup.element.removeChild(this.rotateLeft.element);
+ }
+ if (this.rotateRight) {
+ i = this.buttonGroup.buttons.indexOf(this.rotateRight);
+ this.buttonGroup.buttons.splice(i, 1);
+ this.buttonGroup.element.removeChild(this.rotateRight.element);
+ }
+ }
+
+ this._addUpdatePixelDensityRatioEvent();
+
+ if ('navigatorAutoResize' in this) {
+ $.console.warn('navigatorAutoResize is deprecated, this value will be ignored.');
+ }
+
+ //Instantiate a navigator if configured
+ if ( this.showNavigator){
+ this.navigator = new $.Navigator({
+ element: this.navigatorElement,
+ id: this.navigatorId,
+ position: this.navigatorPosition,
+ sizeRatio: this.navigatorSizeRatio,
+ maintainSizeRatio: this.navigatorMaintainSizeRatio,
+ top: this.navigatorTop,
+ left: this.navigatorLeft,
+ width: this.navigatorWidth,
+ height: this.navigatorHeight,
+ autoFade: this.navigatorAutoFade,
+ prefixUrl: this.prefixUrl,
+ viewer: this,
+ navigatorRotate: this.navigatorRotate,
+ background: this.navigatorBackground,
+ opacity: this.navigatorOpacity,
+ borderColor: this.navigatorBorderColor,
+ displayRegionColor: this.navigatorDisplayRegionColor,
+ crossOriginPolicy: this.crossOriginPolicy,
+ animationTime: this.animationTime,
+ drawer: this.drawer.getType(),
+ drawerOptions: this.drawerOptions,
+ loadTilesWithAjax: this.loadTilesWithAjax,
+ ajaxHeaders: this.ajaxHeaders,
+ ajaxWithCredentials: this.ajaxWithCredentials,
+ });
+ }
+
+ // Sequence mode
+ if (this.sequenceMode) {
+ this.bindSequenceControls();
+ }
+
+ // Open initial tilesources
+ if (this.tileSources) {
+ this.open( this.tileSources );
+ }
+
+ // Add custom controls
+ for ( i = 0; i < this.customControls.length; i++ ) {
+ this.addControl(
+ this.customControls[ i ].id,
+ {anchor: this.customControls[ i ].anchor}
+ );
+ }
+
+ // Initial fade out
+ $.requestAnimationFrame( function(){
+ beginControlsAutoHide( _this );
+ } );
+
+ // Register the viewer
+ $._viewers.set(this.element, this);
+};
+
+$.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, /** @lends OpenSeadragon.Viewer.prototype */{
+
+
+ /**
+ * @function
+ * @returns {Boolean}
+ */
+ isOpen: function () {
+ return !!this.world.getItemCount();
+ },
+
+ /**
+ * Checks whether all TiledImage instances in the viewer's world are fully loaded.
+ * This determines if the entire viewer content is ready for optimal display without partial tile loading.
+ * @private
+ * @returns {Boolean} True if all TiledImages report being fully loaded,
+ * false if any image still has pending tiles
+ */
+ _areAllFullyLoaded: function() {
+ const count = this.world.getItemCount();
+
+ // Iterate through all TiledImages in the viewer's world
+ for (let i = 0; i < count; i++) {
+ let tiledImage = this.world.getItemAt(i);
+
+ // Return immediately if any image isn't fully loaded
+ if (!tiledImage.getFullyLoaded()) {
+ return false;
+ }
+ }
+ // All images passed the check
+ return true;
+ },
+
+ /**
+ * @function
+ * @returns {Boolean} True if all required tiles are loaded, false otherwise
+ */
+ getFullyLoaded: function() {
+ return this._fullyLoaded;
+ },
+
+ /**
+ * Executes the provided callback when the TiledImage is fully loaded. If already loaded,
+ * schedules the callback asynchronously. Otherwise, attaches a one-time event listener
+ * for the 'fully-loaded-change' event.
+ * @param {Function} callback - Function to execute when loading completes
+ * @memberof OpenSeadragon.Viewer.prototype
+ */
+ whenFullyLoaded: function(callback) {
+ if (this.getFullyLoaded()) {
+ setTimeout(callback, 1); // Asynchronous execution
+ } else {
+ this.addOnceHandler('fully-loaded-change', function() {
+ callback(); // Maintain context
+ });
+ }
+ },
+
+ // deprecated
+ openDzi: function ( dzi ) {
+ $.console.error( "[Viewer.openDzi] this function is deprecated; use Viewer.open() instead." );
+ return this.open( dzi );
+ },
+
+ // deprecated
+ openTileSource: function ( tileSource ) {
+ $.console.error( "[Viewer.openTileSource] this function is deprecated; use Viewer.open() instead." );
+ return this.open( tileSource );
+ },
+
+ //deprecated
+ get buttons () {
+ $.console.warn('Viewer.buttons is deprecated; Please use Viewer.buttonGroup');
+ return this.buttonGroup;
+ },
+
+ /**
+ * Open tiled images into the viewer, closing any others.
+ * To get the TiledImage instance created by open, add an event listener for
+ * {@link OpenSeadragon.Viewer.html#.event:open}, which when fired can be used to get access
+ * to the instance, i.e., viewer.world.getItemAt(0).
+ * @function
+ * @param {OpenSeadragon.TileSourceSpecifier|OpenSeadragon.TileSourceSpecifier[]} tileSources - This can be a TiledImage
+ * specifier, a TileSource specifier, or an array of either. A TiledImage specifier
+ * is the same as the options parameter for {@link OpenSeadragon.Viewer#addTiledImage},
+ * except for the index property; images are added in sequence.
+ * A TileSource specifier is anything you could pass as the tileSource property
+ * of the options parameter for {@link OpenSeadragon.Viewer#addTiledImage}.
+ * @param {Number} [initialPage = undefined] - If sequenceMode is true, display this page initially
+ * for the given tileSources. If specified, will overwrite the Viewer's existing initialPage property.
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:open
+ * @fires OpenSeadragon.Viewer.event:open-failed
+ */
+ open: function (tileSources, initialPage = undefined) {
+ const _this = this;
+
+ this.close();
+
+ if (!tileSources) {
+ return this;
+ }
+
+ if (this.sequenceMode && $.isArray(tileSources)) {
+ if (this.referenceStrip) {
+ this.referenceStrip.destroy();
+ this.referenceStrip = null;
+ }
+
+ if (typeof initialPage !== 'undefined' && !isNaN(initialPage)) {
+ this.initialPage = initialPage;
+ }
+
+ this.tileSources = tileSources;
+ this._sequenceIndex = Math.max(0, Math.min(this.tileSources.length - 1, this.initialPage));
+ if (this.tileSources.length) {
+ this.open(this.tileSources[this._sequenceIndex]);
+
+ if ( this.showReferenceStrip ){
+ this.addReferenceStrip();
+ }
+ }
+
+ this._updateSequenceButtons( this._sequenceIndex );
+ return this;
+ }
+
+ if (!$.isArray(tileSources)) {
+ tileSources = [tileSources];
+ }
+
+ if (!tileSources.length) {
+ return this;
+ }
+
+ this._opening = true;
+
+ const expected = tileSources.length;
+ let successes = 0;
+ let failures = 0;
+ let failEvent;
+
+ const checkCompletion = function() {
+ if (successes + failures === expected) {
+ if (successes) {
+ if (_this._firstOpen || !_this.preserveViewport) {
+ _this.viewport.goHome( true );
+ _this.viewport.update();
+ }
+
+ _this._firstOpen = false;
+
+ let source = tileSources[0];
+ if (source.tileSource) {
+ source = source.tileSource;
+ }
+
+ // Global overlays
+ if( _this.overlays && !_this.preserveOverlays ){
+ for ( let i = 0; i < _this.overlays.length; i++ ) {
+ _this.currentOverlays[ i ] = getOverlayObject( _this, _this.overlays[ i ] );
+ }
+ }
+
+ _this._drawOverlays();
+ _this._opening = false;
+
+ /**
+ * Raised when the viewer has opened and loaded one or more TileSources.
+ *
+ * @event open
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {OpenSeadragon.TileSource} source - The tile source that was opened.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ // TODO: what if there are multiple sources?
+ _this.raiseEvent( 'open', { source: source } );
+ } else {
+ _this._opening = false;
+
+ /**
+ * Raised when an error occurs loading a TileSource.
+ *
+ * @event open-failed
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {String} message - Information about what failed.
+ * @property {String} source - The tile source that failed.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ _this.raiseEvent( 'open-failed', failEvent );
+ }
+ }
+ };
+
+ const doOne = function(index, options) {
+ if (!$.isPlainObject(options) || !options.tileSource) {
+ options = {
+ tileSource: options
+ };
+ }
+
+ if (options.index !== undefined) {
+ $.console.warn('[Viewer.open] Ignoring user-supplied index; preserving order by setting index to ' + index + '. If you need to set indexes, use addTiledImage instead.');
+ delete options.index;
+ // ensure we keep the order we received
+ options.index = index;
+ }
+
+ if (options.collectionImmediately === undefined) {
+ options.collectionImmediately = true;
+ }
+
+ const originalSuccess = options.success;
+ options.success = function(event) {
+ successes++;
+
+ // TODO: now that options has other things besides tileSource, the overlays
+ // should probably be at the options level, not the tileSource level.
+ if (options.tileSource.overlays) {
+ for (let i = 0; i < options.tileSource.overlays.length; i++) {
+ _this.addOverlay(options.tileSource.overlays[i]);
+ }
+ }
+
+ if (originalSuccess) {
+ originalSuccess(event);
+ }
+
+ checkCompletion();
+ };
+
+ const originalError = options.error;
+ options.error = function(event) {
+ failures++;
+
+ if (!failEvent) {
+ failEvent = event;
+ }
+
+ if (originalError) {
+ originalError(event);
+ }
+
+ checkCompletion();
+ };
+
+ _this.addTiledImage(options);
+ };
+
+ // TileSources
+ for (let i = 0; i < tileSources.length; i++) {
+ doOne(i, tileSources[i]);
+ }
+
+ return this;
+ },
+
+ /**
+ * Updates data within every tile in the viewer. Should be called
+ * when tiles are outdated and should be re-processed. Useful mainly
+ * for plugins that change tile data.
+ * @function
+ * @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data
+ * @fires OpenSeadragon.Viewer.event:tile-invalidated
+ * @return {OpenSeadragon.Promise>}
+ */
+ requestInvalidate: function (restoreTiles = true) {
+ if ( !THIS[ this.hash ] || !this._drawerList ) {
+ //this viewer has already been destroyed or is a child in connected mode: returning immediately
+ return $.Promise.resolve();
+ }
+
+ const tStamp = $.now();
+ // if drawer option broadCastTileInvalidation is enabled, this is NOOP for any but the base drawer, that runs update on all
+ return $.Promise.all(this._drawerList.map(drawer => drawer.viewer.world.requestInvalidate(restoreTiles, tStamp)));
+ },
+
+
+ /**
+ * @function
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:close
+ */
+ close: function ( ) {
+ if ( !THIS[ this.hash ] ) {
+ //this viewer has already been destroyed: returning immediately
+ return this;
+ }
+
+ this._opening = false;
+
+ if ( this.navigator ) {
+ this.navigator.close();
+ }
+
+ if (!this.preserveOverlays) {
+ this.clearOverlays();
+ this.overlaysContainer.innerHTML = "";
+ }
+
+ THIS[ this.hash ].animating = false;
+
+ this.world.removeAll();
+ this.tileCache.clear();
+ this.imageLoader.clear();
+ /**
+ * Raised when the viewer is closed (see {@link OpenSeadragon.Viewer#close}).
+ *
+ * @event close
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'close' );
+
+ return this;
+ },
+
+
+ /**
+ * Function to destroy the viewer and clean up everything created by OpenSeadragon.
+ *
+ * Example:
+ * var viewer = OpenSeadragon({
+ * [...]
+ * });
+ *
+ * //when you are done with the viewer:
+ * viewer.destroy();
+ * viewer = null; //important
+ *
+ * @function
+ * @fires OpenSeadragon.Viewer.event:before-destroy
+ * @fires OpenSeadragon.Viewer.event:destroy
+ */
+ destroy: function( ) {
+ if ( !THIS[ this.hash ] ) {
+ //this viewer has already been destroyed: returning immediately
+ return;
+ }
+
+ /**
+ * Raised when the viewer is about to be destroyed (see {@link OpenSeadragon.Viewer#before-destroy}).
+ *
+ * @event before-destroy
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'before-destroy' );
+
+ this._removeUpdatePixelDensityRatioEvent();
+
+ this.close();
+
+ this.clearOverlays();
+ this.overlaysContainer.innerHTML = "";
+
+ //TODO: implement this...
+ //this.unbindSequenceControls()
+ //this.unbindStandardControls()
+ if (this._resizeObserver){
+ this._resizeObserver.disconnect();
+ }
+
+ if (this.referenceStrip) {
+ this.referenceStrip.destroy();
+ this.referenceStrip = null;
+ }
+
+ if ( this._updateRequestId !== null ) {
+ $.cancelAnimationFrame( this._updateRequestId );
+ this._updateRequestId = null;
+ }
+
+ if ( this.drawer ) {
+ this.drawer.destroy();
+ }
+
+ if ( this.navigator ) {
+ this.navigator.destroy();
+ THIS[ this.navigator.hash ] = null;
+ delete THIS[ this.navigator.hash ];
+ this.navigator = null;
+ }
+
+
+ if (this.buttonGroup) {
+ this.buttonGroup.destroy();
+ } else if (this.customButtons) {
+ while (this.customButtons.length) {
+ this.customButtons.pop().destroy();
+ }
+ }
+
+ if (this.paging) {
+ this.paging.destroy();
+ }
+
+ // Remove both the canvas and container elements added by OpenSeadragon
+ // This will also remove its children (like the canvas)
+ if (this.container && this.container.parentNode === this.element) {
+ this.element.removeChild(this.container);
+ }
+ this.container.onsubmit = null;
+ this.clearControls();
+
+ // destroy the mouse trackers
+ if (this.innerTracker){
+ this.innerTracker.destroy();
+ }
+ if (this.outerTracker){
+ this.outerTracker.destroy();
+ }
+
+ THIS[ this.hash ] = null;
+ delete THIS[ this.hash ];
+
+ // clear all our references to dom objects
+ this.canvas = null;
+ this.container = null;
+
+ // Unregister the viewer
+ $._viewers.delete(this.element);
+
+ // clear our reference to the main element - they will need to pass it in again, creating a new viewer
+ this.element = null;
+
+
+
+ /**
+ * Raised when the viewer is destroyed (see {@link OpenSeadragon.Viewer#destroy}).
+ *
+ * @event destroy
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'destroy' );
+
+ this.removeAllHandlers();
+ },
+
+ /**
+ * Check if the viewer has been destroyed or not yet initialized.
+ * @return {boolean}
+ */
+ isDestroyed() {
+ return !THIS[ this.hash ];
+ },
+
+ /**
+ * Request a drawer for this viewer, as a supported string or drawer constructor.
+ * @param {String | OpenSeadragon.DrawerBase} drawerCandidate The type of drawer to try to construct.
+ * @param { Object } options
+ * @param { Boolean } [options.mainDrawer] Whether to use this as the viewer's main drawer. Default = true.
+ * @param { Boolean } [options.redrawImmediately] Whether to immediately draw a new frame. Only used if options.mainDrawer = true. Default = true.
+ * @param { Object } [options.drawerOptions] Options for this drawer. Defaults to viewer.drawerOptions.
+ * for this viewer type. See {@link OpenSeadragon.Options}.
+ * @returns {Object | Boolean} The drawer that was created, or false if the requested drawer is not supported
+ */
+ requestDrawer(drawerCandidate, options){
+ const defaultOpts = {
+ mainDrawer: true,
+ redrawImmediately: true,
+ drawerOptions: null
+ };
+ options = $.extend(true, defaultOpts, options);
+ const mainDrawer = options.mainDrawer;
+ const redrawImmediately = options.redrawImmediately;
+ const drawerOptions = options.drawerOptions;
+
+ const oldDrawer = this.drawer;
+
+ let Drawer = null;
+
+ //if the candidate inherits from a drawer base, use it
+ if (drawerCandidate && drawerCandidate.prototype instanceof $.DrawerBase) {
+ Drawer = drawerCandidate;
+ drawerCandidate = 'custom';
+ } else if (typeof drawerCandidate === "string") {
+ Drawer = $.determineDrawer(drawerCandidate);
+ }
+
+ if (!Drawer) {
+ $.console.warn('Unsupported drawer %s! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.', drawerCandidate);
+ }
+
+ // Guard isSupported() in try/catch so a buggy or throwing plugin drawer cannot crash the whole viewer
+ let supported = false;
+ if (Drawer) {
+ try {
+ supported = Drawer.isSupported();
+ } catch (e) {
+ $.console.warn('Error in %s isSupported(); treating this drawer as unsupported:', drawerCandidate, e && e.message ? e.message : e);
+ }
+ }
+ if (supported) {
+ // if the drawer is supported, create it and return it.
+ // first destroy the previous drawer
+ if(oldDrawer && mainDrawer){
+ oldDrawer.destroy();
+ }
+
+ // create the new drawer
+ const newDrawer = new Drawer({
+ viewer: this,
+ viewport: this.viewport,
+ element: this.canvas,
+ debugGridColor: this.debugGridColor,
+ options: drawerOptions || this.drawerOptions[drawerCandidate],
+ });
+
+ if(mainDrawer){
+ this.drawer = newDrawer;
+ if(redrawImmediately){
+ this.forceRedraw();
+ }
+ }
+
+ return newDrawer;
+ }
+
+ return false;
+ },
+
+ /**
+ * @function
+ * @returns {Boolean}
+ */
+ isMouseNavEnabled: function () {
+ return this.innerTracker.tracking;
+ },
+
+ /**
+ * @function
+ * @param {Boolean} enabled - true to enable, false to disable
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:mouse-enabled
+ */
+ setMouseNavEnabled: function( enabled ){
+ this.innerTracker.setTracking( enabled );
+ this.outerTracker.setTracking( enabled );
+ /**
+ * Raised when mouse/touch navigation is enabled or disabled (see {@link OpenSeadragon.Viewer#setMouseNavEnabled}).
+ *
+ * @event mouse-enabled
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} enabled
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'mouse-enabled', { enabled: enabled } );
+ return this;
+ },
+
+
+ /**
+ * @function
+ * @returns {Boolean}
+ */
+ isKeyboardNavEnabled: function () {
+ return this.keyboardNavEnabled;
+ },
+
+ /**
+ * @function
+ * @param {Boolean} enabled - true to enable, false to disable
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:keyboard-enabled
+ */
+ setKeyboardNavEnabled: function( enabled ){
+ this.keyboardNavEnabled = enabled;
+
+ /**
+ * Raised when keyboard navigation is enabled or disabled (see {@link OpenSeadragon.Viewer#setKeyboardNavEnabled}).
+ *
+ * @event keyboard-enabled
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} enabled
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'keyboard-enabled', { enabled: enabled } );
+ return this;
+ },
+
+
+ /**
+ * @function
+ * @returns {Boolean}
+ */
+ areControlsEnabled: function () {
+ let enabled = this.controls.length;
+ for( let i = 0; i < this.controls.length; i++ ){
+ enabled = enabled && this.controls[ i ].isVisible();
+ }
+ return enabled;
+ },
+
+
+ /**
+ * Shows or hides the controls (e.g. the default navigation buttons).
+ *
+ * @function
+ * @param {Boolean} true to show, false to hide.
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:controls-enabled
+ */
+ setControlsEnabled: function( enabled ) {
+ if( enabled ){
+ abortControlsAutoHide( this );
+ } else {
+ beginControlsAutoHide( this );
+ }
+ /**
+ * Raised when the navigation controls are shown or hidden (see {@link OpenSeadragon.Viewer#setControlsEnabled}).
+ *
+ * @event controls-enabled
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} enabled
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'controls-enabled', { enabled: enabled } );
+ return this;
+ },
+
+ /**
+ * Turns debugging mode on or off for this viewer.
+ *
+ * @function
+ * @param {Boolean} debugMode true to turn debug on, false to turn debug off.
+ */
+ setDebugMode: function(debugMode){
+
+ for (let i = 0; i < this.world.getItemCount(); i++) {
+ this.world.getItemAt(i).debugMode = debugMode;
+ }
+
+ this.debugMode = debugMode;
+ this.forceRedraw();
+ },
+
+ /**
+ * Update headers to include when making AJAX requests.
+ *
+ * Unless `propagate` is set to false (which is likely only useful in rare circumstances),
+ * the updated headers are propagated to all tiled images, each of which will subsequently
+ * propagate the changed headers to all their tiles.
+ * If applicable, the headers of the viewer's navigator and reference strip will also be updated.
+ *
+ * Note that the rules for merging headers still apply, i.e. headers returned by
+ * {@link OpenSeadragon.TileSource#getTileAjaxHeaders} take precedence over
+ * `TiledImage.ajaxHeaders`, which take precedence over the headers here in the viewer.
+ *
+ * @function
+ * @param {Object} ajaxHeaders Updated AJAX headers.
+ * @param {Boolean} [propagate=true] Whether to propagate updated headers to tiled images, etc.
+ */
+ setAjaxHeaders: function(ajaxHeaders, propagate) {
+ if (ajaxHeaders === null) {
+ ajaxHeaders = {};
+ }
+ if (!$.isPlainObject(ajaxHeaders)) {
+ $.console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
+ return;
+ }
+ if (propagate === undefined) {
+ propagate = true;
+ }
+
+ this.ajaxHeaders = ajaxHeaders;
+
+ if (propagate) {
+ for (let i = 0; i < this.world.getItemCount(); i++) {
+ this.world.getItemAt(i)._updateAjaxHeaders(true);
+ }
+
+ if (this.navigator) {
+ this.navigator.setAjaxHeaders(this.ajaxHeaders, true);
+ }
+
+ if (this.referenceStrip && this.referenceStrip.miniViewers) {
+ for (const key in this.referenceStrip.miniViewers) {
+ this.referenceStrip.miniViewers[key].setAjaxHeaders(this.ajaxHeaders, true);
+ }
+ }
+ }
+ },
+
+ /**
+ * Adds the given button to this viewer.
+ *
+ * @function
+ * @param {OpenSeadragon.Button} button
+ */
+ addButton: function( button ){
+ this.buttonGroup.addButton(button);
+ },
+
+ /**
+ * @function
+ * @returns {Boolean}
+ */
+ isFullPage: function () {
+ return THIS[this.hash] && THIS[ this.hash ].fullPage;
+ },
+
+
+ /**
+ * Toggle full page mode.
+ * @function
+ * @param {Boolean} fullPage
+ * If true, enter full page mode. If false, exit full page mode.
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:pre-full-page
+ * @fires OpenSeadragon.Viewer.event:full-page
+ */
+ setFullPage: function( fullPage ) {
+
+ const body = document.body;
+ const bodyStyle = body.style;
+ const docStyle = document.documentElement.style;
+ const _this = this;
+ let nodes;
+
+ //don't bother modifying the DOM if we are already in full page mode.
+ if ( fullPage === this.isFullPage() ) {
+ return this;
+ }
+
+ const fullPageEventArgs = {
+ fullPage: fullPage,
+ preventDefaultAction: false
+ };
+ /**
+ * Raised when the viewer is about to change to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
+ *
+ * @event pre-full-page
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} fullPage - True if entering full-page mode, false if exiting full-page mode.
+ * @property {Boolean} preventDefaultAction - Set to true to prevent full-page mode change. Default: false.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'pre-full-page', fullPageEventArgs );
+ if ( fullPageEventArgs.preventDefaultAction ) {
+ return this;
+ }
+
+ if ( fullPage && this.element ) {
+
+ this.elementSize = $.getElementSize( this.element );
+ this.pageScroll = $.getPageScroll();
+
+ this.elementMargin = this.element.style.margin;
+ this.element.style.margin = "0";
+ this.elementPadding = this.element.style.padding;
+ this.element.style.padding = "0";
+
+ this.bodyMargin = bodyStyle.margin;
+ this.docMargin = docStyle.margin;
+ bodyStyle.margin = "0";
+ docStyle.margin = "0";
+
+ this.bodyPadding = bodyStyle.padding;
+ this.docPadding = docStyle.padding;
+ bodyStyle.padding = "0";
+ docStyle.padding = "0";
+
+ this.bodyWidth = bodyStyle.width;
+ this.docWidth = docStyle.width;
+ bodyStyle.width = "100%";
+ docStyle.width = "100%";
+
+ this.bodyHeight = bodyStyle.height;
+ this.docHeight = docStyle.height;
+ bodyStyle.height = "100%";
+ docStyle.height = "100%";
+
+ this.bodyDisplay = bodyStyle.display;
+ bodyStyle.display = "block";
+
+ //when entering full screen on the ipad it wasn't sufficient to
+ //leave the body intact as only only the top half of the screen
+ //would respond to touch events on the canvas, while the bottom half
+ //treated them as touch events on the document body. Thus we make
+ //them invisible (display: none) and apply the older values when we
+ //go out of full screen.
+ this.previousDisplayValuesOfBodyChildren = [];
+ THIS[ this.hash ].prevElementParent = this.element.parentNode;
+ THIS[ this.hash ].prevNextSibling = this.element.nextSibling;
+ THIS[ this.hash ].prevElementWidth = this.element.style.width;
+ THIS[ this.hash ].prevElementHeight = this.element.style.height;
+ nodes = body.children.length;
+ for ( let i = 0; i < nodes; i++ ) {
+ const element = body.children[i];
+ if (element === this.element) {
+ // Do not hide ourselves...
+ continue;
+ }
+ this.previousDisplayValuesOfBodyChildren.push({
+ element,
+ display: element.style.display
+ });
+ element.style.display = 'none';
+ }
+
+ //If we've got a toolbar, we need to enable the user to use css to
+ //preserve it in fullpage mode
+ if ( this.toolbar && this.toolbar.element ) {
+ //save a reference to the parent so we can put it back
+ //in the long run we need a better strategy
+ this.toolbar.parentNode = this.toolbar.element.parentNode;
+ this.toolbar.nextSibling = this.toolbar.element.nextSibling;
+ body.appendChild( this.toolbar.element );
+
+ //Make sure the user has some ability to style the toolbar based
+ //on the mode
+ $.addClass( this.toolbar.element, 'fullpage' );
+ }
+
+ $.addClass( this.element, 'fullpage' );
+ body.appendChild( this.element );
+
+ this.element.style.height = '100vh';
+ this.element.style.width = '100vw';
+
+ if ( this.toolbar && this.toolbar.element ) {
+ this.element.style.height = (
+ $.getElementSize( this.element ).y - $.getElementSize( this.toolbar.element ).y
+ ) + 'px';
+ }
+
+ THIS[ this.hash ].fullPage = true;
+
+ // mouse will be inside container now
+ $.delegate( this, onContainerEnter )( {} );
+
+ } else {
+
+ this.element.style.margin = this.elementMargin;
+ this.element.style.padding = this.elementPadding;
+
+ bodyStyle.margin = this.bodyMargin;
+ docStyle.margin = this.docMargin;
+
+ bodyStyle.padding = this.bodyPadding;
+ docStyle.padding = this.docPadding;
+
+ bodyStyle.width = this.bodyWidth;
+ docStyle.width = this.docWidth;
+
+ bodyStyle.height = this.bodyHeight;
+ docStyle.height = this.docHeight;
+
+ bodyStyle.display = this.bodyDisplay;
+
+ body.removeChild( this.element );
+ nodes = this.previousDisplayValuesOfBodyChildren.length;
+ for ( let i = 0; i < nodes; i++ ) {
+ const { element, display } = this.previousDisplayValuesOfBodyChildren[i];
+ element.style.display = display;
+ }
+
+ $.removeClass( this.element, 'fullpage' );
+ THIS[ this.hash ].prevElementParent.insertBefore(
+ this.element,
+ THIS[ this.hash ].prevNextSibling
+ );
+
+ //If we've got a toolbar, we need to enable the user to use css to
+ //reset it to its original state
+ if ( this.toolbar && this.toolbar.element ) {
+ body.removeChild( this.toolbar.element );
+
+ //Make sure the user has some ability to style the toolbar based
+ //on the mode
+ $.removeClass( this.toolbar.element, 'fullpage' );
+
+ this.toolbar.parentNode.insertBefore(
+ this.toolbar.element,
+ this.toolbar.nextSibling
+ );
+ delete this.toolbar.parentNode;
+ delete this.toolbar.nextSibling;
+ }
+
+ this.element.style.width = THIS[ this.hash ].prevElementWidth;
+ this.element.style.height = THIS[ this.hash ].prevElementHeight;
+
+ // After exiting fullPage or fullScreen, it can take some time
+ // before the browser can actually set the scroll.
+ let restoreScrollCounter = 0;
+ const restoreScroll = function() {
+ $.setPageScroll( _this.pageScroll );
+ const pageScroll = $.getPageScroll();
+ restoreScrollCounter++;
+ if (restoreScrollCounter < 10 &&
+ (pageScroll.x !== _this.pageScroll.x ||
+ pageScroll.y !== _this.pageScroll.y)) {
+ $.requestAnimationFrame( restoreScroll );
+ }
+ };
+ $.requestAnimationFrame( restoreScroll );
+
+ THIS[ this.hash ].fullPage = false;
+
+ // mouse will likely be outside now
+ $.delegate( this, onContainerLeave )( { } );
+
+ }
+
+ if ( this.navigator && this.viewport ) {
+ this.navigator.update( this.viewport );
+ }
+
+ /**
+ * Raised when the viewer has changed to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
+ *
+ * @event full-page
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} fullPage - True if changed to full-page mode, false if exited full-page mode.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'full-page', { fullPage: fullPage } );
+
+ return this;
+ },
+
+ /**
+ * Toggle full screen mode if supported. Toggle full page mode otherwise.
+ * @function
+ * @param {Boolean} fullScreen
+ * If true, enter full screen mode. If false, exit full screen mode.
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:pre-full-screen
+ * @fires OpenSeadragon.Viewer.event:full-screen
+ */
+ setFullScreen: function( fullScreen ) {
+ const _this = this;
+
+ if ( !$.supportsFullScreen ) {
+ return this.setFullPage( fullScreen );
+ }
+
+ if ( $.isFullScreen() === fullScreen ) {
+ return this;
+ }
+
+ const fullScreenEventArgs = {
+ fullScreen: fullScreen,
+ preventDefaultAction: false
+ };
+ /**
+ * Raised when the viewer is about to change to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
+ * Note: the pre-full-screen event is not raised when the user is exiting
+ * full-screen mode by pressing the Esc key. In that case, consider using
+ * the full-screen, pre-full-page or full-page events.
+ *
+ * @event pre-full-screen
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} fullScreen - True if entering full-screen mode, false if exiting full-screen mode.
+ * @property {Boolean} preventDefaultAction - Set to true to prevent full-screen mode change. Default: false.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'pre-full-screen', fullScreenEventArgs );
+ if ( fullScreenEventArgs.preventDefaultAction ) {
+ return this;
+ }
+
+ if ( fullScreen ) {
+
+ this.setFullPage( true );
+ // If the full page mode is not actually entered, we need to prevent
+ // the full screen mode.
+ if ( !this.isFullPage() ) {
+ return this;
+ }
+
+ this.fullPageStyleWidth = this.element.style.width;
+ this.fullPageStyleHeight = this.element.style.height;
+ this.element.style.width = '100%';
+ this.element.style.height = '100%';
+
+ const onFullScreenChange = function() {
+ if (!THIS[ _this.hash ]) {
+ $.removeEvent( document, $.fullScreenEventName, onFullScreenChange );
+ $.removeEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
+ return;
+ }
+
+ const isFullScreen = $.isFullScreen();
+ if ( !isFullScreen ) {
+ $.removeEvent( document, $.fullScreenEventName, onFullScreenChange );
+ $.removeEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
+
+ _this.setFullPage( false );
+ if ( _this.isFullPage() ) {
+ _this.element.style.width = _this.fullPageStyleWidth;
+ _this.element.style.height = _this.fullPageStyleHeight;
+ }
+ }
+ if ( _this.navigator && _this.viewport ) {
+ //09/08/2018 - Fabroh : Fix issue #1504 : Ensure to get the navigator updated on fullscreen out with custom location with a timeout
+ setTimeout(function(){
+ _this.navigator.update( _this.viewport );
+ });
+ }
+ /**
+ * Raised when the viewer has changed to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
+ *
+ * @event full-screen
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} fullScreen - True if changed to full-screen mode, false if exited full-screen mode.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ _this.raiseEvent( 'full-screen', { fullScreen: isFullScreen } );
+ };
+ $.addEvent( document, $.fullScreenEventName, onFullScreenChange );
+ $.addEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
+
+ $.requestFullScreen( document.body );
+
+ } else {
+ $.exitFullScreen();
+ }
+ return this;
+ },
+
+ /**
+ * @function
+ * @returns {Boolean}
+ */
+ isVisible: function () {
+ return this.container.style.visibility !== "hidden";
+ },
+
+
+ //
+ /**
+ * @function
+ * @returns {Boolean} returns true if the viewer is in fullscreen
+ */
+ isFullScreen: function () {
+ return $.isFullScreen() && this.isFullPage();
+ },
+
+ /**
+ * @function
+ * @param {Boolean} visible
+ * @returns {OpenSeadragon.Viewer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:visible
+ */
+ setVisible: function( visible ){
+ this.container.style.visibility = visible ? "" : "hidden";
+ /**
+ * Raised when the viewer is shown or hidden (see {@link OpenSeadragon.Viewer#setVisible}).
+ *
+ * @event visible
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Boolean} visible
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'visible', { visible: visible } );
+ return this;
+ },
+
+ /**
+ * @typedef OpenSeadragon.TileSourceSpecifier
+ * @property {Object} options
+ * @property {OpenSeadragon.TileSource|String|Object|Function} options.tileSource - The TileSource specifier.
+ * A String implies a url used to determine the tileSource implementation
+ * based on the file extension of url. JSONP is implied by *.js,
+ * otherwise the url is retrieved as text and the resulting text is
+ * introspected to determine if its json, xml, or text and parsed.
+ * An Object implies an inline configuration which has a single
+ * property sufficient for being able to determine tileSource
+ * implementation. If the object has a property which is a function
+ * named 'getTileUrl', it is treated as a custom TileSource.
+ * @property {Number} [options.index] The index of the item. Added on top of
+ * all other items if not specified.
+ * @property {Boolean} [options.replace=false] If true, the item at options.index will be
+ * removed and the new item is added in its place. options.tileSource will be
+ * interpreted and fetched if necessary before the old item is removed to avoid leaving
+ * a gap in the world.
+ * @property {Number} [options.x=0] The X position for the image in viewport coordinates.
+ * @property {Number} [options.y=0] The Y position for the image in viewport coordinates.
+ * @property {Number} [options.width=1] The width for the image in viewport coordinates.
+ * @property {Number} [options.height] The height for the image in viewport coordinates.
+ * @property {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates
+ * to fit the image into. If specified, x, y, width and height get ignored.
+ * @property {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER]
+ * How to anchor the image in the bounds if options.fitBounds is set.
+ * @property {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to
+ * (portions of the image outside of this area will not be visible). Only works on
+ * browsers that support the HTML5 canvas.
+ * @property {Number} [options.opacity=1] Proportional opacity of the tiled images (1=opaque, 0=hidden)
+ * @property {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks)
+ * @property {Boolean} [options.zombieCache] In the case that this method removes any TiledImage instance,
+ * allow the item-referenced cache to remain in memory even without active tiles. Default false.
+ * @property {Number} [options.degrees=0] Initial rotation of the tiled image around
+ * its top left corner in degrees.
+ * @property {Boolean} [options.flipped=false] Whether to horizontally flip the image.
+ * @property {String} [options.compositeOperation] How the image is composited onto other images.
+ * @property {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image,
+ * overriding viewer.crossOriginPolicy.
+ * @property {Boolean} [options.ajaxWithCredentials] Whether to set withCredentials on tile AJAX
+ * @property {Boolean} [options.loadTilesWithAjax]
+ * Whether to load tile data using AJAX requests.
+ * Defaults to the setting in {@link OpenSeadragon.Options}.
+ * @property {Object} [options.ajaxHeaders]
+ * A set of headers to include when making tile AJAX requests.
+ * Note that these headers will be merged over any headers specified in {@link OpenSeadragon.Options}.
+ * Specifying a falsy value for a header will clear its existing value set at the Viewer level (if any).
+ * @property {Function} [options.success] A function that gets called when the image is
+ * successfully added. It's passed the event object which contains a single property:
+ * "item", which is the resulting instance of TiledImage.
+ * @property {Function} [options.error] A function that gets called if the image is
+ * unable to be added. It's passed the error event object, which contains "message"
+ * and "source" properties.
+ * @property {Boolean} [options.collectionImmediately=false] If collectionMode is on,
+ * specifies whether to snap to the new arrangement immediately or to animate to it.
+ * @property {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}.
+ * @param {string|string[]} [options.originalDataType=undefined]
+ * A default format to convert tiles to at the beginning. The format is the base tile format,
+ * and this can optimize rendering or processing logics in case for example a plugin always requires a certain
+ * format to convert to.
+ */
+
+ /**
+ * Add a tiled image to the viewer.
+ * options.tileSource can be anything that {@link OpenSeadragon.Viewer#open}
+ * supports except arrays of images.
+ * Note that you can specify options.width or options.height, but not both.
+ * The other dimension will be calculated according to the item's aspect ratio.
+ * If collectionMode is on (see {@link OpenSeadragon.Options}), the new image is
+ * automatically arranged with the others.
+ * @function
+ * @param {OpenSeadragon.TileSourceSpecifier} options
+ * @fires OpenSeadragon.World.event:add-item
+ * @fires OpenSeadragon.Viewer.event:add-item-failed
+ */
+ addTiledImage: function( options ) {
+ $.console.assert(options, "[Viewer.addTiledImage] options is required");
+ $.console.assert(options.tileSource, "[Viewer.addTiledImage] options.tileSource is required");
+ $.console.assert(!options.replace || (options.index > -1 && options.index < this.world.getItemCount()),
+ "[Viewer.addTiledImage] if options.replace is used, options.index must be a valid index in Viewer.world");
+
+ this._hideMessage();
+
+ const originalSuccess = options.success;
+ const originalError = options.error;
+ if (options.replace) {
+ options.replaceItem = this.world.getItemAt(options.index);
+ }
+
+ const myQueueItem = {
+ options: options
+ };
+
+ this._loadQueue.push(myQueueItem);
+
+ const refreshWorld = theItem => {
+ if (this.collectionMode) {
+ this.world.arrange({
+ immediately: theItem.options.collectionImmediately,
+ rows: this.collectionRows,
+ columns: this.collectionColumns,
+ layout: this.collectionLayout,
+ tileSize: this.collectionTileSize,
+ tileMargin: this.collectionTileMargin
+ });
+ this.world.setAutoRefigureSizes(true);
+ }
+ };
+
+ const raiseAddItemFailed = ( event ) => {
+ for (let i = 0; i < this._loadQueue.length; i++) {
+ if (this._loadQueue[i] === myQueueItem) {
+ this._loadQueue.splice(i, 1);
+ break;
+ }
+ }
+
+ if (this._loadQueue.length === 0) {
+ refreshWorld(myQueueItem);
+ }
+
+ /**
+ * Raised when an error occurs while adding a item.
+ * @event add-item-failed
+ * @memberOf OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {String} message
+ * @property {String} source
+ * @property {Object} options The options passed to the addTiledImage method.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ this.raiseEvent( 'add-item-failed', event );
+
+ if (originalError) {
+ originalError(event);
+ }
+ };
+
+ if ($.isArray(options.tileSource)) {
+ setTimeout(function() {
+ raiseAddItemFailed({
+ message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.",
+ source: options.tileSource,
+ options: options
+ });
+ });
+ return;
+ }
+
+ // ensure nobody provided such entry
+ delete myQueueItem.tiledImage;
+ options.success = event => {
+ myQueueItem.tiledImage = event.item;
+ myQueueItem.originalSuccess = originalSuccess;
+
+ let queueItem, optionsClone;
+ while (this._loadQueue.length) {
+ queueItem = this._loadQueue[0];
+ const tiledImage = queueItem.tiledImage;
+ if (!tiledImage) {
+ break;
+ }
+
+ this._loadQueue.splice(0, 1);
+ const tileSource = tiledImage.source;
+
+ if (queueItem.options.replace) {
+ const replaced = queueItem.options.replaceItem;
+ const newIndex = this.world.getIndexOfItem(replaced);
+ if (newIndex !== -1) {
+ queueItem.options.index = newIndex;
+ }
+ if (!replaced._zombieCache && replaced.source.equals(tileSource)) {
+ replaced.allowZombieCache(true);
+ }
+ this.world.removeItem(replaced);
+ }
+
+ if (this.collectionMode) {
+ this.world.setAutoRefigureSizes(false);
+ }
+
+ if (this.navigator) {
+ optionsClone = $.extend({}, queueItem.options, {
+ replace: false, // navigator already removed the layer, nothing to replace
+ originalTiledImage: tiledImage,
+ tileSource: tileSource
+ });
+
+ this.navigator.addTiledImage(optionsClone);
+ }
+
+ this.world.addItem( tiledImage, {
+ index: queueItem.options.index
+ });
+
+ if (this._loadQueue.length === 0) {
+ //this restores the autoRefigureSizes flag to true.
+ refreshWorld(queueItem);
+ }
+
+ if (this.world.getItemCount() === 1 && !this.preserveViewport) {
+ this.viewport.goHome(true);
+ }
+
+ if (queueItem.originalSuccess) {
+ queueItem.originalSuccess({
+ item: tiledImage
+ });
+ }
+
+ // It might happen processReadyItems() is called after viewer.destroy()
+ if (this.drawer) {
+ // This is necessary since drawer might react upon finalized tiled image, after
+ // all events have been processed.
+ this.drawer.tiledImageCreated(tiledImage);
+ }
+ }
+ };
+ options.error = raiseAddItemFailed;
+ this.instantiateTiledImageClass(options);
+ },
+
+ /**
+ * Create a TiledImage Instance. This instance is not integrated into the viewer
+ * and can be used to for example draw custom data in offscreen fashion by instantiating
+ * offscreen drawer, creating detached tiled images, forcing them to load certain region
+ * and calling drawer.draw([my tiled images]).
+ * @param {OpenSeadragon.TileSourceSpecifier} options options to create the image. Some properties
+ * are unused, these properties drive how the image is inserted into the world, and therefore
+ * they are not used in the pure creation of the TiledImage.
+ * @return {OpenSeadragon.Promise} A promise that resolves to the created TiledImage.
+ * Also, old options.error and options.success callbacks can be used instead to handle the output.
+ */
+ instantiateTiledImageClass: function( options) {
+ return this.instantiateTileSourceClass(options).then(event => {
+ // add everybody at the front of the queue that's ready to go
+ const tiledImage = new $.TiledImage({
+ viewer: this,
+ source: event.source,
+ viewport: this.viewport,
+ drawer: this.drawer,
+ tileCache: this.tileCache,
+ imageLoader: this.imageLoader,
+ x: options.x,
+ y: options.y,
+ width: options.width,
+ height: options.height,
+ fitBounds: options.fitBounds,
+ fitBoundsPlacement: options.fitBoundsPlacement,
+ clip: options.clip,
+ placeholderFillStyle: options.placeholderFillStyle,
+ opacity: options.opacity,
+ preload: options.preload,
+ degrees: options.degrees,
+ flipped: options.flipped,
+ compositeOperation: options.compositeOperation,
+ springStiffness: this.springStiffness,
+ animationTime: this.animationTime,
+ minZoomImageRatio: this.minZoomImageRatio,
+ wrapHorizontal: this.wrapHorizontal,
+ wrapVertical: this.wrapVertical,
+ maxTilesPerFrame: this.maxTilesPerFrame,
+ loadDestinationTilesOnAnimation: this.loadDestinationTilesOnAnimation,
+ immediateRender: this.immediateRender,
+ blendTime: this.blendTime,
+ alwaysBlend: this.alwaysBlend,
+ minPixelRatio: this.minPixelRatio,
+ smoothTileEdgesMinZoom: this.smoothTileEdgesMinZoom,
+ iOSDevice: this.iOSDevice,
+ crossOriginPolicy: options.crossOriginPolicy,
+ ajaxWithCredentials: options.ajaxWithCredentials,
+ loadTilesWithAjax: options.loadTilesWithAjax,
+ ajaxHeaders: options.ajaxHeaders,
+ debugMode: this.debugMode,
+ subPixelRoundingForTransparency: this.subPixelRoundingForTransparency,
+ callTileLoadedWithCachedData: this.callTileLoadedWithCachedData,
+ originalDataType: options.originalDataType
+ });
+
+ options.success({
+ item: tiledImage
+ });
+ return tiledImage;
+ }).catch(e => {
+ if (options.error) {
+ options.error(e);
+ return e;
+ }
+ throw e;
+ });
+ },
+
+ /**
+ * Attempts to initialize a TileSource from various input types and configuration formats.
+ * Handles string URLs, raw XML/JSON strings, inline configuration objects, or custom TileSource implementations.
+ *
+ * @function
+ * @param {OpenSeadragon.TileSourceSpecifier} options options to create the image. Some properties
+ * @return {OpenSeadragon.Promise