From 2423b1775df1b93bce2af70886559fc48c449e75 Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Wed, 22 Apr 2026 17:02:58 +0200 Subject: [PATCH 1/9] New feature for having multiple registrations active. --- .../annotations/js/exact-browser-sync.js | 41 ++++++++++-- .../static/annotations/js/exact-quad-tree.js | 5 +- .../static/annotations/js/exact-sync.js | 64 ++++++++++++++++--- .../templates/annotations/annotate.html | 7 +- .../templates/annotations/annotate_v2.html | 7 +- exact/exact/images/api_views.py | 6 +- exact/exact/images/models.py | 2 +- 7 files changed, 110 insertions(+), 22 deletions(-) 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..d56c84ea 100644 --- a/exact/exact/annotations/static/annotations/js/exact-quad-tree.js +++ b/exact/exact/annotations/static/annotations/js/exact-quad-tree.js @@ -100,7 +100,10 @@ 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); this.background_viewer.addHandler("open", function (event) { diff --git a/exact/exact/annotations/static/annotations/js/exact-sync.js b/exact/exact/annotations/static/annotations/js/exact-sync.js index 77673110..96dee413 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 + $.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' +'&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,target_image.image_set.show_registration' + + '&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, // carries image_set.show_registration + 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/templates/annotations/annotate.html b/exact/exact/annotations/templates/annotations/annotate.html index 75625b25..1918d4b9 100644 --- a/exact/exact/annotations/templates/annotations/annotate.html +++ b/exact/exact/annotations/templates/annotations/annotate.html @@ -853,7 +853,12 @@
{{ selected_image.name }}
-
+
+ + +
diff --git a/exact/exact/annotations/templates/annotations/annotate_v2.html b/exact/exact/annotations/templates/annotations/annotate_v2.html index 0b595869..1f7cd504 100644 --- a/exact/exact/annotations/templates/annotations/annotate_v2.html +++ b/exact/exact/annotations/templates/annotations/annotate_v2.html @@ -868,7 +868,12 @@
{{ selected_image.name }}
-
+
+ + +
diff --git a/exact/exact/images/api_views.py b/exact/exact/images/api_views.py index 4f67d748..cf48d46e 100644 --- a/exact/exact/images/api_views.py +++ b/exact/exact/images/api_views.py @@ -337,13 +337,13 @@ def register_images(self, request, pk=None): source_image = get_object_or_404(models.Image, id=pk) target_image = get_object_or_404(models.Image, id=int(request.data.get("target_image", 0))) - image_registration = models.ImageRegistration.objects.filter(source_image=source_image, target_image=target_image).first() + image_registration = models.ImageRegistration.objects.filter(source_image=source_image, target_image=target_image).first() # register the two images - return_status = HTTP_202_ACCEPTED + return_status = HTTP_202_ACCEPTED if image_registration is None: - image_registration = models.ImageRegistration(source_image=source_image, target_image=target_image) + image_registration = models.ImageRegistration(source_image=source_image, target_image=target_image) return_status = HTTP_201_CREATED image_registration.perform_registration(**request.data) diff --git a/exact/exact/images/models.py b/exact/exact/images/models.py index 20620992..f1e2e4a2 100644 --- a/exact/exact/images/models.py +++ b/exact/exact/images/models.py @@ -830,7 +830,7 @@ def create_inverse_registration(self): "t_22": self.transformation_matrix["t_22"], } - new_registration = ImageRegistration.objects.create(source_image=self.target_image, target_image=self.source_image, + new_registration = ImageRegistration.objects.create(source_image=self.target_image, target_image=self.source_image, registration_error=self.registration_error, runtime=self.runtime, transformation_matrix=new_transformation_matrix) new_registration.save() From 888bc4ed644ebaece9c4e36e27bd17e2b1627b05 Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Wed, 22 Apr 2026 19:52:52 +0200 Subject: [PATCH 2/9] fixed alignment in registration --- .../static/annotations/js/exact-quad-tree.js | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) 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 d56c84ea..0f98e5c3 100644 --- a/exact/exact/annotations/static/annotations/js/exact-quad-tree.js +++ b/exact/exact/annotations/static/annotations/js/exact-quad-tree.js @@ -129,18 +129,27 @@ class EXACTRegistrationHandler { 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); } From 096ef9b37491938dc3e8d7d9cc1c38929c66d6de Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Thu, 23 Apr 2026 16:20:10 +0200 Subject: [PATCH 3/9] Removed show_registration --- .../static/annotations/js/exact-quad-tree.js | 3 +-- .../static/annotations/js/exact-sync.js | 6 +++--- exact/exact/images/forms.py | 3 --- .../0037_remove_imageset_show_registration.py | 17 +++++++++++++++++ exact/exact/images/models.py | 1 - exact/exact/images/serializers.py | 1 - .../templates/images/create_imageset.html | 7 ------- .../images/templates/images/edit_imageset.html | 7 ------- .../exact/images/templates/images/imageset.html | 10 ---------- .../images/templates/images/imageset_v2.html | 10 ---------- 10 files changed, 21 insertions(+), 44 deletions(-) create mode 100644 exact/exact/images/migrations/0037_remove_imageset_show_registration.py 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 0f98e5c3..930b4d9a 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(); diff --git a/exact/exact/annotations/static/annotations/js/exact-sync.js b/exact/exact/annotations/static/annotations/js/exact-sync.js index 96dee413..314dc1bf 100644 --- a/exact/exact/annotations/static/annotations/js/exact-sync.js +++ b/exact/exact/annotations/static/annotations/js/exact-sync.js @@ -53,7 +53,7 @@ class EXACTRegistrationSync { 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' + +'&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: context.gHeaders, @@ -68,7 +68,7 @@ class EXACTRegistrationSync { // 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,target_image.image_set.show_registration' + + '&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, @@ -82,7 +82,7 @@ class EXACTRegistrationSync { const inv = reg.inv_matrix; context.registeredImagePairs[reg.target_image.name] = { id: reg.id, - source_image: reg.target_image, // carries image_set.show_registration + source_image: reg.target_image, target_image: reg.source_image, transformation_matrix: reg.inv_matrix, inv_matrix: reg.transformation_matrix, diff --git a/exact/exact/images/forms.py b/exact/exact/images/forms.py index 260fc2e1..3f962fee 100644 --- a/exact/exact/images/forms.py +++ b/exact/exact/images/forms.py @@ -11,7 +11,6 @@ class Meta: 'location', 'public', 'public_collaboration', - 'show_registration' ] @@ -24,7 +23,6 @@ class Meta: 'public', 'public_collaboration', 'team', - 'show_registration' ] @@ -41,7 +39,6 @@ class Meta: 'priority', 'collaboration_type', 'main_annotation_type', - 'show_registration' ] diff --git a/exact/exact/images/migrations/0037_remove_imageset_show_registration.py b/exact/exact/images/migrations/0037_remove_imageset_show_registration.py new file mode 100644 index 00000000..7445a0a6 --- /dev/null +++ b/exact/exact/images/migrations/0037_remove_imageset_show_registration.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.18 on 2026-04-23 16:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('images', '0036_imageregistration_registration_points'), + ] + + operations = [ + migrations.RemoveField( + model_name='imageset', + name='show_registration', + ), + ] diff --git a/exact/exact/images/models.py b/exact/exact/images/models.py index f1e2e4a2..77004b8f 100644 --- a/exact/exact/images/models.py +++ b/exact/exact/images/models.py @@ -437,7 +437,6 @@ class ZipState: pinned_by = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='pinned_sets') zip_state = models.IntegerField(choices=ZIP_STATES, default=ZipState.INVALID) collaboration_type = models.IntegerField(choices=COLLABORATION_TYPES, default=CollaborationTypes.COLLABORATIVE) - show_registration = models.BooleanField(default=False) def root_path(self): diff --git a/exact/exact/images/serializers.py b/exact/exact/images/serializers.py index b7a2d661..2f2ab0e6 100644 --- a/exact/exact/images/serializers.py +++ b/exact/exact/images/serializers.py @@ -154,7 +154,6 @@ class Meta: 'team', 'creator', 'collaboration_type', - 'show_registration' ) expandable_fields = { diff --git a/exact/exact/images/templates/images/create_imageset.html b/exact/exact/images/templates/images/create_imageset.html index 876e1a7e..8101418f 100644 --- a/exact/exact/images/templates/images/create_imageset.html +++ b/exact/exact/images/templates/images/create_imageset.html @@ -27,13 +27,6 @@
{{ error }}
{% endfor %} -
- - {% render_field form.show_registration %} - {% for error in form.show_registration.errors %} -
{{ error }}
- {% endfor %} -
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %} diff --git a/exact/exact/images/templates/images/edit_imageset.html b/exact/exact/images/templates/images/edit_imageset.html index 1ca6cdc4..0f9065df 100644 --- a/exact/exact/images/templates/images/edit_imageset.html +++ b/exact/exact/images/templates/images/edit_imageset.html @@ -41,13 +41,6 @@
{{ error }}
{% endfor %} -
- - {% render_field form.show_registration %} - {% for error in form.show_registration.errors %} -
{{ error }}
- {% endfor %} -
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %} diff --git a/exact/exact/images/templates/images/imageset.html b/exact/exact/images/templates/images/imageset.html index 0fff49d5..b5acd7e0 100644 --- a/exact/exact/images/templates/images/imageset.html +++ b/exact/exact/images/templates/images/imageset.html @@ -574,16 +574,6 @@

Imageset management

{% render_field edit_form.main_annotation_type class+='form-control' %} - - - - - - {% render_field edit_form.show_registration class+='form-control' %} - - {% if "delete_set" in imageset_perms %} diff --git a/exact/exact/images/templates/images/imageset_v2.html b/exact/exact/images/templates/images/imageset_v2.html index ba4e01b7..b44bd0d9 100644 --- a/exact/exact/images/templates/images/imageset_v2.html +++ b/exact/exact/images/templates/images/imageset_v2.html @@ -813,16 +813,6 @@

{{imageset.name}} {{ imageset.prio_symbol | safe }} {% render_field edit_form.main_annotation_type class+='form-control' %} - - - - - - {% render_field edit_form.show_registration class+='form-control' %} - - {% if "delete_set" in imageset_perms %} From 59231a7f4f660563b6cdd71f03b5d23665c09c42 Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Thu, 23 Apr 2026 21:29:47 +0200 Subject: [PATCH 4/9] Switched to more recent version of openseadragon. --- .../static/annotations/js/exact-quad-tree.js | 2 +- .../annotations/js/openseadragon.min.js | 109 +++++++++++++++++- .../annotations/js/openseadragon.min.js.map | 2 +- 3 files changed, 107 insertions(+), 6 deletions(-) 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 930b4d9a..8e2100a4 100644 --- a/exact/exact/annotations/static/annotations/js/exact-quad-tree.js +++ b/exact/exact/annotations/static/annotations/js/exact-quad-tree.js @@ -150,7 +150,7 @@ class EXACTRegistrationHandler { 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/openseadragon.min.js b/exact/exact/annotations/static/annotations/js/openseadragon.min.js index b95f5d15..b53236bd 100644 --- a/exact/exact/annotations/static/annotations/js/openseadragon.min.js +++ b/exact/exact/annotations/static/annotations/js/openseadragon.min.js @@ -1,9 +1,110 @@ -//! openseadragon 2.4.2 -//! Built on 2020-03-05 -//! Git commit: v2.4.2-0-c450749 +//! 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/ -function OpenSeadragon(e){return new OpenSeadragon.Viewer(e)}!function(n){n.version={versionStr:"2.4.2",major:parseInt("2",10),minor:parseInt("4",10),revision:parseInt("2",10)};var t={"[object Boolean]":"boolean","[object Number]":"number","[object String]":"string","[object Function]":"function","[object Array]":"array","[object Date]":"date","[object RegExp]":"regexp","[object Object]":"object"},i=Object.prototype.toString,o=Object.prototype.hasOwnProperty;n.isFunction=function(e){return"function"===n.type(e)};n.isArray=Array.isArray||function(e){return"array"===n.type(e)};n.isWindow=function(e){return e&&"object"==typeof e&&"setInterval"in e};n.type=function(e){return null==e?String(e):t[i.call(e)]||"object"};n.isPlainObject=function(e){if(!e||"object"!==OpenSeadragon.type(e)||e.nodeType||n.isWindow(e))return!1;if(e.constructor&&!o.call(e,"constructor")&&!o.call(e.constructor.prototype,"isPrototypeOf"))return!1;var t;for(var i in e)t=i;return void 0===t||o.call(e,t)};n.isEmptyObject=function(e){for(var t in e)return!1;return!0};n.freezeObject=function(e){Object.freeze?n.freezeObject=Object.freeze:n.freezeObject=function(e){return e};return n.freezeObject(e)};n.supportsCanvas=(e=document.createElement("canvas"),!(!n.isFunction(e.getContext)||!e.getContext("2d")));var e;n.isCanvasTainted=function(e){var t=!1;try{e.getContext("2d").getImageData(0,0,1,1)}catch(e){t=!0}return t};n.pixelDensityRatio=function(){if(n.supportsCanvas){var e=document.createElement("canvas").getContext("2d");var t=window.devicePixelRatio||1;var i=e.webkitBackingStorePixelRatio||e.mozBackingStorePixelRatio||e.msBackingStorePixelRatio||e.oBackingStorePixelRatio||e.backingStorePixelRatio||1;return Math.max(t,1)/i}return 1}()}(OpenSeadragon);!function($){$.extend=function(){var e,t,i,n,o,r,s=arguments[0]||{},a=arguments.length,l=!1,h=1;if("boolean"==typeof s){l=s;s=arguments[1]||{};h=2}"object"==typeof s||OpenSeadragon.isFunction(s)||(s={});if(a===h){s=this;--h}for(;h=i.x&&t.x=i.y},getEvent:function(e){$.getEvent=e?function(e){return e}:function(){return window.event};return $.getEvent(e)},getMousePosition:function(e){if("number"==typeof e.pageX)$.getMousePosition=function(e){var t=new $.Point;e=$.getEvent(e);t.x=e.pageX;t.y=e.pageY;return t};else{if("number"!=typeof e.clientX)throw new Error("Unknown event mouse position, no known technique.");$.getMousePosition=function(e){var t=new $.Point;e=$.getEvent(e);t.x=e.clientX+document.body.scrollLeft+document.documentElement.scrollLeft;t.y=e.clientY+document.body.scrollTop+document.documentElement.scrollTop;return t}}return $.getMousePosition(e)},getPageScroll:function(){var e=document.documentElement||{},t=document.body||{};if("number"==typeof window.pageXOffset)$.getPageScroll=function(){return new $.Point(window.pageXOffset,window.pageYOffset)};else if(t.scrollLeft||t.scrollTop)$.getPageScroll=function(){return new $.Point(document.body.scrollLeft,document.body.scrollTop)};else{if(!e.scrollLeft&&!e.scrollTop)return new $.Point(0,0);$.getPageScroll=function(){return new $.Point(document.documentElement.scrollLeft,document.documentElement.scrollTop)}}return $.getPageScroll()},setPageScroll:function(e){if(void 0!==window.scrollTo)$.setPageScroll=function(e){window.scrollTo(e.x,e.y)};else{var t=$.getPageScroll();if(t.x===e.x&&t.y===e.y)return;document.body.scrollLeft=e.x;document.body.scrollTop=e.y;var i=$.getPageScroll();if(i.x!==t.x&&i.y!==t.y){$.setPageScroll=function(e){document.body.scrollLeft=e.x;document.body.scrollTop=e.y};return}document.documentElement.scrollLeft=e.x;document.documentElement.scrollTop=e.y;if((i=$.getPageScroll()).x!==t.x&&i.y!==t.y){$.setPageScroll=function(e){document.documentElement.scrollLeft=e.x;document.documentElement.scrollTop=e.y};return}$.setPageScroll=function(e){}}return $.setPageScroll(e)},getWindowSize:function(){var e=document.documentElement||{},t=document.body||{};if("number"==typeof window.innerWidth)$.getWindowSize=function(){return new $.Point(window.innerWidth,window.innerHeight)};else if(e.clientWidth||e.clientHeight)$.getWindowSize=function(){return new $.Point(document.documentElement.clientWidth,document.documentElement.clientHeight)};else{if(!t.clientWidth&&!t.clientHeight)throw new Error("Unknown window size, no known technique.");$.getWindowSize=function(){return new $.Point(document.body.clientWidth,document.body.clientHeight)}}return $.getWindowSize()},makeCenteredNode:function(e){e=$.getElement(e);var t=[$.makeNeutralElement("div"),$.makeNeutralElement("div"),$.makeNeutralElement("div")];$.extend(t[0].style,{display:"table",height:"100%",width:"100%"});$.extend(t[1].style,{display:"table-row"});$.extend(t[2].style,{display:"table-cell",verticalAlign:"middle",textAlign:"center"});t[0].appendChild(t[1]);t[1].appendChild(t[2]);t[2].appendChild(e);return t[0]},makeNeutralElement:function(e){var t=document.createElement(e),i=t.style;i.background="transparent none";i.border="none";i.margin="0px";i.padding="0px";i.position="static";return t},now:function(){Date.now?$.now=Date.now:$.now=function(){return(new Date).getTime()};return $.now()},makeTransparentImage:function(e){$.makeTransparentImage=function(e){var t=$.makeNeutralElement("img");t.src=e;return t};$.Browser.vendor==$.BROWSERS.IE&&$.Browser.version<7&&($.makeTransparentImage=function(e){var t=$.makeNeutralElement("img"),i=null;(i=$.makeNeutralElement("span")).style.display="inline-block";t.onload=function(){i.style.width=i.style.width||t.width+"px";i.style.height=i.style.height||t.height+"px";t.onload=null;t=null};t.src=e;i.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+e+"', sizingMethod='scale')";return i});return $.makeTransparentImage(e)},setElementOpacity:function(e,t,i){var n;e=$.getElement(e);i&&!$.Browser.alpha&&(t=Math.round(t));if($.Browser.opacity)e.style.opacity=t<1?t:"";else if(t<1){n="alpha(opacity="+Math.round(100*t)+")";e.style.filter=n}else e.style.filter=""},setElementTouchActionNone:function(e){void 0!==(e=$.getElement(e)).style.touchAction?e.style.touchAction="none":void 0!==e.style.msTouchAction&&(e.style.msTouchAction="none")},addClass:function(e,t){(e=$.getElement(e)).className?-1===(" "+e.className+" ").indexOf(" "+t+" ")&&(e.className+=" "+t):e.className=t},indexOf:function(e,t,i){Array.prototype.indexOf?this.indexOf=function(e,t,i){return e.indexOf(t,i)}:this.indexOf=function(e,t,i){var n,o,r=i||0;if(!e)throw new TypeError;if(0===(o=e.length)||o<=r)return-1;r<0&&(r=o-Math.abs(r));for(n=r;nt.touches.length-s){v.console.warn("Tracked touch contact count doesn't match event.touches.length. Removing all tracked touch pointers.");b(e,t,l)}for(n=0;n\s*$/))o=m.parseXml(o);else if(o.match(/^\s*[\{\[].*[\}\]]\s*$/))try{var e=m.parseJSON(o);o=e}catch(e){}function h(e,t){if(e.ready)s(e);else{e.addHandler("ready",function(){s(e)});e.addHandler("open-failed",function(e){a({message:e.message,source:t})})}}setTimeout(function(){if("string"==m.type(o))(o=new m.TileSource({url:o,crossOriginPolicy:void 0!==r.crossOriginPolicy?r.crossOriginPolicy:n.crossOriginPolicy,ajaxWithCredentials:n.ajaxWithCredentials,ajaxHeaders:n.ajaxHeaders,useCanvas:n.useCanvas,success:function(e){s(e.tileSource)}})).addHandler("open-failed",function(e){a(e)});else if(m.isPlainObject(o)||o.nodeType){void 0!==o.crossOriginPolicy||void 0===r.crossOriginPolicy&&void 0===n.crossOriginPolicy||(o.crossOriginPolicy=void 0!==r.crossOriginPolicy?r.crossOriginPolicy:n.crossOriginPolicy);void 0===o.ajaxWithCredentials&&(o.ajaxWithCredentials=n.ajaxWithCredentials);void 0===o.useCanvas&&(o.useCanvas=n.useCanvas);if(m.isFunction(o.getTileUrl)){var e=new m.TileSource(o);e.getTileUrl=o.getTileUrl;s(e)}else{var t=m.TileSource.determineType(l,o);if(!t){a({message:"Unable to load TileSource",source:o});return}var i=t.prototype.configure.apply(l,[o]);h(new t(i),o)}}else h(o,o)})}(this,i.tileSource,i,function(e){n.tileSource=e;s()},function(e){e.options=i;t(e);s()})}function s(){var e,t,i;for(;o._loadQueue.length&&(e=o._loadQueue[0]).tileSource;){o._loadQueue.splice(0,1);if(e.options.replace){var n=o.world.getIndexOfItem(e.options.replaceItem);-1!=n&&(e.options.index=n);o.world.removeItem(e.options.replaceItem)}t=new m.TiledImage({viewer:o,source:e.tileSource,viewport:o.viewport,drawer:o.drawer,tileCache:o.tileCache,imageLoader:o.imageLoader,x:e.options.x,y:e.options.y,width:e.options.width,height:e.options.height,fitBounds:e.options.fitBounds,fitBoundsPlacement:e.options.fitBoundsPlacement,clip:e.options.clip,placeholderFillStyle:e.options.placeholderFillStyle,opacity:e.options.opacity,preload:e.options.preload,degrees:e.options.degrees,compositeOperation:e.options.compositeOperation,springStiffness:o.springStiffness,animationTime:o.animationTime,minZoomImageRatio:o.minZoomImageRatio,wrapHorizontal:o.wrapHorizontal,wrapVertical:o.wrapVertical,immediateRender:o.immediateRender,blendTime:o.blendTime,alwaysBlend:o.alwaysBlend,minPixelRatio:o.minPixelRatio,smoothTileEdgesMinZoom:o.smoothTileEdgesMinZoom,iOSDevice:o.iOSDevice,crossOriginPolicy:e.options.crossOriginPolicy,ajaxWithCredentials:e.options.ajaxWithCredentials,loadTilesWithAjax:e.options.loadTilesWithAjax,ajaxHeaders:e.options.ajaxHeaders,debugMode:o.debugMode});o.collectionMode&&o.world.setAutoRefigureSizes(!1);o.world.addItem(t,{index:e.options.index});0===o._loadQueue.length&&r(e);1!==o.world.getItemCount()||o.preserveViewport||o.viewport.goHome(!0);if(o.navigator){i=m.extend({},e.options,{replace:!1,originalTiledImage:t,tileSource:e.tileSource});o.navigator.addTiledImage(i)}e.options.success&&e.options.success({item:t})}}},addSimpleImage:function(e){m.console.assert(e,"[Viewer.addSimpleImage] options is required");m.console.assert(e.url,"[Viewer.addSimpleImage] options.url is required");var t=m.extend({},e,{tileSource:{type:"image",url:e.url}});delete t.url;this.addTiledImage(t)},addLayer:function(t){var i=this;m.console.error("[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead.");var e=m.extend({},t,{success:function(e){i.raiseEvent("add-layer",{options:t,drawer:e.item})},error:function(e){i.raiseEvent("add-layer-failed",e)}});this.addTiledImage(e);return this},getLayerAtLevel:function(e){m.console.error("[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead.");return this.world.getItemAt(e)},getLevelOfLayer:function(e){m.console.error("[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead.");return this.world.getIndexOfItem(e)},getLayersCount:function(){m.console.error("[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead.");return this.world.getItemCount()},setLayerLevel:function(e,t){m.console.error("[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead.");return this.world.setItemIndex(e,t)},removeLayer:function(e){m.console.error("[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead.");return this.world.removeItem(e)},forceRedraw:function(){c[this.hash].forceRedraw=!0;return this},bindSequenceControls:function(){var e=m.delegate(this,v),t=m.delegate(this,f),i=m.delegate(this,$),n=m.delegate(this,G),o=this.navImages,r=!0;if(this.showSequenceControl){(this.previousButton||this.nextButton)&&(r=!1);this.previousButton=new m.Button({element:this.previousButton?m.getElement(this.previousButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.PreviousPage"),srcRest:D(this.prefixUrl,o.previous.REST),srcGroup:D(this.prefixUrl,o.previous.GROUP),srcHover:D(this.prefixUrl,o.previous.HOVER),srcDown:D(this.prefixUrl,o.previous.DOWN),onRelease:n,onFocus:e,onBlur:t});this.nextButton=new m.Button({element:this.nextButton?m.getElement(this.nextButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.NextPage"),srcRest:D(this.prefixUrl,o.next.REST),srcGroup:D(this.prefixUrl,o.next.GROUP),srcHover:D(this.prefixUrl,o.next.HOVER),srcDown:D(this.prefixUrl,o.next.DOWN),onRelease:i,onFocus:e,onBlur:t});this.navPrevNextWrap||this.previousButton.disable();this.tileSources&&this.tileSources.length||this.nextButton.disable();if(r){this.paging=new m.ButtonGroup({buttons:[this.previousButton,this.nextButton],clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.pagingControl=this.paging.element;this.toolbar?this.toolbar.addControl(this.pagingControl,{anchor:m.ControlAnchor.BOTTOM_RIGHT}):this.addControl(this.pagingControl,{anchor:this.sequenceControlAnchor||m.ControlAnchor.TOP_LEFT})}}return this},bindStandardControls:function(){var e=m.delegate(this,M),t=m.delegate(this,H),i=m.delegate(this,F),n=m.delegate(this,z),o=m.delegate(this,L),r=m.delegate(this,N),s=m.delegate(this,W),a=m.delegate(this,V),l=m.delegate(this,U),h=m.delegate(this,j),c=m.delegate(this,v),u=m.delegate(this,f),d=this.navImages,p=[],g=!0;if(this.showNavigationControl){(this.zoomInButton||this.zoomOutButton||this.homeButton||this.fullPageButton||this.rotateLeftButton||this.rotateRightButton||this.flipButton)&&(g=!1);if(this.showZoomControl){p.push(this.zoomInButton=new m.Button({element:this.zoomInButton?m.getElement(this.zoomInButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomIn"),srcRest:D(this.prefixUrl,d.zoomIn.REST),srcGroup:D(this.prefixUrl,d.zoomIn.GROUP),srcHover:D(this.prefixUrl,d.zoomIn.HOVER),srcDown:D(this.prefixUrl,d.zoomIn.DOWN),onPress:e,onRelease:t,onClick:i,onEnter:e,onExit:t,onFocus:c,onBlur:u}));p.push(this.zoomOutButton=new m.Button({element:this.zoomOutButton?m.getElement(this.zoomOutButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomOut"),srcRest:D(this.prefixUrl,d.zoomOut.REST),srcGroup:D(this.prefixUrl,d.zoomOut.GROUP),srcHover:D(this.prefixUrl,d.zoomOut.HOVER),srcDown:D(this.prefixUrl,d.zoomOut.DOWN),onPress:n,onRelease:t,onClick:o,onEnter:n,onExit:t,onFocus:c,onBlur:u}))}this.showHomeControl&&p.push(this.homeButton=new m.Button({element:this.homeButton?m.getElement(this.homeButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Home"),srcRest:D(this.prefixUrl,d.home.REST),srcGroup:D(this.prefixUrl,d.home.GROUP),srcHover:D(this.prefixUrl,d.home.HOVER),srcDown:D(this.prefixUrl,d.home.DOWN),onRelease:r,onFocus:c,onBlur:u}));this.showFullPageControl&&p.push(this.fullPageButton=new m.Button({element:this.fullPageButton?m.getElement(this.fullPageButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.FullPage"),srcRest:D(this.prefixUrl,d.fullpage.REST),srcGroup:D(this.prefixUrl,d.fullpage.GROUP),srcHover:D(this.prefixUrl,d.fullpage.HOVER),srcDown:D(this.prefixUrl,d.fullpage.DOWN),onRelease:s,onFocus:c,onBlur:u}));if(this.showRotationControl){p.push(this.rotateLeftButton=new m.Button({element:this.rotateLeftButton?m.getElement(this.rotateLeftButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateLeft"),srcRest:D(this.prefixUrl,d.rotateleft.REST),srcGroup:D(this.prefixUrl,d.rotateleft.GROUP),srcHover:D(this.prefixUrl,d.rotateleft.HOVER),srcDown:D(this.prefixUrl,d.rotateleft.DOWN),onRelease:a,onFocus:c,onBlur:u}));p.push(this.rotateRightButton=new m.Button({element:this.rotateRightButton?m.getElement(this.rotateRightButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateRight"),srcRest:D(this.prefixUrl,d.rotateright.REST),srcGroup:D(this.prefixUrl,d.rotateright.GROUP),srcHover:D(this.prefixUrl,d.rotateright.HOVER),srcDown:D(this.prefixUrl,d.rotateright.DOWN),onRelease:l,onFocus:c,onBlur:u}))}this.showFlipControl&&p.push(this.flipButton=new m.Button({element:this.flipButton?m.getElement(this.flipButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Flip"),srcRest:D(this.prefixUrl,d.flip.REST),srcGroup:D(this.prefixUrl,d.flip.GROUP),srcHover:D(this.prefixUrl,d.flip.HOVER),srcDown:D(this.prefixUrl,d.flip.DOWN),onRelease:h,onFocus:c,onBlur:u}));if(g){this.buttons=new m.ButtonGroup({buttons:p,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.navControl=this.buttons.element;this.addHandler("open",m.delegate(this,A));this.toolbar?this.toolbar.addControl(this.navControl,{anchor:this.navigationControlAnchor||m.ControlAnchor.TOP_LEFT}):this.addControl(this.navControl,{anchor:this.navigationControlAnchor||m.ControlAnchor.TOP_LEFT})}}return this},currentPage:function(){return this._sequenceIndex},goToPage:function(e){if(this.tileSources&&0<=e&&e=t.flickMinSpeed){var i=0;this.panHorizontal&&(i=t.flickMomentum*e.speed*Math.cos(e.direction));var n=0;this.panVertical&&(n=t.flickMomentum*e.speed*Math.sin(e.direction));var o=this.viewport.pixelFromPoint(this.viewport.getCenter(!0));var r=this.viewport.pointFromPixel(new m.Point(o.x-i,o.y-n));this.viewport.panTo(r,!1)}this.viewport.applyConstraints()}this.raiseEvent("canvas-drag-end",{tracker:e.eventSource,position:e.position,speed:e.speed,direction:e.direction,shift:e.shift,originalEvent:e.originalEvent})}function S(e){this.raiseEvent("canvas-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function E(e){window.location!=window.parent.location&&m.MouseTracker.resetAllMouseTrackers();this.raiseEvent("canvas-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function P(e){this.raiseEvent("canvas-press",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,insideElementPressed:e.insideElementPressed,insideElementReleased:e.insideElementReleased,originalEvent:e.originalEvent})}function R(e){this.raiseEvent("canvas-release",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,insideElementPressed:e.insideElementPressed,insideElementReleased:e.insideElementReleased,originalEvent:e.originalEvent})}function _(e){this.raiseEvent("canvas-nonprimary-press",{tracker:e.eventSource,position:e.position,pointerType:e.pointerType,button:e.button,buttons:e.buttons,originalEvent:e.originalEvent})}function b(e){this.raiseEvent("canvas-nonprimary-release",{tracker:e.eventSource,position:e.position,pointerType:e.pointerType,button:e.button,buttons:e.buttons,originalEvent:e.originalEvent})}function C(e){var t,i,n;if(!e.preventDefaultAction&&this.viewport){if((t=this.gestureSettingsByDeviceType(e.pointerType)).pinchToZoom){i=this.viewport.pointFromPixel(e.center,!0);n=this.viewport.pointFromPixel(e.lastCenter,!0).minus(i);this.panHorizontal||(n.x=0);this.panVertical||(n.y=0);this.viewport.zoomBy(e.distance/e.lastDistance,i,!0);t.zoomToRefPoint&&this.viewport.panBy(n,!0);this.viewport.applyConstraints()}if(t.pinchRotate){var o=Math.atan2(e.gesturePoints[0].currentPos.y-e.gesturePoints[1].currentPos.y,e.gesturePoints[0].currentPos.x-e.gesturePoints[1].currentPos.x);var r=Math.atan2(e.gesturePoints[0].lastPos.y-e.gesturePoints[1].lastPos.y,e.gesturePoints[0].lastPos.x-e.gesturePoints[1].lastPos.x);this.viewport.setRotation(this.viewport.getRotation()+(o-r)*(180/Math.PI))}}this.raiseEvent("canvas-pinch",{tracker:e.eventSource,gesturePoints:e.gesturePoints,lastCenter:e.lastCenter,center:e.center,lastDistance:e.lastDistance,distance:e.distance,shift:e.shift,originalEvent:e.originalEvent});return!1}function O(e){var t,i,n;if((n=m.now())-this._lastScrollTime>this.minScrollDeltaTime){this._lastScrollTime=n;this.viewport.flipped&&(e.position.x=this.viewport.getContainerSize().x-e.position.x);if(!e.preventDefaultAction&&this.viewport&&(t=this.gestureSettingsByDeviceType(e.pointerType)).scrollToZoom){i=Math.pow(this.zoomPerScroll,e.scroll);this.viewport.zoomBy(i,t.zoomToRefPoint?this.viewport.pointFromPixel(e.position,!0):null);this.viewport.applyConstraints()}this.raiseEvent("canvas-scroll",{tracker:e.eventSource,position:e.position,scroll:e.scroll,shift:e.shift,originalEvent:e.originalEvent});if(t&&t.scrollToZoom)return!1}else if((t=this.gestureSettingsByDeviceType(e.pointerType))&&t.scrollToZoom)return!1}function I(e){c[this.hash].mouseInside=!0;g(this);this.raiseEvent("container-enter",{tracker:e.eventSource,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function k(e){if(e.pointers<1){c[this.hash].mouseInside=!1;c[this.hash].animating||p(this)}this.raiseEvent("container-exit",{tracker:e.eventSource,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function B(e){!function(e){if(e._opening)return;if(e.autoResize){var t=u(e.container);var i=c[e.hash].prevContainerSize;if(!t.equals(i)){var n=e.viewport;if(e.preserveImageSizeOnResize){var o=i.x/t.x;var r=n.getZoom()*o;var s=n.getCenter();n.resize(t,!1);n.zoomTo(r,null,!0);n.panTo(s,!0)}else{var a=n.getBounds();n.resize(t,!0);n.fitBoundsWithConstraints(a,!0)}c[e.hash].prevContainerSize=t;c[e.hash].forceRedraw=!0}}var l=e.viewport.update();var h=e.world.update()||l;l&&e.raiseEvent("viewport-change");e.referenceStrip&&(h=e.referenceStrip.update(e.viewport)||h);if(!c[e.hash].animating&&h){e.raiseEvent("animation-start");g(e)}if(h||c[e.hash].forceRedraw||e.world.needsDraw()){!function(e){e.imageLoader.clear();e.drawer.clear();e.world.draw();e.raiseEvent("update-viewport",{})}(e);e._drawOverlays();e.navigator&&e.navigator.update(e.viewport);c[e.hash].forceRedraw=!1;h&&e.raiseEvent("animation")}if(c[e.hash].animating&&!h){e.raiseEvent("animation-finish");c[e.hash].mouseInside||p(e)}c[e.hash].animating=h}(e);e.isOpen()?e._updateRequestId=r(e,B):e._updateRequestId=!1}function D(e,t){return e?e+t:t}function M(){c[this.hash].lastZoomTime=m.now();c[this.hash].zoomFactor=this.zoomPerSecond;c[this.hash].zooming=!0;n(this)}function z(){c[this.hash].lastZoomTime=m.now();c[this.hash].zoomFactor=1/this.zoomPerSecond;c[this.hash].zooming=!0;n(this)}function H(){c[this.hash].zooming=!1}function n(e){m.requestAnimationFrame(m.delegate(e,t))}function t(){var e,t,i;if(c[this.hash].zooming&&this.viewport){t=(e=m.now())-c[this.hash].lastZoomTime;i=Math.pow(c[this.hash].zoomFactor,t/1e3);this.viewport.zoomBy(i);this.viewport.applyConstraints();c[this.hash].lastZoomTime=e;n(this)}}function F(){if(this.viewport){c[this.hash].zooming=!1;this.viewport.zoomBy(this.zoomPerClick/1);this.viewport.applyConstraints()}}function L(){if(this.viewport){c[this.hash].zooming=!1;this.viewport.zoomBy(1/this.zoomPerClick);this.viewport.applyConstraints()}}function A(){this.buttons.emulateEnter();this.buttons.emulateExit()}function N(){this.viewport&&this.viewport.goHome()}function W(){this.isFullPage()&&!m.isFullScreen()?this.setFullPage(!1):this.setFullScreen(!this.isFullPage());this.buttons&&this.buttons.emulateExit();this.fullPageButton.element.focus();this.viewport&&this.viewport.applyConstraints()}function V(){if(this.viewport){var e=this.viewport.getRotation();e=this.viewport.flipped?m.positiveModulo(e+this.rotationIncrement,360):m.positiveModulo(e-this.rotationIncrement,360);this.viewport.setRotation(e)}}function U(){if(this.viewport){var e=this.viewport.getRotation();e=this.viewport.flipped?m.positiveModulo(e-this.rotationIncrement,360):m.positiveModulo(e+this.rotationIncrement,360);this.viewport.setRotation(e)}}function j(){this.viewport.toggleFlip()}function G(){var e=this._sequenceIndex-1;this.navPrevNextWrap&&e<0&&(e+=this.tileSources.length);this.goToPage(e)}function $(){var e=this._sequenceIndex+1;this.navPrevNextWrap&&e>=this.tileSources.length&&(e=0);this.goToPage(e)}}(OpenSeadragon);!function(c){c.Navigator=function(i){var e,t,n=i.viewer,o=this;if(i.id){this.element=document.getElementById(i.id);i.controlOptions={anchor:c.ControlAnchor.NONE,attachToViewer:!1,autoFade:!1}}else{i.id="navigator-"+c.now();this.element=c.makeNeutralElement("div");i.controlOptions={anchor:c.ControlAnchor.TOP_RIGHT,attachToViewer:!0,autoFade:i.autoFade};if(i.position)if("BOTTOM_RIGHT"==i.position)i.controlOptions.anchor=c.ControlAnchor.BOTTOM_RIGHT;else if("BOTTOM_LEFT"==i.position)i.controlOptions.anchor=c.ControlAnchor.BOTTOM_LEFT;else if("TOP_RIGHT"==i.position)i.controlOptions.anchor=c.ControlAnchor.TOP_RIGHT;else if("TOP_LEFT"==i.position)i.controlOptions.anchor=c.ControlAnchor.TOP_LEFT;else if("ABSOLUTE"==i.position){i.controlOptions.anchor=c.ControlAnchor.ABSOLUTE;i.controlOptions.top=i.top;i.controlOptions.left=i.left;i.controlOptions.height=i.height;i.controlOptions.width=i.width}}this.element.id=i.id;this.element.className+=" navigator";(i=c.extend(!0,{sizeRatio:c.DEFAULT_SETTINGS.navigatorSizeRatio},i,{element:this.element,tabIndex:-1,showNavigator:!1,mouseNavEnabled:!1,showNavigationControl:!1,showSequenceControl:!1,immediateRender:!0,blendTime:0,animationTime:0,autoResize:i.autoResize,minZoomImageRatio:1,background:i.background,opacity:i.opacity,borderColor:i.borderColor,displayRegionColor:i.displayRegionColor})).minPixelRatio=this.minPixelRatio=n.minPixelRatio;c.setElementTouchActionNone(this.element);this.borderWidth=2;this.fudge=new c.Point(1,1);this.totalBorderWidths=new c.Point(2*this.borderWidth,2*this.borderWidth).minus(this.fudge);i.controlOptions.anchor!=c.ControlAnchor.NONE&&function(e,t){e.margin="0px";e.border=t+"px solid "+i.borderColor;e.padding="0px";e.background=i.background;e.opacity=i.opacity;e.overflow="hidden"}(this.element.style,this.borderWidth);this.displayRegion=c.makeNeutralElement("div");this.displayRegion.id=this.element.id+"-displayregion";this.displayRegion.className="displayregion";!function(e,t){e.position="relative";e.top="0px";e.left="0px";e.fontSize="0px";e.overflow="hidden";e.border=t+"px solid "+i.displayRegionColor;e.margin="0px";e.padding="0px";e.background="transparent";e.float="left";e.cssFloat="left";e.styleFloat="left";e.zIndex=999999999;e.cursor="default"}(this.displayRegion.style,this.borderWidth);this.displayRegionContainer=c.makeNeutralElement("div");this.displayRegionContainer.id=this.element.id+"-displayregioncontainer";this.displayRegionContainer.className="displayregioncontainer";this.displayRegionContainer.style.width="100%";this.displayRegionContainer.style.height="100%";n.addControl(this.element,i.controlOptions);this._resizeWithViewer=i.controlOptions.anchor!=c.ControlAnchor.ABSOLUTE&&i.controlOptions.anchor!=c.ControlAnchor.NONE;if(i.width&&i.height){this.setWidth(i.width);this.setHeight(i.height)}else if(this._resizeWithViewer){e=c.getElementSize(n.element);this.element.style.height=Math.round(e.y*i.sizeRatio)+"px";this.element.style.width=Math.round(e.x*i.sizeRatio)+"px";this.oldViewerSize=e;t=c.getElementSize(this.element);this.elementArea=t.x*t.y}this.oldContainerSize=new c.Point(0,0);c.Viewer.apply(this,[i]);this.displayRegionContainer.appendChild(this.displayRegion);this.element.getElementsByTagName("div")[0].appendChild(this.displayRegionContainer);function r(e){u(o.displayRegionContainer,e);u(o.displayRegion,-e);o.viewport.setRotation(e)}if(i.navigatorRotate){r(i.viewer.viewport?i.viewer.viewport.getRotation():i.viewer.degrees||0);i.viewer.addHandler("rotate",function(e){r(e.degrees)})}this.innerTracker.destroy();this.innerTracker=new c.MouseTracker({element:this.element,dragHandler:c.delegate(this,a),clickHandler:c.delegate(this,s),releaseHandler:c.delegate(this,l),scrollHandler:c.delegate(this,h)});this.addHandler("reset-size",function(){o.viewport&&o.viewport.goHome(!0)});n.world.addHandler("item-index-change",function(t){window.setTimeout(function(){var e=o.world.getItemAt(t.previousIndex);o.world.setItemIndex(e,t.newIndex)},1)});n.world.addHandler("remove-item",function(e){var t=e.item;var i=o._getMatchingItem(t);i&&o.world.removeItem(i)});this.update(n.viewport)};c.extend(c.Navigator.prototype,c.EventSource.prototype,c.Viewer.prototype,{updateSize:function(){if(this.viewport){var e=new c.Point(0===this.container.clientWidth?1:this.container.clientWidth,0===this.container.clientHeight?1:this.container.clientHeight);if(!e.equals(this.oldContainerSize)){this.viewport.resize(e,!0);this.viewport.goHome(!0);this.oldContainerSize=e;this.drawer.clear();this.world.draw()}}},setWidth:function(e){this.width=e;this.element.style.width="number"==typeof e?e+"px":e;this._resizeWithViewer=!1},setHeight:function(e){this.height=e;this.element.style.height="number"==typeof e?e+"px":e;this._resizeWithViewer=!1},setFlip:function(e){this.viewport.setFlip(e);this.setDisplayTransform(this.viewer.viewport.getFlip()?"scale(-1,1)":"scale(1,1)");return this},setDisplayTransform:function(e){i(this.displayRegion,e);i(this.canvas,e);i(this.element,e)},update:function(e){var t,i,n,o,r,s;t=c.getElementSize(this.viewer.element);if(this._resizeWithViewer&&t.x&&t.y&&!t.equals(this.oldViewerSize)){this.oldViewerSize=t;if(this.maintainSizeRatio||!this.elementArea){i=t.x*this.sizeRatio;n=t.y*this.sizeRatio}else{i=Math.sqrt(this.elementArea*(t.x/t.y));n=this.elementArea/i}this.element.style.width=Math.round(i)+"px";this.element.style.height=Math.round(n)+"px";this.elementArea||(this.elementArea=i*n);this.updateSize()}if(e&&this.viewport){o=e.getBoundsNoRotate(!0);r=this.viewport.pixelFromPointNoRotate(o.getTopLeft(),!1);s=this.viewport.pixelFromPointNoRotate(o.getBottomRight(),!1).minus(this.totalBorderWidths);var a=this.displayRegion.style;a.display=this.world.getItemCount()?"block":"none";a.top=Math.round(r.y)+"px";a.left=Math.round(r.x)+"px";var l=Math.abs(r.x-s.x);var h=Math.abs(r.y-s.y);a.width=Math.round(Math.max(l,0))+"px";a.height=Math.round(Math.max(h,0))+"px"}},addTiledImage:function(e){var n=this;var o=e.originalTiledImage;delete e.original;var t=c.extend({},e,{success:function(e){var t=e.item;t._originalForNavigator=o;n._matchBounds(t,o,!0);function i(){n._matchBounds(t,o)}o.addHandler("bounds-change",i);o.addHandler("clip-change",i);o.addHandler("opacity-change",function(){n._matchOpacity(t,o)});o.addHandler("composite-operation-change",function(){n._matchCompositeOperation(t,o)})}});return c.Viewer.prototype.addTiledImage.apply(this,[t])},_getMatchingItem:function(e){var t=this.world.getItemCount();var i;for(var n=0;n=1/this.aspectRatio-1e-15&&(a=this.getNumTiles(e).y-1);return new d.Point(s,a)},getTileBounds:function(e,t,i,n){var o=this.dimensions.times(this.getLevelScale(e)),r=this.getTileWidth(e),s=this.getTileHeight(e),a=0===t?0:r*t-this.tileOverlap,l=0===i?0:s*i-this.tileOverlap,h=r+(0===t?1:2)*this.tileOverlap,c=s+(0===i?1:2)*this.tileOverlap,u=1/o.x;h=Math.min(h,o.x-a);c=Math.min(c,o.y-l);return n?new d.Rect(0,0,h,c):new d.Rect(a*u,l*u,h*u,c*u)},getImageInfo:function(n){var e,i,o,r,t,s,a,l=this;n&&-1<(a=(s=(t=n.split("/"))[t.length-1]).lastIndexOf("."))&&(t[t.length-1]=s.slice(0,a));i=function(e){"string"==typeof e&&(e=d.parseXml(e));var t=d.TileSource.determineType(l,e,n);if(t){void 0===(r=t.prototype.configure.apply(l,[e,n])).ajaxWithCredentials&&(r.ajaxWithCredentials=l.ajaxWithCredentials);o=new t(r);l.ready=!0;l.raiseEvent("ready",{tileSource:o})}else l.raiseEvent("open-failed",{message:"Unable to load TileSource",source:n})};if(n.match(/\.js$/)){e=n.split("/").pop().replace(".js","");d.jsonp({url:n,async:!1,callbackName:e,callback:i})}else d.makeAjaxRequest({url:n,withCredentials:this.ajaxWithCredentials,headers:this.ajaxHeaders,success:function(e){var t=function(t){var e,i,n=t.responseText,o=t.status;{if(!t)throw new Error(d.getString("Errors.Security"));if(200!==t.status&&0!==t.status){o=t.status;e=404==o?"Not Found":t.statusText;throw new Error(d.getString("Errors.Status",o,e))}}if(n.match(/\s*<.*/))try{i=t.responseXML&&t.responseXML.documentElement?t.responseXML:d.parseXml(n)}catch(e){i=t.responseText}else if(n.match(/\s*[\{\[].*/))try{i=d.parseJSON(n)}catch(e){i=n}else i=n;return i}(e);i(t)},error:function(e,t){var i;try{i="HTTP "+e.status+" attempting to load TileSource"}catch(e){i=(void 0!==t&&t.toString?t.toString():"Unknown error")+" attempting to load TileSource"}l.raiseEvent("open-failed",{message:i,source:n})}})},supports:function(e,t){return!1},configure:function(e,t){throw new Error("Method not implemented.")},getTileUrl:function(e,t,i){throw new Error("Method not implemented.")},getTileAjaxHeaders:function(e,t,i){return{}},tileExists:function(e,t,i){var n=this.getNumTiles(e);return e>=this.minLevel&&e<=this.maxLevel&&0<=t&&0<=i&&tthis.maxLevel)return!1;if(!c||!c.length)return!0;for(h=c.length-1;0<=h;h--)if(!(e<(n=c[h]).minLevel||e>n.maxLevel)){o=this.getLevelScale(e);r=n.x*o;s=n.y*o;a=r+n.width*o;l=s+n.height*o;r=Math.floor(r/this._tileWidth);s=Math.floor(s/this._tileWidth);a=Math.ceil(a/this._tileWidth);l=Math.ceil(l/this._tileWidth);if(r<=t&&t=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t}return h.TileSource.prototype.getLevelScale.call(this,e)},getNumTiles:function(e){if(this.emulateLegacyImagePyramid){return this.getLevelScale(e)?new h.Point(1,1):new h.Point(0,0)}return h.TileSource.prototype.getNumTiles.call(this,e)},getTileAtPoint:function(e,t){return this.emulateLegacyImagePyramid?new h.Point(0,0):h.TileSource.prototype.getTileAtPoint.call(this,e,t)},getTileUrl:function(e,t,i){if(this.emulateLegacyImagePyramid){var n=null;0=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n}var o,r,s,a,l,h,c,u,d,p,g,m,v,f=Math.pow(.5,this.maxLevel-e),w=Math.ceil(this.width*f),y=Math.ceil(this.height*f);o=this.getTileWidth(e);r=this.getTileHeight(e);s=Math.ceil(o/f);a=Math.ceil(r/f);v=1===this.version?"native."+this.tileFormat:"default."+this.tileFormat;if(we.tileSize||parseInt(t.y,10)>e.tileSize;){t.x=Math.floor(t.x/2);t.y=Math.floor(t.y/2);e.imageSizes.push({x:t.x,y:t.y});e.gridSize.push(this._getGridSize(t.x,t.y,e.tileSize))}e.imageSizes.reverse();e.gridSize.reverse();e.minLevel=0;e.maxLevel=e.gridSize.length-1;OpenSeadragon.TileSource.apply(this,[e])};e.extend(e.ZoomifyTileSource.prototype,e.TileSource.prototype,{_getGridSize:function(e,t,i){return{x:Math.ceil(e/i),y:Math.ceil(t/i)}},_calculateAbsoluteTileNumber:function(e,t,i){var n=0;var o={};for(var r=0;r");return n.sort(function(e,t){return e.height-t.height})}(t.levels);if(0=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t},getNumTiles:function(e){return this.getLevelScale(e)?new l.Point(1,1):new l.Point(0,0)},getTileUrl:function(e,t,i){var n=null;0=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n}});function h(e,t){return t.levels}}(OpenSeadragon);!function(a){a.ImageTileSource=function(e){e=a.extend({buildPyramid:!0,crossOriginPolicy:!1,ajaxWithCredentials:!1,useCanvas:!0},e);a.TileSource.apply(this,[e])};a.extend(a.ImageTileSource.prototype,a.TileSource.prototype,{supports:function(e,t){return e.type&&"image"===e.type},configure:function(e,t){return e},getImageInfo:function(e){var t=this._image=new Image;var i=this;this.crossOriginPolicy&&(t.crossOrigin=this.crossOriginPolicy);this.ajaxWithCredentials&&(t.useCredentials=this.ajaxWithCredentials);a.addEvent(t,"load",function(){i.width=Object.prototype.hasOwnProperty.call(t,"naturalWidth")?t.naturalWidth:t.width;i.height=Object.prototype.hasOwnProperty.call(t,"naturalHeight")?t.naturalHeight:t.height;i.aspectRatio=i.width/i.height;i.dimensions=new a.Point(i.width,i.height);i._tileWidth=i.width;i._tileHeight=i.height;i.tileOverlap=0;i.minLevel=0;i.levels=i._buildLevels();i.maxLevel=i.levels.length-1;i.ready=!0;i.raiseEvent("ready",{tileSource:i})});a.addEvent(t,"error",function(){i.raiseEvent("open-failed",{message:"Error loading image at "+e,source:e})});t.src=e},getLevelScale:function(e){var t=NaN;e>=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t},getNumTiles:function(e){return this.getLevelScale(e)?new a.Point(1,1):new a.Point(0,0)},getTileUrl:function(e,t,i){var n=null;e>=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n},getContext2D:function(e,t,i){var n=null;e>=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].context2D);return n},_buildLevels:function(){var e=[{url:this._image.src,width:Object.prototype.hasOwnProperty.call(this._image,"naturalWidth")?this._image.naturalWidth:this._image.width,height:Object.prototype.hasOwnProperty.call(this._image,"naturalHeight")?this._image.naturalHeight:this._image.height}];if(!this.buildPyramid||!a.supportsCanvas||!this.useCanvas){delete this._image;return e}var t=Object.prototype.hasOwnProperty.call(this._image,"naturalWidth")?this._image.naturalWidth:this._image.width;var i=Object.prototype.hasOwnProperty.call(this._image,"naturalHeight")?this._image.naturalHeight:this._image.height;var n=document.createElement("canvas");var o=n.getContext("2d");n.width=t;n.height=i;o.drawImage(this._image,0,0,t,i);e[0].context2D=o;delete this._image;if(a.isCanvasTainted(n))return e;for(;2<=t&&2<=i;){t=Math.floor(t/2);i=Math.floor(i/2);var r=document.createElement("canvas");var s=r.getContext("2d");r.width=t;r.height=i;s.drawImage(n,0,0,t,i);e.splice(0,0,{context2D:s,width:t,height:i});n=r;o=s}return e}})}(OpenSeadragon);!function(o){o.TileSourceCollection=function(e,t,i,n){o.console.error("TileSourceCollection is deprecated; use World instead")}}(OpenSeadragon);!function(o){o.ButtonState={REST:0,GROUP:1,HOVER:2,DOWN:3};o.Button=function(e){var t=this;o.EventSource.call(this);o.extend(!0,this,{tooltip:null,srcRest:null,srcGroup:null,srcHover:null,srcDown:null,clickTimeThreshold:o.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:o.DEFAULT_SETTINGS.clickDistThreshold,fadeDelay:0,fadeLength:2e3,onPress:null,onRelease:null,onClick:null,onEnter:null,onExit:null,onFocus:null,onBlur:null},e);this.element=e.element||o.makeNeutralElement("div");if(!e.element){this.imgRest=o.makeTransparentImage(this.srcRest);this.imgGroup=o.makeTransparentImage(this.srcGroup);this.imgHover=o.makeTransparentImage(this.srcHover);this.imgDown=o.makeTransparentImage(this.srcDown);this.imgRest.alt=this.imgGroup.alt=this.imgHover.alt=this.imgDown.alt=this.tooltip;this.element.style.position="relative";o.setElementTouchActionNone(this.element);this.imgGroup.style.position=this.imgHover.style.position=this.imgDown.style.position="absolute";this.imgGroup.style.top=this.imgHover.style.top=this.imgDown.style.top="0px";this.imgGroup.style.left=this.imgHover.style.left=this.imgDown.style.left="0px";this.imgHover.style.visibility=this.imgDown.style.visibility="hidden";o.Browser.vendor==o.BROWSERS.FIREFOX&&o.Browser.version<3&&(this.imgGroup.style.top=this.imgHover.style.top=this.imgDown.style.top="");this.element.appendChild(this.imgRest);this.element.appendChild(this.imgGroup);this.element.appendChild(this.imgHover);this.element.appendChild(this.imgDown)}this.addHandler("press",this.onPress);this.addHandler("release",this.onRelease);this.addHandler("click",this.onClick);this.addHandler("enter",this.onEnter);this.addHandler("exit",this.onExit);this.addHandler("focus",this.onFocus);this.addHandler("blur",this.onBlur);this.currentState=o.ButtonState.GROUP;this.fadeBeginTime=null;this.shouldFade=!1;this.element.style.display="inline-block";this.element.style.position="relative";this.element.title=this.tooltip;this.tracker=new o.MouseTracker({element:this.element,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,enterHandler:function(e){if(e.insideElementPressed){i(t,o.ButtonState.DOWN);t.raiseEvent("enter",{originalEvent:e.originalEvent})}else e.buttonDownAny||i(t,o.ButtonState.HOVER)},focusHandler:function(e){this.enterHandler(e);t.raiseEvent("focus",{originalEvent:e.originalEvent})},exitHandler:function(e){n(t,o.ButtonState.GROUP);e.insideElementPressed&&t.raiseEvent("exit",{originalEvent:e.originalEvent})},blurHandler:function(e){this.exitHandler(e);t.raiseEvent("blur",{originalEvent:e.originalEvent})},pressHandler:function(e){i(t,o.ButtonState.DOWN);t.raiseEvent("press",{originalEvent:e.originalEvent})},releaseHandler:function(e){if(e.insideElementPressed&&e.insideElementReleased){n(t,o.ButtonState.HOVER);t.raiseEvent("release",{originalEvent:e.originalEvent})}else e.insideElementPressed?n(t,o.ButtonState.GROUP):i(t,o.ButtonState.HOVER)},clickHandler:function(e){e.quick&&t.raiseEvent("click",{originalEvent:e.originalEvent})},keyHandler:function(e){if(13!==e.keyCode)return!0;t.raiseEvent("click",{originalEvent:e.originalEvent});t.raiseEvent("release",{originalEvent:e.originalEvent});return!1}});n(this,o.ButtonState.REST)};o.extend(o.Button.prototype,o.EventSource.prototype,{notifyGroupEnter:function(){i(this,o.ButtonState.GROUP)},notifyGroupExit:function(){n(this,o.ButtonState.REST)},disable:function(){this.notifyGroupExit();this.element.disabled=!0;o.setElementOpacity(this.element,.2,!0)},enable:function(){this.element.disabled=!1;o.setElementOpacity(this.element,1,!0);this.notifyGroupEnter()}});function r(e){o.requestAnimationFrame(function(){!function(e){var t,i,n;if(e.shouldFade){t=o.now();i=t-e.fadeBeginTime;n=1-i/e.fadeLength;n=Math.min(1,n);n=Math.max(0,n);e.imgGroup&&o.setElementOpacity(e.imgGroup,n,!0);0=o.ButtonState.GROUP&&e.currentState==o.ButtonState.REST){!function(e){e.shouldFade=!1;e.imgGroup&&o.setElementOpacity(e.imgGroup,1,!0)}(e);e.currentState=o.ButtonState.GROUP}if(t>=o.ButtonState.HOVER&&e.currentState==o.ButtonState.GROUP){e.imgHover&&(e.imgHover.style.visibility="");e.currentState=o.ButtonState.HOVER}if(t>=o.ButtonState.DOWN&&e.currentState==o.ButtonState.HOVER){e.imgDown&&(e.imgDown.style.visibility="");e.currentState=o.ButtonState.DOWN}}}function n(e,t){if(!e.element.disabled){if(t<=o.ButtonState.HOVER&&e.currentState==o.ButtonState.DOWN){e.imgDown&&(e.imgDown.style.visibility="hidden");e.currentState=o.ButtonState.HOVER}if(t<=o.ButtonState.GROUP&&e.currentState==o.ButtonState.HOVER){e.imgHover&&(e.imgHover.style.visibility="hidden");e.currentState=o.ButtonState.GROUP}if(t<=o.ButtonState.REST&&e.currentState==o.ButtonState.GROUP){!function(e){e.shouldFade=!0;e.fadeBeginTime=o.now()+e.fadeDelay;window.setTimeout(function(){r(e)},e.fadeDelay)}(e);e.currentState=o.ButtonState.REST}}}}(OpenSeadragon);!function(o){o.ButtonGroup=function(e){o.extend(!0,this,{buttons:[],clickTimeThreshold:o.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:o.DEFAULT_SETTINGS.clickDistThreshold,labelText:""},e);var t,i=this.buttons.concat([]),n=this;this.element=e.element||o.makeNeutralElement("div");if(!e.group){this.element.style.display="inline-block";for(t=0;tT&&(T=P.x);P.yS&&(S=P.y)}return new R.Rect(y,x,T-y,S-x)},_getSegments:function(){var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();return[[e,t],[t,n],[n,i],[i,e]]},rotate:function(e,t){if(0===(e=R.positiveModulo(e,360)))return this.clone();t=t||this.getCenter();var i=this.getTopLeft().rotate(e,t);var n=this.getTopRight().rotate(e,t).minus(i);n=n.apply(function(e){return Math.abs(e)<1e-15?0:e});var o=Math.atan(n.y/n.x);n.x<0?o+=Math.PI:n.y<0&&(o+=2*Math.PI);return new R.Rect(i.x,i.y,this.width,this.height,o/Math.PI*180)},getBoundingBox:function(){if(0===this.degrees)return this.clone();var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();var o=Math.min(e.x,t.x,i.x,n.x);var r=Math.max(e.x,t.x,i.x,n.x);var s=Math.min(e.y,t.y,i.y,n.y);var a=Math.max(e.y,t.y,i.y,n.y);return new R.Rect(o,s,r-o,a-s)},getIntegerBoundingBox:function(){var e=this.getBoundingBox();var t=Math.floor(e.x);var i=Math.floor(e.y);var n=Math.ceil(e.width+e.x-t);var o=Math.ceil(e.height+e.y-i);return new R.Rect(t,i,n,o)},containsPoint:function(e,t){t=t||0;var i=this.getTopLeft();var n=this.getTopRight();var o=this.getBottomLeft();var r=n.minus(i);var s=o.minus(i);return(e.x-i.x)*r.x+(e.y-i.y)*r.y>=-t&&(e.x-n.x)*r.x+(e.y-n.y)*r.y<=t&&(e.x-i.x)*s.x+(e.y-i.y)*s.y>=-t&&(e.x-o.x)*s.x+(e.y-o.y)*s.y<=t},toString:function(){return"["+Math.round(100*this.x)/100+", "+Math.round(100*this.y)/100+", "+Math.round(100*this.width)/100+"x"+Math.round(100*this.height)/100+", "+Math.round(100*this.degrees)/100+"deg]"}}}(OpenSeadragon);!function(d){var s={};d.ReferenceStrip=function(e){var t,i,n,r=e.viewer,o=d.getElementSize(r.element);if(!e.id){e.id="referencestrip-"+d.now();this.element=d.makeNeutralElement("div");this.element.id=e.id;this.element.className="referencestrip"}e=d.extend(!0,{sizeRatio:d.DEFAULT_SETTINGS.referenceStripSizeRatio,position:d.DEFAULT_SETTINGS.referenceStripPosition,scroll:d.DEFAULT_SETTINGS.referenceStripScroll,clickTimeThreshold:d.DEFAULT_SETTINGS.clickTimeThreshold},e,{element:this.element,showNavigator:!1,mouseNavEnabled:!1,showNavigationControl:!1,showSequenceControl:!1});d.extend(this,e);s[this.id]={animating:!1};this.minPixelRatio=this.viewer.minPixelRatio;(i=this.element.style).marginTop="0px";i.marginRight="0px";i.marginBottom="0px";i.marginLeft="0px";i.left="0px";i.bottom="0px";i.border="0px";i.background="#000";i.position="relative";d.setElementTouchActionNone(this.element);d.setElementOpacity(this.element,.8);this.viewer=r;this.innerTracker=new d.MouseTracker({element:this.element,dragHandler:d.delegate(this,a),scrollHandler:d.delegate(this,l),enterHandler:d.delegate(this,c),exitHandler:d.delegate(this,u),keyDownHandler:d.delegate(this,p),keyHandler:d.delegate(this,g)});if(e.width&&e.height){this.element.style.width=e.width+"px";this.element.style.height=e.height+"px";r.addControl(this.element,{anchor:d.ControlAnchor.BOTTOM_LEFT})}else if("horizontal"==e.scroll){this.element.style.width=o.x*e.sizeRatio*r.tileSources.length+12*r.tileSources.length+"px";this.element.style.height=o.y*e.sizeRatio+"px";r.addControl(this.element,{anchor:d.ControlAnchor.BOTTOM_LEFT})}else{this.element.style.height=o.y*e.sizeRatio*r.tileSources.length+12*r.tileSources.length+"px";this.element.style.width=o.x*e.sizeRatio+"px";r.addControl(this.element,{anchor:d.ControlAnchor.TOP_LEFT})}this.panelWidth=o.x*this.sizeRatio+8;this.panelHeight=o.y*this.sizeRatio+8;this.panels=[];this.miniViewers={};for(n=0;ns+n.x-this.panelWidth){t=Math.min(t,o-n.x);this.element.style.marginLeft=-t+"px";h(this,n.x,-t)}else if(ta+n.y-this.panelHeight){t=Math.min(t,r-n.y);this.element.style.marginTop=-t+"px";h(this,n.y,-t)}else if(t-(n-r.x)){this.element.style.marginLeft=t+2*e.delta.x+"px";h(this,r.x,t+2*e.delta.x)}}else if(-e.delta.x<0&&t<0){this.element.style.marginLeft=t+2*e.delta.x+"px";h(this,r.x,t+2*e.delta.x)}}else if(0<-e.delta.y){if(i>-(o-r.y)){this.element.style.marginTop=i+2*e.delta.y+"px";h(this,r.y,i+2*e.delta.y)}}else if(-e.delta.y<0&&i<0){this.element.style.marginTop=i+2*e.delta.y+"px";h(this,r.y,i+2*e.delta.y)}return!1}function l(e){var t=Number(this.element.style.marginLeft.replace("px","")),i=Number(this.element.style.marginTop.replace("px","")),n=Number(this.element.style.width.replace("px","")),o=Number(this.element.style.height.replace("px","")),r=d.getElementSize(this.viewer.canvas);if(this.element)if("horizontal"==this.scroll){if(0-(n-r.x)){this.element.style.marginLeft=t-60*e.scroll+"px";h(this,r.x,t-60*e.scroll)}}else if(e.scroll<0&&t<0){this.element.style.marginLeft=t-60*e.scroll+"px";h(this,r.x,t-60*e.scroll)}}else if(e.scroll<0){if(i>r.y-o){this.element.style.marginTop=i+60*e.scroll+"px";h(this,r.y,i+60*e.scroll)}}else if(0=this.target.time?t:e+(t-e)*(n=this.springStiffness,o=(this.current.time-this.start.time)/(this.target.time-this.start.time),(1-Math.exp(n*-o))/(1-Math.exp(-n)));var n,o;var r=this.current.value;this._exponential?this.current.value=Math.exp(i):this.current.value=i;return r!=this.current.value},isAtTargetValue:function(){return this.current.value===this.target.value}}}(OpenSeadragon);!function(t){function n(e){t.extend(!0,this,{timeout:t.DEFAULT_SETTINGS.timeout,jobId:null},e);this.image=null}n.prototype={errorMsg:null,start:function(){var r=this;var e=this.abort;this.image=new Image;this.image.onload=function(){r.finish(!0)};this.image.onabort=this.image.onerror=function(){r.errorMsg="Image load aborted";r.finish(!1)};this.jobId=window.setTimeout(function(){r.errorMsg="Image load exceeded timeout ("+r.timeout+" ms)";r.finish(!1)},this.timeout);if(this.loadWithAjax){this.request=t.makeAjaxRequest({url:this.src,withCredentials:this.ajaxWithCredentials,headers:this.ajaxHeaders,responseType:"arraybuffer",success:function(t){var i;try{i=new window.Blob([t.response])}catch(e){var n=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder||window.MSBlobBuilder;if("TypeError"===e.name&&n){var o=new n;o.append(t.response);i=o.getBlob()}}if(0===i.size){r.errorMsg="Empty image response.";r.finish(!1)}var e=(window.URL||window.webkitURL).createObjectURL(i);r.image.src=e},error:function(e){r.errorMsg="Image load aborted - XHR error";r.finish(!1)}});this.abort=function(){r.request.abort();"function"==typeof e&&e()}}else{!1!==this.crossOriginPolicy&&(this.image.crossOrigin=this.crossOriginPolicy);this.image.src=this.src}},finish:function(e){this.image.onload=this.image.onerror=this.image.onabort=null;e||(this.image=null);this.jobId&&window.clearTimeout(this.jobId);this.callback(this)}};t.ImageLoader=function(e){t.extend(!0,this,{jobLimit:t.DEFAULT_SETTINGS.imageLoaderLimit,timeout:t.DEFAULT_SETTINGS.timeout,jobQueue:[],jobsInProgress:0},e)};t.ImageLoader.prototype={addJob:function(t){var i=this,e=new n({src:t.src,loadWithAjax:t.loadWithAjax,ajaxHeaders:t.loadWithAjax?t.ajaxHeaders:null,crossOriginPolicy:t.crossOriginPolicy,ajaxWithCredentials:t.ajaxWithCredentials,callback:function(e){!function(e,t,i){e.jobsInProgress--;if((!e.jobLimit||e.jobsInProgressthis.canvas.width&&(r.width=this.canvas.width-r.x);if(r.y<0){r.height+=r.y;r.y=0}r.y+r.height>this.canvas.height&&(r.height=this.canvas.height-r.y);this.context.drawImage(this.sketchCanvas,r.x,r.y,r.width,r.height,r.x,r.y,r.width,r.height)}else{t=o.scale||1;var s=(i=o.translate)instanceof u.Point?i:new u.Point(0,0);var a=0;var l=0;if(i){var h=this.sketchCanvas.width-this.canvas.width;var c=this.sketchCanvas.height-this.canvas.height;a=Math.round(h/2);l=Math.round(c/2)}this.context.drawImage(this.sketchCanvas,s.x-a*t,s.y-l*t,(this.canvas.width+2*a)*t,(this.canvas.height+2*l)*t,-a,-l,this.canvas.width+2*a,this.canvas.height+2*l)}this.context.restore()}},drawDebugInfo:function(e,t,i,n){if(this.useCanvas){var o=this.viewer.world.getIndexOfItem(n)%this.debugGridColor.length;var r=this.context;r.save();r.lineWidth=2*u.pixelDensityRatio;r.font="small-caps bold "+13*u.pixelDensityRatio+"px arial";r.strokeStyle=this.debugGridColor[o];r.fillStyle=this.debugGridColor[o];0!==this.viewport.degrees&&this._offsetForRotation({degrees:this.viewport.degrees});n.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:n.getRotation(!0),point:n.viewport.pixelFromPointNoRotate(n._getRotationPoint(!0),!0)});0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip();r.strokeRect(e.position.x*u.pixelDensityRatio,e.position.y*u.pixelDensityRatio,e.size.x*u.pixelDensityRatio,e.size.y*u.pixelDensityRatio);var s=(e.position.x+e.size.x/2)*u.pixelDensityRatio;var a=(e.position.y+e.size.y/2)*u.pixelDensityRatio;r.translate(s,a);r.rotate(Math.PI/180*-this.viewport.degrees);r.translate(-s,-a);if(0===e.x&&0===e.y){r.fillText("Zoom: "+this.viewport.getZoom(),e.position.x*u.pixelDensityRatio,(e.position.y-30)*u.pixelDensityRatio);r.fillText("Pan: "+this.viewport.getBounds().toString(),e.position.x*u.pixelDensityRatio,(e.position.y-20)*u.pixelDensityRatio)}r.fillText("Level: "+e.level,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+20)*u.pixelDensityRatio);r.fillText("Column: "+e.x,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+30)*u.pixelDensityRatio);r.fillText("Row: "+e.y,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+40)*u.pixelDensityRatio);r.fillText("Order: "+i+" of "+t,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+50)*u.pixelDensityRatio);r.fillText("Size: "+e.size.toString(),(e.position.x+10)*u.pixelDensityRatio,(e.position.y+60)*u.pixelDensityRatio);r.fillText("Position: "+e.position.toString(),(e.position.x+10)*u.pixelDensityRatio,(e.position.y+70)*u.pixelDensityRatio);0!==this.viewport.degrees&&this._restoreRotationChanges();n.getRotation(!0)%360!=0&&this._restoreRotationChanges();0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip();r.restore()}},debugRect:function(e){if(this.useCanvas){var t=this.context;t.save();t.lineWidth=2*u.pixelDensityRatio;t.strokeStyle=this.debugGridColor[0];t.fillStyle=this.debugGridColor[0];t.strokeRect(e.x*u.pixelDensityRatio,e.y*u.pixelDensityRatio,e.width*u.pixelDensityRatio,e.height*u.pixelDensityRatio);t.restore()}},setImageSmoothingEnabled:function(e){if(this.useCanvas){this._imageSmoothingEnabled=e;this._updateImageSmoothingEnabled(this.context);this.viewer.forceRedraw()}},_updateImageSmoothingEnabled:function(e){e.msImageSmoothingEnabled=this._imageSmoothingEnabled;e.imageSmoothingEnabled=this._imageSmoothingEnabled},getCanvasSize:function(e){var t=this._getContext(e).canvas;return new u.Point(t.width,t.height)},getCanvasCenter:function(){return new u.Point(this.canvas.width/2,this.canvas.height/2)},_offsetForRotation:function(e){var t=e.point?e.point.times(u.pixelDensityRatio):this.getCanvasCenter();var i=this._getContext(e.useSketch);i.save();i.translate(t.x,t.y);if(this.viewer.viewport.flipped){i.rotate(Math.PI/180*-e.degrees);i.scale(-1,1)}else i.rotate(Math.PI/180*e.degrees);i.translate(-t.x,-t.y)},_flip:function(e){var t=(e=e||{}).point?e.point.times(u.pixelDensityRatio):this.getCanvasCenter();var i=this._getContext(e.useSketch);i.translate(t.x,0);i.scale(-1,1);i.translate(-t.x,0)},_restoreRotationChanges:function(e){this._getContext(e).restore()},_calculateCanvasSize:function(){var e=u.pixelDensityRatio;var t=this.viewport.getContainerSize();return{x:Math.round(t.x*e),y:Math.round(t.y*e)}},_calculateSketchCanvasSize:function(){var e=this._calculateCanvasSize();if(0===this.viewport.getRotation())return e;var t=Math.ceil(Math.sqrt(e.x*e.x+e.y*e.y));return{x:t,y:t}}}}(OpenSeadragon);!function(p){p.Viewport=function(e){var t=arguments;t.length&&t[0]instanceof p.Point&&(e={containerSize:t[0],contentSize:t[1],config:t[2]});if(e.config){p.extend(!0,e,e.config);delete e.config}this._margins=p.extend({left:0,top:0,right:0,bottom:0},e.margins||{});delete e.margins;p.extend(!0,this,{containerSize:null,contentSize:null,zoomPoint:null,viewer:null,springStiffness:p.DEFAULT_SETTINGS.springStiffness,animationTime:p.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:p.DEFAULT_SETTINGS.minZoomImageRatio,maxZoomPixelRatio:p.DEFAULT_SETTINGS.maxZoomPixelRatio,visibilityRatio:p.DEFAULT_SETTINGS.visibilityRatio,wrapHorizontal:p.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:p.DEFAULT_SETTINGS.wrapVertical,defaultZoomLevel:p.DEFAULT_SETTINGS.defaultZoomLevel,minZoomLevel:p.DEFAULT_SETTINGS.minZoomLevel,maxZoomLevel:p.DEFAULT_SETTINGS.maxZoomLevel,degrees:p.DEFAULT_SETTINGS.degrees,flipped:p.DEFAULT_SETTINGS.flipped,homeFillsViewer:p.DEFAULT_SETTINGS.homeFillsViewer},e);this._updateContainerInnerSize();this.centerSpringX=new p.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.centerSpringY=new p.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.zoomSpring=new p.Spring({exponential:!0,initial:1,springStiffness:this.springStiffness,animationTime:this.animationTime});this._oldCenterX=this.centerSpringX.current.value;this._oldCenterY=this.centerSpringY.current.value;this._oldZoom=this.zoomSpring.current.value;this._setContentBounds(new p.Rect(0,0,1,1),1);this.goHome(!0);this.update()};p.Viewport.prototype={resetContentSize:function(e){p.console.assert(e,"[Viewport.resetContentSize] contentSize is required");p.console.assert(e instanceof p.Point,"[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point");p.console.assert(0this._contentBoundsNoRotate.width?t.x+=(r+s)/2:s<0?t.x+=s:0this._contentBoundsNoRotate.height?t.y+=(c+u)/2:u<0?t.y+=u:0=o?s.height=s.width/o:s.width=s.height*o;s.x=r.x-s.width/2;s.y=r.y-s.height/2;var a=1/s.width;if(n){var l=s.getAspectRatio();var h=this._applyZoomConstraints(a);if(a!==h){a=h;s.width=1/a;s.x=r.x-s.width/2;s.height=s.width/l;s.y=r.y-s.height/2}r=(s=this._applyBoundaryConstraints(s)).getCenter();this._raiseConstraintsEvent(i)}if(i){this.panTo(r,!0);return this.zoomTo(a,null,!0)}this.panTo(this.getCenter(!0),!0);this.zoomTo(this.getZoom(!0),null,!0);var c=this.getBounds();var u=this.getZoom();if(0===u||Math.abs(a/u-1)<1e-8){this.zoomTo(a,!0);return this.panTo(r,i)}var d=(s=s.rotate(-this.getRotation())).getTopLeft().times(a).minus(c.getTopLeft().times(u)).divide(a-u);return this.zoomTo(a,d,i)},fitBounds:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!1})},fitBoundsWithConstraints:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!0})},fitVertically:function(e){var t=new p.Rect(this._contentBounds.x+this._contentBounds.width/2,this._contentBounds.y,0,this._contentBounds.height);return this.fitBounds(t,e)},fitHorizontally:function(e){var t=new p.Rect(this._contentBounds.x,this._contentBounds.y+this._contentBounds.height/2,this._contentBounds.width,0);return this.fitBounds(t,e)},getConstrainedBounds:function(e){var t;t=this.getBounds(e);return this._applyBoundaryConstraints(t)},panBy:function(e,t){var i=new p.Point(this.centerSpringX.target.value,this.centerSpringY.target.value);return this.panTo(i.plus(e),t)},panTo:function(e,t){if(t){this.centerSpringX.resetTo(e.x);this.centerSpringY.resetTo(e.y)}else{this.centerSpringX.springTo(e.x);this.centerSpringY.springTo(e.y)}this.viewer&&this.viewer.raiseEvent("pan",{center:e,immediately:t});return this},zoomBy:function(e,t,i){return this.zoomTo(this.zoomSpring.target.value*e,t,i)},zoomTo:function(e,t,i){var n=this;this.zoomPoint=t instanceof p.Point&&!isNaN(t.x)&&!isNaN(t.y)?t:null;i?this._adjustCenterSpringsForZoomPoint(function(){n.zoomSpring.resetTo(e)}):this.zoomSpring.springTo(e);this.viewer&&this.viewer.raiseEvent("zoom",{zoom:e,refPoint:t,immediately:i});return this},setRotation:function(e){if(!this.viewer||!this.viewer.drawer.canRotate())return this;this.degrees=p.positiveModulo(e,360);this._setContentBounds(this.viewer.world.getHomeBounds(),this.viewer.world.getContentFactor());this.viewer.forceRedraw();this.viewer.raiseEvent("rotate",{degrees:e});return this},getRotation:function(){return this.degrees},resize:function(e,t){var i,n=this.getBoundsNoRotate(),o=n;this.containerSize.x=e.x;this.containerSize.y=e.y;this._updateContainerInnerSize();if(t){i=e.x/this.containerSize.x;o.width=n.width*i;o.height=o.width/this.getAspectRatio()}this.viewer&&this.viewer.raiseEvent("resize",{newContainerSize:e,maintain:t});return this.fitBounds(o,!0)},_updateContainerInnerSize:function(){this._containerInnerSize=new p.Point(Math.max(1,this.containerSize.x-(this._margins.left+this._margins.right)),Math.max(1,this.containerSize.y-(this._margins.top+this._margins.bottom)))},update:function(){var e=this;this._adjustCenterSpringsForZoomPoint(function(){e.zoomSpring.update()});this.centerSpringX.update();this.centerSpringY.update();var t=this.centerSpringX.current.value!==this._oldCenterX||this.centerSpringY.current.value!==this._oldCenterY||this.zoomSpring.current.value!==this._oldZoom;this._oldCenterX=this.centerSpringX.current.value;this._oldCenterY=this.centerSpringY.current.value;this._oldZoom=this.zoomSpring.current.value;return t},_adjustCenterSpringsForZoomPoint:function(e){if(this.zoomPoint){var t=this.pixelFromPoint(this.zoomPoint,!0);e();var i=this.pixelFromPoint(this.zoomPoint,!0).minus(t);var n=this.deltaPointsFromPixels(i,!0);this.centerSpringX.shiftBy(n.x);this.centerSpringY.shiftBy(n.y);this.zoomSpring.isAtTargetValue()&&(this.zoomPoint=null)}else e()},deltaPixelsFromPointsNoRotate:function(e,t){return e.times(this._containerInnerSize.x*this.getZoom(t))},deltaPixelsFromPoints:function(e,t){return this.deltaPixelsFromPointsNoRotate(e.rotate(this.getRotation()),t)},deltaPointsFromPixelsNoRotate:function(e,t){return e.divide(this._containerInnerSize.x*this.getZoom(t))},deltaPointsFromPixels:function(e,t){return this.deltaPointsFromPixelsNoRotate(e,t).rotate(-this.getRotation())},pixelFromPointNoRotate:function(e,t){return this._pixelFromPointNoRotate(e,this.getBoundsNoRotate(t))},pixelFromPoint:function(e,t){return this._pixelFromPoint(e,this.getBoundsNoRotate(t))},_pixelFromPointNoRotate:function(e,t){return e.minus(t.getTopLeft()).times(this._containerInnerSize.x/t.width).plus(new p.Point(this._margins.left,this._margins.top))},_pixelFromPoint:function(e,t){return this._pixelFromPointNoRotate(e.rotate(this.getRotation(),this.getCenter(!0)),t)},pointFromPixelNoRotate:function(e,t){var i=this.getBoundsNoRotate(t);return e.minus(new p.Point(this._margins.left,this._margins.top)).divide(this._containerInnerSize.x/i.width).plus(i.getTopLeft())},pointFromPixel:function(e,t){return this.pointFromPixelNoRotate(e,t).rotate(-this.getRotation(),this.getCenter(!0))},_viewportToImageDelta:function(e,t){var i=this._contentBoundsNoRotate.width;return new p.Point(e*this._contentSizeNoRotate.x/i,t*this._contentSizeNoRotate.x/i)},viewportToImageCoordinates:function(e,t){if(e instanceof p.Point)return this.viewportToImageCoordinates(e.x,e.y);if(this.viewer){var i=this.viewer.world.getItemCount();if(1o){r=this._clip.x/this._clip.height*e.height;s=this._clip.y/this._clip.height*e.height}else{r=this._clip.x/this._clip.width*e.width;s=this._clip.y/this._clip.width*e.width}}if(e.getAspectRatio()>o){var h=e.height/l;var c=0;n.isHorizontallyCentered?c=(e.width-e.height*o)/2:n.isRight&&(c=e.width-e.height*o);this.setPosition(new y.Point(e.x-r+c,e.y-s),i);this.setHeight(h,i)}else{var u=e.width/a;var d=0;n.isVerticallyCentered?d=(e.height-e.width/o)/2:n.isBottom&&(d=e.height-e.width/o);this.setPosition(new y.Point(e.x-r,e.y-s+d),i);this.setWidth(u,i)}},getClip:function(){return this._clip?this._clip.clone():null},setClip:function(e){y.console.assert(!e||e instanceof y.Rect,"[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null");e instanceof y.Rect?this._clip=e.clone():this._clip=null;this._needsDraw=!0;this.raiseEvent("clip-change")},getOpacity:function(){return this.opacity},setOpacity:function(e){if(e!==this.opacity){this.opacity=e;this._needsDraw=!0;this.raiseEvent("opacity-change",{opacity:this.opacity})}},getPreload:function(){return this._preload},setPreload:function(e){this._preload=!!e;this._needsDraw=!0},getRotation:function(e){return e?this._degreesSpring.current.value:this._degreesSpring.target.value},setRotation:function(e,t){if(this._degreesSpring.target.value!==e||!this._degreesSpring.isAtTargetValue()){t?this._degreesSpring.resetTo(e):this._degreesSpring.springTo(e);this._needsDraw=!0;this._raiseBoundsChange()}},_getRotationPoint:function(e){return this.getBoundsNoRotate(e).getCenter()},getCompositeOperation:function(){return this.compositeOperation},setCompositeOperation:function(e){if(e!==this.compositeOperation){this.compositeOperation=e;this._needsDraw=!0;this.raiseEvent("composite-operation-change",{compositeOperation:this.compositeOperation})}},_setScale:function(e,t){var i=this._scaleSpring.target.value===e;if(t){if(i&&this._scaleSpring.current.value===e)return;this._scaleSpring.resetTo(e);this._updateForScale();this._needsDraw=!0}else{if(i)return;this._scaleSpring.springTo(e);this._updateForScale();this._needsDraw=!0}i||this._raiseBoundsChange()},_updateForScale:function(){this._worldWidthTarget=this._scaleSpring.target.value;this._worldHeightTarget=this.normHeight*this._scaleSpring.target.value;this._worldWidthCurrent=this._scaleSpring.current.value;this._worldHeightCurrent=this.normHeight*this._scaleSpring.current.value},_raiseBoundsChange:function(){this.raiseEvent("bounds-change")},_isBottomItem:function(){return this.viewer.world.getItemAt(0)===this},_getLevelsInterval:function(){var e=Math.max(this.source.minLevel,Math.floor(Math.log(this.minZoomImageRatio)/Math.log(2)));var t=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(0),!0).x*this._scaleSpring.current.value;var i=Math.min(Math.abs(this.source.maxLevel),Math.abs(Math.floor(Math.log(t/this.minPixelRatio)/Math.log(2))));i=Math.max(i,this.source.minLevel||0);return{lowestLevel:e=Math.min(e,i),highestLevel:i}},_updateViewport:function(){this._needsDraw=!1;this._tilesLoading=0;this.loadingCoverage={};for(;0=this.minPixelRatio)a=c=!0;else if(!a)continue;var d=e.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(h),!1).x*this._scaleSpring.current.value;var p=e.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(Math.max(this.source.getClosestLevel(),0)),!1).x*this._scaleSpring.current.value;var g=this.immediateRender?1:p;s=m(this,a,c,h,Math.min(1,(u-.5)/.5),g/Math.abs(g-d),t,l,s);if(f(this.coverage,h))break}!function(n,e){if(0===n.opacity||0===e.length&&!n.placeholderFillStyle)return;var t=e[0];var i;t&&(i=n.opacity<1||n.compositeOperation&&"source-over"!==n.compositeOperation||!n._isBottomItem()&&t._hasTransparencyChannel());var o;var r;var s=n.viewport.getZoom(!0);var a=n.viewportToImageZoom(s);if(1n.smoothTileEdgesMinZoom&&!n.iOSDevice&&n.getRotation(!0)%360==0&&y.supportsCanvas){i=!0;o=t.getScaleForEdgeSmoothing();r=t.getTranslationForEdgeSmoothing(o,n._drawer.getCanvasSize(!1),n._drawer.getCanvasSize(!0))}var l;if(i){if(!o){l=n.viewport.viewportToViewerElementRectangle(n.getClippedBounds(!0)).getIntegerBoundingBox();n._drawer.viewer.viewport.getFlip()&&(0===n.viewport.degrees&&n.getRotation(!0)%360==0||(l.x=n._drawer.viewer.container.clientWidth-(l.x+l.width)));l=l.times(y.pixelDensityRatio)}n._drawer._clear(!0,l)}if(!o){0!==n.viewport.degrees&&n._drawer._offsetForRotation({degrees:n.viewport.degrees,useSketch:i});n.getRotation(!0)%360!=0&&n._drawer._offsetForRotation({degrees:n.getRotation(!0),point:n.viewport.pixelFromPointNoRotate(n._getRotationPoint(!0),!0),useSketch:i});0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip()}var h=!1;if(n._clip){n._drawer.saveContext(i);var c=n.imageToViewportRectangle(n._clip,!0);c=c.rotate(-n.getRotation(!0),n._getRotationPoint(!0));var u=n._drawer.viewportToDrawerRectangle(c);o&&(u=u.times(o));r&&(u=u.translate(r));n._drawer.setClip(u,i);h=!0}if(n._croppingPolygons){n._drawer.saveContext(i);try{var d=n._croppingPolygons.map(function(e){return e.map(function(e){var t=n.imageToViewportCoordinates(e.x,e.y,!0).rotate(-n.getRotation(!0),n._getRotationPoint(!0));var i=n._drawer.viewportCoordToDrawerCoord(t);o&&(i=i.times(o));return i})});n._drawer.clipWithPolygons(d,i)}catch(e){y.console.error(e)}h=!0}if(n.placeholderFillStyle&&!1===n._hasOpaqueTile){var p=n._drawer.viewportToDrawerRectangle(n.getBounds(!0));o&&(p=p.times(o));r&&(p=p.translate(r));var g=null;g="function"==typeof n.placeholderFillStyle?n.placeholderFillStyle(n,n._drawer.context):n.placeholderFillStyle;n._drawer.drawRectangle(p,g,i)}for(var m=e.length-1;0<=m;m--){t=e[m];n._drawer.drawTile(t,n._drawingHandler,i,o,r);t.beingDrawn=!0;n.viewer&&n.viewer.raiseEvent("tile-drawn",{tiledImage:n,tile:t})}h&&n._drawer.restoreContext(i);if(!o){n.getRotation(!0)%360!=0&&n._drawer._restoreRotationChanges(i);0!==n.viewport.degrees&&n._drawer._restoreRotationChanges(i)}if(i){if(o){0!==n.viewport.degrees&&n._drawer._offsetForRotation({degrees:n.viewport.degrees,useSketch:!1});n.getRotation(!0)%360!=0&&n._drawer._offsetForRotation({degrees:n.getRotation(!0),point:n.viewport.pixelFromPointNoRotate(n._getRotationPoint(!0),!0),useSketch:!1})}n._drawer.blendSketch({opacity:n.opacity,scale:o,translate:r,compositeOperation:n.compositeOperation,bounds:l});if(o){n.getRotation(!0)%360!=0&&n._drawer._restoreRotationChanges(!1);0!==n.viewport.degrees&&n._drawer._restoreRotationChanges(!1)}}o||0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip();!function(e,t){if(e.debugMode)for(var i=t.length-1;0<=i;i--){var n=t[i];try{e._drawer.drawDebugInfo(n,t.length,i,e)}catch(e){y.console.error(e)}}}(n,e)}(this,this.lastDrawn);if(s&&!s.context2D){!function(n,o,r){o.loading=!0;n._imageLoader.addJob({src:o.url,loadWithAjax:o.loadWithAjax,ajaxHeaders:o.ajaxHeaders,crossOriginPolicy:n.crossOriginPolicy,ajaxWithCredentials:n.ajaxWithCredentials,callback:function(e,t,i){!function(t,i,e,n,o,r){if(!n){y.console.log("Tile %s failed to load: %s - error: %s",i,i.url,o);t.viewer.raiseEvent("tile-load-failed",{tile:i,tiledImage:t,time:e,message:o,tileRequest:r});i.loading=!1;i.exists=!1;return}if(ee.visibility)return t;if(t.visibility==e.visibility&&t.squaredDistancethis._maxImageCacheCount){var o=null;var r=-1;var s=null;var a,l,h,c,u,d;for(var p=this._tilesLoaded.length-1;0<=p;p--)if(!((a=(d=this._tilesLoaded[p]).tile).level<=t||a.beingDrawn))if(o){c=a.lastTouchTime;l=o.lastTouchTime;u=a.level;h=o.level;if(c=this._items.length)throw new Error("Index bigger than number of layers.");if(t!==i&&-1!==i){this._items.splice(i,1);this._items.splice(t,0,e);this._needsDraw=!0;this.raiseEvent("item-index-change",{item:e,previousIndex:i,newIndex:t})}},removeItem:function(e){v.console.assert(e,"[World.removeItem] item is required");var t=v.indexOf(this._items,e);if(-1!==t){e.removeHandler("bounds-change",this._delegatedFigureSizes);e.removeHandler("clip-change",this._delegatedFigureSizes);e.destroy();this._items.splice(t,1);this._figureSizes();this._needsDraw=!0;this._raiseRemoveItem(e)}},removeAll:function(){this.viewer._cancelPendingImages();var e;var t;for(t=0;tu.height?r:r*(u.width/u.height))*(u.height/u.width);g=new v.Point(l+(r-d)/2,h+(r-p)/2);c.setPosition(g,t);c.setWidth(d,t);"horizontal"===i?l+=s:h+=s}this.setAutoRefigureSizes(!0)},_figureSizes:function(){var e=this._homeBounds?this._homeBounds.clone():null;var t=this._contentSize?this._contentSize.clone():null;var i=this._contentFactor||0;if(this._items.length){var n=this._items[0];var o=n.getBounds();this._contentFactor=n.getContentSize().x/o.width;var r=n.getClippedBounds().getBoundingBox();var s=r.x;var a=r.y;var l=r.x+r.width;var h=r.y+r.height;for(var c=1;c=i.x&&t.x=i.y},getMousePosition:function(e){if("number"==typeof e.pageX)u.getMousePosition=function(e){const t=new u.Point;t.x=e.pageX;t.y=e.pageY;return t};else{if("number"!=typeof e.clientX)throw new Error("Unknown event mouse position, no known technique.");u.getMousePosition=function(e){const t=new u.Point;t.x=e.clientX+document.body.scrollLeft+document.documentElement.scrollLeft;t.y=e.clientY+document.body.scrollTop+document.documentElement.scrollTop;return t}}return u.getMousePosition(e)},getPageScroll:function(){var e=document.documentElement||{};var t=document.body||{};if("number"==typeof window.pageXOffset)u.getPageScroll=function(){return new u.Point(window.pageXOffset,window.pageYOffset)};else if(t.scrollLeft||t.scrollTop)u.getPageScroll=function(){return new u.Point(document.body.scrollLeft,document.body.scrollTop)};else{if(!e.scrollLeft&&!e.scrollTop)return new u.Point(0,0);u.getPageScroll=function(){return new u.Point(document.documentElement.scrollLeft,document.documentElement.scrollTop)}}return u.getPageScroll()},setPageScroll:function(t){if(void 0!==window.scrollTo)u.setPageScroll=function(e){window.scrollTo(e.x,e.y)};else{var i=u.getPageScroll();if(i.x===t.x&&i.y===t.y)return;document.body.scrollLeft=t.x;document.body.scrollTop=t.y;let e=u.getPageScroll();if(e.x!==i.x&&e.y!==i.y){u.setPageScroll=function(e){document.body.scrollLeft=e.x;document.body.scrollTop=e.y};return}document.documentElement.scrollLeft=t.x;document.documentElement.scrollTop=t.y;e=u.getPageScroll();if(e.x!==i.x&&e.y!==i.y){u.setPageScroll=function(e){document.documentElement.scrollLeft=e.x;document.documentElement.scrollTop=e.y};return}u.setPageScroll=function(e){}}u.setPageScroll(t)},getWindowSize:function(){var e=document.documentElement||{};var t=document.body||{};if("number"==typeof window.innerWidth)u.getWindowSize=function(){return new u.Point(window.innerWidth,window.innerHeight)};else if(e.clientWidth||e.clientHeight)u.getWindowSize=function(){return new u.Point(document.documentElement.clientWidth,document.documentElement.clientHeight)};else{if(!t.clientWidth&&!t.clientHeight)throw new Error("Unknown window size, no known technique.");u.getWindowSize=function(){return new u.Point(document.body.clientWidth,document.body.clientHeight)}}return u.getWindowSize()},makeCenteredNode:function(e){e=u.getElement(e);const t=[u.makeNeutralElement("div"),u.makeNeutralElement("div"),u.makeNeutralElement("div")];u.extend(t[0].style,{display:"table",height:"100%",width:"100%"});u.extend(t[1].style,{display:"table-row"});u.extend(t[2].style,{display:"table-cell",verticalAlign:"middle",textAlign:"center"});t[0].appendChild(t[1]);t[1].appendChild(t[2]);t[2].appendChild(e);return t[0]},trace:function(e,t=!1){this.__traceLogs=[];setInterval(()=>{if(this.__traceLogs.length){console.log(this.__traceLogs.join("\n"));this.__traceLogs=[]}},2e3);this.trace=function(e,t=!1){if("string"!=typeof e){const i=(e=e instanceof OpenSeadragon.Tile?e.getCache(e.originalCacheKey):e)._tiles[0];this.__traceLogs.push(`Cache ${i.toString()} loaded ${i.loaded} loading ${i.loading} cacheCount ${Object.keys(i._caches).length} - CACHE `+e.__invStamp);t&&this.__traceLogs.push(...(new Error).stack.split("\n").slice(1))}else{this.__traceLogs.push(e);t&&this.__traceLogs.push(...(new Error).stack.split("\n").slice(1))}};this.trace(e,t)},makeNeutralElement:function(e){e=document.createElement(e);const t=e.style;t.background="transparent none";t.border="none";t.margin="0px";t.padding="0px";t.position="static";return e},now:function(){Date.now?u.now=Date.now:u.now=function(){return(new Date).getTime()};return u.now()},makeTransparentImage:function(e){const t=u.makeNeutralElement("img");t.src=e;return t},setElementOpacity:function(e,t,i){e=u.getElement(e);i&&!u.Browser.alpha&&(t=Math.round(t));if(u.Browser.opacity)e.style.opacity=t<1?t:"";else if(t<1){t=Math.round(100*t);e.style.filter="alpha(opacity="+t+")"}else e.style.filter=""},setElementTouchActionNone:function(e){void 0!==(e=u.getElement(e)).style.touchAction?e.style.touchAction="none":void 0!==e.style.msTouchAction&&(e.style.msTouchAction="none")},setElementPointerEvents:function(e,t){void 0!==(e=u.getElement(e)).style&&void 0!==e.style.pointerEvents&&(e.style.pointerEvents=t)},setElementPointerEventsNone:function(e){u.setElementPointerEvents(e,"none")},addClass:function(e,t){(e=u.getElement(e)).className?-1===(" "+e.className+" ").indexOf(" "+t+" ")&&(e.className+=" "+t):e.className=t},indexOf:function(e,t,i){Array.prototype.indexOf?this.indexOf=function(e,t,i){return e.indexOf(t,i)}:this.indexOf=function(t,i,e){let n=e||0;if(!t)throw new TypeError;var r=t.length;if(0===r||n>=r)return-1;n<0&&(n=r-Math.abs(n));for(let e=n;e{for(;e instanceof u.Promise;)e=e._value;this._value=e},e=>{for(;e instanceof u.Promise;)e=e._value;this._value=e;this._error=!0})}catch(e){this._value=e;this._error=!0}}then(e){if(!this._error)try{this._value=e(this._value)}catch(e){this._value=e;this._error=!0}return this}catch(e){if(this._error)try{this._value=e(this._value);this._error=!1}catch(e){this._value=e;this._error=!0}return this}get _value(){return this.__value}set _value(e){e&&e.constructor===this.constructor&&(e=e._value);this.__value=e}static resolve(t){return new this(e=>e(t))}static reject(i){return new this((e,t)=>t(i))}static all(t){return new this(e=>e(t.map(e=>e())))}static race(t){return t.length<1?this.resolve():new this(e=>e(t[0]()))}}}(OpenSeadragon);!function(e,t){if("function"==typeof define&&define.amd)define([],function(){return t});else if("object"==typeof module&&module.exports)module.exports=t;else{(e=e||"object"==typeof window&&window)||t.console.error("OpenSeadragon must run in browser environment!");e.OpenSeadragon=t}}(this,OpenSeadragon);!function(e){e.Mat3=class y{constructor(e){this.values=e=e||[0,0,0,0,0,0,0,0,0]}static makeIdentity(){return new y([1,0,0,0,1,0,0,0,1])}static makeTranslation(e,t){return new y([1,0,0,0,1,0,e,t,1])}static makeRotation(e){var t=Math.cos(e);e=Math.sin(e);return new y([t,-e,0,e,t,0,0,0,1])}static makeScaling(e,t){return new y([e,0,0,0,t,0,0,0,1])}multiply(e){var t=this.values;var i=e.values;var n=t[0],r=t[1],o=t[2];var s=t[3],a=t[4],l=t[5];var h=t[6],c=t[7],u=t[8];var d=i[0],p=i[1],g=i[2];var m=i[3],f=i[4],v=i[5];e=i[6],t=i[7],i=i[8];return new y([d*n+p*s+g*h,d*r+p*a+g*c,d*o+p*l+g*u,m*n+f*s+v*h,m*r+f*a+v*c,m*o+f*l+v*u,e*n+t*s+i*h,e*r+t*a+i*c,e*o+t*l+i*u])}setValues(e,t,i,n,r,o,s,a,l){this.values[0]=e;this.values[1]=t;this.values[2]=i;this.values[3]=n;this.values[4]=r;this.values[5]=o;this.values[6]=s;this.values[7]=a;this.values[8]=l}scaleAndTranslate(e,t,i,n){var r=this.values;var o=r[0];var s=r[1];var a=r[2];var l=r[3];var h=r[4];r=r[5];return new y([e*o,e*s,e*a,t*l,t*h,t*r,i*o+n*l,i*s+n*h,i*a+n*r])}scaleAndTranslateSelf(e,t,i,n){const r=this.values;var o=r[0],s=r[1],a=r[2];var l=r[3],h=r[4],c=r[5];r[0]=e*o;r[1]=e*s;r[2]=e*a;r[3]=t*l;r[4]=t*h;r[5]=t*c;r[6]=i*o+n*l+r[6];r[7]=i*s+n*h+r[7];r[8]=i*a+n*c+r[8]}scaleAndTranslateOtherSetSelf(e){var t=e.values;const i=this.values;var n=i[0];var r=i[4];var o=i[6];e=i[7];i[0]=n*t[0];i[1]=n*t[1];i[2]=n*t[2];i[3]=r*t[3];i[4]=r*t[4];i[5]=r*t[5];i[6]=o*t[0]+e*t[3]+t[6];i[7]=o*t[1]+e*t[4]+t[7];i[8]=o*t[2]+e*t[5]+t[8]}}}(OpenSeadragon);!function(t){const e={supportsFullScreen:!1,isFullScreen:function(){return!1},getFullScreenElement:function(){return null},requestFullScreen:function(){},exitFullScreen:function(){},cancelFullScreen:function(){},fullScreenEventName:"",fullScreenErrorEventName:""};if(document.exitFullscreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.fullscreenElement};e.requestFullScreen=function(e){return e.requestFullscreen().catch(function(e){t.console.error("Fullscreen request failed: ",e)})};e.exitFullScreen=function(){document.exitFullscreen().catch(function(e){t.console.error("Error while exiting fullscreen: ",e)})};e.fullScreenEventName="fullscreenchange";e.fullScreenErrorEventName="fullscreenerror"}else if(document.msExitFullscreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.msFullscreenElement};e.requestFullScreen=function(e){return e.msRequestFullscreen()};e.exitFullScreen=function(){document.msExitFullscreen()};e.fullScreenEventName="MSFullscreenChange";e.fullScreenErrorEventName="MSFullscreenError"}else if(document.webkitExitFullscreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.webkitFullscreenElement};e.requestFullScreen=function(e){return e.webkitRequestFullscreen()};e.exitFullScreen=function(){document.webkitExitFullscreen()};e.fullScreenEventName="webkitfullscreenchange";e.fullScreenErrorEventName="webkitfullscreenerror"}else if(document.webkitCancelFullScreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.webkitCurrentFullScreenElement};e.requestFullScreen=function(e){return e.webkitRequestFullScreen()};e.exitFullScreen=function(){document.webkitCancelFullScreen()};e.fullScreenEventName="webkitfullscreenchange";e.fullScreenErrorEventName="webkitfullscreenerror"}else if(document.mozCancelFullScreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.mozFullScreenElement};e.requestFullScreen=function(e){return e.mozRequestFullScreen()};e.exitFullScreen=function(){document.mozCancelFullScreen()};e.fullScreenEventName="mozfullscreenchange";e.fullScreenErrorEventName="mozfullscreenerror"}e.isFullScreen=function(){return null!==e.getFullScreenElement()};e.cancelFullScreen=function(){t.console.error("cancelFullScreen is deprecated. Use exitFullScreen instead.");e.exitFullScreen()};t.extend(t,e)}(OpenSeadragon);!function(c){c.EventSource=function(){this.events={};this._rejectedEventList={}};c.EventSource.prototype={addOnceHandler:function(t,i,e,n,r){const o=this;n=n||1;let s=0;function a(e){s++;s===n&&o.removeHandler(t,a);return i(e)}return this.addHandler(t,a,e,r)},addHandler:function(e,i,n,r){if(Object.prototype.hasOwnProperty.call(this._rejectedEventList,e)){c.console.error(`Error adding handler for ${e}. `+this._rejectedEventList[e]);return!1}let o=this.events[e];o||(this.events[e]=o=[]);if(i&&c.isFunction(i)){let e=o.length,t={handler:i,userData:n||null,priority:r||0};o[e]=t;for(;0{const o=h.length;!function e(t){if(t>=o||!h[t]){n(l);return null}a.eventSource=s;a.userData=h[t].userData;let i;try{i=h[t].handler(a)}catch(e){return r(e)}i=i&&"promise"===c.type(i)?i:c.Promise.resolve();return i.then(()=>!a.stopPropagation||"function"==typeof a.stopPropagation&&!1===a.stopPropagation()?e(t+1):e(o))}(0).catch(r)})}},raiseEvent:function(e,t){if(Object.prototype.hasOwnProperty.call(this._rejectedEventList,e)){c.console.error(`Error adding handler for ${e}. `+this._rejectedEventList[e]);return!1}const i=this.getHandler(e);i&&i(this,t||{});return!0},raiseEventAwaiting:function(e,t,i=null){const n=this.getAwaitingHandler(e,i);return n?n(this,t||{}):c.Promise.resolve(i)},rejectEventHandler(e,t=""){this._rejectedEventList[e]=t},allowEventHandler(e){delete this._rejectedEventList[e]}}}(OpenSeadragon);!function(c){const n=[];const u={};c.MouseTracker=function(e){n.push(this);var t=arguments;c.isPlainObject(e)||(e={element:t[0],clickTimeThreshold:t[1],clickDistThreshold:t[2]});this.hash=function(){let e=Date.now().toString(36)+Math.random().toString(36).substring(2);for(;e in u;)e=Date.now().toString(36)+Math.random().toString(36).substring(2);return e}();this.element=c.getElement(e.element);this.clickTimeThreshold=e.clickTimeThreshold||c.DEFAULT_SETTINGS.clickTimeThreshold;this.clickDistThreshold=e.clickDistThreshold||c.DEFAULT_SETTINGS.clickDistThreshold;this.dblClickTimeThreshold=e.dblClickTimeThreshold||c.DEFAULT_SETTINGS.dblClickTimeThreshold;this.dblClickDistThreshold=e.dblClickDistThreshold||c.DEFAULT_SETTINGS.dblClickDistThreshold;this.userData=e.userData||null;this.stopDelay=e.stopDelay||50;this.preProcessEventHandler=e.preProcessEventHandler||null;this.contextMenuHandler=e.contextMenuHandler||null;this.enterHandler=e.enterHandler||null;this.leaveHandler=e.leaveHandler||null;this.exitHandler=e.exitHandler||null;this.overHandler=e.overHandler||null;this.outHandler=e.outHandler||null;this.pressHandler=e.pressHandler||null;this.nonPrimaryPressHandler=e.nonPrimaryPressHandler||null;this.releaseHandler=e.releaseHandler||null;this.nonPrimaryReleaseHandler=e.nonPrimaryReleaseHandler||null;this.moveHandler=e.moveHandler||null;this.scrollHandler=e.scrollHandler||null;this.clickHandler=e.clickHandler||null;this.dblClickHandler=e.dblClickHandler||null;this.dragHandler=e.dragHandler||null;this.dragEndHandler=e.dragEndHandler||null;this.pinchHandler=e.pinchHandler||null;this.stopHandler=e.stopHandler||null;this.keyDownHandler=e.keyDownHandler||null;this.keyUpHandler=e.keyUpHandler||null;this.keyHandler=e.keyHandler||null;this.focusHandler=e.focusHandler||null;this.blurHandler=e.blurHandler||null;const i=this;u[this.hash]={click:function(e){!function(e,t){var i={originalEvent:t,eventType:"click",pointerType:"mouse",isEmulated:!1};A(e,i);i.preventDefault&&!i.defaultPrevented&&c.cancelEvent(t);i.stopPropagation&&c.stopEvent(t)}(i,e)},dblclick:function(e){!function(e,t){var i={originalEvent:t,eventType:"dblclick",pointerType:"mouse",isEmulated:!1};A(e,i);i.preventDefault&&!i.defaultPrevented&&c.cancelEvent(t);i.stopPropagation&&c.stopEvent(t)}(i,e)},keydown:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"keydown",pointerType:"",isEmulated:!1};A(e,n);if(e.keyDownHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,keyCode:t.keyCode||t.charCode,ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,originalEvent:t,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.keyDownHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},keyup:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"keyup",pointerType:"",isEmulated:!1};A(e,n);if(e.keyUpHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,keyCode:t.keyCode||t.charCode,ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,originalEvent:t,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.keyUpHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},keypress:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"keypress",pointerType:"",isEmulated:!1};A(e,n);if(e.keyHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,keyCode:t.keyCode||t.charCode,ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,originalEvent:t,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.keyHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},focus:function(e){!function(e,t){var i={originalEvent:t,eventType:"focus",pointerType:"",isEmulated:!1};A(e,i);e.focusHandler&&!i.preventGesture&&e.focusHandler({eventSource:e,originalEvent:t,userData:e.userData})}(i,e)},blur:function(e){!function(e,t){var i={originalEvent:t,eventType:"blur",pointerType:"",isEmulated:!1};A(e,i);e.blurHandler&&!i.preventGesture&&e.blurHandler({eventSource:e,originalEvent:t,userData:e.userData})}(i,e)},contextmenu:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"contextmenu",pointerType:"mouse",isEmulated:!1};A(e,n);if(e.contextMenuHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,position:f(g(t),e.element),originalEvent:n.originalEvent,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.contextMenuHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},wheel:function(e){w(i,e,e)},mousewheel:function(e){y(i,e)},DOMMouseScroll:function(e){y(i,e)},MozMousePixelScroll:function(e){y(i,e)},losecapture:function(e){!function(e,t){var i={id:c.MouseTracker.mousePointerId,type:"mouse"};var n={originalEvent:t,eventType:"lostpointercapture",pointerType:"mouse",isEmulated:!1};A(e,n);t.target===e.element&&F(e,i,!1);n.stopPropagation&&c.stopEvent(t)}(i,e)},mouseenter:function(e){_(i,e)},mouseleave:function(e){T(i,e)},mouseover:function(e){x(i,e)},mouseout:function(e){S(i,e)},mousedown:function(e){E(i,e)},mouseup:function(e){C(i,e)},mousemove:function(e){P(i,e)},touchstart:function(e){!function(t,i){var n=i.changedTouches.length;const r=t.getActivePointersListByType("touch");var o=c.now();r.getLength()>i.touches.length-n&&c.console.warn("Tracked touch contact count doesn't match event.touches.length");var s={originalEvent:i,eventType:"pointerdown",pointerType:"touch",isEmulated:!1};A(t,s);for(let e=0;e{e[t]=i[t];delete i[t];return e},{}),i.drawerOptions);m.extend(!0,this,{id:i.id,hash:i.hash||a++,viewer:null,initialPage:0,element:null,container:null,canvas:null,overlays:[],overlaysContainer:null,previousDisplayValuesOfBodyChildren:[],customControls:[],source:null,drawer:null,drawerCandidates:null,world:null,viewport:null,navigator:null,collectionViewport:null,collectionDrawer:null,navImages:null,buttonGroup:null,profiler:null},m.DEFAULT_SETTINGS,i);if(void 0===this.hash)throw new Error("A hash must be defined, either by specifying options.id or options.hash.");void 0!==u[this.hash]&&m.console.warn("Hash "+this.hash+" has already been used.");u[this.hash]={fsBoundsDelta:new m.Point(1,1),prevContainerSize:null,animating:!1,forceRedraw:!1,needsResize:!1,forceResize:!1,mouseInside:!1,group:null,zooming:!1,zoomFactor:null,lastZoomTime:null,fullPage:!1,onfullscreenchange:null,lastClickTime:null,draggingToZoom:!1};this._sequenceIndex=0;this._firstOpen=!0;this._updateRequestId=null;this._loadQueue=[];this.currentOverlays=[];this._updatePixelDensityRatioBind=null;this._lastScrollTime=m.now();this._fullyLoaded=!1;this._navActionFrames={};this._navActionVirtuallyHeld={};this._minNavActionFrames=10;this._activeActions={panUp:!1,panDown:!1,panLeft:!1,panRight:!1,zoomIn:!1,zoomOut:!1};m.EventSource.call(this);this.addHandler("open-failed",function(e){e=m.getString("Errors.OpenFailed",e.eventSource,e.message);n._showMessage(e)});m.ControlDock.call(this,i);this.xmlPath&&(this.tileSources=[this.xmlPath]);this.element=this.element||document.getElementById(this.id);this.canvas=m.makeNeutralElement("div");this.canvas.className="openseadragon-canvas";if(!document.querySelector("style[data-openseadragon-mobile-css]")){const o=document.createElement("style");o.setAttribute("data-openseadragon-mobile-css","true");o.textContent="@media (hover: none) { .openseadragon-canvas:focus { outline: none !important; }}";document.head.appendChild(o)}!function(e){e.width="100%";e.height="100%";e.overflow="hidden";e.position="absolute";e.top="0px";e.left="0px"}(this.canvas.style);m.setElementTouchActionNone(this.canvas);""!==i.tabIndex&&(this.canvas.tabIndex=void 0===i.tabIndex?0:i.tabIndex);this.container.className="openseadragon-container";!function(e){e.width="100%";e.height="100%";e.position="relative";e.overflow="hidden";e.left="0px";e.top="0px";e.textAlign="left"}(this.container.style);m.setElementTouchActionNone(this.container);this.container.insertBefore(this.canvas,this.container.firstChild);this.element.appendChild(this.container);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 m.MouseTracker({userData:"Viewer.innerTracker",element:this.canvas,startDisabled:!this.mouseNavEnabled,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,dblClickTimeThreshold:this.dblClickTimeThreshold,dblClickDistThreshold:this.dblClickDistThreshold,contextMenuHandler:m.delegate(this,p),keyDownHandler:m.delegate(this,y),keyUpHandler:m.delegate(this,g),keyHandler:m.delegate(this,w),clickHandler:m.delegate(this,_),dblClickHandler:m.delegate(this,T),dragHandler:m.delegate(this,x),dragEndHandler:m.delegate(this,S),enterHandler:m.delegate(this,E),leaveHandler:m.delegate(this,C),pressHandler:m.delegate(this,b),releaseHandler:m.delegate(this,P),nonPrimaryPressHandler:m.delegate(this,R),nonPrimaryReleaseHandler:m.delegate(this,D),scrollHandler:m.delegate(this,L),pinchHandler:m.delegate(this,I),focusHandler:m.delegate(this,A),blurHandler:m.delegate(this,F)});this.outerTracker=new m.MouseTracker({userData:"Viewer.outerTracker",element:this.container,startDisabled:!this.mouseNavEnabled,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,dblClickTimeThreshold:this.dblClickTimeThreshold,dblClickDistThreshold:this.dblClickDistThreshold,enterHandler:m.delegate(this,O),leaveHandler:m.delegate(this,k)});this.toolbar&&(this.toolbar=new m.ControlDock({element:this.toolbar}));this.bindStandardControls();u[this.hash].prevContainerSize=l(this.container);if(window.ResizeObserver){this._autoResizePolling=!1;this._resizeObserver=new ResizeObserver(function(){u[n.hash].needsResize=!0});this._resizeObserver.observe(this.container,{})}else this._autoResizePolling=!0;this.world=new m.World({viewer:this});this.world.addHandler("add-item",function(e){n.source=n.world.getItemAt(0).source;u[n.hash].forceRedraw=!0;n._updateRequestId||(n._updateRequestId=c(n,B));const t=e.item;function i(){var e=n._areAllFullyLoaded();if(e!==n._fullyLoaded){n._fullyLoaded=e;n.raiseEvent("fully-loaded-change",{fullyLoaded:e})}}t._fullyLoadedHandlerForViewer=i;t.addHandler("fully-loaded-change",i)});this.world.addHandler("remove-item",function(e){const t=e.item;if(t._fullyLoadedHandlerForViewer){t.removeHandler("fully-loaded-change",t._fullyLoadedHandlerForViewer);delete t._fullyLoadedHandlerForViewer}n.world.getItemCount()?n.source=n.world.getItemAt(0).source:n.source=null;u[n.hash].forceRedraw=!0});this.world.addHandler("metrics-change",function(e){n.viewport&&n.viewport._setContentBounds(n.world.getHomeBounds(),n.world.getContentFactor())});this.world.addHandler("item-index-change",function(e){n.source=n.world.getItemAt(0).source});this.viewport=new m.Viewport({containerSize:u[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());this.imageLoader=new m.ImageLoader({jobLimit:this.imageLoaderLimit,timeout:i.timeout,tileRetryMax:this.tileRetryMax,tileRetryDelay:this.tileRetryDelay});this.tileCache=new m.TileCache({viewer:this,maxImageCacheCount:this.maxImageCacheCount});if(Object.prototype.hasOwnProperty.call(this.drawerOptions,"useCanvas")){m.console.error('useCanvas is deprecated, use the "drawer" option to indicate preferred drawer(s)');this.drawerOptions.useCanvas||(this.drawer=m.HTMLDrawer);delete this.drawerOptions.useCanvas}let r=Array.isArray(this.drawer)?this.drawer:[this.drawer];if(0===r.length){r=[m.DEFAULT_SETTINGS.drawer].flat();m.console.warn("No valid drawers were selected. Using the default value.")}r=r.flatMap(function(e){return"auto"===e?V():[e]});r=r.filter(function(e,t,i){return i.indexOf(e)===t});this.drawerCandidates=r.map(G).filter(Boolean);this.drawer=null;for(const s of r)if(this.requestDrawer(s,{mainDrawer:!0,redrawImmediately:!1}))break;if(!this.drawer){m.console.error("No drawer could be created!");throw"Error with creating the selected drawer(s)"}this.drawer.setImageSmoothingEnabled(this.imageSmoothingEnabled);this.overlaysContainer=m.makeNeutralElement("div");this.canvas.appendChild(this.overlaysContainer);if(!this.drawer.canRotate()){if(this.rotateLeft){t=this.buttonGroup.buttons.indexOf(this.rotateLeft);this.buttonGroup.buttons.splice(t,1);this.buttonGroup.element.removeChild(this.rotateLeft.element)}if(this.rotateRight){t=this.buttonGroup.buttons.indexOf(this.rotateRight);this.buttonGroup.buttons.splice(t,1);this.buttonGroup.element.removeChild(this.rotateRight.element)}}this._addUpdatePixelDensityRatioEvent();"navigatorAutoResize"in this&&m.console.warn("navigatorAutoResize is deprecated, this value will be ignored.");this.showNavigator&&(this.navigator=new m.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}));this.sequenceMode&&this.bindSequenceControls();this.tileSources&&this.open(this.tileSources);for(t=0;te.viewer.world.requestInvalidate(t,i)))},close:function(){if(!u[this.hash])return this;this._opening=!1;this.navigator&&this.navigator.close();if(!this.preserveOverlays){this.clearOverlays();this.overlaysContainer.innerHTML=""}u[this.hash].animating=!1;this.world.removeAll();this.tileCache.clear();this.imageLoader.clear();this.raiseEvent("close");return this},destroy:function(){if(u[this.hash]){this.raiseEvent("before-destroy");this._removeUpdatePixelDensityRatioEvent();this.close();this.clearOverlays();this.overlaysContainer.innerHTML="";this._resizeObserver&&this._resizeObserver.disconnect();if(this.referenceStrip){this.referenceStrip.destroy();this.referenceStrip=null}if(null!==this._updateRequestId){m.cancelAnimationFrame(this._updateRequestId);this._updateRequestId=null}this.drawer&&this.drawer.destroy();if(this.navigator){this.navigator.destroy();u[this.navigator.hash]=null;delete u[this.navigator.hash];this.navigator=null}if(this.buttonGroup)this.buttonGroup.destroy();else if(this.customButtons)for(;this.customButtons.length;)this.customButtons.pop().destroy();this.paging&&this.paging.destroy();this.container&&this.container.parentNode===this.element&&this.element.removeChild(this.container);this.container.onsubmit=null;this.clearControls();this.innerTracker&&this.innerTracker.destroy();this.outerTracker&&this.outerTracker.destroy();u[this.hash]=null;delete u[this.hash];this.canvas=null;this.container=null;m._viewers.delete(this.element);this.element=null;this.raiseEvent("destroy");this.removeAllHandlers()}},isDestroyed(){return!u[this.hash]},requestDrawer(t,e){var i=(e=m.extend(!0,{mainDrawer:!0,redrawImmediately:!0,drawerOptions:null},e)).mainDrawer;var n=e.redrawImmediately;e=e.drawerOptions;const r=this.drawer;let o=null;if(t&&t.prototype instanceof m.DrawerBase){o=t;t="custom"}else"string"==typeof t&&(o=m.determineDrawer(t));o||m.console.warn("Unsupported drawer %s! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.",t);let s=!1;if(o)try{s=o.isSupported()}catch(e){m.console.warn("Error in %s isSupported(); treating this drawer as unsupported:",t,e&&e.message?e.message:e)}if(s){r&&i&&r.destroy();t=new o({viewer:this,viewport:this.viewport,element:this.canvas,debugGridColor:this.debugGridColor,options:e||this.drawerOptions[t]});if(i){this.drawer=t;n&&this.forceRedraw()}return t}return!1},isMouseNavEnabled:function(){return this.innerTracker.tracking},setMouseNavEnabled:function(e){this.innerTracker.setTracking(e);this.outerTracker.setTracking(e);this.raiseEvent("mouse-enabled",{enabled:e});return this},isKeyboardNavEnabled:function(){return this.keyboardNavEnabled},setKeyboardNavEnabled:function(e){this.keyboardNavEnabled=e;this.raiseEvent("keyboard-enabled",{enabled:e});return this},areControlsEnabled:function(){let t=this.controls.length;for(let e=0;e{if(this.collectionMode){this.world.arrange({immediately:e.options.collectionImmediately,rows:this.collectionRows,columns:this.collectionColumns,layout:this.collectionLayout,tileSize:this.collectionTileSize,tileMargin:this.collectionTileMargin});this.world.setAutoRefigureSizes(!0)}};const i=e=>{for(let e=0;e{l.tiledImage=e.item;l.originalSuccess=a;let t,i;for(;this._loadQueue.length;){t=this._loadQueue[0];var n=t.tiledImage;if(!n)break;this._loadQueue.splice(0,1);var r=n.source;if(t.options.replace){const s=t.options.replaceItem;var o=this.world.getIndexOfItem(s);-1!==o&&(t.options.index=o);!s._zombieCache&&s.source.equals(r)&&s.allowZombieCache(!0);this.world.removeItem(s)}this.collectionMode&&this.world.setAutoRefigureSizes(!1);if(this.navigator){i=m.extend({},t.options,{replace:!1,originalTiledImage:n,tileSource:r});this.navigator.addTiledImage(i)}this.world.addItem(n,{index:t.options.index});0===this._loadQueue.length&&h(t);1!==this.world.getItemCount()||this.preserveViewport||this.viewport.goHome(!0);t.originalSuccess&&t.originalSuccess({item:n});this.drawer&&this.drawer.tiledImageCreated(n)}};e.error=i;this.instantiateTiledImageClass(e)}},instantiateTiledImageClass:function(t){return this.instantiateTileSourceClass(t).then(e=>{e=new m.TiledImage({viewer:this,source:e.source,viewport:this.viewport,drawer:this.drawer,tileCache:this.tileCache,imageLoader:this.imageLoader,x:t.x,y:t.y,width:t.width,height:t.height,fitBounds:t.fitBounds,fitBoundsPlacement:t.fitBoundsPlacement,clip:t.clip,placeholderFillStyle:t.placeholderFillStyle,opacity:t.opacity,preload:t.preload,degrees:t.degrees,flipped:t.flipped,compositeOperation:t.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:t.crossOriginPolicy,ajaxWithCredentials:t.ajaxWithCredentials,loadTilesWithAjax:t.loadTilesWithAjax,ajaxHeaders:t.ajaxHeaders,debugMode:this.debugMode,subPixelRoundingForTransparency:this.subPixelRoundingForTransparency,callTileLoadedWithCachedData:this.callTileLoadedWithCachedData,originalDataType:t.originalDataType});t.success({item:e});return e}).catch(e=>{if(t.error){t.error(e);return e}throw e})},instantiateTileSourceClass(s){return new m.Promise((i,n)=>{void 0===s.placeholderFillStyle&&(s.placeholderFillStyle=this.placeholderFillStyle);void 0===s.opacity&&(s.opacity=this.opacity);void 0===s.preload&&(s.preload=this.preload);void 0===s.compositeOperation&&(s.compositeOperation=this.compositeOperation);void 0===s.crossOriginPolicy&&(s.crossOriginPolicy=(void 0!==s.tileSource.crossOriginPolicy?s.tileSource:this).crossOriginPolicy);void 0===s.ajaxWithCredentials&&(s.ajaxWithCredentials=this.ajaxWithCredentials);void 0===s.loadTilesWithAjax&&(s.loadTilesWithAjax=this.loadTilesWithAjax);m.isPlainObject(s.ajaxHeaders)||(s.ajaxHeaders={});let r=s.tileSource;if("string"===m.type(r))if(r.match(/^\s*<.*>\s*$/))r=m.parseXml(r);else if(r.match(/^\s*[{[].*[}\]]\s*$/))try{r=m.parseJSON(r)}catch(e){}function o(e,t){if(e.ready)i({source:e});else{e.addHandler("ready",function(e){i({source:e.tileSource})});e.addHandler("open-failed",function(e){n({message:e.message,source:t})})}}setTimeout(()=>{if("string"===m.type(r)){r=new m.TileSource({url:r,crossOriginPolicy:(void 0!==s.crossOriginPolicy?s:this).crossOriginPolicy,ajaxWithCredentials:this.ajaxWithCredentials,ajaxHeaders:m.extend({},this.ajaxHeaders,s.ajaxHeaders),splitHashDataForPost:this.splitHashDataForPost});o(r,r)}else if(m.isPlainObject(r)||r.nodeType){void 0!==r.crossOriginPolicy||void 0===s.crossOriginPolicy&&void 0===this.crossOriginPolicy||(r.crossOriginPolicy=(void 0!==s.crossOriginPolicy?s:this).crossOriginPolicy);void 0===r.ajaxWithCredentials&&(r.ajaxWithCredentials=this.ajaxWithCredentials);if(m.isFunction(r.getTileUrl)){const e=new m.TileSource(r);e.getTileUrl=r.getTileUrl;r.ready=!1;o(e,r)}else{const t=m.TileSource.determineType(this,r,null);if(t){const i=t.prototype.configure.apply(this,[r]);i.ready=!1;o(new t(i),r)}else n({message:"Unable to load TileSource",source:r,error:!0})}}else o(r,r)})})},addSimpleImage:function(e){m.console.assert(e,"[Viewer.addSimpleImage] options is required");m.console.assert(e.url,"[Viewer.addSimpleImage] options.url is required");const t=m.extend({},e,{tileSource:{type:"image",url:e.url}});delete t.url;this.addTiledImage(t)},addLayer:function(t){const i=this;m.console.error("[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead.");var e=m.extend({},t,{success:function(e){i.raiseEvent("add-layer",{options:t,drawer:e.item})},error:function(e){i.raiseEvent("add-layer-failed",e)}});this.addTiledImage(e);return this},getLayerAtLevel:function(e){m.console.error("[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead.");return this.world.getItemAt(e)},getLevelOfLayer:function(e){m.console.error("[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead.");return this.world.getIndexOfItem(e)},getLayersCount:function(){m.console.error("[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead.");return this.world.getItemCount()},setLayerLevel:function(e,t){m.console.error("[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead.");return this.world.setItemIndex(e,t)},removeLayer:function(e){m.console.error("[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead.");return this.world.removeItem(e)},forceRedraw:function(){u[this.hash].forceRedraw=!0;return this},forceResize:function(){u[this.hash].needsResize=!0;u[this.hash].forceResize=!0},bindSequenceControls:function(){var e=m.delegate(this,f);var t=m.delegate(this,v);var i=m.delegate(this,this.goToNextPage);var n=m.delegate(this,this.goToPreviousPage);var r=this.navImages;let o=!0;if(this.showSequenceControl){(this.previousButton||this.nextButton)&&(o=!1);this.previousButton=new m.Button({element:this.previousButton?m.getElement(this.previousButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.PreviousPage"),srcRest:M(this.prefixUrl,r.previous.REST),srcGroup:M(this.prefixUrl,r.previous.GROUP),srcHover:M(this.prefixUrl,r.previous.HOVER),srcDown:M(this.prefixUrl,r.previous.DOWN),onRelease:n,onFocus:e,onBlur:t});this.nextButton=new m.Button({element:this.nextButton?m.getElement(this.nextButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.NextPage"),srcRest:M(this.prefixUrl,r.next.REST),srcGroup:M(this.prefixUrl,r.next.GROUP),srcHover:M(this.prefixUrl,r.next.HOVER),srcDown:M(this.prefixUrl,r.next.DOWN),onRelease:i,onFocus:e,onBlur:t});this.navPrevNextWrap||this.previousButton.disable();this.tileSources&&this.tileSources.length||this.nextButton.disable();if(o){this.paging=new m.ButtonGroup({buttons:[this.previousButton,this.nextButton],clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.pagingControl=this.paging.element;this.toolbar?this.toolbar.addControl(this.pagingControl,{anchor:m.ControlAnchor.BOTTOM_RIGHT}):this.addControl(this.pagingControl,{anchor:this.sequenceControlAnchor||m.ControlAnchor.TOP_LEFT})}}return this},bindStandardControls:function(){var e=m.delegate(this,this.startZoomInAction);var t=m.delegate(this,this.endZoomAction);var i=m.delegate(this,this.singleZoomInAction);var n=m.delegate(this,this.startZoomOutAction);var r=m.delegate(this,this.singleZoomOutAction);var o=m.delegate(this,H);var s=m.delegate(this,N);var a=m.delegate(this,U);var l=m.delegate(this,W);var h=m.delegate(this,j);var c=m.delegate(this,f);var u=m.delegate(this,v);var d=this.navImages;const p=[];let g=!0;if(this.showNavigationControl){(this.zoomInButton||this.zoomOutButton||this.homeButton||this.fullPageButton||this.rotateLeftButton||this.rotateRightButton||this.flipButton)&&(g=!1);if(this.showZoomControl){p.push(this.zoomInButton=new m.Button({element:this.zoomInButton?m.getElement(this.zoomInButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomIn"),srcRest:M(this.prefixUrl,d.zoomIn.REST),srcGroup:M(this.prefixUrl,d.zoomIn.GROUP),srcHover:M(this.prefixUrl,d.zoomIn.HOVER),srcDown:M(this.prefixUrl,d.zoomIn.DOWN),onPress:e,onRelease:t,onClick:i,onEnter:e,onExit:t,onFocus:c,onBlur:u}));p.push(this.zoomOutButton=new m.Button({element:this.zoomOutButton?m.getElement(this.zoomOutButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomOut"),srcRest:M(this.prefixUrl,d.zoomOut.REST),srcGroup:M(this.prefixUrl,d.zoomOut.GROUP),srcHover:M(this.prefixUrl,d.zoomOut.HOVER),srcDown:M(this.prefixUrl,d.zoomOut.DOWN),onPress:n,onRelease:t,onClick:r,onEnter:n,onExit:t,onFocus:c,onBlur:u}))}this.showHomeControl&&p.push(this.homeButton=new m.Button({element:this.homeButton?m.getElement(this.homeButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Home"),srcRest:M(this.prefixUrl,d.home.REST),srcGroup:M(this.prefixUrl,d.home.GROUP),srcHover:M(this.prefixUrl,d.home.HOVER),srcDown:M(this.prefixUrl,d.home.DOWN),onRelease:o,onFocus:c,onBlur:u}));this.showFullPageControl&&p.push(this.fullPageButton=new m.Button({element:this.fullPageButton?m.getElement(this.fullPageButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.FullPage"),srcRest:M(this.prefixUrl,d.fullpage.REST),srcGroup:M(this.prefixUrl,d.fullpage.GROUP),srcHover:M(this.prefixUrl,d.fullpage.HOVER),srcDown:M(this.prefixUrl,d.fullpage.DOWN),onRelease:s,onFocus:c,onBlur:u}));if(this.showRotationControl){p.push(this.rotateLeftButton=new m.Button({element:this.rotateLeftButton?m.getElement(this.rotateLeftButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateLeft"),srcRest:M(this.prefixUrl,d.rotateleft.REST),srcGroup:M(this.prefixUrl,d.rotateleft.GROUP),srcHover:M(this.prefixUrl,d.rotateleft.HOVER),srcDown:M(this.prefixUrl,d.rotateleft.DOWN),onRelease:a,onFocus:c,onBlur:u}));p.push(this.rotateRightButton=new m.Button({element:this.rotateRightButton?m.getElement(this.rotateRightButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateRight"),srcRest:M(this.prefixUrl,d.rotateright.REST),srcGroup:M(this.prefixUrl,d.rotateright.GROUP),srcHover:M(this.prefixUrl,d.rotateright.HOVER),srcDown:M(this.prefixUrl,d.rotateright.DOWN),onRelease:l,onFocus:c,onBlur:u}))}this.showFlipControl&&p.push(this.flipButton=new m.Button({element:this.flipButton?m.getElement(this.flipButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Flip"),srcRest:M(this.prefixUrl,d.flip.REST),srcGroup:M(this.prefixUrl,d.flip.GROUP),srcHover:M(this.prefixUrl,d.flip.HOVER),srcDown:M(this.prefixUrl,d.flip.DOWN),onRelease:h,onFocus:c,onBlur:u}));if(g){this.buttonGroup=new m.ButtonGroup({buttons:p,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.navControl=this.buttonGroup.element;this.addHandler("open",m.delegate(this,z));(this.toolbar||this).addControl(this.navControl,{anchor:this.navigationControlAnchor||m.ControlAnchor.TOP_LEFT})}else this.customButtons=p}return this},currentPage:function(){return this._sequenceIndex},goToPage:function(e){if(this.tileSources&&0<=e&&e=this.tileSources.length&&(e=0);this.goToPage(e)},isAnimating:function(){return u[this.hash].animating},startZoomInAction:function(){u[this.hash].lastZoomTime=m.now();u[this.hash].zoomFactor=this.zoomPerSecond;u[this.hash].zooming=!0;o(this)},startZoomOutAction:function(){u[this.hash].lastZoomTime=m.now();u[this.hash].zoomFactor=1/this.zoomPerSecond;u[this.hash].zooming=!0;o(this)},endZoomAction:function(){u[this.hash].zooming=!1},singleZoomInAction:function(){if(this.viewport){u[this.hash].zooming=!1;this.viewport.zoomBy(+this.zoomPerClick);this.viewport.applyConstraints()}},singleZoomOutAction:function(){if(this.viewport){u[this.hash].zooming=!1;this.viewport.zoomBy(1/this.zoomPerClick);this.viewport.applyConstraints()}}});function l(e){e=m.getElement(e);return new m.Point(0===e.clientWidth?1:e.clientWidth,0===e.clientHeight?1:e.clientHeight)}function h(i,n){if(n instanceof m.Overlay)return n;let e=null;if(n.element)e=m.getElement(n.element);else{var t=n.id||"openseadragon-overlay-"+Math.floor(1e7*Math.random());e=m.getElement(n.id);if(!e){e=document.createElement("a");e.href="#/overlay/"+t}e.id=t;m.addClass(e,n.className||"openseadragon-overlay")}let r=n.location;let o=n.width;let s=n.height;if(!r){let e=n.x;let t=n.y;if(void 0!==n.px){i=i.viewport.imageToViewportRectangle(new m.Rect(n.px,n.py,o||0,s||0));e=i.x;t=i.y;o=void 0!==o?i.width:void 0;s=void 0!==s?i.height:void 0}r=new m.Point(e,t)}let a=n.placement;a&&"string"===m.type(a)&&(a=m.Placement[n.placement.toUpperCase()]);return new m.Overlay({element:e,location:r,placement:a,onDraw:n.onDraw,checkResize:n.checkResize,width:o,height:s,rotationMode:n.rotationMode})}function s(t,i){for(let e=t.length-1;0<=e;e--)if(t[e].element===i)return e;return-1}function c(e,t){return m.requestAnimationFrame(function(){t(e)})}function n(e){m.requestAnimationFrame(function(){!function(t){if(t.controlsShouldFade){var i=1-(m.now()-t.controlsFadeBeginTime)/t.controlsFadeLength;i=Math.min(1,i);i=Math.max(0,i);for(let e=t.controls.length-1;0<=e;e--)t.controls[e].autoFade&&t.controls[e].setOpacity(i);0{t=i(e,t);if(t&&this._activeActions[t]){this._activeActions[t]=!1;this._navActionFrames[t]=n.flickMinSpeed){let e=0;this.panHorizontal&&(e=n.flickMomentum*i.speed*Math.cos(i.direction));let t=0;this.panVertical&&(t=n.flickMomentum*i.speed*Math.sin(i.direction));i=this.viewport.pixelFromPoint(this.viewport.getCenter(!0));i=this.viewport.pointFromPixel(new m.Point(i.x-e,i.y-t));this.viewport.panTo(i,!1)}this.viewport.applyConstraints()}n.dblClickDragToZoom&&!0===u[this.hash].draggingToZoom&&(u[this.hash].draggingToZoom=!1)}function E(e){this.raiseEvent("canvas-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function C(e){this.raiseEvent("canvas-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function b(e){this.raiseEvent("canvas-press",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,insideElementPressed:e.insideElementPressed,insideElementReleased:e.insideElementReleased,originalEvent:e.originalEvent});if(this.gestureSettingsByDeviceType(e.pointerType).dblClickDragToZoom){var t=u[this.hash].lastClickTime;e=m.now();if(null!==t){e-tthis.minScrollDeltaTime){this._lastScrollTime=n;t={tracker:e.eventSource,position:e.position,scroll:e.scroll,shift:e.shift,originalEvent:e.originalEvent,preventDefaultAction:!1,preventDefault:!0};this.raiseEvent("canvas-scroll",t);if(!t.preventDefaultAction&&this.viewport){this.viewport.flipped&&(e.position.x=this.viewport.getContainerSize().x-e.position.x);if((i=this.gestureSettingsByDeviceType(e.pointerType)).scrollToZoom){n=Math.pow(this.zoomPerScroll,e.scroll);this.viewport.zoomBy(n,i.zoomToRefPoint?this.viewport.pointFromPixel(e.position,!0):null);this.viewport.applyConstraints()}}e.preventDefault=t.preventDefault}else e.preventDefault=!0}function O(e){u[this.hash].mouseInside=!0;r(this);this.raiseEvent("container-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function k(e){if(e.pointers<1){u[this.hash].mouseInside=!1;u[this.hash].animating||d(this)}this.raiseEvent("container-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function B(e){!function(i){!function(i){for(const e in i._activeActions)if(i._activeActions[e]||i._navActionVirtuallyHeld[e]){i._navActionFrames[e]++;i._navActionFrames[e]>=i._minNavActionFrames&&(i._navActionVirtuallyHeld[e]=!1)}function n(e){return i._activeActions[e]||i._navActionVirtuallyHeld[e]}var r=i.pixelsPerArrowPress/10;r=i.viewport.deltaPointsFromPixels(new OpenSeadragon.Point(r,r));if(n("zoomIn")){i.viewport.zoomBy(1.01,null,!0);i.viewport.applyConstraints()}else if(n("zoomOut")){i.viewport.zoomBy(.99,null,!0);i.viewport.applyConstraints()}else{let e=0;let t=0;if(!i.preventVerticalPan){n("panUp")&&(t-=r.y);n("panDown")&&(t+=r.y)}if(!i.preventHorizontalPan){n("panLeft")&&(e-=r.x);n("panRight")&&(e+=r.x)}if(0!==e||0!==t){i.viewport.panBy(new OpenSeadragon.Point(e,t),!0);i.viewport.applyConstraints()}}}(i);if(!i._opening&&u[i.hash]){let t=!1;if(i.autoResize||u[i.hash].forceResize){let e;if(i._autoResizePolling){e=l(i.container);var n=u[i.hash].prevContainerSize;e.equals(n)||(u[i.hash].needsResize=!0)}if(u[i.hash].needsResize){!function(e,t){const i=e.viewport;var n=i.getZoom();var r=i.getCenter();i.resize(t,e.preserveImageSizeOnResize);i.panTo(r,!0);let o;if(e.preserveImageSizeOnResize)o=u[e.hash].prevContainerSize.x/t.x;else{var s=new m.Point(0,0);r=new m.Point(u[e.hash].prevContainerSize.x,u[e.hash].prevContainerSize.y).distanceTo(s);s=new m.Point(t.x,t.y).distanceTo(s);o=s/r*u[e.hash].prevContainerSize.x/t.x}i.zoomTo(n*o,null,!0);u[e.hash].prevContainerSize=t;u[e.hash].forceRedraw=!0;u[e.hash].needsResize=!1;u[e.hash].forceResize=!1}(i,e||l(i.container));t=!0}}n=i.viewport.update()||t;let e=i.world.update(n)||n;n&&i.raiseEvent("viewport-change");i.referenceStrip&&(e=i.referenceStrip.update(i.viewport)||e);n=u[i.hash].animating;if(!n&&e){i.raiseEvent("animation-start");r(i)}n=n&&!e;n&&(u[i.hash].animating=!1);if(e||n||u[i.hash].forceRedraw||i.world.needsDraw()){!function(e){e.imageLoader.clear();e.world.draw();e.raiseEvent("update-viewport",{})}(i);i._drawOverlays();i.navigator&&i.navigator.update(i.viewport);u[i.hash].forceRedraw=!1;e&&i.raiseEvent("animation")}if(n){i.raiseEvent("animation-finish");u[i.hash].mouseInside||d(i)}u[i.hash].animating=e}}(e);e.isOpen()?e._updateRequestId=c(e,B):e._updateRequestId=!1}function M(e,t){return e?e+t:t}function o(e){m.requestAnimationFrame(m.delegate(e,t))}function t(){if(u[this.hash].zooming&&this.viewport){var e=m.now();var t=e-u[this.hash].lastZoomTime;t=Math.pow(u[this.hash].zoomFactor,t/1e3);this.viewport.zoomBy(t);this.viewport.applyConstraints();u[this.hash].lastZoomTime=e;o(this)}}function z(){if(this.buttonGroup){this.buttonGroup.emulateEnter();this.buttonGroup.emulateLeave()}}function H(){this.viewport&&this.viewport.goHome()}function N(){this.isFullPage()&&!m.isFullScreen()?this.setFullPage(!1):this.setFullScreen(!this.isFullPage());this.buttonGroup&&this.buttonGroup.emulateLeave();this.fullPageButton.element.focus();this.viewport&&this.viewport.applyConstraints()}function U(){if(this.viewport){let e=this.viewport.getRotation();this.viewport.flipped?e+=this.rotationIncrement:e-=this.rotationIncrement;this.viewport.setRotation(e)}}function W(){if(this.viewport){let e=this.viewport.getRotation();this.viewport.flipped?e-=this.rotationIncrement:e+=this.rotationIncrement;this.viewport.setRotation(e)}}function j(){this.viewport.toggleFlip()}function G(e){if("string"==typeof e)return e;const t=e&&e.prototype;return t&&t instanceof OpenSeadragon.DrawerBase&&m.isFunction(t.getType)?t.getType.call(e):void 0}function V(){var e=window.matchMedia("(pointer: coarse)").matches;return/iPad|iPhone|iPod|Mac/.test(navigator.userAgent)&&e?["canvas"]:["webgl","canvas"]}m.determineDrawer=function(e){"auto"===e&&(e=V()[0]);for(const i in OpenSeadragon){var t=OpenSeadragon[i];const n=t.prototype;if(n&&n instanceof OpenSeadragon.DrawerBase&&m.isFunction(n.getType)&&n.getType.call(t)===e)return t}return null}}(OpenSeadragon);!function(h){h.Navigator=function(i){const e=i.viewer;const n=this;var t;if(i.element||i.id){if(i.element){i.id&&h.console.warn("Given option.id for Navigator was ignored since option.element was provided and is being used instead.");i.element.id?i.id=i.element.id:i.id="navigator-"+h.now();this.element=i.element}else this.element=document.getElementById(i.id);i.controlOptions={anchor:h.ControlAnchor.NONE,attachToViewer:!1,autoFade:!1}}else{i.id="navigator-"+h.now();this.element=h.makeNeutralElement("div");i.controlOptions={anchor:h.ControlAnchor.TOP_RIGHT,attachToViewer:!0,autoFade:i.autoFade};if(i.position)if("BOTTOM_RIGHT"===i.position)i.controlOptions.anchor=h.ControlAnchor.BOTTOM_RIGHT;else if("BOTTOM_LEFT"===i.position)i.controlOptions.anchor=h.ControlAnchor.BOTTOM_LEFT;else if("TOP_RIGHT"===i.position)i.controlOptions.anchor=h.ControlAnchor.TOP_RIGHT;else if("TOP_LEFT"===i.position)i.controlOptions.anchor=h.ControlAnchor.TOP_LEFT;else if("ABSOLUTE"===i.position){i.controlOptions.anchor=h.ControlAnchor.ABSOLUTE;i.controlOptions.top=i.top;i.controlOptions.left=i.left;i.controlOptions.height=i.height;i.controlOptions.width=i.width}}this.element.id=i.id;this.element.className+=" navigator";(i=h.extend(!0,{sizeRatio:h.DEFAULT_SETTINGS.navigatorSizeRatio},i,{element:this.element,tabIndex:-1,showNavigator:!1,mouseNavEnabled:!1,showNavigationControl:!1,showSequenceControl:!1,immediateRender:!0,blendTime:0,animationTime:i.animationTime,autoResize:!1,minZoomImageRatio:1,background:i.background,opacity:i.opacity,borderColor:i.borderColor,displayRegionColor:i.displayRegionColor})).minPixelRatio=this.minPixelRatio=e.minPixelRatio;h.setElementTouchActionNone(this.element);this.borderWidth=2;this.fudge=new h.Point(1,1);this.totalBorderWidths=new h.Point(2*this.borderWidth,2*this.borderWidth).minus(this.fudge);i.controlOptions.anchor!==h.ControlAnchor.NONE&&function(e,t){e.margin="0px";e.border=t+"px solid "+i.borderColor;e.padding="0px";e.background=i.background;e.opacity=i.opacity;e.overflow="hidden"}(this.element.style,this.borderWidth);this.displayRegion=h.makeNeutralElement("div");this.displayRegion.id=this.element.id+"-displayregion";this.displayRegion.className="displayregion";!function(e,t){e.position="relative";e.top="0px";e.left="0px";e.fontSize="0px";e.overflow="hidden";e.border=t+"px solid "+i.displayRegionColor;e.margin="0px";e.padding="0px";e.background="transparent";e.float="left";e.cssFloat="left";e.zIndex=999999999;e.cursor="default";e.boxSizing="content-box"}(this.displayRegion.style,this.borderWidth);h.setElementPointerEventsNone(this.displayRegion);h.setElementTouchActionNone(this.displayRegion);this.displayRegionContainer=h.makeNeutralElement("div");this.displayRegionContainer.id=this.element.id+"-displayregioncontainer";this.displayRegionContainer.className="displayregioncontainer";this.displayRegionContainer.style.width="100%";this.displayRegionContainer.style.height="100%";h.setElementPointerEventsNone(this.displayRegionContainer);h.setElementTouchActionNone(this.displayRegionContainer);e.addControl(this.element,i.controlOptions);this._resizeWithViewer=i.controlOptions.anchor!==h.ControlAnchor.ABSOLUTE&&i.controlOptions.anchor!==h.ControlAnchor.NONE;if(i.width&&i.height){this.setWidth(i.width);this.setHeight(i.height)}else if(this._resizeWithViewer){t=h.getElementSize(e.element);this.element.style.height=Math.round(t.y*i.sizeRatio)+"px";this.element.style.width=Math.round(t.x*i.sizeRatio)+"px";this.oldViewerSize=t;t=h.getElementSize(this.element);this.elementArea=t.x*t.y}this.oldContainerSize=new h.Point(0,0);h.Viewer.apply(this,[i]);this.displayRegionContainer.appendChild(this.displayRegion);this.element.getElementsByTagName("div")[0].appendChild(this.displayRegionContainer);function r(e,t){c(n.displayRegionContainer,e);c(n.displayRegion,-e);n.viewport.setRotation(e,t)}if(i.navigatorRotate){r(i.viewer.viewport?i.viewer.viewport.getRotation():i.viewer.degrees||0,!0);i.viewer.addHandler("rotate",function(e){r(e.degrees,e.immediately)})}this.innerTracker.destroy();this.innerTracker=new h.MouseTracker({userData:"Navigator.innerTracker",element:this.element,dragHandler:h.delegate(this,s),clickHandler:h.delegate(this,o),releaseHandler:h.delegate(this,a),scrollHandler:h.delegate(this,l),preProcessEventHandler:function(e){"wheel"===e.eventType&&(e.preventDefault=!0)}});this.outerTracker.userData="Navigator.outerTracker";h.setElementPointerEventsNone(this.canvas);h.setElementPointerEventsNone(this.container);this.addHandler("reset-size",function(){n.viewport&&n.viewport.goHome(!0)});e.world.addHandler("item-index-change",function(t){window.setTimeout(function(){var e=n.world.getItemAt(t.previousIndex);n.world.setItemIndex(e,t.newIndex)},1)});e.world.addHandler("remove-item",function(e){e=e.item;e=n._getMatchingItem(e);e&&n.world.removeItem(e)});this.update(e.viewport)};h.extend(h.Navigator.prototype,h.EventSource.prototype,h.Viewer.prototype,{updateSize:function(){if(this.viewport){const e=new h.Point(0===this.container.clientWidth?1:this.container.clientWidth,0===this.container.clientHeight?1:this.container.clientHeight);if(!e.equals(this.oldContainerSize)){this.viewport.resize(e,!0);this.viewport.goHome(!0);this.oldContainerSize=e;this.world.update();this.world.draw();this.update(this.viewer.viewport)}}},setWidth:function(e){this.width=e;this.element.style.width="number"==typeof e?e+"px":e;this._resizeWithViewer=!1;this.updateSize()},setHeight:function(e){this.height=e;this.element.style.height="number"==typeof e?e+"px":e;this._resizeWithViewer=!1;this.updateSize()},setFlip:function(e){this.viewport.setFlip(e);this.setDisplayTransform(this.viewer.viewport.getFlip()?"scale(-1,1)":"scale(1,1)");return this},setDisplayTransform:function(e){i(this.canvas,e);i(this.element,e)},update:function(e){let t;let i;let n;let r;let o;e=e||this.viewer.viewport;t=h.getElementSize(this.viewer.element);if(this._resizeWithViewer&&t.x&&t.y&&!t.equals(this.oldViewerSize)){this.oldViewerSize=t;if(this.maintainSizeRatio||!this.elementArea){i=t.x*this.sizeRatio;n=t.y*this.sizeRatio}else{i=Math.sqrt(this.elementArea*(t.x/t.y));n=this.elementArea/i}this.element.style.width=Math.round(i)+"px";this.element.style.height=Math.round(n)+"px";this.elementArea||(this.elementArea=i*n);this.updateSize()}if(e&&this.viewport){r=e.getBoundsNoRotate(!0);o=this.viewport.pixelFromPointNoRotate(r.getTopLeft(),!1);a=this.viewport.pixelFromPointNoRotate(r.getBottomRight(),!1).minus(this.totalBorderWidths);if(!this.navigatorRotate){var s=e.getRotation(!0);c(this.displayRegion,-s)}const l=this.displayRegion.style;l.display=this.world.getItemCount()?"block":"none";l.top=o.y.toFixed(2)+"px";l.left=o.x.toFixed(2)+"px";s=a.x-o.x;var a=a.y-o.y;l.width=Math.round(Math.max(s,0))+"px";l.height=Math.round(Math.max(a,0))+"px"}},addTiledImage:function(e){const n=this;const r=e.originalTiledImage;delete e.original;e=h.extend({},e,{success:function(e){const t=e.item;t._originalForNavigator=r;n._matchBounds(t,r,!0);n._matchOpacity(t,r);n._matchCompositeOperation(t,r);function i(){n._matchBounds(t,r)}r.addHandler("bounds-change",i);r.addHandler("clip-change",i);r.addHandler("opacity-change",function(){n._matchOpacity(t,r)});r.addHandler("composite-operation-change",function(){n._matchCompositeOperation(t,r)})}});return h.Viewer.prototype.addTiledImage.apply(this,[e])},destroy:function(){return h.Viewer.prototype.destroy.apply(this)},_getMatchingItem:function(t){var i=this.world.getItemCount();for(let e=0;e{const t=e.tileSource;this.ready=!0;this.aspectRatio=t.width&&t.height?t.width/t.height:1;this.dimensions=new c.Point(t.width,t.height);if(t.tileSize){this._tileWidth=this._tileHeight=t.tileSize;delete this.tileSize}else{if(t.tileWidth){this._tileWidth=t.tileWidth;delete this.tileWidth}else this._tileWidth=0;if(t.tileHeight){this._tileHeight=t.tileHeight;delete this.tileHeight}else this._tileHeight=0}this.tileOverlap=t.tileOverlap||0;this.minLevel=t.minLevel||0;this.maxLevel=void 0!==t.maxLevel&&null!==t.maxLevel?t.maxLevel:t.width&&t.height?Math.ceil(Math.log(Math.max(t.width,t.height))/Math.log(2)):0;t.success&&c.isFunction(t.success)&&t.success(this)},null,1/0);if("string"===c.type(e)){this.url=e;e=void 0}else c.extend(!0,this,e);if(this.url&&!this.ready){this.aspectRatio=1;this.dimensions=new c.Point(10,10);this._tileWidth=0;this._tileHeight=0;this.tileOverlap=0;this.minLevel=0;this.maxLevel=0;this.ready=!1;this._uniqueIdentifier=this.url;setTimeout(()=>this.getImageInfo(this.url))}else{this._uniqueIdentifier=Math.floor(1e10*Math.random()).toString(36);this.ready||void 0===this.ready?this.raiseEvent("ready",{tileSource:this}):setTimeout(()=>this.raiseEvent("ready",{tileSource:this}))}return this};c.TileSource.prototype={getTileSize:function(e){c.console.error("[TileSource.getTileSize] is deprecated. Use TileSource.getTileWidth() and TileSource.getTileHeight() instead");return this._tileWidth},getTileWidth:function(e){return this._tileWidth||this.getTileSize(e)},getTileHeight:function(e){return this._tileHeight||this.getTileSize(e)},setMaxLevel:function(e){this.maxLevel=e;this._memoizeLevelScale()},getLevelScale:function(e){this._memoizeLevelScale();return this.getLevelScale(e)},_memoizeLevelScale:function(){const t={};let e;for(e=0;e<=this.maxLevel;e++)t[e]=1/Math.pow(2,this.maxLevel-e);this.getLevelScale=function(e){return t[e]}},getNumTiles:function(e){var t=this.getLevelScale(e);var i=Math.ceil(t*this.dimensions.x/this.getTileWidth(e));e=Math.ceil(t*this.dimensions.y/this.getTileHeight(e));return new c.Point(i,e)},getPixelRatio:function(e){var t=this.dimensions.times(this.getLevelScale(e));e=1/t.x*c.pixelDensityRatio;t=1/t.y*c.pixelDensityRatio;return new c.Point(e,t)},getClosestLevel:function(){let e;var t;for(e=this.minLevel+1;e<=this.maxLevel&&!(1<(t=this.getNumTiles(e)).x||1=1/this.aspectRatio-1e-15&&(o=this.getNumTiles(e).y-1);return new c.Point(r,o)},getTileBounds:function(e,t,i,n){var r=this.dimensions.times(this.getLevelScale(e));var o=this.getTileWidth(e);var s=this.getTileHeight(e);var a=0===t?0:o*t-this.tileOverlap;e=0===i?0:s*i-this.tileOverlap;t=o+(0===t?1:2)*this.tileOverlap;s+=(0===i?1:2)*this.tileOverlap;i=1/r.x;t=Math.min(t,r.x-a);s=Math.min(s,r.y-e);return n?new c.Rect(0,0,t,s):new c.Rect(a*i,e*i,t*i,s*i)},getImageInfo:function(r){const o=this;let t;let i;let n;let e;let s;var a;if(r){e=r.split("/");s=e[e.length-1];-1<(a=s.lastIndexOf("."))&&(e[e.length-1]=s.slice(0,a))}let l=null;if(this.splitHashDataForPost){var h=r.indexOf("#");if(-1!==h){l=r.substring(h+1);r=r.substr(0,h)}}t=function(e){"string"==typeof e&&(e=c.parseXml(e));const t=c.TileSource.determineType(o,e,r);if(t){n=t.prototype.configure.apply(o,[e,r,l]);void 0===n.ajaxWithCredentials&&(n.ajaxWithCredentials=o.ajaxWithCredentials);n.ready=!0;i=new t(n);o.ready=!0;o.raiseEvent("ready",{tileSource:i})}else o.raiseEvent("open-failed",{message:"Unable to load TileSource",source:r})};if(r.match(/\.js$/)){h=r.split("/").pop().replace(".js","");c.jsonp({url:r,async:!1,callbackName:h,callback:t})}else c.makeAjaxRequest({url:r,postData:l,withCredentials:this.ajaxWithCredentials,headers:this.ajaxHeaders,success:function(e){e=function(t){const i=t.responseText;var e=t.status;var n;let r;{if(!t)throw new Error(c.getString("Errors.Security"));if(200!==t.status&&0!==t.status){e=t.status;n=404===e?"Not Found":t.statusText;throw new Error(c.getString("Errors.Status",e,n))}}if(i.match(/^\s*<.*/))try{r=t.responseXML&&t.responseXML.documentElement?t.responseXML:c.parseXml(i)}catch(e){r=t.responseText}else if(i.match(/\s*[{[].*/))try{r=c.parseJSON(i)}catch(e){r=i}else r=i;return r}(e);t(e)},error:function(e,i){let n;try{n="HTTP "+e.status+" attempting to load TileSource: "+r}catch(e){let t;t=void 0!==i&&i.toString?i.toString():"Unknown error";n=t+" attempting to load TileSource: "+r}c.console.error(n);o.raiseEvent("open-failed",{message:n,source:r,postData:l})}})},supports:function(e,t){return!1},equals:function(e){return this===e},batchEnabled(){return!1},batchCompatible(e){return!1},batchMaxJobs(){return-1},batchTimeout(){return 5},configure:function(e,t,i){throw new Error("Method not implemented.")},destroy:function(e){},getTileUrl:function(e,t,i){throw new Error("Method not implemented.")},getTilePostData:function(e,t,i){return null},getTileAjaxHeaders:function(e,t,i){return{}},getTileHashKey:function(e,t,i,n,r,o){function s(e){return r?e+"+"+JSON.stringify(r):e}return s("string"!=typeof n?this._uniqueIdentifier+":"+e+"/"+t+"_"+i:n)},tileExists:function(e,t,i){var n=this.getNumTiles(e);return e>=this.minLevel&&e<=this.maxLevel&&0<=t&&0<=i&&tthis.maxLevel)return!1;if(!r||!r.length)return!0;for(let e=r.length-1;0<=e;e--){var h=r[e];if(!(th.maxLevel)){l=this.getLevelScale(t);o=h.x*l;s=h.y*l;a=o+h.width*l;l=s+h.height*l;o=Math.floor(o/this._tileWidth);s=Math.floor(s/this._tileWidth);a=Math.ceil(a/this._tileWidth);l=Math.ceil(l/this._tileWidth);if(o<=i&&ie.width-t.width);if(a[i-1].width=this.minLevel&&t<=this.maxLevel&&(e=this.levels[t].width/this.levels[this.maxLevel].width);return e}return l.TileSource.prototype.getLevelScale.call(this,t)},getNumTiles:function(e){if(this.emulateLegacyImagePyramid)return this.getLevelScale(e)?new l.Point(1,1):new l.Point(0,0);if(this.levelSizes){var t=this.levelSizes[e];var i=Math.ceil(t.width/this.getTileWidth(e));t=Math.ceil(t.height/this.getTileHeight(e));return new l.Point(i,t)}return l.TileSource.prototype.getNumTiles.call(this,e)},getTileAtPoint:function(i,n){if(this.emulateLegacyImagePyramid)return new l.Point(0,0);if(this.levelSizes){var r=0<=n.x&&n.x<=1&&0<=n.y&&n.y<=1/this.aspectRatio;l.console.assert(r,"[TileSource.getTileAtPoint] must be called with a valid point.");var o=this.levelSizes[i].width;r=n.x*o;o=n.y*o;let e=Math.floor(r/this.getTileWidth(i));let t=Math.floor(o/this.getTileHeight(i));1<=n.x&&(e=this.getNumTiles(i).x-1);n.y>=1/this.aspectRatio-1e-15&&(t=this.getNumTiles(i).y-1);return new l.Point(e,t)}return l.TileSource.prototype.getTileAtPoint.call(this,i,n)},getTileUrl:function(t,e,i){if(this.emulateLegacyImagePyramid){let e=null;0=this.minLevel&&t<=this.maxLevel&&(e=this.levels[t].url);return e}var n=Math.pow(.5,this.maxLevel-t);let r;let o;let s;let a;let l;let h;let c;let u;var d;var p;let g;if(this.levelSizes){r=this.levelSizes[t].width;o=this.levelSizes[t].height}else{r=Math.ceil(this.width*n);o=Math.ceil(this.height*n)}d=this.getTileWidth(t);p=this.getTileHeight(t);t=Math.round(d/n);n=Math.round(p/n);g=1===this.version?"native."+this.tileFormat:"default."+this.tileFormat;if(r({width:Math.ceil(e.x_tiles*this._tileWidth),height:Math.ceil(e.y_tiles*this._tileHeight),xTiles:Math.ceil(e.x_tiles),yTiles:Math.ceil(e.y_tiles)}));this.levelScales=t.map(e=>e.scale/n);this.minLevel=0;this.maxLevel=Math.ceil(this.levelSizes.length-1)},getImageInfo:function(n){const r=this;o.makeAjaxRequest({url:n,type:"GET",async:!0,success:function(e){try{var t=JSON.parse(e.responseText);r.parseMetadata(t);r.ready=!0;r.raiseEvent("ready",{tileSource:r})}catch(e){t="IrisTileSource: Error parsing metadata: "+e.message;o.console.error(t);r.raiseEvent("open-failed",{message:t,source:n})}},error:function(e,t){var i="IrisTileSource: Unable to get metadata from "+n;o.console.error(i);r.raiseEvent("open-failed",{message:i,source:n})}})},getNumTiles:function(e){return ethis.maxLevel||!this.levelSizes[e]?new o.Point(0,0):new o.Point(Math.ceil(this.levelSizes[e].xTiles),Math.ceil(this.levelSizes[e].yTiles))},getTileUrl:function(e,t,i){t=i*this.levelSizes[e].xTiles+t;return`${this.serverUrl}/slides/${this.slideId}/layers/${e}/tiles/`+t},getLevelScale:function(e){return this.levelScales[e]},configure:function(e){return e}});o.extend(!0,o.IrisTileSource.prototype,o.EventSource.prototype)}(OpenSeadragon);!function(s){s.OsmTileSource=function(e,t,i,n,r){let o;o=s.isPlainObject(e)?e:{width:e,height:t,tileSize:i,tileOverlap:n,tilesUrl:r};if(!o.width||!o.height){o.width=67108864;o.height=67108864}if(!o.tileSize){o.tileSize=256;o.tileOverlap=0}o.tilesUrl||(o.tilesUrl="http://tile.openstreetmap.org/");o.minLevel=8;s.TileSource.apply(this,[o])};s.extend(s.OsmTileSource.prototype,s.TileSource.prototype,{supports:function(e,t){return e.type&&"openstreetmaps"===e.type},configure:function(e,t,i){return e},getTileUrl:function(e,t,i){return this.tilesUrl+(e-8)+"/"+t+"/"+i+".png"},equals:function(e){return e&&this.tilesUrl===e.tilesUrl}})}(OpenSeadragon);!function(h){h.TmsTileSource=function(e,t,i,n,r){let o;o=h.isPlainObject(e)?e:{width:e,height:t,tileSize:i,tileOverlap:n,tilesUrl:r};var s=256*Math.ceil(o.width/256);var a=256*Math.ceil(o.height/256);let l;l=ae.tileSize||parseInt(t.y,10)>e.tileSize;){t.x=Math.floor(t.x/2);t.y=Math.floor(t.y/2);e.imageSizes.push({x:t.x,y:t.y});e.gridSize.push(this._getGridSize(t.x,t.y,e.tileSize))}e.imageSizes.reverse();e.gridSize.reverse();e.minLevel=0;e.maxLevel=e.gridSize.length-1;i.TileSource.apply(this,[e])};i.extend(i.ZoomifyTileSource.prototype,i.TileSource.prototype,{_getGridSize:function(e,t,i){return{x:Math.ceil(e/i),y:Math.ceil(t/i)}},_calculateAbsoluteTileNumber:function(t,e,i){let n=0;let r={};for(let e=0;e");return i.sort(function(e,t){return e.height-t.height})}(t.levels);if(0=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t},getNumTiles:function(e){return this.getLevelScale(e)?new s.Point(1,1):new s.Point(0,0)},getTileUrl:function(e,t,i){let n=null;0=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n},equals:function(t){if(!t||!t.levels||t.levels.length!==this.levels.length)return!1;for(let e=this.minLevel;e<=this.maxLevel;e++)if(this.levels[e].url!==t.levels[e].url)return!1;return!0}})}(OpenSeadragon);!function(r){r.ImageTileSource=class extends r.TileSource{constructor(e){super(r.extend({buildPyramid:!0,crossOriginPolicy:!1,ajaxWithCredentials:!1},e))}supports(e,t){return e.type&&"image"===e.type}configure(e,t,i){return e}getImageInfo(e){const t=new Image,i=this;this.crossOriginPolicy&&(t.crossOrigin=this.crossOriginPolicy);r.addEvent(t,"load",function(){i.width=t.naturalWidth;i.height=t.naturalHeight;i.tileWidth=i.width;i.tileHeight=i.height;i.tileOverlap=0;i.minLevel=0;i.image=t;i.levels=i._buildLevels(t);i.maxLevel=i.levels.length-1;i.raiseEvent("ready",{tileSource:i})});r.addEvent(t,"error",function(){i.image=null;i.raiseEvent("open-failed",{message:"Error loading image at "+e,source:e})});t.src=e}getLevelScale(e){let t=NaN;e>=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t}getNumTiles(e){return this.getLevelScale(e)?new r.Point(1,1):new r.Point(0,0)}getTileUrl(e,t,i){return e===this.maxLevel?this.url:this.url+`?l=${e}&x=${t}&y=`+i}equals(e){return this.url===e.url}getTilePostData(e,t,i){return{level:e,x:t,y:i}}getContext2D(e,t,i){r.console.error("Using [TiledImage.getContext2D] (for plain images only) is deprecated. Use overridden downloadTileStart (https://openseadragon.github.io/examples/advanced-data-model/) instead.");return this._createContext2D()}downloadTileStart(e){var t=e.postData;if(t.level!==this.maxLevel)if(t.level>=this.minLevel&&t.level<=this.maxLevel){var i=this.levels[t.level];i=this._createContext2D(this.image,i.width,i.height);e.finish(i,null,"context2d")}else e.fail(`Invalid level ${t.level} for plain image source. Did you forget to set buildPyramid=true?`);else e.finish(this.image,null,"image")}downloadTileAbort(e){}_buildLevels(e){const t=[{url:e.src,width:e.naturalWidth,height:e.naturalHeight}];if(!this.buildPyramid||!r.supportsCanvas||!this.useCanvas)return t;let i=e.naturalWidth,n=e.naturalHeight;for(;2<=i&&2<=n;){i=Math.floor(i/2);n=Math.floor(n/2);t.push({width:i,height:n})}return t.reverse()}_createContext2D(e,t,i){const n=document.createElement("canvas"),r=n.getContext("2d");n.width=t;n.height=i;r.drawImage(e,0,0,t,i);return r}}}(OpenSeadragon);!function(r){r.TileSourceCollection=function(e,t,i,n){r.console.error("TileSourceCollection is deprecated; use World instead")}}(OpenSeadragon);!function(o){const e=o;e.PriorityQueue=class{constructor(e=void 0){this.nodes_=[];e&&this.insertAll(e)}insert(e,t){this.insertNode(new Node(e,t))}insertNode(e){const t=this.nodes_;e.index=t.length;t.push(e);this.moveUp_(e.index)}insertAll(e){let t,i;if(!(e instanceof o.PriorityQueue))throw"insertAll supports only OpenSeadragon.PriorityQueue object!";t=e.getKeys();i=e.getValues();if(this.getCount()<=0){const n=this.nodes_;for(let e=0;e>1;){var r=this.getLeftChildIndex_(e);var o=this.getRightChildIndex_(e);r=on.key)break;t[e]=t[r];t[e].index=e;e=r}t[e]=n;n&&(n.index=e)}moveUp_(e){const t=this.nodes_;const i=t[e];for(;0i.key))break;t[e]=t[n];t[e].index=e;e=n}t[e]=i;i&&(i.index=e)}getLeftChildIndex_(e){return 2*e+1}getRightChildIndex_(e){return 2*e+2}getParentIndex_(e){return e-1>>1}getValues(){return this.nodes_.map(e=>e.value)}getKeys(){return this.nodes_.map(e=>e.key)}containsValue(t){return this.nodes_.some(e=>e.value==t)}containsKey(t){return this.nodes_.some(e=>e.value==t)}clone(){return new o.PriorityQueue(this)}getCount(){return this.nodes_.length}isEmpty(){return 0===this.nodes_.length}clear(){this.nodes_.length=0}};e.PriorityQueue.Node=class Node{constructor(e,t){this.key=e;this.value=t;this.index=0}clone(){return new Node(this.key,this.value)}}}(OpenSeadragon);!function(u){const m=u;class e{constructor(){this.adjacencyList={};this.vertices={}}addVertex(e){if(this.vertices[e])return!1;this.vertices[e]=new u.PriorityQueue.Node(0,e);this.adjacencyList[e]=[];return!0}addEdge(e,t,i,n){i<0&&u.console.error("WeightedGraph: negative weights will make for invalid shortest path computation!");const r=this.adjacencyList[e],o=r.findIndex(e=>e.target===this.vertices[t]),s={target:this.vertices[t],origin:this.vertices[e],weight:i,transform:n};if(o<0){this.adjacencyList[e].push(s);return!0}this.adjacencyList[e][o]=s;return!1}dijkstra(e,t){const i=[];if(e===t)return{path:i,cost:0};const n=new m.PriorityQueue;let r;for(var o in this.vertices){o=this.vertices[o];if(o.value===e){o.key=0;n.insertNode(o)}else{o.key=1/0;delete o.index}o._previous=null}for(;0e.target.value===d));r=p}return{path:i.reverse(),cost:h}}}}let c;let t=0;const d=new Map;let p=!1;const g="undefined"!=typeof SharedArrayBuffer&&!0===self.crossOriginIsolated;function s(o,s,{timeoutMs:a=15e3}={}){const l=function(){if(c)return c;var e=URL.createObjectURL(new Blob([` +self.onmessage = async (e) => { + const { id, op, } = e.data; + let error; + try { + if (op === 'decodeFromBlob') { + const bmp = await createImageBitmap(e.data.blob, { colorSpaceConversion: 'none' }); + postMessage({ id, ok: true, bmp }, [bmp]); + return; + } + if (op === 'decodeFromBytes') { + const u8 = new Uint8Array(e.data.bytes); + const b = new Blob([u8], { type: e.data.mime || '' }); + const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' }); + postMessage({ id, ok: true, bmp }, [bmp]); + return; + } + if (op === 'fetchDecode') { + const res = await fetch(e.data.url, e.data.setup); + if (!res.ok) throw new Error('HTTP ' + res.status); + const b = await res.blob(); + const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' }); + postMessage({ id, ok: true, bmp }, [bmp]); + return; + } + error = 'Unknown op: ' + op; + } catch (err) { + error = String(err && err.message || err); + } + postMessage({ id, ok: false, err: error }); +}; +`],{type:"text/javascript"}));c=new Worker(e);c.onmessage=e=>{var{id:t,ok:i,bmp:n,err:e}=e.data||{};const r=d.get(t);if(r){d.delete(t);if(r.timer){clearTimeout(r.timer);r.timer=null}i?r.resolve(n):r.reject(new Error(e))}};c.onerror=e=>{for(var[,t]of d){if(t.timer){clearTimeout(t.timer);t.timer=null}t.reject(new Error("Worker error"))}d.clear()};return c}();const h=++t;return new u.Promise((e,t)=>{s.id=h;s.op=o;const i={resolve:e,reject:t,timer:null};0{i.timer=null;d.delete(h);t(new Error(`Worker timeout (${o})`))},a));d.set(h,i);if("decodeFromBytes"!==o)l.postMessage(s);else if(g){e=s.u8;var n=new SharedArrayBuffer(e.byteLength);new Uint8Array(n).set(e);l.postMessage({id:h,op:o,bytes:n,mime:s.mime})}else{if(!p){p=!0;console.warn("[Converter] SharedArrayBuffer unavailable; falling back to ArrayBuffer.")}const r=s.u8;n=0===r.byteOffset&&r.byteLength===r.buffer.byteLength?r:r.slice();l.postMessage({id:h,op:o,bytes:n.buffer,mime:s.mime},[n.buffer])}})}m.DataTypeConverter=class{constructor(){this.graph=new e;this.destructors={};this.copyings={};const i=(e,t)=>{const i=document.createElement("canvas");i.width=t.width;i.height=t.height;const n=i.getContext("2d",{willReadFrequently:!0});n.drawImage(t,0,0);return n};this.learn("rasterBlob","image",(e,r)=>new u.Promise((e,t)=>{var i=(window.URL||window.webkitURL).createObjectURL(r);if(!u.supportsAsync)return t("Not supported in sync mode!");const n=new Image;n.onerror=n.onabort=e=>{(window.URL||window.webkitURL).revokeObjectURL(r);t(e)};n.onload=()=>{(window.URL||window.webkitURL).revokeObjectURL(r);e(n)};n.decoding="async";n.src=i}),1,2);this.learn("context2d","rasterBlob",(e,i)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");i.canvas.toBlob(e)}),1,2);this.learn("rasterBlob","imageBitmap",(e,i)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");(c?s("decodeFromBlob",{blob:i}):createImageBitmap(i,{colorSpaceConversion:"none"})).then(e).catch(t)}),1,1);this.learn("imageBitmap","context2d",(e,t)=>{const i=document.createElement("canvas");i.width=t.width;i.height=t.height;const n=i.getContext("2d",{willReadFrequently:!0});n.drawImage(t,0,0);return n},1,2);this.learn("image","imageBitmap",(e,t)=>createImageBitmap(t,{colorSpaceConversion:"none"}),1,2);this.learn("image","context2d",i,1,2);this.learn("image","image",(e,t)=>((n,r)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");const i=new Image;i.onerror=i.onabort=e=>t("Failed to load image: "+r);i.onload=()=>e(i);n.tiledImage&&n.tiledImage.crossOriginPolicy&&(i.crossOrigin=n.tiledImage.crossOriginPolicy);i.src=r}))(e,t.src),1,1);this.learn("context2d","context2d",(e,t)=>i(0,t.canvas));this.learn("rasterBlob","rasterBlob",(e,t)=>t,0,1);this.learn("imageBitmap","imageBitmap",(e,r)=>new u.Promise((e,t)=>{try{if(!u.supportsAsync)return t("Not supported in sync mode!");if(!r)return t(new Error("No ImageBitmap to copy"));if("undefined"!=typeof OffscreenCanvas&&r.width&&r.height){const i=new OffscreenCanvas(r.width,r.height);const n=i.getContext("2d",{willReadFrequently:!1});n.drawImage(r,0,0);return"function"!=typeof i.transferToImageBitmap?createImageBitmap(i,{colorSpaceConversion:"none"}).then(e):e(i.transferToImageBitmap())}return createImageBitmap(r,{colorSpaceConversion:"none"}).then(e)}catch(e){return t(e)}}),1,1);this.learnDestroy("context2d",e=>{e.canvas.width=0;e.canvas.height=0})}guessType(e){if(Array.isArray(e)){const n=[];for(const r of e)if(void 0!==r&&null!==r){var t=this.guessType(r);n.includes(t)||n.push(t)}n.sort();return`Array [${n.join(",")}]`}const i=u.type(e);return"dom-node"===i?i.nodeName.toLowerCase():"object"===i&&u.isFunction(e.getType)?e.getType():i}learn(e,t,i,n=0,r=1){u.console.assert(0<=n&&n<=7,"[DataTypeConverter] Conversion costPower must be between <0, 7>.");u.console.assert(u.isFunction(i),"[DataTypeConverter:learn] Callback must be a valid function!");if(e===t)this.copyings[t]=i;else{n++;r=Math.min(Math.max(r,1),15);this.graph.addVertex(e);this.graph.addVertex(t);this.graph.addEdge(e,t,10*n^5+r,i);this._known={}}}learnDestroy(e,t){this.destructors[e]=t}convert(s,e,t,...i){const a=this.getConversionPath(t,i);if(!a){u.console.error(`[OpenSeadragon.converter.convert] Conversion ${t} ---> ${i} cannot be done!`);return u.Promise.resolve()}const l=a.length;const h=this;const c=(t,i,n=!0)=>{if(i>=l)return u.Promise.resolve(t);const r=a[i];let e;try{e=r.transform(s,t)}catch(e){n&&h.destroy(t,r.origin.value);return u.Promise.reject(`[OpenSeadragon.converter.convert] sync failure (while converting using ${r.origin.value} -> ${r.target.value})`)}if(void 0===e){n&&h.destroy(t,r.origin.value);return u.Promise.reject(`[OpenSeadragon.converter.convert] data mid result undefined value (while converting using ${r.origin.value} -> ${r.target.value})`)}n&&h.destroy(t,r.origin.value);const o="promise"===u.type(e)?e:u.Promise.resolve(e);return o.then(e=>c(e,i+1))};return c(e,0,!1)}copy(e,t,i){const n=this.copyings[i];if(n){t=n(e,t);return"promise"===u.type(t)?t:u.Promise.resolve(t)}u.console.warn("[OpenSeadragon.converter.copy] is not supported with type %s",i);return u.Promise.resolve(void 0)}destroy(e,t){const i=this.destructors[t];if(i){e=i(e);return"promise"===u.type(e)?e:u.Promise.resolve(e)}}getConversionPath(i,e){let n;let r=this._known[i];r||(this._known[i]=r={});if(Array.isArray(e)){u.console.assert(0e.cost){n=e;t=e.cost}}}else{u.console.assert("string"==typeof e,"[getConversionPath] conversion 'to' type must be defined.");n=r[e];if(void 0===n){n=this.graph.dijkstra(i,e);this._known[i][e]=n}}return n?n.path:void 0}getConversionPathFinalType(e){if(e&&e.length)return e[e.length-1].target.value}getKnownTypes(){return Object.keys(this.graph.vertices)}existsType(e){return!!this.graph.vertices[e]}};u.converter=new u.DataTypeConverter;u.converter.learn("__private__imageUrl","imageBitmap",(r,o)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");let i;if(r.tiledImage&&r.tiledImage.crossOriginPolicy){var n=r.tiledImage.crossOriginPolicy;"anonymous"===n?i={mode:"cors",credentials:"omit"}:"use-credentials"===n?i={mode:"cors",credentials:"include"}:n&&u.console.error(`Unsupported crossOriginPolicy ${n}. Ignoring the property.`)}return(c?s("fetchDecode",{url:o,setup:i}):fetch(o,i).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status} loading `+o);return e.blob()}).then(e=>createImageBitmap(e,{colorSpaceConversion:"none"}))).then(e).catch(t)}),1,1);u.converter.learn("__private__imageUrl","__private__imageUrl",(e,t)=>t,0,1)}(OpenSeadragon);!function(i){i.ButtonState={REST:0,GROUP:1,HOVER:2,DOWN:3};i.Button=function(e){const t=this;i.EventSource.call(this);i.extend(!0,this,{tooltip:null,srcRest:null,srcGroup:null,srcHover:null,srcDown:null,clickTimeThreshold:i.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:i.DEFAULT_SETTINGS.clickDistThreshold,fadeDelay:0,fadeLength:2e3,onPress:null,onRelease:null,onClick:null,onEnter:null,onExit:null,onFocus:null,onBlur:null,userData:null},e);this.element=e.element||i.makeNeutralElement("div");if(!e.element){this.imgRest=i.makeTransparentImage(this.srcRest);this.imgGroup=i.makeTransparentImage(this.srcGroup);this.imgHover=i.makeTransparentImage(this.srcHover);this.imgDown=i.makeTransparentImage(this.srcDown);this.imgRest.alt=this.imgGroup.alt=this.imgHover.alt=this.imgDown.alt=this.tooltip;i.setElementPointerEventsNone(this.imgRest);i.setElementPointerEventsNone(this.imgGroup);i.setElementPointerEventsNone(this.imgHover);i.setElementPointerEventsNone(this.imgDown);this.element.style.position="relative";i.setElementTouchActionNone(this.element);this.imgGroup.style.position=this.imgHover.style.position=this.imgDown.style.position="absolute";this.imgGroup.style.top=this.imgHover.style.top=this.imgDown.style.top="0px";this.imgGroup.style.left=this.imgHover.style.left=this.imgDown.style.left="0px";this.imgHover.style.visibility=this.imgDown.style.visibility="hidden";this.element.appendChild(this.imgRest);this.element.appendChild(this.imgGroup);this.element.appendChild(this.imgHover);this.element.appendChild(this.imgDown)}this.addHandler("press",this.onPress);this.addHandler("release",this.onRelease);this.addHandler("click",this.onClick);this.addHandler("enter",this.onEnter);this.addHandler("exit",this.onExit);this.addHandler("focus",this.onFocus);this.addHandler("blur",this.onBlur);this.currentState=i.ButtonState.GROUP;this.fadeBeginTime=null;this.shouldFade=!1;this.element.style.display="inline-block";this.element.style.position="relative";this.element.title=this.tooltip;this.tracker=new i.MouseTracker({userData:"Button.tracker",element:this.element,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,enterHandler:function(e){if(e.insideElementPressed){r(t,i.ButtonState.DOWN);t.raiseEvent("enter",{originalEvent:e.originalEvent})}else e.buttonDownAny||r(t,i.ButtonState.HOVER)},focusHandler:function(e){t.tracker.enterHandler(e);t.raiseEvent("focus",{originalEvent:e.originalEvent})},leaveHandler:function(e){o(t,i.ButtonState.GROUP);e.insideElementPressed&&t.raiseEvent("exit",{originalEvent:e.originalEvent})},blurHandler:function(e){t.tracker.leaveHandler(e);t.raiseEvent("blur",{originalEvent:e.originalEvent})},pressHandler:function(e){r(t,i.ButtonState.DOWN);t.raiseEvent("press",{originalEvent:e.originalEvent})},releaseHandler:function(e){if(e.insideElementPressed&&e.insideElementReleased){o(t,i.ButtonState.HOVER);t.raiseEvent("release",{originalEvent:e.originalEvent})}else e.insideElementPressed?o(t,i.ButtonState.GROUP):r(t,i.ButtonState.HOVER)},clickHandler:function(e){e.quick&&t.raiseEvent("click",{originalEvent:e.originalEvent})},keyHandler:function(e){if(13===e.keyCode){t.raiseEvent("click",{originalEvent:e.originalEvent});t.raiseEvent("release",{originalEvent:e.originalEvent});e.preventDefault=!0}else e.preventDefault=!1}});o(this,i.ButtonState.REST)};i.extend(i.Button.prototype,i.EventSource.prototype,{notifyGroupEnter:function(){r(this,i.ButtonState.GROUP)},notifyGroupExit:function(){o(this,i.ButtonState.REST)},disable:function(){this.notifyGroupExit();this.element.disabled=!0;this.tracker.setTracking(!1);i.setElementOpacity(this.element,.2,!0)},enable:function(){this.element.disabled=!1;this.tracker.setTracking(!0);i.setElementOpacity(this.element,1,!0);this.notifyGroupEnter()},destroy:function(){if(this.imgRest){this.element.removeChild(this.imgRest);this.imgRest=null}if(this.imgGroup){this.element.removeChild(this.imgGroup);this.imgGroup=null}if(this.imgHover){this.element.removeChild(this.imgHover);this.imgHover=null}if(this.imgDown){this.element.removeChild(this.imgDown);this.imgDown=null}this.removeAllHandlers();this.tracker.destroy();this.element=null}});function n(e){i.requestAnimationFrame(function(){!function(e){var t;if(e.shouldFade){t=i.now();t=t-e.fadeBeginTime;t=1-t/e.fadeLength;t=Math.min(1,t);t=Math.max(0,t);e.imgGroup&&i.setElementOpacity(e.imgGroup,t,!0);0=i.ButtonState.GROUP&&e.currentState===i.ButtonState.REST){!function(e){e.shouldFade=!1;e.imgGroup&&i.setElementOpacity(e.imgGroup,1,!0)}(e);e.currentState=i.ButtonState.GROUP}if(t>=i.ButtonState.HOVER&&e.currentState===i.ButtonState.GROUP){e.imgHover&&(e.imgHover.style.visibility="");e.currentState=i.ButtonState.HOVER}if(t>=i.ButtonState.DOWN&&e.currentState===i.ButtonState.HOVER){e.imgDown&&(e.imgDown.style.visibility="");e.currentState=i.ButtonState.DOWN}}}function o(e,t){if(!e.element.disabled){if(t<=i.ButtonState.HOVER&&e.currentState===i.ButtonState.DOWN){e.imgDown&&(e.imgDown.style.visibility="hidden");e.currentState=i.ButtonState.HOVER}if(t<=i.ButtonState.GROUP&&e.currentState===i.ButtonState.HOVER){e.imgHover&&(e.imgHover.style.visibility="hidden");e.currentState=i.ButtonState.GROUP}if(t<=i.ButtonState.REST&&e.currentState===i.ButtonState.GROUP){!function(e){e.shouldFade=!0;e.fadeBeginTime=i.now()+e.fadeDelay;window.setTimeout(function(){n(e)},e.fadeDelay)}(e);e.currentState=i.ButtonState.REST}}}}(OpenSeadragon);!function(r){r.ButtonGroup=function(e){r.extend(!0,this,{buttons:[],clickTimeThreshold:r.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:r.DEFAULT_SETTINGS.clickDistThreshold,labelText:""},e);let t=this.buttons.concat([]),i=this,n;this.element=e.element||r.makeNeutralElement("div");if(!e.group){this.element.style.display="inline-block";for(n=0;nh&&(h=d.x);d.yu&&(u=d.y)}return new p.Rect(l,c,h-l,u-c)},_getSegments:function(){var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();return[[e,t],[t,n],[n,i],[i,e]]},rotate:function(e,t){if(0===(e=p.positiveModulo(e,360)))return this.clone();t=t||this.getCenter();var i=this.getTopLeft().rotate(e,t);const n=this.getTopRight().rotate(e,t);let r=n.minus(i);r=r.apply(function(e){return Math.abs(e)<1e-15?0:e});let o=Math.atan(r.y/r.x);r.x<0?o+=Math.PI:r.y<0&&(o+=2*Math.PI);return new p.Rect(i.x,i.y,this.width,this.height,o/Math.PI*180)},getBoundingBox:function(){if(0===this.degrees)return this.clone();var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();var r=Math.min(e.x,t.x,i.x,n.x);var o=Math.max(e.x,t.x,i.x,n.x);var s=Math.min(e.y,t.y,i.y,n.y);n=Math.max(e.y,t.y,i.y,n.y);return new p.Rect(r,s,o-r,n-s)},getIntegerBoundingBox:function(){var e=this.getBoundingBox();var t=Math.floor(e.x);var i=Math.floor(e.y);var n=Math.ceil(e.width+e.x-t);e=Math.ceil(e.height+e.y-i);return new p.Rect(t,i,n,e)},containsPoint:function(e,t){t=t||0;var i=this.getTopLeft();const n=this.getTopRight();const r=this.getBottomLeft();var o=n.minus(i);var s=r.minus(i);return(e.x-i.x)*o.x+(e.y-i.y)*o.y>=-t&&(e.x-n.x)*o.x+(e.y-n.y)*o.y<=t&&(e.x-i.x)*s.x+(e.y-i.y)*s.y>=-t&&(e.x-r.x)*s.x+(e.y-r.y)*s.y<=t},toString:function(){return"["+Math.round(100*this.x)/100+", "+Math.round(100*this.y)/100+", "+Math.round(100*this.width)/100+"x"+Math.round(100*this.height)/100+", "+Math.round(100*this.degrees)/100+"deg]"}}}(OpenSeadragon);!function(c){const s={};c.ReferenceStrip=function(e){const t=e.viewer;var i=c.getElementSize(t.element);let n;let r;if(!e.id){e.id="referencestrip-"+c.now();this.element=c.makeNeutralElement("div");this.element.id=e.id;this.element.className="referencestrip"}e=c.extend(!0,{sizeRatio:c.DEFAULT_SETTINGS.referenceStripSizeRatio,position:c.DEFAULT_SETTINGS.referenceStripPosition,scroll:c.DEFAULT_SETTINGS.referenceStripScroll,clickTimeThreshold:c.DEFAULT_SETTINGS.clickTimeThreshold},e,{element:this.element});c.extend(this,e);s[this.id]={animating:!1};this.minPixelRatio=this.viewer.minPixelRatio;this.element.tabIndex=0;const o=this.element.style;o.marginTop="0px";o.marginRight="0px";o.marginBottom="0px";o.marginLeft="0px";o.left="0px";o.bottom="0px";o.border="0px";o.background="#000";o.position="relative";c.setElementTouchActionNone(this.element);c.setElementOpacity(this.element,.8);this.viewer=t;this.tracker=new c.MouseTracker({userData:"ReferenceStrip.tracker",element:this.element,clickHandler:c.delegate(this,a),dragHandler:c.delegate(this,l),scrollHandler:c.delegate(this,h),enterHandler:c.delegate(this,d),leaveHandler:c.delegate(this,p),keyDownHandler:c.delegate(this,g),keyHandler:c.delegate(this,m),preProcessEventHandler:function(e){"wheel"===e.eventType&&(e.preventDefault=!0)}});if(e.width&&e.height){this.element.style.width=e.width+"px";this.element.style.height=e.height+"px";t.addControl(this.element,{anchor:c.ControlAnchor.BOTTOM_LEFT})}else if("horizontal"===e.scroll){this.element.style.width=i.x*e.sizeRatio*t.tileSources.length+12*t.tileSources.length+"px";this.element.style.height=i.y*e.sizeRatio+"px";t.addControl(this.element,{anchor:c.ControlAnchor.BOTTOM_LEFT})}else{this.element.style.height=i.y*e.sizeRatio*t.tileSources.length+12*t.tileSources.length+"px";this.element.style.width=i.x*e.sizeRatio+"px";t.addControl(this.element,{anchor:c.ControlAnchor.TOP_LEFT})}this.panelWidth=i.x*this.sizeRatio+8;this.panelHeight=i.y*this.sizeRatio+8;this.panels=[];this.miniViewers={};for(r=0;ro+i.x-this.panelWidth){a=Math.min(a,n-i.x);this.element.style.marginLeft=-a+"px";u(this,i.x,-a)}else if(as+i.y-this.panelHeight){a=Math.min(a,r-i.y);this.element.style.marginTop=-a+"px";u(this,i.y,-a)}else if(a-(n-o.x)){this.element.style.marginLeft=t+2*e.delta.x+"px";u(this,o.x,t+2*e.delta.x)}}else if(-e.delta.x<0&&t<0){this.element.style.marginLeft=t+2*e.delta.x+"px";u(this,o.x,t+2*e.delta.x)}}else if(0<-e.delta.y){if(i>-(r-o.y)){this.element.style.marginTop=i+2*e.delta.y+"px";u(this,o.y,i+2*e.delta.y)}}else if(-e.delta.y<0&&i<0){this.element.style.marginTop=i+2*e.delta.y+"px";u(this,o.y,i+2*e.delta.y)}}}function h(e){if(this.element){var t=Number(this.element.style.marginLeft.replace("px",""));var i=Number(this.element.style.marginTop.replace("px",""));var n=Number(this.element.style.width.replace("px",""));var r=Number(this.element.style.height.replace("px",""));var o=c.getElementSize(this.viewer.canvas);if("horizontal"===this.scroll){if(0-(n-o.x)){this.element.style.marginLeft=t-60*e.scroll+"px";u(this,o.x,t-60*e.scroll)}}else if(e.scroll<0&&t<0){this.element.style.marginLeft=t-60*e.scroll+"px";u(this,o.x,t-60*e.scroll)}}else if(e.scroll<0){if(i>o.y-r){this.element.style.marginTop=i+60*e.scroll+"px";u(this,o.y,i+60*e.scroll)}}else if(0=this.target.time)this.current.value=this.target.value;else{i=e+(t-e)*(i=this.springStiffness,n=(this.current.time-this.start.time)/(this.target.time-this.start.time),(1-Math.exp(i*-n))/(1-Math.exp(-i)));this._exponential?this.current.value=Math.exp(i):this.current.value=i}var i,n;return this.current.value!==this.target.value},isAtTargetValue:function(){return this.current.value===this.target.value}}}(OpenSeadragon);!function(r){r.ImageJob=function(e){this.data=null;this.userData={};this.errorMsg=null;this.timeout=r.DEFAULT_SETTINGS.timeout;this.isBatched=!1;r.extend(!0,this,{jobId:null,tries:0},e)};r.ImageJob.prototype={start:function(){this.tries++;const e=this;const t=this.abort;this.jobId=window.setTimeout(function(){e.fail("Image load exceeded timeout ("+e.timeout+" ms)",null)},this.timeout);this.abort=function(){e.source.downloadTileAbort(e);"function"==typeof t&&t();e.fail("Image load aborted.",null)};this.source.downloadTileStart(this)},prepareForBatch:function(){this.tries++;this.jobId=-1},finish:function(e,t,i){if(this.jobId)if(null!=(n=e)&&!1!==n){var n;this.data=e;this.request=t;this.errorMsg=null;this.dataType=i;window.clearTimeout(this.jobId);this.jobId=null;this.callback(this)}else this.fail(i||"[downloadTileStart->finish()] Retrieved data is invalid!",t)},fail:function(e,t){this.data=null;this.request=t;this.errorMsg=e;this.dataType=null;if(this.jobId){window.clearTimeout(this.jobId);this.jobId=null}this.callback(this)}};r.BatchImageJob=function(e){r.extend(!0,this,{timeout:r.DEFAULT_SETTINGS.timeout,jobId:null,data:null,dataType:null,errorMsg:null},e);this.jobs=e.jobs||[];this.source=e.source};r.BatchImageJob.prototype={start:function(){this._finishedJobs=0;const t=this;this.jobId=window.setTimeout(function(){t.fail("Batch image load exceeded timeout ("+t.timeout+" ms)",null)},this.timeout);this.abort=function(){t.source.downloadTileBatchAbort(t);for(var e of this.jobs)e.jobId&&e.abort&&e.abort()};var e=(t,i)=>(...e)=>{if(this.jobId){this._finishedJobs++;t.call(i,...e);if(this._finishedJobs===this.jobs.length){window.clearTimeout(this.jobId);this.jobId=null;this.callback&&this.callback(this)}}};for(var i of this.jobs){i.finish=e(i.finish,i);i.fail=e(i.fail,i);i.prepareForBatch()}this.source.downloadTileBatchStart(this)},finish:function(e,t,i){r.console.error("Finish call on batch job is not desirable: call finish on individual child jobs!",e,t)},fail:function(t,i){this.data=null;this.request=i;this.errorMsg=t;this.dataType=null;for(let e=0;efunction(t,e,i){if(e.errorMsg&&null===e.data&&e.tries<1+t.tileRetryMax){e.isBatched=!1;t.failedTiles.push(e)}e.isBatched||t.jobsInProgress--;if(t.canAcceptNewJob()&&0this._flushBatchBucket(i),i.waitTimeout);this._batchBuckets.push(i)}i.jobs.push(e);if(1<=i.maxJobs&&i.jobs.length>=i.maxJobs){clearTimeout(i.timer);this._flushBatchBucket(i)}},_flushBatchBucket:function(e){e.timer=null;var t=this._batchBuckets.indexOf(e);-1function(e,t){e.jobsInProgress--;t.jobs.length=0}(i,e)});if(!this.jobLimit||this.jobsInProgressthis.addCache(this.cacheKey,e,t.type,!0,!1)))},buildDistinctMainCacheKey:function(){return this.cacheKey===this.originalCacheKey?"mod://"+this.originalCacheKey:this.cacheKey},getCache:function(e=this._cKey){const t=this._caches[e];t&&t.withTileReference(this);return t},addCache:function(e,t,i=void 0,n=!1,r=!0){const o=this.tiledImage;if(!o)return null;if(!i){if(!this.__typeWarningReported){d.console.warn(this,"[Tile.addCache] called without type specification. Automated deduction is potentially unsafe: prefer specification of data type explicitly.");this.__typeWarningReported=!0}"function"==typeof t&&d.console.error("[TileCache.cacheTile] options.data as a callback requires type argument! Current is "+i);i=d.converter.guessType(t)}var s=e===this.cacheKey;if(r&&(s||n)){r=o.getDrawer().getSupportedDataFormats();const l=d.converter.getConversionPath(i,r);d.console.assert(l,"[Tile.addCache] data was set for the default tile cache we are unable"+`to render. Make sure OpenSeadragon.converter was taught to convert ${i} to (one of): `+l.toString())}i=o._tileCache.cacheTile({data:t,dataType:i,tile:this,cacheKey:e,cutoff:o.source.getClosestLevel()});const a=this._caches[e];if(a!==i){this._caches[e]=i;if(a){a.removeTile(this);o._tileCache.safeUnloadCache(a)}}!s&&n&&this._updateMainCacheKey(e);return i},setCache(e,t,i=!1,n=!0){const r=this.tiledImage;if(!r)return null;var o=e===this.cacheKey;if(n){d.console.assert(t instanceof d.CacheRecord,"[Tile.setCache] cache must be a CacheRecord object!");if(o||i){n=r.getDrawer().getSupportedDataFormats();const a=d.converter.getConversionPath(t.type,n);d.console.assert(a,"[Tile.setCache] data was set for the default tile cache we are unable"+`to render. Make sure OpenSeadragon.converter was taught to convert ${t.type} to (one of): `+a.toString())}}const s=this._caches[e];if(s!==t){(this._caches[e]=t).addTile(this);if(s){s.removeTile(this);r._tileCache.safeUnloadCache(s)}}!o&&i&&this._updateMainCacheKey(e);return t},_updateMainCacheKey:function(e){let t=this._caches[this._cKey];t&&t.destroyInternalCache();this._cKey=e},getCacheSize:function(){return Object.keys(this._caches).length},removeCache:function(e,t=!0){var i=this._caches[e];if(i){var n=this.cacheKey,r=this.originalCacheKey,o=n===r;if(o||r!==e){if(n===e){if(o||!this._caches[r]){d.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!","If you want to remove the main cache, first set different cache as main with tile.addCache()");return}this._updateMainCacheKey(r)}this.tiledImage._tileCache.unloadCacheForTile(this,e,t,!1)&&delete this._caches[e];return i}d.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!","If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData().")}else this.tiledImage._tileCache.unloadCacheForTile(this,e,t,!0)},getScaleForEdgeSmoothing:function(){d.console.warn("[Tile.getScaleForEdgeSmoothing] is deprecated, the following error is the consequence:");var e=this.getCanvasContext();if(e)return e.canvas.width/(this.size.x*d.pixelDensityRatio);d.console.warn("[Tile.drawCanvas] attempting to get tile scale %s when tile's not cached",this.toString());return 1},getTranslationForEdgeSmoothing:function(e,t,i){var n=Math.max(1,Math.ceil((i.x-t.x)/2));t=Math.max(1,Math.ceil((i.y-t.y)/2));return new d.Point(n,t).minus(this.position.times(d.pixelDensityRatio).times(e||1).apply(function(e){return e%1}))},reflectCacheRenamed:function(e,t){var i=this._caches[e];if(i){e===this._ocKey&&(this._ocKey=t);e===this._cKey&&(this._cKey=t);this._caches[t]=i;delete this._caches[e]}},equals(e){return this._ocKey===e._ocKey},unload:function(e=!1){this.loaded&&this.tiledImage._tileCache.unloadTile(this,e)},_unload:function(){this.tiledImage=null;this._caches={};this.loaded=!1;this.loading=!1;this._cKey=this._ocKey}}}(OpenSeadragon);!function(h){h.OverlayPlacement=h.Placement;h.OverlayRotationMode=h.freezeObject({NO_ROTATION:1,EXACT:2,BOUNDING_BOX:3});h.Overlay=function(e,t,i){let n;n=h.isPlainObject(e)?e:{element:e,location:t,placement:i};this.elementWrapper=document.createElement("div");this.element=n.element;this.elementWrapper.appendChild(this.element);this.element.id&&(this.elementWrapper.id="overlay-wrapper-"+this.element.id);this.elementWrapper.classList.add("openseadragon-overlay-wrapper");this.style=this.elementWrapper.style;this._init(n)};h.Overlay.prototype={_init:function(e){this.location=e.location;this.placement=void 0===e.placement?h.Placement.TOP_LEFT:e.placement;this.onDraw=e.onDraw;this.checkResize=void 0===e.checkResize||e.checkResize;this.width=void 0===e.width?null:e.width;this.height=void 0===e.height?null:e.height;this.rotationMode=e.rotationMode||h.OverlayRotationMode.EXACT;if(this.location instanceof h.Rect){this.width=this.location.width;this.height=this.location.height;this.location=this.location.getTopLeft();this.placement=h.Placement.TOP_LEFT}this.scales=null!==this.width&&null!==this.height;this.bounds=new h.Rect(this.location.x,this.location.y,this.width,this.height);this.position=this.location},adjust:function(e,t){var i=h.Placement.properties[this.placement];if(i){i.isHorizontallyCentered?e.x-=t.x/2:i.isRight&&(e.x-=t.x);i.isVerticallyCentered?e.y-=t.y/2:i.isBottom&&(e.y-=t.y)}},destroy:function(){const e=this.elementWrapper;const t=this.style;if(e.parentNode){e.parentNode.removeChild(e);if(e.prevElementParent){t.display="none";document.body.appendChild(e)}}this.onDraw=null;t.top="";t.left="";t.position="";null!==this.width&&(t.width="");null!==this.height&&(t.height="");var i=h.getCssPropertyWithVendorPrefix("transformOrigin");var n=h.getCssPropertyWithVendorPrefix("transform");if(i&&n){t[i]="";t[n]=""}},drawHTML:function(e,t){const i=this.elementWrapper;if(i.parentNode!==e){i.prevElementParent=i.parentNode;i.prevNextSibling=i.nextSibling;e.appendChild(i);this.style.position="absolute";this.size=h.getElementSize(this.elementWrapper)}var n=this._getOverlayPositionAndSize(t);var r=n.position;var o=this.size=n.size;let s="";t.overlayPreserveContentDirection&&(s=t.flipped?" scaleX(-1)":" scaleX(1)");e=t.flipped?-n.rotate:n.rotate;n=t.flipped?" scaleX(-1)":"";if(this.onDraw)this.onDraw(r,o,this.element);else{const a=this.style;const l=this.element.style;l.display="block";a.left=r.x+"px";a.top=r.y+"px";null!==this.width&&(l.width=o.x+"px");null!==this.height&&(l.height=o.y+"px");r=h.getCssPropertyWithVendorPrefix("transformOrigin");o=h.getCssPropertyWithVendorPrefix("transform");if(r&&o)if(e&&!t.flipped){l[o]="";a[r]=this._getTransformOrigin();a[o]="rotate("+e+"deg)"}else if(!e&&t.flipped){l[o]=s;a[r]=this._getTransformOrigin();a[o]=n}else if(e&&t.flipped){l[o]=s;a[r]=this._getTransformOrigin();a[o]="rotate("+e+"deg)"+n}else{l[o]="";a[r]="";a[o]=""}a.display="flex"}},_getOverlayPositionAndSize:function(e){let t=e.pixelFromPoint(this.location,!0);let i=this._getSizeInPixels(e);this.adjust(t,i);let n=0;if(e.getRotation(!0)&&this.rotationMode!==h.OverlayRotationMode.NO_ROTATION)if(this.rotationMode===h.OverlayRotationMode.BOUNDING_BOX&&null!==this.width&&null!==this.height){var r=new h.Rect(t.x,t.y,i.x,i.y);const o=this._getBoundingBox(r,e.getRotation(!0));t=o.getTopLeft();i=o.getSize()}else n=e.getRotation(!0);e.flipped&&(t.x=e.getContainerSize().x-t.x);return{position:t,size:i,rotate:n}},_getSizeInPixels:function(e){let t=this.size.x;let i=this.size.y;if(null!==this.width||null!==this.height){var n=e.deltaPixelsFromPointsNoRotate(new h.Point(this.width||0,this.height||0),!0);null!==this.width&&(t=n.x);null!==this.height&&(i=n.y)}if(this.checkResize&&(null===this.width||null===this.height)){n=this.size=h.getElementSize(this.elementWrapper);null===this.width&&(t=n.x);null===this.height&&(i=n.y)}return new h.Point(t,i)},_getBoundingBox:function(e,t){var i=this._getPlacementPoint(e);return e.rotate(t,i).getBoundingBox()},_getPlacementPoint:function(e){const t=new h.Point(e.x,e.y);var i=h.Placement.properties[this.placement];if(i){i.isHorizontallyCentered?t.x+=e.width/2:i.isRight&&(t.x+=e.width);i.isVerticallyCentered?t.y+=e.height/2:i.isBottom&&(t.y+=e.height)}return t},_getTransformOrigin:function(){let e="";var t=h.Placement.properties[this.placement];if(!t)return e;t.isLeft?e="left":t.isRight&&(e="right");t.isTop?e+=" top":t.isBottom&&(e+=" bottom");return e},update:function(e,t){t=h.isPlainObject(e)?e:{location:e,placement:t};this._init({location:t.location||this.location,placement:(void 0!==t.placement?t:this).placement,onDraw:t.onDraw||this.onDraw,checkResize:t.checkResize||this.checkResize,width:(void 0!==t.width?t:this).width,height:(void 0!==t.height?t:this).height,rotationMode:t.rotationMode||this.rotationMode})},getBounds:function(e){h.console.assert(e,"A viewport must now be passed to Overlay.getBounds.");let t=this.width;let i=this.height;if(null===t||null===i){var n=e.deltaPointsFromPixelsNoRotate(this.size,!0);null===t&&(t=n.x);null===i&&(i=n.y)}n=this.location.clone();this.adjust(n,new h.Point(t,i));return this._adjustBoundsForRotation(e,new h.Rect(n.x,n.y,t,i))},_adjustBoundsForRotation:function(e,t){if(!e||0===e.getRotation(!0)||this.rotationMode===h.OverlayRotationMode.EXACT)return t;if(this.rotationMode!==h.OverlayRotationMode.BOUNDING_BOX)return t.rotate(-e.getRotation(!0),this._getPlacementPoint(t));if(null===this.width||null===this.height)return t;t=this._getOverlayPositionAndSize(e);return e.viewerElementToViewportRectangle(new h.Rect(t.position.x,t.position.y,t.size.x,t.size.y))}}}(OpenSeadragon);!function(n){const i=n;i.DrawerBase=class{constructor(e){n.console.assert(e.viewer,"[Drawer] options.viewer is required");n.console.assert(e.viewport,"[Drawer] options.viewport is required");n.console.assert(e.element,"[Drawer] options.element is required");this._id=this.getType()+n.now();this.viewer=e.viewer;this.viewport=e.viewport;this.debugGridColor="string"==typeof e.debugGridColor?[e.debugGridColor]:e.debugGridColor||n.DEFAULT_SETTINGS.debugGridColor;this.options=n.extend({usePrivateCache:!1,preloadCache:!0,offScreen:!1,broadCastTileInvalidation:!0},this.defaultOptions,e.options);this.container=n.getElement(e.element);this._renderingTarget=this._createDrawingElement();if(!this.options.offScreen){this.canvas.style.width="100%";this.canvas.style.height="100%";this.canvas.style.position="absolute";this.canvas.style.left="0";n.setElementOpacity(this.canvas,this.viewer.opacity,!0);n.setElementPointerEventsNone(this.canvas);n.setElementTouchActionNone(this.canvas);this.container.style.textAlign="left";this.container.appendChild(this.canvas);if(this.options.broadCastTileInvalidation){let e=this.viewer;for(;e.viewer;)e=e.viewer;this._parentViewer=e;e._registerDrawer(this)}else{this.viewer._registerDrawer(this);this._parentViewer=this.viewer}}this._checkInterfaceImplementation();this.setInternalCacheNeedsRefresh()}get defaultOptions(){return{}}get canvas(){return this._renderingTarget}get element(){n.console.error("Drawer.element is deprecated. Use Drawer.container instead.");return this.container}getId(){return this._id}getType(){n.console.error("Drawer.getType must be implemented by child class")}getRequiredDataFormats(){return this.getSupportedDataFormats()}getSupportedDataFormats(){throw"Drawer.getSupportedDataFormats must define its supported rendering data types!"}getDataToDraw(e){const t=e.getCache(e.cacheKey);if(t){var i=t.getDataForRendering(this,e);return i&&i.data}n.console.warn("Attempt to draw tile %s when not cached!",e)}static isSupported(){n.console.error("Drawer.isSupported must be implemented by child class")}_createDrawingElement(){n.console.error("Drawer._createDrawingElement must be implemented by child class");return null}draw(e){n.console.error("Drawer.draw must be implemented by child class")}canRotate(){n.console.error("Drawer.canRotate must be implemented by child class")}destroy(){this._parentViewer._unregisterDrawer(this)}destroyInternalCache(){this.viewer.tileCache.clearDrawerInternalCache(this)}minimumOverlapRequired(e){return!1}setImageSmoothingEnabled(e){n.console.error("Drawer.setImageSmoothingEnabled must be implemented by child class")}drawDebuggingRect(e){n.console.warn("[drawer].drawDebuggingRect is not implemented by this drawer")}clear(){n.console.warn("[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.")}internalCacheCreate(e,t){}internalCacheFree(e){}setInternalCacheNeedsRefresh(){this._dataNeedsRefresh=n.now()}tiledImageCreated(e){}_checkInterfaceImplementation(){if(this._createDrawingElement===n.DrawerBase.prototype._createDrawingElement)throw new Error("[drawer]._createDrawingElement must be implemented by child class");if(this.draw===n.DrawerBase.prototype.draw)throw new Error("[drawer].draw must be implemented by child class");if(this.canRotate===n.DrawerBase.prototype.canRotate)throw new Error("[drawer].canRotate must be implemented by child class");if(this.destroy===n.DrawerBase.prototype.destroy)throw new Error("[drawer].destroy must be implemented by child class");if(this.setImageSmoothingEnabled===n.DrawerBase.prototype.setImageSmoothingEnabled)throw new Error("[drawer].setImageSmoothingEnabled must be implemented by child class")}viewportToDrawerRectangle(e){var t=this.viewport.pixelFromPointNoRotate(e.getTopLeft(),!0);e=this.viewport.deltaPixelsFromPointsNoRotate(e.getSize(),!0);return new n.Rect(t.x*n.pixelDensityRatio,t.y*n.pixelDensityRatio,e.x*n.pixelDensityRatio,e.y*n.pixelDensityRatio)}viewportCoordToDrawerCoord(e){e=this.viewport.pixelFromPointNoRotate(e,!0);return new n.Point(e.x*n.pixelDensityRatio,e.y*n.pixelDensityRatio)}_calculateCanvasSize(){var e=n.pixelDensityRatio;var t=this.viewport.getContainerSize();return new i.Point(Math.round(t.x*e),Math.round(t.y*e))}_raiseTiledImageDrawnEvent(e,t){this.viewer&&this.viewer.raiseEvent("tiled-image-drawn",{tiledImage:e,tiles:t})}_raiseDrawerErrorEvent(e,t){this.viewer&&this.viewer.raiseEvent("drawer-error",{tiledImage:e,drawer:this,error:t})}}}(OpenSeadragon);!function(o){var e=o;class t extends e.DrawerBase{constructor(e){super(e);this.viewer.rejectEventHandler("tile-drawing","The HTMLDrawer does not raise the tile-drawing event");this.viewer.allowEventHandler("tile-drawn");o.converter.learn("image",t.imageCacheType,function(e,t){var i=o.makeNeutralElement("div");const n=t.cloneNode();n.style.msInterpolationMode="nearest-neighbor";n.style.width="100%";n.style.height="100%";const r=i.style;r.position="absolute";return{element:i,imgElement:n,style:r,data:t}},1,1);o.converter.learn(t.imageCacheType,"image",(e,t)=>t.data,1,3);o.converter.learnDestroy(t.imageCacheType,function(e){e.imgElement&&e.imgElement.parentNode&&e.imgElement.parentNode.removeChild(e.imgElement);e.element&&e.element.parentNode&&e.element.parentNode.removeChild(e.element)})}static get imageCacheType(){return"htmlDrawer[image]"}static get canvasCacheType(){return"htmlDrawer[canvas]"}static isSupported(){return!0}getType(){return"html"}getSupportedDataFormats(){return[t.imageCacheType,t.canvasCacheType]}minimumOverlapRequired(e){return!0}_createDrawingElement(){return o.makeNeutralElement("div")}draw(e){const t=this;this._prepareNewFrame();e.forEach(function(e){0!==e.opacity&&t._drawTiles(e)})}canRotate(){return!1}destroy(){super.destroy();this.container.removeChild(this.canvas)}setImageSmoothingEnabled(){}_prepareNewFrame(){this.canvas.innerHTML=""}_drawTiles(t){var i=t.getTilesToDraw().map(e=>e.tile);if(0!==t.opacity&&(0!==i.length||t.placeholderFillStyle))for(let e=i.length-1;0<=e;e--){var n=i[e];this._drawTile(n);this.viewer&&this.viewer.raiseEvent("tile-drawn",{tiledImage:t,tile:n})}}_drawTile(e){o.console.assert(e,"[Drawer._drawTile] tile is required");let t=this.canvas;if(e.loaded){const i=this.getDataToDraw(e);if(i){i.element.parentNode!==t&&t.appendChild(i.element);i.imgElement.parentNode!==i.element&&i.element.appendChild(i.imgElement);i.style.top=e.position.y+"px";i.style.left=e.position.x+"px";i.style.height=e.size.y+"px";i.style.width=e.size.x+"px";e.flipped&&(i.style.transform="scaleX(-1)");o.setElementOpacity(i.element,e.opacity)}}else o.console.warn("Attempting to draw tile %s when it's not yet loaded.",e.toString())}}o.HTMLDrawer=t}(OpenSeadragon);!function(d){var e=d;class t extends e.DrawerBase{constructor(e){super(e);this.context=this.canvas.getContext("2d");this.sketchCanvas=null;this.sketchContext=null;this._imageSmoothingEnabled=!0;this.viewer.allowEventHandler("tile-drawn");this.viewer.allowEventHandler("tile-drawing")}static isSupported(){return d.supportsCanvas}getType(){return"canvas"}getSupportedDataFormats(){return["context2d"]}_createDrawingElement(){const e=d.makeNeutralElement("canvas");var t=this._calculateCanvasSize();e.width=t.x;e.height=t.y;return e}draw(e){this._prepareNewFrame();this.viewer.viewport.getFlip()!==this._viewportFlipped&&this._flip();for(const t of e)0!==t.opacity&&this._drawTiles(t)}canRotate(){return!0}destroy(){super.destroy();this.canvas.width=1;this.canvas.height=1;this.sketchCanvas=null;this.sketchContext=null;this.container.removeChild(this.canvas)}minimumOverlapRequired(e){return!0}setImageSmoothingEnabled(e){this._imageSmoothingEnabled=!!e;this._updateImageSmoothingEnabled(this.context);this.viewer.forceRedraw()}drawDebuggingRect(e){const t=this.context;t.save();t.lineWidth=2*d.pixelDensityRatio;t.strokeStyle=this.debugGridColor[0];t.fillStyle=this.debugGridColor[0];t.strokeRect(e.x*d.pixelDensityRatio,e.y*d.pixelDensityRatio,e.width*d.pixelDensityRatio,e.height*d.pixelDensityRatio);t.restore()}get _viewportFlipped(){return this.context.getTransform().a<0}_raiseTileDrawingEvent(e,t,i,n){this.viewer.raiseEvent("tile-drawing",{tiledImage:e,context:t,tile:i,rendered:n})}_prepareNewFrame(){var e=this._calculateCanvasSize();if(this.canvas.width!==e.x||this.canvas.height!==e.y){this.canvas.width=e.x;this.canvas.height=e.y;this._updateImageSmoothingEnabled(this.context);if(null!==this.sketchCanvas){e=this._calculateSketchCanvasSize();this.sketchCanvas.width=e.x;this.sketchCanvas.height=e.y;this._updateImageSmoothingEnabled(this.sketchContext)}}this._clear()}_clear(e,t){const i=this._getContext(e);if(t)i.clearRect(t.x,t.y,t.width,t.height);else{t=i.canvas;i.clearRect(0,0,t.width,t.height)}}_drawTiles(a){var l=a.getTilesToDraw().map(e=>e.tile);if(0!==a.opacity&&(0!==l.length||a.placeholderFillStyle)){let t=l[0];let i;t&&(i=a.opacity<1||a.compositeOperation&&"source-over"!==a.compositeOperation||!a._isBottomItem()&&a.source.hasTransparency(null,t.getUrl(),t.ajaxHeaders,t.postData));let n;let r;var h=this.viewport.getZoom(!0);h=a.viewportToImageZoom(h);if(1a.smoothTileEdgesMinZoom&&!a.iOSDevice&&a.getRotation(!0)%360==0){i=!0;h=t.length&&this.getDataToDraw(t);n=h?h.canvas.width/(t.size.x*d.pixelDensityRatio):1;r=t.getTranslationForEdgeSmoothing(n,this._getCanvasSize(!1),this._getCanvasSize(!0))}let e;if(i){if(!n){e=this.viewport.viewportToViewerElementRectangle(a.getClippedBounds(!0)).getIntegerBoundingBox();e=e.times(d.pixelDensityRatio)}this._clear(!0,e)}n||this._setRotations(a,i);let o=!1;if(a._clip){this._saveContext(i);let e=a.imageToViewportRectangle(a._clip,!0);e=e.rotate(-a.getRotation(!0),a._getRotationPoint(!0));let t=this.viewportToDrawerRectangle(e);n&&(t=t.times(n));r&&(t=t.translate(r));this._setClip(t,i);o=!0}if(a._croppingPolygons){const u=this;o||this._saveContext(i);try{var c=a._croppingPolygons.map(function(e){return e.map(function(e){e=a.imageToViewportCoordinates(e.x,e.y,!0).rotate(-a.getRotation(!0),a._getRotationPoint(!0));let t=u.viewportCoordToDrawerCoord(e);n&&(t=t.times(n));r&&(t=t.plus(r));return t})});this._clipWithPolygons(c,i)}catch(e){d.console.error(e)}o=!0}a._hasOpaqueTile=!1;if(a.placeholderFillStyle&&!1===a._hasOpaqueTile){let e=this.viewportToDrawerRectangle(a.getBoundsNoRotate(!0));n&&(e=e.times(n));r&&(e=e.translate(r));let t=null;t="function"==typeof a.placeholderFillStyle?a.placeholderFillStyle(a,this.context):a.placeholderFillStyle;this._drawRectangle(e,t,i)}c=function(e){if("number"==typeof e)return m(e);if(!e||!d.Browser)return p;let t=e[d.Browser.vendor];g(t)&&(t=e["*"]);return m(t)}(a.subPixelRoundingForTransparency);let s=!1;c===d.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS?s=!0:c===d.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST&&(s=!(this.viewer&&this.viewer.isAnimating()));for(let e=0;ethis.canvas.width&&(e.width=this.canvas.width-e.x);if(e.y<0){e.height+=e.y;e.y=0}e.y+e.height>this.canvas.height&&(e.height=this.canvas.height-e.y);this.context.drawImage(this.sketchCanvas,e.x,e.y,e.width,e.height,e.x,e.y,e.width,e.height)}else{n=s.scale||1;i=(r=s.translate)instanceof d.Point?r:new d.Point(0,0);let e=0;let t=0;if(r){o=this.sketchCanvas.width-this.canvas.width;r=this.sketchCanvas.height-this.canvas.height;e=Math.round(o/2);t=Math.round(r/2)}this.context.drawImage(this.sketchCanvas,i.x-e*n,i.y-t*n,(this.canvas.width+2*e)*n,(this.canvas.height+2*t)*n,-e,-t,this.canvas.width+2*e,this.canvas.height+2*t)}this.context.restore()}_drawDebugInfoOnTile(e,t,i,n){var r=this.viewer.world.getIndexOfItem(n)%this.debugGridColor.length;const o=this.context;o.save();o.lineWidth=2*d.pixelDensityRatio;o.font="small-caps bold "+13*d.pixelDensityRatio+"px arial";o.strokeStyle=this.debugGridColor[r];o.fillStyle=this.debugGridColor[r];this._setRotations(n);this._viewportFlipped&&this._flip({point:e.position.plus(e.size.divide(2))});o.strokeRect(e.position.x*d.pixelDensityRatio,e.position.y*d.pixelDensityRatio,e.size.x*d.pixelDensityRatio,e.size.y*d.pixelDensityRatio);var s=(e.position.x+e.size.x/2)*d.pixelDensityRatio;var a=(e.position.y+e.size.y/2)*d.pixelDensityRatio;o.translate(s,a);r=this.viewport.getRotation(!0);o.rotate(Math.PI/180*-r);o.translate(-s,-a);if(0===e.x&&0===e.y){o.fillText("Zoom: "+this.viewport.getZoom(),e.position.x*d.pixelDensityRatio,(e.position.y-30)*d.pixelDensityRatio);o.fillText("Pan: "+this.viewport.getBounds().toString(),e.position.x*d.pixelDensityRatio,(e.position.y-20)*d.pixelDensityRatio)}o.fillText("Level: "+e.level,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+20)*d.pixelDensityRatio);o.fillText("Column: "+e.x,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+30)*d.pixelDensityRatio);o.fillText("Row: "+e.y,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+40)*d.pixelDensityRatio);o.fillText("Order: "+i+" of "+t,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+50)*d.pixelDensityRatio);o.fillText("Size: "+e.size.toString(),(e.position.x+10)*d.pixelDensityRatio,(e.position.y+60)*d.pixelDensityRatio);o.fillText("Position: "+e.position.toString(),(e.position.x+10)*d.pixelDensityRatio,(e.position.y+70)*d.pixelDensityRatio);this.viewport.getRotation(!0)%360!=0&&this._restoreRotationChanges();n.getRotation(!0)%360!=0&&this._restoreRotationChanges();o.restore()}_updateImageSmoothingEnabled(e){e.msImageSmoothingEnabled=this._imageSmoothingEnabled;e.imageSmoothingEnabled=this._imageSmoothingEnabled}_getCanvasSize(e){e=this._getContext(e).canvas;return new d.Point(e.width,e.height)}_getCanvasCenter(){return new d.Point(this.canvas.width/2,this.canvas.height/2)}_setRotations(e,t=!1){let i=!1;if(this.viewport.getRotation(!0)%360!=0){this._offsetForRotation({degrees:this.viewport.getRotation(!0),useSketch:t,saveContext:i});i=!1}e.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:e.getRotation(!0),point:this.viewport.pixelFromPointNoRotate(e._getRotationPoint(!0),!0),useSketch:t,saveContext:i})}_offsetForRotation(e){var t=e.point?e.point.times(d.pixelDensityRatio):this._getCanvasCenter();const i=this._getContext(e.useSketch);i.save();i.translate(t.x,t.y);i.rotate(Math.PI/180*e.degrees);i.translate(-t.x,-t.y)}_flip(e){var t=(e=e||{}).point?e.point.times(d.pixelDensityRatio):this._getCanvasCenter();const i=this._getContext(e.useSketch);i.translate(t.x,0);i.scale(-1,1);i.translate(-t.x,0)}_restoreRotationChanges(e){const t=this._getContext(e);t.restore()}_calculateCanvasSize(){var e=d.pixelDensityRatio;var t=this.viewport.getContainerSize();return{x:Math.round(t.x*e),y:Math.round(t.y*e)}}_calculateSketchCanvasSize(){var e=this._calculateCanvasSize();if(0===this.viewport.getRotation())return e;e=Math.ceil(Math.sqrt(e.x*e.x+e.y*e.y));return{x:e,y:e}}}d.CanvasDrawer=t;const p=d.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;function g(e){return e!==d.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS&&e!==d.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST&&e!==d.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER}function m(e){return g(e)?p:e}}(OpenSeadragon);!function(C){const l=C;class d{constructor(e){this._renderingCanvas=e.renderingCanvas;this._unpackWithPremultipliedAlpha=!!e.unpackWithPremultipliedAlpha;this._imageSmoothingEnabled=void 0===e.imageSmoothingEnabled||e.imageSmoothingEnabled;this._initShaderProgram=e.initShaderProgram;this._gl=null;this._isWebGL2=!1;this._extTextureFilterAnisotropic=null;this._maxAnisotropy=0;this._firstPass=null;this._secondPass=null;this._glFrameBuffer=null;this._renderToTexture=null;this._glNumTextures=0;this._unitQuad=null;this._destroyed=!1;this._gl=this._renderingCanvas.getContext("webgl2");if(this._gl){this._isWebGL2=!0;this._setupWebGLExtensions()}else{this._gl=this._renderingCanvas.getContext("webgl");this._isWebGL2=!1;this._gl&&this._setupWebGLExtensions()}this._gl&&this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,this._unpackWithPremultipliedAlpha)}getContext(){return this._gl}isWebGL2(){return this._isWebGL2}getMaxTextures(){return this._gl?this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS):0}getRenderingCanvas(){return this._renderingCanvas}getFirstPass(){return this._firstPass}getSecondPass(){return this._secondPass}getFrameBuffer(){return this._glFrameBuffer}getRenderToTexture(){return this._renderToTexture}getUnitQuad(){return this._unitQuad}_setupWebGLExtensions(){const e=this._gl;this._extTextureFilterAnisotropic=e.getExtension("EXT_texture_filter_anisotropic")||e.getExtension("WEBKIT_EXT_texture_filter_anisotropic")||e.getExtension("MOZ_EXT_texture_filter_anisotropic");this._extTextureFilterAnisotropic&&(this._maxAnisotropy=e.getParameter(this._extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT))}getTextureFilter(){var e=this._gl;return this._imageSmoothingEnabled?e.LINEAR:e.NEAREST}_applyAnisotropy(){if(this._imageSmoothingEnabled&&this._extTextureFilterAnisotropic&&!(this._maxAnisotropy<=0)){const e=this._gl;e.texParameterf(e.TEXTURE_2D,this._extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(4,this._maxAnisotropy))}}setupRenderer(e,t){const i=this._gl;if(i){this._unitQuad=this.makeQuadVertexBuffer(0,1,0,1);this._makeFirstPassShaderProgram();this._makeSecondPassShaderProgram();this._renderToTexture=i.createTexture();i.activeTexture(i.TEXTURE0);i.bindTexture(i.TEXTURE_2D,this._renderToTexture);i.texImage2D(i.TEXTURE_2D,0,i.RGBA,e,t,0,i.RGBA,i.UNSIGNED_BYTE,null);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter());this._applyAnisotropy();i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE);this._glFrameBuffer=i.createFramebuffer();i.bindFramebuffer(i.FRAMEBUFFER,this._glFrameBuffer);i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,this._renderToTexture,0);i.enable(i.BLEND);i.blendFunc(i.ONE,i.ONE_MINUS_SRC_ALPHA)}else C.console.error("WebGL context not available for setupRenderer")}resizeRenderer(e,t){const i=this._gl;if(i){i.viewport(0,0,e,t);i.deleteTexture(this._renderToTexture);this._renderToTexture=i.createTexture();i.activeTexture(i.TEXTURE0);i.bindTexture(i.TEXTURE_2D,this._renderToTexture);i.texImage2D(i.TEXTURE_2D,0,i.RGBA,e,t,0,i.RGBA,i.UNSIGNED_BYTE,null);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter());this._applyAnisotropy();i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE);i.bindFramebuffer(i.FRAMEBUFFER,this._glFrameBuffer);i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,this._renderToTexture,0)}}createTexture(e,t={}){const i=this._gl;if(!i)return null;var n=i.createTexture();i.activeTexture(i.TEXTURE0);i.bindTexture(i.TEXTURE_2D,n);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter());i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,this.getTextureFilter());this._applyAnisotropy();try{var r=void 0!==t.unpackWithPremultipliedAlpha?t.unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha;i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,r);i.texImage2D(i.TEXTURE_2D,0,i.RGBA,i.RGBA,i.UNSIGNED_BYTE,e);return n}catch(e){i.deleteTexture(n);return null}}deleteTexture(e){this._gl&&e&&this._gl.deleteTexture(e)}setImageSmoothingEnabled(e){this._imageSmoothingEnabled=!!e}setUnpackWithPremultipliedAlpha(e){this._unpackWithPremultipliedAlpha=!!e;this._gl&&this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,this._unpackWithPremultipliedAlpha)}makeQuadVertexBuffer(e,t,i,n){return new Float32Array([e,n,t,n,e,i,e,i,t,n,t,i])}_makeFirstPassShaderProgram(){const t=this._glNumTextures=this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS);var e=` + attribute vec2 a_output_position; + attribute vec2 a_texture_position; + attribute float a_index; + + ${[...Array(t).keys()].map(e=>`uniform mat3 u_matrix_${e};`).join("\n")} // create a uniform mat3 for each potential tile to draw + + varying vec2 v_texture_position; + varying float v_image_index; + + void main() { + + mat3 transform_matrix; // value will be set by the if/elses in makeConditional() + + ${[...Array(t).keys()].map(e=>`${0n.getUniformLocation(r,"u_matrix_"+e)),uImages:n.getUniformLocation(r,"u_images"),uOpacities:n.getUniformLocation(r,"u_opacities"),bufferOutputPosition:n.createBuffer(),bufferTexturePosition:n.createBuffer(),bufferIndex:n.createBuffer()};n.uniform1iv(this._firstPass.uImages,[...Array(t).keys()]);const o=new Float32Array(12*t);for(let e=0;eArray(6).fill(e)).flat();n.bufferData(n.ARRAY_BUFFER,new Float32Array(i),n.STATIC_DRAW);n.enableVertexAttribArray(this._firstPass.aIndex)}_makeSecondPassShaderProgram(){const e=this._gl;var t=this._initShaderProgram(e,` + attribute vec2 a_output_position; + attribute vec2 a_texture_position; + + varying vec2 v_texture_position; + + void main() { + // Transform to clip space (0:1 --> -1:1) + gl_Position = vec4(vec3(a_output_position * 2.0 - 1.0, 1), 1); + + v_texture_position = a_texture_position; + } + `,` + precision mediump float; + + // our texture + uniform sampler2D u_image; + + // the texCoords passed in from the vertex shader. + varying vec2 v_texture_position; + + // the opacity multiplier for the image + uniform float u_opacity_multiplier; + + void main() { + gl_FragColor = texture2D(u_image, v_texture_position); + gl_FragColor *= u_opacity_multiplier; + } + `);e.useProgram(t);this._secondPass={shaderProgram:t,aOutputPosition:e.getAttribLocation(t,"a_output_position"),aTexturePosition:e.getAttribLocation(t,"a_texture_position"),uImage:e.getUniformLocation(t,"u_image"),uOpacityMultiplier:e.getUniformLocation(t,"u_opacity_multiplier"),bufferOutputPosition:e.createBuffer(),bufferTexturePosition:e.createBuffer()};e.bindBuffer(e.ARRAY_BUFFER,this._secondPass.bufferOutputPosition);e.bufferData(e.ARRAY_BUFFER,this._unitQuad,e.STATIC_DRAW);e.enableVertexAttribArray(this._secondPass.aOutputPosition);e.bindBuffer(e.ARRAY_BUFFER,this._secondPass.bufferTexturePosition);e.bufferData(e.ARRAY_BUFFER,this._unitQuad,e.DYNAMIC_DRAW);e.enableVertexAttribArray(this._secondPass.aTexturePosition)}destroy(){if(!this._destroyed){this._destroyed=!0;const i=this._gl;if(i){try{var t=i.getParameter(i.MAX_TEXTURE_IMAGE_UNITS);if(t&&00!==e))return!0;C.console.warn("[WebGLDrawer.isSupported] Functional test failed: no non-zero pixels read back.");return!1}catch(e){C.console.warn("[WebGLDrawer.isSupported] Functional test failed:",e&&e.message?e.message:e);return!1}finally{try{t&&e&&e.deleteTexture(t);if(e)e.destroy();else if(i){const u=i.getExtension("WEBGL_lose_context");u&&u.loseContext()}}catch(e){}}}getType(){return"webgl"}isWebGL2(){return!!this._glContext&&this._glContext.isWebGL2()}setContextRecoveryEnabled(e){this._enableContextRecovery=!!e}isContextRecoveryEnabled(){return this._enableContextRecovery}minimumOverlapRequired(e){return e.hasIssue("webgl")}_createDrawingElement(){const e=C.makeNeutralElement("canvas");var t=this._calculateCanvasSize();e.width=t.x;e.height=t.y;return e}_getBackupCanvasDrawer(){if(!this._backupCanvasDrawer){this._backupCanvasDrawer=this.viewer.requestDrawer("canvas",{mainDrawer:!1});this._backupCanvasDrawer.canvas.style.setProperty("visibility","hidden");this._backupCanvasDrawer.getSupportedDataFormats=()=>this._supportedFormats;this._backupCanvasDrawer.getDataToDraw=this.getDataToDraw.bind(this)}return this._backupCanvasDrawer}_draw(e,t=0){const w=this._glContext?this._glContext.getContext():null;if(w){const _=this._glContext.getFirstPass();const T=this._glContext.getSecondPass();const x=this._glContext.getFrameBuffer();const S=this._glContext.getRenderToTexture();var i=this.viewport.getBoundsNoRotateWithMargins(!0);const r=i,o=new l.Point(i.x+i.width/2,i.y+i.height/2),s=this.viewport.getRotation(!0)*Math.PI/180;var n=this.viewport.flipped?-1:1;i=C.Mat3.makeTranslation(-o.x,-o.y);const a=C.Mat3.makeScaling(2/r.width*n,-2/r.height);n=C.Mat3.makeRotation(-s);const E=a.multiply(n).multiply(i);w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT);this._outputContext.clearRect(0,0,this._outputCanvas.width,this._outputCanvas.height);let y=!1;e.forEach((i,e)=>{if(i.getIssue("webgl")){if(y){this._outputContext.drawImage(this._renderingCanvas,0,0);w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT);y=!1}if(this._canvasFallbackAllowed){const t=this._getBackupCanvasDrawer();t.draw([i]);this._outputContext.drawImage(t.canvas,0,0)}}else{const m=i.getTilesToDraw();i.placeholderFillStyle&&!1===i._hasOpaqueTile&&this._drawPlaceholder(i);if(0!==m.length&&0!==i.getOpacity()){var n=m[0];var r=i.compositeOperation||this.viewer.compositeOperation||i._clip||i._croppingPolygons||i.debugMode;var o=r||i.opacity<1||n.tile.hasTransparency;if(r){y&&this._outputContext.drawImage(this._renderingCanvas,0,0);w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT)}w.useProgram(_.shaderProgram);if(o){w.bindFramebuffer(w.FRAMEBUFFER,x);w.clear(w.COLOR_BUFFER_BIT)}else w.bindFramebuffer(w.FRAMEBUFFER,null);let t=E;var s=i.getRotation(!0);if(s%360!=0){n=C.Mat3.makeRotation(-s*Math.PI/180);s=i.getBoundsNoRotate(!0).getCenter();const v=C.Mat3.makeTranslation(s.x,s.y);s=C.Mat3.makeTranslation(-s.x,-s.y);s=v.multiply(n).multiply(s);t=E.multiply(s)}var a=this._glContext.getMaxTextures();if(a<=0||null==a)throw new Error(`WebGL error: bad value for gl parameter MAX_TEXTURE_IMAGE_UNITS (${a}). This could happen + if too many contexts have been created and not released, or there is another problem with the graphics card.`);var l=new Float32Array(12*a);var h=new Array(a);const f=new Array(a);var c=new Array(a);for(let e=0;e{w.uniformMatrix3fv(_.uTransformMatrices[t],!1,e)});w.uniform1fv(_.uOpacities,new Float32Array(c));w.bindBuffer(w.ARRAY_BUFFER,_.bufferOutputPosition);w.vertexAttribPointer(_.aOutputPosition,2,w.FLOAT,!1,0,0);w.bindBuffer(w.ARRAY_BUFFER,_.bufferTexturePosition);w.vertexAttribPointer(_.aTexturePosition,2,w.FLOAT,!1,0,0);w.bindBuffer(w.ARRAY_BUFFER,_.bufferIndex);w.vertexAttribPointer(_.aIndex,1,w.FLOAT,!1,0,0);w.drawArrays(w.TRIANGLES,0,6*p)}}if(o){w.useProgram(T.shaderProgram);w.bindFramebuffer(w.FRAMEBUFFER,null);w.activeTexture(w.TEXTURE0);w.bindTexture(w.TEXTURE_2D,S);w.uniform1f(T.uOpacityMultiplier,i.opacity);w.bindBuffer(w.ARRAY_BUFFER,T.bufferTexturePosition);w.vertexAttribPointer(T.aTexturePosition,2,w.FLOAT,!1,0,0);w.bindBuffer(w.ARRAY_BUFFER,T.bufferOutputPosition);w.vertexAttribPointer(T.aOutputPosition,2,w.FLOAT,!1,0,0);w.drawArrays(w.TRIANGLES,0,6)}y=!0;if(r){this._applyContext2dPipeline(i,m,e);y=!1;w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT)}0===e&&this._raiseTiledImageDrawnEvent(i,m.map(e=>e.tile))}}});y&&this._outputContext.drawImage(this._renderingCanvas,0,0)}}draw(t,i=!1){try{this._draw(t,i)}catch(e){if(!this._isWebGLContextError(e))throw e;if(this._enableContextRecovery&&!i){C.console.warn("WebGL context error detected during draw operation, attempting to recreate context...",e);if(this._recreateContext()){C.console.info("WebGL context recreated successfully, retrying draw operation");this.viewer&&this.viewer.raiseEvent("webgl-context-recovered",{drawer:this,error:e});this.draw(t,!0)}else this._fallbackToCanvasDrawer(e,t)}else{if(!this._enableContextRecovery)throw e;this._fallbackToCanvasDrawer(e,t)}}}setImageSmoothingEnabled(e){if(this._imageSmoothingEnabled!==e){this._imageSmoothingEnabled=e;this._glContext&&this._glContext.setImageSmoothingEnabled(e);this.setInternalCacheNeedsRefresh();this.viewer.forceRedraw()}}setUnpackWithPremultipliedAlpha(e){if(this._unpackWithPremultipliedAlpha!==e){this._unpackWithPremultipliedAlpha=e;this._glContext&&this._glContext.setUnpackWithPremultipliedAlpha(e);this.setInternalCacheNeedsRefresh();this.viewer.forceRedraw()}}drawDebuggingRect(e){const t=this._outputContext;t.save();t.lineWidth=2*C.pixelDensityRatio;t.strokeStyle=this.debugGridColor[0];t.fillStyle=this.debugGridColor[0];t.strokeRect(e.x*C.pixelDensityRatio,e.y*C.pixelDensityRatio,e.width*C.pixelDensityRatio,e.height*C.pixelDensityRatio);t.restore()}_applyContext2dPipeline(e,t,i){this._outputContext.save();this._outputContext.globalCompositeOperation=0===i?null:e.compositeOperation||this.viewer.compositeOperation;if(e._croppingPolygons||e._clip){this._renderToClippingCanvas(e);this._outputContext.drawImage(this._clippingCanvas,0,0)}else this._outputContext.drawImage(this._renderingCanvas,0,0);this._outputContext.restore();if(e.debugMode){i=this.viewer.viewport.getFlip();i&&this._flip();this._drawDebugInfo(t,e,i);i&&this._flip()}}_getTileData(e,t,i,n,r,o,s,a,l){var h=i.texture;var c=i.position;var u=i.overlapFraction;o.set(c,12*r);i=e.positionedBounds.width*u.x;o=e.positionedBounds.height*u.y;c=e.positionedBounds.x+(0===e.x?0:i);u=e.positionedBounds.y+(0===e.y?0:o);i=e.positionedBounds.x+e.positionedBounds.width-(e.isRightMost?0:i);o=e.positionedBounds.y+e.positionedBounds.height-(e.isBottomMost?0:o);const d=new C.Mat3([i-c,0,0,0,o-u,0,c,u,1]);e.flipped&&d.scaleAndTranslateSelf(-1,1,1,0);d.scaleAndTranslateOtherSetSelf(n);l[r]=e.opacity;s[r]=h;a[r]=d.values}_setupRenderer(){this._glContext&&this._glContext.getContext()?this._glContext.setupRenderer(this._renderingCanvas.width,this._renderingCanvas.height):C.console.error("_setupCanvases must be called before _setupRenderer")}_resizeRenderer(){this._glContext&&this._glContext.resizeRenderer(this._renderingCanvas.width,this._renderingCanvas.height)}_setupCanvases(){const t=this;this._outputCanvas=this.canvas;this._outputContext=this._outputCanvas.getContext("2d");this._renderingCanvas=document.createElement("canvas");this._clippingCanvas=document.createElement("canvas");this._clippingContext=this._clippingCanvas.getContext("2d");this._renderingCanvas.width=this._clippingCanvas.width=this._outputCanvas.width;this._renderingCanvas.height=this._clippingCanvas.height=this._outputCanvas.height;this._glContext=new d({renderingCanvas:this._renderingCanvas,unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha,imageSmoothingEnabled:this._imageSmoothingEnabled,initShaderProgram:this.constructor.initShaderProgram});this._resizeHandler=function(){if(t._outputCanvas!==t.viewer.drawer.canvas){t._outputCanvas.style.width=t.viewer.drawer.canvas.clientWidth+"px";t._outputCanvas.style.height=t.viewer.drawer.canvas.clientHeight+"px"}var e=t._calculateCanvasSize();if(t._outputCanvas.width!==e.x||t._outputCanvas.height!==e.y){t._outputCanvas.width=e.x;t._outputCanvas.height=e.y}t._renderingCanvas.style.width=t._outputCanvas.clientWidth+"px";t._renderingCanvas.style.height=t._outputCanvas.clientHeight+"px";t._renderingCanvas.width=t._clippingCanvas.width=t._outputCanvas.width;t._renderingCanvas.height=t._clippingCanvas.height=t._outputCanvas.height;t._resizeRenderer()};this.viewer.addHandler("resize",this._resizeHandler)}_isWebGLContextError(e){if(!e||!e.message)return!1;const t=e.message.toLowerCase();return t.includes("max_texture_image_units")||t.includes("webgl")&&(t.includes("context")||t.includes("lost")||t.includes("invalid"))}_recreateContext(){if(this._destroyed)return null;try{var e=this._renderingCanvas;var t=e.width;var i=e.height;var n=e.style.width;var r=e.style.height;this.destroyInternalCache();if(this._glContext){this._glContext.destroy();this._glContext=null}this._renderingCanvas=document.createElement("canvas");this._renderingCanvas.width=t;this._renderingCanvas.height=i;n&&(this._renderingCanvas.style.width=n);r&&(this._renderingCanvas.style.height=r);this._glContext=new d({renderingCanvas:this._renderingCanvas,unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha,imageSmoothingEnabled:this._imageSmoothingEnabled,initShaderProgram:this.constructor.initShaderProgram});if(!this._glContext.getContext()){C.console.error("Failed to recreate WebGL context: no GL context");return null}try{var o=this._glContext.getMaxTextures();if(!o||o<=0){C.console.error("Failed to recreate WebGL context: invalid MAX_TEXTURE_IMAGE_UNITS");return null}}catch(e){C.console.error("Failed to verify new WebGL context:",e);return null}this._setupRenderer();this.setInternalCacheNeedsRefresh();return this}catch(e){C.console.error("Failed to recreate WebGL context:",e);return null}}_fallbackToCanvasDrawer(e,t){if(!this._canvasFallbackAllowed){this._raiseContextRecoveryFailedEvent(e,null);throw e}var i=this.viewer.requestDrawer("canvas",{mainDrawer:!0,redrawImmediately:!1});if(!i){C.console.error("Failed to create canvas drawer as fallback");this._raiseContextRecoveryFailedEvent(e,null);throw e}C.console.error("Failed to recreate WebGL context, switching to canvas drawer");this._raiseContextRecoveryFailedEvent(e,i);this.viewer.world.requestInvalidate(!0)}_raiseContextRecoveryFailedEvent(e,t=null){this.viewer&&this.viewer.raiseEvent("webgl-context-recovery-failed",{drawer:this,canvasDrawer:t,error:e})}internalCacheCreate(i,n){const r=n.tiledImage;if(!(this._glContext?this._glContext.getContext():null)){C.console.error("WebGL context not available in internalCacheCreate");return{}}let o;let s=i.data;let e=!1;if(s instanceof CanvasRenderingContext2D){s=s.canvas;e=!0}if(!r.getIssue("webgl"))if(e&&C.isCanvasTainted(s)){r.setIssue("webgl","WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?");this._raiseDrawerErrorEvent(r,this._canvasFallbackAllowed?"Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.":"Tainted data cannot be used by the WebGLDrawer, and canvas fallback is not enabled.");this.setInternalCacheNeedsRefresh()}else{let e,t;if(n.sourceBounds){e=Math.min(n.sourceBounds.width,s.width)/s.width;t=Math.min(n.sourceBounds.height,s.height)/s.height}else{e=1;t=1}var a=r.source.tileOverlap;var l=this._calculateOverlapFraction(n,r);if(0{e=t.imageToViewportCoordinates(e.x,e.y,!0).rotate(this.viewer.viewport.getRotation(!0),this.viewer.viewport.getCenter(!0));return this.viewportCoordToDrawerCoord(e)});this._clippingContext.beginPath();n.forEach((e,t)=>{this._clippingContext[0===t?"moveTo":"lineTo"](e.x,e.y)});this._clippingContext.clip();this._setClip()}if(t._croppingPolygons){const r=t._croppingPolygons.map(e=>e.map(e=>{e=t.imageToViewportCoordinates(e.x,e.y,!0).rotate(this.viewer.viewport.getRotation(!0),this.viewer.viewport.getCenter(!0));return this.viewportCoordToDrawerCoord(e)}));this._clippingContext.beginPath();r.forEach(e=>{e.forEach((e,t)=>{this._clippingContext[0===t?"moveTo":"lineTo"](e.x,e.y)})});this._clippingContext.clip()}if(this.viewer.viewport.getFlip()){e=new C.Point(this.canvas.width/2,this.canvas.height/2);this._clippingContext.translate(e.x,0);this._clippingContext.scale(-1,1);this._clippingContext.translate(-e.x,0)}this._clippingContext.drawImage(this._renderingCanvas,0,0);this._clippingContext.restore()}_setRotations(e){let t=!1;if(this.viewport.getRotation(!0)%360!=0){this._offsetForRotation({degrees:this.viewport.getRotation(!0),saveContext:t});t=!1}e.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:e.getRotation(!0),point:this.viewport.pixelFromPointNoRotate(e._getRotationPoint(!0),!0),saveContext:t})}_offsetForRotation(e){var t=e.point?e.point.times(C.pixelDensityRatio):this._getCanvasCenter();const i=this._outputContext;i.save();i.translate(t.x,t.y);i.rotate(Math.PI/180*e.degrees);i.translate(-t.x,-t.y)}_flip(e){e=(e=e||{}).point?e.point.times(C.pixelDensityRatio):this._getCanvasCenter();const t=this._outputContext;t.translate(e.x,0);t.scale(-1,1);t.translate(-e.x,0)}_drawDebugInfo(t,i,n){for(let e=t.length-1;0<=e;e--){var r=t[e].tile;try{this._drawDebugInfoOnTile(r,t.length,e,i,n)}catch(e){C.console.error(e)}}}_drawDebugInfoOnTile(e,t,i,n,r){var o=this.viewer.world.getIndexOfItem(n)%this.debugGridColor.length;const s=this.context;s.save();s.lineWidth=2*C.pixelDensityRatio;s.font="small-caps bold "+13*C.pixelDensityRatio+"px arial";s.strokeStyle=this.debugGridColor[o];s.fillStyle=this.debugGridColor[o];this._setRotations(n);r&&this._flip({point:e.position.plus(e.size.divide(2))});s.strokeRect(e.position.x*C.pixelDensityRatio,e.position.y*C.pixelDensityRatio,e.size.x*C.pixelDensityRatio,e.size.y*C.pixelDensityRatio);var a=(e.position.x+e.size.x/2)*C.pixelDensityRatio;o=(e.position.y+e.size.y/2)*C.pixelDensityRatio;s.translate(a,o);r=this.viewport.getRotation(!0);s.rotate(Math.PI/180*-r);s.translate(-a,-o);if(0===e.x&&0===e.y){s.fillText("Zoom: "+this.viewport.getZoom(),e.position.x*C.pixelDensityRatio,(e.position.y-30)*C.pixelDensityRatio);s.fillText("Pan: "+this.viewport.getBounds().toString(),e.position.x*C.pixelDensityRatio,(e.position.y-20)*C.pixelDensityRatio)}s.fillText("Level: "+e.level,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+20)*C.pixelDensityRatio);s.fillText("Column: "+e.x,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+30)*C.pixelDensityRatio);s.fillText("Row: "+e.y,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+40)*C.pixelDensityRatio);s.fillText("Order: "+i+" of "+t,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+50)*C.pixelDensityRatio);s.fillText("Size: "+e.size.toString(),(e.position.x+10)*C.pixelDensityRatio,(e.position.y+60)*C.pixelDensityRatio);s.fillText("Position: "+e.position.toString(),(e.position.x+10)*C.pixelDensityRatio,(e.position.y+70)*C.pixelDensityRatio);this.viewport.getRotation(!0)%360!=0&&this._restoreRotationChanges();n.getRotation(!0)%360!=0&&this._restoreRotationChanges();s.restore()}_drawPlaceholder(e){var t=e.getBounds(!0);var i=this.viewportToDrawerRectangle(e.getBounds(!0));const n=this._outputContext;let r;r="function"==typeof e.placeholderFillStyle?e.placeholderFillStyle(e,n):e.placeholderFillStyle;this._offsetForRotation({degrees:this.viewer.viewport.getRotation(!0)});n.fillStyle=r;n.translate(i.x,i.y);n.rotate(Math.PI/180*t.degrees);n.translate(-i.x,-i.y);n.fillRect(i.x,i.y,i.width,i.height);this._restoreRotationChanges()}_getCanvasCenter(){return new C.Point(this.canvas.width/2,this.canvas.height/2)}_restoreRotationChanges(){const e=this._outputContext;e.restore()}static initShaderProgram(e,t,i){function n(e,t,i){t=e.createShader(t);e.shaderSource(t,i);e.compileShader(t);if(e.getShaderParameter(t,e.COMPILE_STATUS))return t;C.console.error("An error occurred compiling the shaders: "+e.getShaderInfoLog(t));e.deleteShader(t);return null}var r=n(e,e.VERTEX_SHADER,t);t=n(e,e.FRAGMENT_SHADER,i);i=e.createProgram();e.attachShader(i,r);e.attachShader(i,t);e.linkProgram(i);if(e.getProgramParameter(i,e.LINK_STATUS))return i;C.console.error("Unable to initialize the shader program: "+e.getProgramInfoLog(i));return null}}}(OpenSeadragon);!function(c){c.Viewport=function(e){var t=arguments;if((e=t.length&&t[0]instanceof c.Point?{containerSize:t[0],contentSize:t[1],config:t[2]}:e).config){c.extend(!0,e,e.config);delete e.config}this._margins=c.extend({left:0,top:0,right:0,bottom:0},e.margins||{});delete e.margins;e.initialDegrees=e.degrees;delete e.degrees;c.extend(!0,this,{containerSize:null,contentSize:null,zoomPoint:null,rotationPivot:null,viewer:null,springStiffness:c.DEFAULT_SETTINGS.springStiffness,animationTime:c.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:c.DEFAULT_SETTINGS.minZoomImageRatio,maxZoomPixelRatio:c.DEFAULT_SETTINGS.maxZoomPixelRatio,visibilityRatio:c.DEFAULT_SETTINGS.visibilityRatio,wrapHorizontal:c.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:c.DEFAULT_SETTINGS.wrapVertical,defaultZoomLevel:c.DEFAULT_SETTINGS.defaultZoomLevel,minZoomLevel:c.DEFAULT_SETTINGS.minZoomLevel,maxZoomLevel:c.DEFAULT_SETTINGS.maxZoomLevel,initialDegrees:c.DEFAULT_SETTINGS.degrees,flipped:c.DEFAULT_SETTINGS.flipped,homeFillsViewer:c.DEFAULT_SETTINGS.homeFillsViewer,silenceMultiImageWarnings:c.DEFAULT_SETTINGS.silenceMultiImageWarnings},e);this._updateContainerInnerSize();this.centerSpringX=new c.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.centerSpringY=new c.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.zoomSpring=new c.Spring({exponential:!0,initial:1,springStiffness:this.springStiffness,animationTime:this.animationTime});this.degreesSpring=new c.Spring({initial:e.initialDegrees,springStiffness:this.springStiffness,animationTime:this.animationTime});this._oldCenterX=this.centerSpringX.current.value;this._oldCenterY=this.centerSpringY.current.value;this._oldZoom=this.zoomSpring.current.value;this._oldDegrees=this.degreesSpring.current.value;this._sizeChanged=!1;this._setContentBounds(new c.Rect(0,0,1,1),1);this.goHome(!0);this.update()};c.Viewport.prototype={get degrees(){c.console.warn("Accessing [Viewport.degrees] is deprecated. Use viewport.getRotation instead.");return this.getRotation()},set degrees(e){c.console.warn("Setting [Viewport.degrees] is deprecated. Use viewport.rotateTo, viewport.rotateBy, or viewport.setRotation instead.");this.rotateTo(e)},resetContentSize:function(e){c.console.assert(e,"[Viewport.resetContentSize] contentSize is required");c.console.assert(e instanceof c.Point,"[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point");c.console.assert(0r.width?this.visibilityRatio*r.width:this.visibilityRatio*n.width;t=r.x-a+e;i=l-n.x-e;if(e>r.width){n.x+=(t+i)/2;o=!0}else if(i<0){n.x+=i;o=!0}else if(0r.height?this.visibilityRatio*r.height:this.visibilityRatio*n.height;t=r.y-a+e;i=l-n.y-e;if(e>r.height){n.y+=(t+i)/2;s=!0}else if(i<0){n.y+=i;s=!0}else if(0=r?s.height=s.width/r:s.width=s.height*r;s.x=o.x-s.width/2;s.y=o.y-s.height/2;let a=1/s.width;if(i){this.panTo(o,!0);this.zoomTo(a,null,!0);n&&this.applyConstraints(!0);return this}t=this.getCenter(!0);e=this.getZoom(!0);this.panTo(t,!0);this.zoomTo(e,null,!0);const l=this.getBounds();r=this.getZoom();if(0===r||Math.abs(a/r-1)<1e-8){this.zoomTo(a,null,!0);this.panTo(o,i);n&&this.applyConstraints(!1);return this}if(n){this.panTo(o,!1);a=this._applyZoomConstraints(a);this.zoomTo(a,null,!1);o=this.getConstrainedBounds();this.panTo(t,!0);this.zoomTo(e,null,!0);this.fitBounds(o)}else{const h=s.rotate(-this.getRotation());r=h.getTopLeft().times(a).minus(l.getTopLeft().times(r)).divide(a-r);this.zoomTo(a,r,i)}return this},fitBounds:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!1})},fitBoundsWithConstraints:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!0})},fitVertically:function(e){var t=new c.Rect(this._contentBounds.x+this._contentBounds.width/2,this._contentBounds.y,0,this._contentBounds.height);return this.fitBounds(t,e)},fitHorizontally:function(e){var t=new c.Rect(this._contentBounds.x,this._contentBounds.y+this._contentBounds.height/2,this._contentBounds.width,0);return this.fitBounds(t,e)},getConstrainedBounds:function(e){e=this.getBounds(e);return this._applyBoundaryConstraints(e)},panBy:function(e,t){const i=new c.Point;if(t){i.x=this.centerSpringX.current.value;i.y=this.centerSpringY.current.value}else{i.x=this.centerSpringX.target.value;i.y=this.centerSpringY.target.value}return this.panTo(i.plus(e),t)},panTo:function(e,t){if(t){this.centerSpringX.resetTo(e.x);this.centerSpringY.resetTo(e.y)}else{this.centerSpringX.springTo(e.x);this.centerSpringY.springTo(e.y)}this.viewer&&this.viewer.raiseEvent("pan",{center:e,immediately:t});return this},zoomBy:function(e,t,i){return this.zoomTo(this.zoomSpring.target.value*e,t,i)},zoomTo:function(e,t,i){const n=this;this.zoomPoint=t instanceof c.Point&&!isNaN(t.x)&&!isNaN(t.y)?t:null;i?this._adjustCenterSpringsForZoomPoint(function(){n.zoomSpring.resetTo(e)}):this.zoomSpring.springTo(e);this.viewer&&this.viewer.raiseEvent("zoom",{zoom:e,refPoint:t,immediately:i});return this},setRotation:function(e,t){return this.rotateTo(e,null,t)},getRotation:function(e){return(e?this.degreesSpring.current:this.degreesSpring.target).value},setRotationWithPivot:function(e,t,i){return this.rotateTo(e,t,i)},rotateTo:function(t,i,e){if(!this.viewer||!this.viewer.drawer.canRotate())return this;if(this.degreesSpring.target.value===t&&this.degreesSpring.isAtTargetValue())return this;this.rotationPivot=i instanceof c.Point&&!isNaN(i.x)&&!isNaN(i.y)?i:null;if(e)if(this.rotationPivot){if(!(t-this._oldDegrees)){this.rotationPivot=null;return this}this._rotateAboutPivot(t)}else this.degreesSpring.resetTo(t);else{var n=c.positiveModulo(this.degreesSpring.current.value,360);let e=c.positiveModulo(t,360);i=e-n;180this.getMaxZoom()&&this.applyConstraints(i)}}}}(OpenSeadragon);!function(v){v.TiledImage=function(e){this._initialized=!1;v.console.assert(e.tileCache,"[TiledImage] options.tileCache is required");v.console.assert(e.drawer,"[TiledImage] options.drawer is required");v.console.assert(e.viewer,"[TiledImage] options.viewer is required");v.console.assert(e.imageLoader,"[TiledImage] options.imageLoader is required");v.console.assert(e.source,"[TiledImage] options.source is required");v.console.assert(!e.clip||e.clip instanceof v.Rect,"[TiledImage] options.clip must be an OpenSeadragon.Rect if present");v.EventSource.call(this);this._optimalWorldIndex=void 0;this._tileCache=e.tileCache;delete e.tileCache;this._drawer=e.drawer;delete e.drawer;this._imageLoader=e.imageLoader;delete e.imageLoader;e.clip instanceof v.Rect&&(this._clip=e.clip.clone());delete e.clip;var t=e.x||0;delete e.x;var i=e.y||0;delete e.y;this.normHeight=e.source.dimensions.y/e.source.dimensions.x;this.contentAspectX=e.source.dimensions.x/e.source.dimensions.y;let n=1;if(e.width){n=e.width;delete e.width;if(e.height){v.console.error("specifying both width and height to a tiledImage is not supported");delete e.height}}else if(e.height){n=e.height/this.normHeight;delete e.height}var r=e.fitBounds;delete e.fitBounds;var o=e.fitBoundsPlacement||OpenSeadragon.Placement.CENTER;delete e.fitBoundsPlacement;var s=e.degrees||0;delete e.degrees;var a=e.ajaxHeaders;delete e.ajaxHeaders;this.crossOriginPolicy=e.crossOriginPolicy;delete e.crossOriginPolicy;v.extend(!0,this,{viewer:null,tilesMatrix:{},coverage:{},loadingCoverage:{},lastResetTime:0,_needsDraw:!0,_needsUpdate:!0,_hasOpaqueTile:!1,_tilesLoading:0,_zombieCache:!1,_tilesToDraw:[],_lastDrawn:[],_arrayCacheMap:[],_isBlending:!1,_wasBlending:!1,_issues:{},springStiffness:v.DEFAULT_SETTINGS.springStiffness,animationTime:v.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:v.DEFAULT_SETTINGS.minZoomImageRatio,wrapHorizontal:v.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:v.DEFAULT_SETTINGS.wrapVertical,immediateRender:v.DEFAULT_SETTINGS.immediateRender,loadDestinationTilesOnAnimation:v.DEFAULT_SETTINGS.loadDestinationTilesOnAnimation,blendTime:v.DEFAULT_SETTINGS.blendTime,alwaysBlend:v.DEFAULT_SETTINGS.alwaysBlend,minPixelRatio:v.DEFAULT_SETTINGS.minPixelRatio,smoothTileEdgesMinZoom:v.DEFAULT_SETTINGS.smoothTileEdgesMinZoom,iOSDevice:v.DEFAULT_SETTINGS.iOSDevice,debugMode:v.DEFAULT_SETTINGS.debugMode,ajaxWithCredentials:v.DEFAULT_SETTINGS.ajaxWithCredentials,placeholderFillStyle:v.DEFAULT_SETTINGS.placeholderFillStyle,opacity:v.DEFAULT_SETTINGS.opacity,preload:v.DEFAULT_SETTINGS.preload,compositeOperation:v.DEFAULT_SETTINGS.compositeOperation,subPixelRoundingForTransparency:v.DEFAULT_SETTINGS.subPixelRoundingForTransparency,maxTilesPerFrame:v.DEFAULT_SETTINGS.maxTilesPerFrame,originalDataType:void 0,_currentMaxTilesPerFrame:10*(e.maxTilesPerFrame||v.DEFAULT_SETTINGS.maxTilesPerFrame)},e);this._preload=this.preload;delete this.preload;this._fullyLoaded=!1;this._xSpring=new v.Spring({initial:t,springStiffness:this.springStiffness,animationTime:this.animationTime});this._ySpring=new v.Spring({initial:i,springStiffness:this.springStiffness,animationTime:this.animationTime});this._scaleSpring=new v.Spring({initial:n,springStiffness:this.springStiffness,animationTime:this.animationTime});this._degreesSpring=new v.Spring({initial:s,springStiffness:this.springStiffness,animationTime:this.animationTime});this._updateForScale();r&&this.fitBounds(r,o,!0);this._ownAjaxHeaders={};this.setAjaxHeaders(a,!1);this._initialized=!0};v.extend(v.TiledImage.prototype,v.EventSource.prototype,{needsDraw:function(){return this._needsDraw},redraw:function(){this._needsDraw=!0},getFullyLoaded:function(){return this._fullyLoaded},whenFullyLoaded:function(e){this.getFullyLoaded()?setTimeout(e,1):this.addOnceHandler("fully-loaded-change",function(){e()})},_setFullyLoaded:function(e){if(e!==this._fullyLoaded){this._fullyLoaded=e;this.raiseEvent("fully-loaded-change",{fullyLoaded:this._fullyLoaded})}},requestInvalidate:function(e=!0,t=!1,i=v.now()){t=t?this._lastDrawn.map(e=>e.tile):this._tileCache.getLoadedTilesFor(this);return this.viewer.world.requestTileInvalidateEvent(t,i,e)},reset:function(){this._tileCache.clearTilesFor(this);this._currentMaxTilesPerFrame=10*this.maxTilesPerFrame;this.lastResetTime=v.now();this._needsDraw=!0;this._fullyLoaded=!1},update:function(e){var t=this._xSpring.update();var i=this._ySpring.update();var n=this._scaleSpring.update();var r=this._degreesSpring.update();r=t||i||n||r||this._needsUpdate;if(r||e||!this._fullyLoaded){e=this._updateLevelsForViewport();this._setFullyLoaded(e)}this._needsUpdate=!1;if(r){this._updateForScale();this._raiseBoundsChange();return this._needsDraw=!0}return!1},setDrawn:function(){this._needsDraw=this._isBlending||this._wasBlending||0r){o=this._clip.x/this._clip.height*t.height;s=this._clip.y/this._clip.height*t.height}else{o=this._clip.x/this._clip.width*t.width;s=this._clip.y/this._clip.width*t.width}}if(t.getAspectRatio()>r){var h=t.height/l;let e=0;i.isHorizontallyCentered?e=(t.width-t.height*r)/2:i.isRight&&(e=t.width-t.height*r);this.setPosition(new v.Point(t.x-o+e,t.y-s),n);this.setHeight(h,n)}else{h=t.width/a;let e=0;i.isVerticallyCentered?e=(t.height-t.width/r)/2:i.isBottom&&(e=t.height-t.width/r);this.setPosition(new v.Point(t.x-o,t.y-s+e),n);this.setWidth(h,n)}},getClip:function(){return this._clip?this._clip.clone():null},setClip:function(e){v.console.assert(!e||e instanceof v.Rect,"[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null");e instanceof v.Rect?this._clip=e.clone():this._clip=null;this._needsUpdate=!0;this._needsDraw=!0;this.raiseEvent("clip-change")},getFlip:function(){return this.flipped},setFlip:function(e){this.flipped=e},get flipped(){return this._flipped},set flipped(e){var t=this._flipped!==!!e;this._flipped=!!e;if(t&&this._initialized){this.update(!0);this._needsDraw=!0;this._raiseBoundsChange()}},get wrapHorizontal(){return this._wrapHorizontal},set wrapHorizontal(e){var t=this._wrapHorizontal!==!!e;this._wrapHorizontal=!!e;if(this._initialized&&t){this.update(!0);this._needsDraw=!0}},get wrapVertical(){return this._wrapVertical},set wrapVertical(e){var t=this._wrapVertical!==!!e;this._wrapVertical=!!e;if(this._initialized&&t){this.update(!0);this._needsDraw=!0}},get debugMode(){return this._debugMode},set debugMode(e){this._debugMode=!!e;this._needsDraw=!0},getOpacity:function(){return this.opacity},setOpacity:function(e){this.opacity=e},get opacity(){return this._opacity},set opacity(e){if(e!==this.opacity){this._opacity=e;this._needsDraw=!0;this._needsUpdate=!0;this.raiseEvent("opacity-change",{opacity:this.opacity})}},getPreload:function(){return this._preload},setPreload:function(e){this._preload=!!e;this._needsDraw=!0},getRotation:function(e){return(e?this._degreesSpring.current:this._degreesSpring.target).value},setRotation:function(e,t){if(this._degreesSpring.target.value!==e||!this._degreesSpring.isAtTargetValue()){t?this._degreesSpring.resetTo(e):this._degreesSpring.springTo(e);this._needsDraw=!0;this._needsUpdate=!0;this._raiseBoundsChange()}},getDrawArea:function(){if(0===this._opacity&&!this._preload)return!1;let e=this._viewportToTiledImageRectangle(this.viewport.getBoundsWithMargins(!0));if(!this.wrapHorizontal&&!this.wrapVertical){var t=this._viewportToTiledImageRectangle(this.getClippedBounds(!0));e=e.intersection(t)}return e},getLoadArea:function(){let e=this._viewportToTiledImageRectangle(this.viewport.getBoundsWithMargins(!1));if(!this.wrapHorizontal&&!this.wrapVertical){var t=this._viewportToTiledImageRectangle(this.getClippedBounds(!1));e=e.intersection(t)}return e},getTilesToDraw:function(){const e=this._lastDrawn;let t=0;for(const i of this._tilesToDraw)if(Array.isArray(i))for(const n of i)e[t++]=n;else i&&(e[t++]=i);e.length=t;this._updateTilesInViewport(e);t=0;for(const r of this._tilesToDraw)if(Array.isArray(r)){for(const o of r)if(o.tile.loaded){o.tile.beingDrawn=!0;e[t++]=o}}else if(r&&r.tile.loaded){r.tile.beingDrawn=!0;e[t++]=r}e.length=t;return e},_getRotationPoint:function(e){return this.getBoundsNoRotate(e).getCenter()},get compositeOperation(){return this._compositeOperation},set compositeOperation(e){if(e!==this._compositeOperation){this._compositeOperation=e;this._needsDraw=!0;this.raiseEvent("composite-operation-change",{compositeOperation:this._compositeOperation})}},getCompositeOperation:function(){return this._compositeOperation},setCompositeOperation:function(e){this.compositeOperation=e},setAjaxHeaders:function(e,t){if(v.isPlainObject(e=null===e?{}:e)){this._ownAjaxHeaders=e;this._updateAjaxHeaders(t)}else v.console.error("[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object")},_updateAjaxHeaders:function(e){void 0===e&&(e=!0);v.isPlainObject(this.viewer.ajaxHeaders)?this.ajaxHeaders=v.extend({},this.viewer.ajaxHeaders,this._ownAjaxHeaders):this.ajaxHeaders=this._ownAjaxHeaders;if(e){let e,t,i,n;for(const s in this.tilesMatrix){e=this.source.getNumTiles(s);var r=this.tilesMatrix[s];for(const a in r){t=(e.x+a%e.x)%e.x;for(const l in r[a]){i=(e.y+l%e.y)%e.y;n=r[a][l];n.loadWithAjax=this.loadTilesWithAjax;if(n.loadWithAjax){var o=this.source.getTileAjaxHeaders(s,t,i);n.ajaxHeaders=v.extend({},this.ajaxHeaders,o)}else n.ajaxHeaders=null}}}for(let e=0;e=n;t--,e++)a[e]=t;for(let e=i+1;e<=this.source.maxLevel;e++){var l=this.tilesMatrix[e]&&this.tilesMatrix[e][0]&&this.tilesMatrix[e][0][0];if(l&&l.isBottomMost&&l.isRightMost&&l.loaded){a.push(e);break}}let h=!1;for(let e=0;e=this.minPixelRatio)h=!0;else if(!h)continue;var d=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(c),!1).x*this._scaleSpring.current.value;var p=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(Math.max(this.source.getClosestLevel(),0)),!1).x*this._scaleSpring.current.value;p=this.immediateRender?1:p;u=Math.min(1,(u-.5)/.5);d=p/Math.abs(p-d);d=this._updateLevel(c,u,d,t,r,s,o);this.viewer.world.ensureTilesUpToDate(d.tilesToDraw);o=d.bestLoadTileCandidates;this._tilesToDraw[c]=d.tilesToDraw;if(this._providesCoverage(this.coverage,c))break}if(o&&0{var n=this._getTile(t,i,r,l,c);d=d||this._getCachedArray(r,e);this.viewer&&this.viewer.raiseEvent("update-tile",{tiledImage:this,tile:n});this._setCoverage(this.coverage,r,t,i,!1);if(n.exists){if(n.loaded){1===n.opacity&&this._setCoverage(this.coverage,r,t,i,!0);d[p++]={tile:n,level:r,levelOpacity:o,currentTime:l};this._setCoverage(this.loadingCoverage,r,t,i,!0)}this._positionTile(n,this.source.tileOverlap,this.viewport,u,s)}if(a&&!n.loaded){let e=n.loading||this._isCovered(this.loadingCoverage,r,t,i);this._setCoverage(this.loadingCoverage,r,t,i,e);if(n.exists){!n.loading&&this._tryFindTileCacheRecord(n)&&(e=!0);n.loading?this._tilesLoading++:e||(h=this._compareTiles(h,n,this._currentMaxTilesPerFrame))}}});this._currentMaxTilesPerFrame>this.maxTilesPerFrame&&(this._currentMaxTilesPerFrame=Math.max(Math.ceil(this._currentMaxTilesPerFrame/2),this.maxTilesPerFrame));d&&(d.length=p);return{bestLoadTileCandidates:h,tilesToDraw:d||[]}},_visitTiles:function(n,r,o){const e=r.getBoundingBox();var t=this._getCornerTiles(n,e.getTopLeft(),e.getBottomRight());var s=t.topLeft;const a=t.bottomRight;var l=this.source.getNumTiles(n);if(this.getFlip()){a.x+=1;this.wrapHorizontal||(a.x=Math.min(a.x,l.x-1))}var h=Math.max(0,(a.x-s.x)*(a.y-s.y));for(let i=s.x;i<=a.x;i++)for(let t=s.y;t<=a.y;t++){let e;if(this.getFlip()){var c=(l.x+i%l.x)%l.x;e=i+l.x-c-c-1}else e=i;null!==r.intersection(this.getTileBounds(n,e,t))&&o(e,t,h)}},_positionTile:function(e,t,i,n,r){const o=e.bounds.getTopLeft();o.x*=this._scaleSpring.current.value;o.y*=this._scaleSpring.current.value;o.x+=this._xSpring.current.value;o.y+=this._ySpring.current.value;const s=e.bounds.getSize();s.x*=this._scaleSpring.current.value;s.y*=this._scaleSpring.current.value;e.positionedBounds.x=o.x;e.positionedBounds.y=o.y;e.positionedBounds.width=s.x;e.positionedBounds.height=s.y;var a=i.pixelFromPointNoRotate(o,!0);const l=i.pixelFromPointNoRotate(o,!1);let h=i.deltaPixelsFromPointsNoRotate(s,!0);const c=i.deltaPixelsFromPointsNoRotate(s,!1);i=l.plus(c.divide(2));i=n.squaredDistanceTo(i);if(this.getDrawer().minimumOverlapRequired(this)){t||(h=h.plus(new v.Point(1,1)));e.isRightMost&&this.wrapHorizontal&&(h.x+=.75);e.isBottomMost&&this.wrapVertical&&(h.y+=.75)}e.position=a;e.size=h;e.squaredDistance=i;e.visibility=r},_getCornerTiles:function(e,t,i){let n;let r;if(this.wrapHorizontal){n=v.positiveModulo(t.x,1);r=v.positiveModulo(i.x,1)}else{n=Math.max(0,t.x);r=Math.min(1,i.x)}let o;let s;var a=1/this.source.aspectRatio;if(this.wrapVertical){o=v.positiveModulo(t.y,a);s=v.positiveModulo(i.y,a)}else{o=Math.max(0,t.y);s=Math.min(a,i.y)}const l=this.source.getTileAtPoint(e,new v.Point(n,o));const h=this.source.getTileAtPoint(e,new v.Point(r,s));e=this.source.getNumTiles(e);if(this.wrapHorizontal){l.x+=e.x*Math.floor(t.x);h.x+=e.x*Math.floor(i.x)}if(this.wrapVertical){l.y+=e.y*Math.floor(t.y/a);h.y+=e.y*Math.floor(i.y/a)}return{topLeft:l,bottomRight:h}},_tryFindTileCacheRecord:function(e){var t=this._tileCache.getCacheRecord(e.originalCacheKey);if(!t)return!1;e.loading=!0;this._setTileLoaded(e,t.data,null,null,t.type);return!0},_getTile:function(e,t,i,n,r){let o,s,a,l,h,c,u,d,p,g=this.tilesMatrix,m=this.source;let f=g[i];f||(g[i]=f={});f[e]||(f[e]={});if(f[e][t]&&!f[e][t].flipped==!this.flipped)p=f[e][t];else{o=(r.x+e%r.x)%r.x;s=(r.y+t%r.y)%r.y;a=this.getTileBounds(i,e,t);l=m.getTileBounds(i,o,s,!0);h=m.tileExists(i,o,s);c=m.getTileUrl(i,o,s);u=m.getTilePostData(i,o,s);if(this.loadTilesWithAjax){d=m.getTileAjaxHeaders(i,o,s);v.isPlainObject(this.ajaxHeaders)&&(d=v.extend({},this.ajaxHeaders,d))}else d=null;p=new v.Tile(i,e,t,a,h,c,void 0,this.loadTilesWithAjax,d,l,u,m.getTileHashKey(i,o,s,c,d,u));this.getFlip()?0==o&&(p.isRightMost=!0):o==r.x-1&&(p.isRightMost=!0);s==r.y-1&&(p.isBottomMost=!0);p.flipped=this.flipped;f[e][t]=p}p.lastTouchTime=n;return p},_loadTile:function(o,s){const a=this;o.loading=!0;(o.tiledImage=this)._imageLoader.addJob({src:o.getUrl(),tile:o,source:this.source,postData:o.postData,loadWithAjax:o.loadWithAjax,ajaxHeaders:o.ajaxHeaders,crossOriginPolicy:this.crossOriginPolicy,ajaxWithCredentials:this.ajaxWithCredentials,callback:function(e,t,i,n,r){a._onTileLoad(o,s,e,t,i,n,r)},abort:function(){o.loading=!1}})||this.viewer.raiseEvent("job-queue-full",{tile:o,tiledImage:this,time:s})},_onTileLoad:function(t,e,i,n,r,o,s){if(null!=i){t.exists=!0;if(e{this._setTileLoaded(t,e,null,r,l)}).catch(e=>{v.console.warn("Failed to satisfy original type [%s] %s from %s: %s",l,t,o,e);this._setTileLoaded(t,i,null,r,o)})}else{v.console.warn("Ignoring default base tile data type %s: no conversion possible from %s",this.originalDataType,o);this._setTileLoaded(t,i,null,r,o)}}else this._setTileLoaded(t,i,null,r,o)}else{v.console.error("Tile %s failed to load: %s - error: %s",t,t.getUrl(),n);this.viewer.raiseEvent("tile-load-failed",{tile:t,tiledImage:this,time:e,message:n,tileRequest:r,tries:s,maxReached:0===this.viewer.tileRetryMax||s>=this.viewer.tileRetryMax});t.loading=!1;t.exists=!1}},_setTileLoaded:function(i,t,e,n,r){i.tiledImage=this;v.console.assert(void 0!==r,"TileSource::downloadTileStart must return a dataType.");let o=!1;i.addCache(i.cacheKey,()=>{o=!0;return t},r,!1,!1);let s=null,a=0,l=!1;const h=this;function c(){a--;if(!(0{s=e}),get image(){v.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile-invalidated' event to modify data instead.");return t},get data(){v.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile-invalidated' event to modify data instead.");return t},getCompletionCallback:function(){v.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: use async event handlers instead, execution order is deducted by addHandler(...) priority argument.");return u()}}).catch(()=>{v.console.error("[tile-loaded] event finished with failure: there might be a problem with a plugin you are using.")}).then(e)}if(o)this.viewer.world.requestTileInvalidateEvent([i],void 0,!1,!0,!0).then(d).catch(d);else{r=i.getCache(i.originalCacheKey);const p=e=>{if(this.viewer.isDestroyed())return v.Promise.resolve();var t=this.getDrawer();return e.isUsableForDrawer(t)?v.Promise.resolve():e.prepareForRendering(t)};for(const g of r._tiles){if(g.cacheKey!==i.cacheKey){const m=g.getCache();p(m).then(()=>i.setCache(g.cacheKey,m,!0,!1)).then(d);return}if(g.processing){g.processingPromise.then(e=>{const t=e.getCache();p(t).then(()=>{i.setCache(e.cacheKey,t,!0,!1);return t.loaded?null:t.await()}).then(d)});return}}p(r).then(d)}},_compareTiles:function(t,i,e){if(!t)return[i];let n=!1;for(let e=0;ee&&t.pop();return t},_sortTilesComparator:function(e,t){return null===e?1:null===t?-1:e.visibility===t.visibility?e.squaredDistance-t.squaredDistance:t.visibility-e.visibility},_getCachedArray:function(e,t=void 0){let i=this._arrayCacheMap[e];i?void 0!==t&&(i.length=t):i=this._arrayCacheMap[e]=void 0!==t?new Array(t):[];return i},_providesCoverage:function(e,t,i,n){var r;var o;let s,a;if(!e[t])return!1;if(void 0!==i&&void 0!==n)return void 0===e[t][i]||void 0===e[t][i][n]||!0===e[t][i][n];for(s in r=e[t])if(Object.prototype.hasOwnProperty.call(r,s))for(a in o=r[s])if(Object.prototype.hasOwnProperty.call(o,a)&&!o[a])return!1;return!0},_isCovered:function(e,t,i,n){return void 0===i||void 0===n?this._providesCoverage(e,t+1):this._providesCoverage(e,t+1,2*i,2*n)&&this._providesCoverage(e,t+1,2*i,2*n+1)&&this._providesCoverage(e,t+1,2*i+1,2*n)&&this._providesCoverage(e,t+1,2*i+1,2*n+1)},_setCoverage:function(e,t,i,n,r){if(e[t]){e[t][i]||(e[t][i]={});e[t][i][n]=r}else v.console.warn("Setting coverage for a tile before its level's coverage has been reset: %s",t)},_resetCoverage:function(e,t){e[t]={}}})}(OpenSeadragon);!function(c){const e=c;const r=Symbol("DRAWER_INTERNAL_CACHE");e.CacheRecord=class{constructor(){this.revive()}get data(){return this._data}get type(){return this._type}await(){return this._promise||c.Promise.resolve(this._data)}getImage(){c.console.error("[CacheRecord.getImage] options.image is deprecated. Moreover, it might not work correctly as the cache system performs conversion asynchronously in case the type needs to be converted.");this.transformTo("image");return this.data}getRenderedContext(){c.console.error("[CacheRecord.getRenderedContext] options.getRenderedContext is deprecated. Moreover, it might not work correctly as the cache system performs conversion asynchronously in case the type needs to be converted.");this.transformTo("context2d");return this.data}setDataAs(e,t){c.console.assert(null!=e,"[CacheRecord.setDataAs] needs valid data to set!");if(this._conversionJobQueue){let i=null;var n=new c.Promise((e,t)=>{i=e});this._conversionJobQueue.push(()=>i(this._overwriteData(e,t)));return n}return this._overwriteData(e,t)}getDataAs(t=void 0,i=!0){return this.loaded?t===this._type?i?c.converter.copy(this._tRef,this._data,t||this._type):this._promise:this._transformDataIfNeeded(this._tRef,this._data,t||this._type,i)||this._promise:this._promise.then(e=>this._transformDataIfNeeded(this._tRef,e,t||this._type,i)||e)}_transformDataIfNeeded(e,t,i,n){if(this._destroyed)return c.Promise.resolve();let r;i!==this._type?r=c.converter.convert(e,t,this._type,i):n&&(r=c.converter.copy(e,t,i));return!!r&&r.then(e=>{if(!this._destroyed)return e;c.converter.destroy(e,i)}).catch(e=>{this._handleConversionError(e)})}getDataForRendering(e,t){if(this._destroyed){c.console.error(`Attempt to draw tile with destroyed main cache ${this}!`);t._unload()}else if(this.loaded)if(this._destroyed){c.console.error(`Attempt to draw tile with destroyed main cache ${this}!`);t._unload()}else{const i=e.getSupportedDataFormats();if(i.includes(this.type)){if(!e.options.usePrivateCache)return this;if(!e.options.preloadCache)return this.prepareInternalCacheSync(e);t=this._getInternalCacheRef(e);if(t&&t.loaded)return t;c.console.error(`Attempt to draw tile cache ${this} with internal cache non-ready state!`)}else{c.console.error(`Attempt to draw tile cache ${this} with unsupported type '${this.type}' for the target drawer!`);this.prepareForRendering(e)}}else this._promise||c.console.error(`Attempt to draw cache ${this} when not loaded!`)}isUsableForDrawer(e){const t=e.getSupportedDataFormats();if(!t.includes(this.type))return!1;if(e.options.usePrivateCache)if(!this._getInternalCacheRef(e))return!1;return!0}prepareForRendering(t){const e=t.getRequiredDataFormats();if(!this.loaded)return this.await().then(e=>this.prepareForRendering(t));let i;i=e.includes(this.type)?this.await():this.transformTo(e);var n=e=>e.catch(e=>{this._handleConversionError(e);return null});return t.options.usePrivateCache&&t.options.preloadCache?n(i.then(e=>this.prepareInternalCacheAsync(t))):n(i)}prepareInternalCacheAsync(t){let e=this._getInternalCacheRef(t);if(this._checkInternalCacheUpToDate(e,t))return e.await();e&&!e.loaded&&e.await().then(()=>e.destroy());c.console.assert(this._tRef,"Data Create called from invalidation routine needs tile reference!");var i=t.internalCacheCreate(this,this._tRef);c.console.assert(void 0!==i,"[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");var n=t.getId();e=this[r][n]=new c.InternalCacheRecord(i,n,e=>t.internalCacheFree(e));return e.await()}prepareInternalCacheSync(t){let e=this._getInternalCacheRef(t);if(this._checkInternalCacheUpToDate(e,t))return e;e&&e.destroy();c.console.assert(this._tRef,"Data Create called from drawing loop needs tile reference!");var i=t.internalCacheCreate(this,this._tRef);c.console.assert(void 0!==i,"[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");var n=t.getId();e=this[r][n]=new c.InternalCacheRecord(i,n,e=>t.internalCacheFree(e));return e}_getInternalCacheRef(t){if(t.options.usePrivateCache){let e=this[r];e=e||(this[r]={});return e[t.getId()]}c.console.error("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false.")}_checkInternalCacheUpToDate(e,t){return e&&e.tstamp>=t._dataNeedsRefresh}transformTo(e=this._type){if(!this.loaded){this._conversionJobQueue=this._conversionJobQueue||[];let i=null;var t=new c.Promise((e,t)=>{i=e});this._conversionJobQueue.push(()=>{if(!this._destroyed)if("string"==typeof e&&e!==this._type||Array.isArray(e)&&!e.includes(this._type)){this._convert(this._type,e);this._promise.then(e=>i(e))}else this._promise.then(e=>{this._checkAwaitsConvert();return i(e)})});return t}("string"==typeof e&&e!==this._type||Array.isArray(e)&&!e.includes(this._type))&&this._convert(this._type,e);return this._promise}destroyInternalCache(e=void 0){const t=this[r];if(t)if(e){const i=t[e];if(i){i.destroy();delete t[e]}}else{for(const n in t)t[n].destroy();delete this[r]}}withTileReference(e){this._tRef=e;return this}toString(){const e=this._tRef||this._tiles.length&&this._tiles[0];return e?`Cache ${this.type} [used e.g. by ${e.toString()}]`:"Orphan cache!"}revive(){c.console.assert(!this.loaded&&!this._type,"[CacheRecord::revive] must not be called when loaded!");this._tiles=[];this._data=null;this._type=null;this.loaded=!1;this._promise=null;this._destroyed=!1;this._ownerTileCache=null;this.cacheKey=null}destroy(){if(!this._destroyed){delete this._conversionJobQueue;this._destroyed=!0;if(this.loaded)this._destroySelfUnsafe(this._data,this._type);else if(this._promise){const t=this._type;this._promise.then(e=>this._destroySelfUnsafe(e,t)).catch(c.console.error)}}}_destroySelfUnsafe(e,t){c.converter.destroy(e,t);this.destroyInternalCache();if(this._destroyed){this.loaded=!1;this._tiles=null;this._data=null;this._type=null;this._tRef=null;this._promise=null}}addTile(e,t,i){if(!this._destroyed){c.console.assert(e,"[CacheRecord.addTile] tile is required");if(null!=t&&this._tiles.length<1){"function"==typeof t&&(t=t());if(this.type&&this._promise)t instanceof c.Promise?this._promise=t.then(e=>{this._overwriteData(e,i)}):this._overwriteData(t,i);else{if(t instanceof c.Promise){this._promise=t.then(e=>{if(!this._destroyed){this.loaded=!0;return this._data=e}try{c.converter.destroy(e,this._type)}catch(e){}}).catch(e=>{this._handleConversionError(e)});this._data=null}else{this._promise=c.Promise.resolve(t);this._data=t;this.loaded=!0}this._type=i}this._tiles.push(e)}else{t=this._tiles.includes(e);!t&&this.type&&this._promise?this._tiles.push(e):t||c.console.warn("Tile %s caching attempt without data argument on uninitialized cache entry!",e)}}}removeTile(t){if(this._destroyed)return!1;for(let e=0;e{if(this._conversionJobQueue&&!this._destroyed){const e=this._conversionJobQueue[0];this._conversionJobQueue.splice(0,1);0===this._conversionJobQueue.length&&delete this._conversionJobQueue;e()}})}_triggerNeedsDraw(){0{if(this._data===i&&this._type===n)return this._data;c.converter.destroy(this._data,this._type);this._type=n;this._data=i;this._promise=c.Promise.resolve(i);const e=this[r];if(e)for(const t in e)e[t].setDataAs(i,n);this._triggerNeedsDraw();return this._data})}_convert(e,t){const o=c.converter,s=o.getConversionPath(e,t);if(s){var i=this._data;const a=s.length;const l=this;const h=(t,i)=>{if(i>=a){l._data=t;l.loaded=!0;l._checkAwaitsConvert();return c.Promise.resolve(t)}const n=s[i];let e;try{e=n.transform(l._tRef,t)}catch(e){o.destroy(t,n.origin.value);return c.Promise.reject(`[CacheRecord._convert] sync failure (while converting using ${n.target.value}, ${n.origin.value})`)}if(void 0===e){l.loaded=!1;o.destroy(t,n.origin.value);return c.Promise.reject(`[CacheRecord._convert] data mid result undefined value (while converting using ${n.target.value}, ${n.origin.value})`)}o.destroy(t,n.origin.value);const r="promise"===c.type(e)?e:c.Promise.resolve(e);return r.then(e=>h(e,i+1))};this.loaded=!1;this._data=void 0;this._type=s[a-1].target.value;this._promise=h(i,0).catch(e=>{this._handleConversionError(e)})}else c.console.error(`[CacheRecord._convert] Conversion ${e} ---> ${t} cannot be done!`)}_handleConversionError(e){c.console.error("[CacheRecord] Conversion/preparation error:",e);this._destroyed=!0;this.loaded=!1;this._data=null;if(this.cacheKey&&this._ownerTileCache)this._ownerTileCache._handleBrokenCacheRecord(this);else{this._promise=c.Promise.resolve(void 0);this._tiles=[];this._tRef=null}}};e.InternalCacheRecord=class{constructor(e,t,i){this.tstamp=c.now();this._ondestroy=i;this._type=t;if(e instanceof c.Promise)(this._promise=e).then(e=>{this.loaded=!0;this._data=e});else{this._promise=null;this.loaded=!0;this._data=e}}get data(){return this._data}get type(){return this._type}await(){return this._promise||c.Promise.resolve(this._data)}withTileReference(e){this._temporaryTileRef=e;return this}destroy(){if(this.loaded){this._ondestroy&&this._ondestroy(this._data);this._data=null;this.loaded=!1}}};e.TileCache=class{constructor(e){this._maxCacheItemCount=(e=e||{}).maxImageCacheCount||c.DEFAULT_SETTINGS.maxImageCacheCount;this._tilesLoaded=[];this._zombiesLoaded=[];this._zombiesLoadedCount=0;this._cachesLoaded=[];this._cachesLoadedCount=0}numTilesLoaded(){return this._tilesLoaded.length}numCachesLoaded(){return this._zombiesLoadedCount+this._cachesLoadedCount}cacheTile(e){c.console.assert(e,"[TileCache.cacheTile] options is required");var t=e.tile;c.console.assert(t,"[TileCache.cacheTile] options.tile is required");c.console.assert(t.cacheKey,"[TileCache.cacheTile] options.tile.cacheKey is required");if(e.image instanceof Image){c.console.warn("[TileCache.cacheTile] options.image is deprecated!");e.data=e.image;e.dataType="image"}var i=e.cacheKey||t.cacheKey;let n=this._cachesLoaded[i];if(!n){if(void 0===e.data){c.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute has been deprecated and will be removed in the future.");e.data=e.image}n=this._zombiesLoaded[i];if(n){if(n._destroyed)n.revive();else{"function"==typeof e.data&&e.data();delete e.data}delete this._zombiesLoaded[i];this._zombiesLoadedCount--;this._cachesLoaded[i]=n;this._cachesLoadedCount++}else{var r=void 0!==e.data&&null!==e.data&&!1!==e.data;c.console.assert(r,"[TileCache.cacheTile] options.data is required to create an CacheRecord");n=this._cachesLoaded[i]=new c.CacheRecord;this._cachesLoadedCount++}}if(!e.dataType){c.console.error("[TileCache.cacheTile] options.dataType is newly required. For easier use of the cache system, use the tile instance API.");"function"==typeof e.data&&c.console.error("[TileCache.cacheTile] options.dataType is mandatory when data item is a callback!");e.dataType=c.converter.guessType(e.data)}n._ownerTileCache=this;n.cacheKey=i;n.addTile(t,e.data,e.dataType);this._freeOldRecordRoutine(t,e.cutoff||0);return n}renameCache(e){var t=e.newCacheKey,i=e.oldCacheKey;let n=this._cachesLoaded[i];if(n){if(this._cachesLoaded[t]){c.console.error("Cannot rename cache %s to %s: the target cache is occupied!",i,t);return null}this._cachesLoaded[t]=n;delete this._cachesLoaded[i]}else{n=this._zombiesLoaded[i];c.console.assert(n,"[TileCache.renameCache] oldCacheKey must reference existing cache!");if(this._zombiesLoaded[t]){c.console.error("Cannot rename zombie cache %s to %s: the target cache is occupied!",i,t);return null}this._zombiesLoaded[t]=n;delete this._zombiesLoaded[i]}n._ownerTileCache=this;n.cacheKey=t;for(const r of n._tiles)r.reflectCacheRenamed(i,t);return n}cloneCache(i){const n=i.tile;var e=i.copyTargetKey;const r=this._cachesLoaded[e]||this._zombiesLoaded[e];c.console.assert(r,"[TileCache.cloneCache] attempt to clone non-existent cache %s!",e);c.console.assert(!this._cachesLoaded[i.newCacheKey],"[TileCache.cloneCache] attempt to copy clone to existing cache %s!",i.newCacheKey);e=i.desiredType||void 0;return r.getDataAs(e,!0).then(e=>{const t=this._cachesLoaded[i.newCacheKey]=new c.CacheRecord;t.addTile(n,e,r.type);this._cachesLoadedCount++;this._freeOldRecordRoutine(n,i.cutoff||0);return t})}injectCache(e){const t=e.targetKey,i=e.tile;if(e.tileAllowNotLoaded||i.loaded||i.loading){var n=this._cachesLoaded[t];if(n)for(const i of[...n._tiles])this.unloadCacheForTile(i,t,!0,!1);this._cachesLoaded[t]&&c.console.error("The inject routine should've freed cache!");const r=e.cache;this._cachesLoaded[t]=r;r._ownerTileCache=this;r.cacheKey=t;for(const o of i.getCache(i.originalCacheKey)._tiles)o.setCache(t,r,e.setAsMainCache,!1)}else c.console.warn("Attempt to inject cache on tile in invalid state: this is probably a bug!")}replaceCache(e){const t=e.victimKey,i=e.consumerKey,n=this._cachesLoaded[t],r=e.tile;if(n&&(e.tileAllowNotLoaded||r.loaded||r.loading)){var o=this._cachesLoaded[i];if(o)for(const r of[...o._tiles])this.unloadCacheForTile(r,i,!0,!1);this._cachesLoaded[i]&&c.console.error("The consume routine should've freed cache!");var s=this.renameCache({oldCacheKey:t,newCacheKey:i});if(s)for(const a of r.getCache(r.originalCacheKey)._tiles)a.setCache(i,s,e.setAsMainCache,!1)}else c.console.warn("Attempt to consume cache on tile in invalid state: this is probably a bug!")}restoreTilesThatShareOriginalCache(e,t,i){for(const n of t._tiles)if(n.cacheKey!==n.originalCacheKey){this.unloadCacheForTile(n,n.cacheKey,i,!0);delete n._caches[n.cacheKey];n.cacheKey=n.originalCacheKey}}_freeOldRecordRoutine(e,i){let n=this._tilesLoaded.length,r=-1;if(this._cachesLoadedCount+this._zombiesLoadedCount>this._maxCacheItemCount)if(0this._maxCacheItemCount;if(t._zombieCache&&n&&0this._maxCacheItemCount}for(let e=this._tilesLoaded.length-1;0<=e;e--)(i=this._tilesLoaded[e]).tiledImage===t&&(i.loaded?i.tiledImage===t&&this._unloadTile(i,!t._zombieCache||n,e):this._tilesLoaded.splice(e,1))}clear(e=0){for(const t in this._zombiesLoaded)this._zombiesLoaded[t].destroy();for(const i in this._tilesLoaded)this._unloadTile(i,!0);this._tilesLoaded=[];this._zombiesLoaded=[];this._zombiesLoadedCount=0;this._cachesLoaded=[];this._cachesLoadedCount=0}clearDrawerInternalCache(e){var t=e.getId();for(const i of this._zombiesLoaded)i&&i.destroyInternalCache(t);for(const n of this._cachesLoaded)n&&n.destroyInternalCache(t)}getLoadedTilesFor(t){return t?this._tilesLoaded.filter(e=>e.tiledImage===t):[...this._tilesLoaded]}getCacheRecord(e){c.console.assert(e,"[TileCache.getCacheRecord] cacheKey is required");return this._cachesLoaded[e]||this._zombiesLoaded[e]}safeUnloadCache(e){if(e&&!e._destroyed&&e.getTileCount()<1){for(const t in this._zombiesLoaded){const i=this._zombiesLoaded[t];if(i===e){delete this._zombiesLoaded[t];i.destroy();return}}c.console.error("Attempt to delete an orphan cache that is not in zombie list: this could be a bug!",e);e.destroy()}}unloadCacheForTile(e,t,i,n){const r=this._cachesLoaded[t];if(r){if(r.removeTile(e)){if(!r.getTileCount()){if(i)r.destroy();else{this._zombiesLoaded[t]=r;this._zombiesLoadedCount++}delete this._cachesLoaded[t];this._cachesLoadedCount--}return!0}c.console.error("[TileCache.unloadCacheForTile] System tried to delete tile from cache it does not belong to! This could mean a bug in the cache system.");return!1}n||c.console.warn("[TileCache.unloadCacheForTile] Attempting to delete missing cache!");return!1}unloadTile(t,e=!1){if(t.loaded){var i=this._tilesLoaded.findIndex(e=>e===t);this._unloadTile(t,e,i)}else c.console.warn("Attempt to unload already unloaded tile.")}_unloadTile(e,t,i=void 0){c.console.assert(e,"[TileCache._unloadTile] tile is required");for(const n in e._caches)this.unloadCacheForTile(e,n,t,!1);void 0!==i&&this._tilesLoaded.splice(i,1);if(e.loaded){const r=e.tiledImage;e._unload();r.viewer.raiseEvent("tile-unloaded",{tile:e,tiledImage:r,destroyed:t})}}}}(OpenSeadragon);!function(v){v.World=function(e){const t=this;v.console.assert(e.viewer,"[World] options.viewer is required");v.EventSource.call(this);this.viewer=e.viewer;this._items=[];this._needsDraw=!1;this.__invalidatedAt=1;this._autoRefigureSizes=!0;this._needsSizesFigured=!1;this._delegatedFigureSizes=function(e){t._autoRefigureSizes?t._figureSizes():t._needsSizesFigured=!0};this._figureSizes()};v.extend(v.World.prototype,v.EventSource.prototype,{addItem:function(e,t){v.console.assert(e,"[World.addItem] item is required");v.console.assert(e instanceof v.TiledImage,"[World.addItem] only TiledImages supported at this time");if(void 0!==(t=t||{}).index){t=Math.max(0,Math.min(this._items.length,t.index));this._items.splice(t,0,e)}else this._items.push(e);this._autoRefigureSizes?this._figureSizes():this._needsSizesFigured=!0;this._needsDraw=!0;e.addHandler("bounds-change",this._delegatedFigureSizes);e.addHandler("clip-change",this._delegatedFigureSizes);this.raiseEvent("add-item",{item:e})},getItemAt:function(e){v.console.assert(void 0!==e,"[World.getItemAt] index is required");return this._items[e]},getIndexOfItem:function(e){v.console.assert(e,"[World.getIndexOfItem] item is required");return v.indexOf(this._items,e)},getItemCount:function(){return this._items.length},setItemIndex:function(e,t){v.console.assert(e,"[World.setItemIndex] item is required");v.console.assert(void 0!==t,"[World.setItemIndex] index is required");var i=this.getIndexOfItem(e);if(t>=this._items.length)throw new Error("Index bigger than number of layers.");this._items.splice(i,1);this._items.splice(t,0,e);this._needsDraw=!0;this.raiseEvent("item-index-change",{item:e,previousIndex:i,newIndex:t})},removeItem:function(e){v.console.assert(e,"[World.removeItem] item is required");var t=v.indexOf(this._items,e);if(-1!==t){e.removeHandler("bounds-change",this._delegatedFigureSizes);e.removeHandler("clip-change",this._delegatedFigureSizes);e.destroy();this._items.splice(t,1);this._figureSizes();this._needsDraw=!0;this._raiseRemoveItem(e)}},removeAll:function(){this.viewer._cancelPendingImages();let t;for(let e=0;e=i;var h=d.level<=(d.tiledImage.source.getClosestLevel()||0);if(l||h)r[o++]=d;else{a._unloadTile(d,!1,e-s);s++}}r.length=o;return this.requestTileInvalidateEvent(r,t,e)},requestTileInvalidateEvent:function(e,d,p=!0,g=!1,m=!1){if(!this.viewer.isOpen())return v.Promise.resolve();void 0===d&&(d=this.__invalidatedAt);const f=[];e=e.map(o=>{if(!o||!g&&!o.loaded&&!o.processing)return Promise.resolve();const t=o.tiledImage;const s=t.getDrawer();const e=s._parentViewer||this.viewer;const a=o.getCache(o.originalCacheKey);var i=o.getCache(o.originalCacheKey);if(i.__invStamp&&i.__invStamp>=d)return Promise.resolve();let l=!1;a.__finishProcessing&&a.__finishProcessing(!0);let n;a.__resolve||(n=new v.Promise(e=>{a.__resolve=e}));a.__finishProcessing=e=>{l=l||e;o.processing=!1;a.__finishProcessing=null;if(!e){a.__resolve(o);a.__resolve=null}};for(const r of a._tiles){r.processing=d;n&&(r.processingPromise=n)}a.__invStamp=d;a.__wasRestored=p;let h=null;const c=()=>{if(h){var e=o.buildDistinctMainCacheKey();t._tileCache.injectCache({tile:o,cache:h,targetKey:e,setAsMainCache:!0,tileAllowNotLoaded:o.loading})}else p&&t._tileCache.restoreTilesThatShareOriginalCache(o,o.getCache(o.originalCacheKey),!0)};const u=()=>l||"number"==typeof a.__invStamp&&a.__invStamp{if(h)return h.getDataAs(t,!1);var e=p?o.originalCacheKey:o.cacheKey;const i=o.getCache(e);if(!i){v.console.error("[Tile::getData] There is no cache available for tile with key %s",e);return v.Promise.reject()}t=t||i.type;h=(new v.CacheRecord).withTileReference(o);return i.getDataAs(t,!0).then(e=>{if(null==e)return v.Promise.reject(new Error("[World.getData] Working cache source data unavailable"));h.addTile(o,e,t);return h.data})},setData:(e,t)=>{if(h)return h.setDataAs(e,t);h=(new v.CacheRecord).withTileReference(o);h.addTile(o,e,t);return v.Promise.resolve()},resetData:()=>{if(h){h.destroy();h=null}},stopPropagation:()=>{return u()}}).catch(e=>{v.console.error("Update routine error:",e);if(h){try{h.destroy()}catch(e){}h=null}l=!0;a.__finishProcessing&&a.__finishProcessing(!0);return null}).then(e=>{if(this.viewer.isDestroyed()){a.__finishProcessing&&a.__finishProcessing(!0);return null}if(l)return null;if(a.__finishProcessing){if(!l&&(o.loaded||o.loading)){if(a.__invStamp{if(l){h.destroy();h=null}else{if(!u()&&e)c();else{h.destroy();h=null}a.__finishProcessing()}});if(p){var t=o.getCache();const i=o.getCache(o.originalCacheKey);return t!==i?i.prepareForRendering(s).then(e=>{if(!l){!u()&&e&&c();a.__finishProcessing()}}):null}}else v.console.error("Invalidation flow error: tile processing state is invalid. "+`Tile: ${o?o.toString():"null"}, `+`loaded: ${o?o.loaded:"n/a"}, loading: ${o?o.loading:"n/a"}, `+`originalCache.__invStamp: ${a.__invStamp}, `+`this.__invalidatedAt: ${this.__invalidatedAt}, `+`tStamp: ${d}, wasOutdatedRun: `+l);if(m){const n=o.getCache();return n.prepareForRendering(s).then(()=>{!l&&a.__finishProcessing&&a.__finishProcessing()})}a.__finishProcessing();return null}l||a.__finishProcessing(!0)}if(m){const r=o.getCache();return r.prepareForRendering(s).then(()=>{!l&&a.__finishProcessing&&a.__finishProcessing()})}if(h){h.destroy();h=null}return null}).catch(e=>{v.console.error("Update routine error:",e);if(h){h.destroy();h=null}a.__finishProcessing()})});return v.Promise.all(e).then(()=>{f.length&&this.requestTileInvalidateEvent(f,void 0,p,!0);g||this.viewer.isDestroyed()||this.draw()})},ensureTilesUpToDate:function(e){let t;let i;for(var n of e){n=n.tile||n;if(n.loaded&&!n.processing){var r=n.getCache(n.originalCacheKey);i=r.__wasRestored;r.__invStampu.height?o:o*(u.width/u.height);p=d*(u.height/u.width);g=new v.Point(l+(o-d)/2,h+(o-p)/2);c.setPosition(g,t);c.setWidth(d,t);"horizontal"===i?l+=s:h+=s}this.setAutoRefigureSizes(!0)},_figureSizes:function(){var e=this._homeBounds?this._homeBounds.clone():null;var t=this._contentSize?this._contentSize.clone():null;var i=this._contentFactor||0;if(this._items.length){let t=this._items[0];var s=t.getBounds();this._contentFactor=t.getContentSize().x/s.width;var a=t.getClippedBounds().getBoundingBox();let i=a.x;let n=a.y;let r=a.x+a.width;let o=a.y+a.height;for(let e=1;e Date: Thu, 23 Apr 2026 23:01:16 +0200 Subject: [PATCH 5/9] Removed animation for overlays. --- .../static/annotations/js/exact-quad-tree.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 8e2100a4..e940ecf2 100644 --- a/exact/exact/annotations/static/annotations/js/exact-quad-tree.js +++ b/exact/exact/annotations/static/annotations/js/exact-quad-tree.js @@ -103,6 +103,16 @@ class EXACTRegistrationHandler { 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() { + if (self.background_viewer.canvas) { + self.background_viewer.canvas.hidden=true; + } + }); + this.viewer.addHandler('animation-finish', function() { + if (self.background_viewer.canvas) { + self.background_viewer.canvas.hidden=false; + }}); this.background_viewer.addHandler("open", function (event) { @@ -122,6 +132,8 @@ class EXACTRegistrationHandler { } + + syncViewBackgroundForeground () { if (this.background_viewer !== undefined) { From 1c51eacff38a12dd5712738f3c409cc61d7393aa Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Fri, 24 Apr 2026 20:37:16 +0200 Subject: [PATCH 6/9] Removed a,s,d,w navigation from OpenSeaDragon. --- .../static/annotations/js/openseadragon.js | 33353 ++++++++++++++++ .../annotations/js/openseadragon.min.js | 106 +- .../annotations/js/openseadragon.min.js.map | 2 +- 3 files changed, 33355 insertions(+), 106 deletions(-) create mode 100644 exact/exact/annotations/static/annotations/js/openseadragon.js 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 + *
{
+         *      avif: true,
+         *      bmp:  false,
+         *      jpeg: true,
+         *      jpg:  true,
+         *      png:  true,
+         *      tif:  false,
+         *      wdp:  false,
+         *      webp: true
+         * }
+         * 
+ * @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} A promise that resolves to info object carrying 'source' and 'message'. + * Message is provided only on error, in that case the source is reference to the original source parameter that + * was defining the TileSource. On success, the source is a TileSource instance. + */ + instantiateTileSourceClass( options ) { + return new $.Promise( ( resolve, reject ) => { + if (options.placeholderFillStyle === undefined) { + options.placeholderFillStyle = this.placeholderFillStyle; + } + if (options.opacity === undefined) { + options.opacity = this.opacity; + } + if (options.preload === undefined) { + options.preload = this.preload; + } + if (options.compositeOperation === undefined) { + options.compositeOperation = this.compositeOperation; + } + if (options.crossOriginPolicy === undefined) { + options.crossOriginPolicy = options.tileSource.crossOriginPolicy !== undefined ? + options.tileSource.crossOriginPolicy : this.crossOriginPolicy; + } + if (options.ajaxWithCredentials === undefined) { + options.ajaxWithCredentials = this.ajaxWithCredentials; + } + if (options.loadTilesWithAjax === undefined) { + options.loadTilesWithAjax = this.loadTilesWithAjax; + } + if (!$.isPlainObject(options.ajaxHeaders)) { + options.ajaxHeaders = {}; + } + + let tileSource = options.tileSource; + + //allow plain xml strings or json strings to be parsed here + if ( $.type( tileSource ) === 'string' ) { + //xml should start with "<" and end with ">" + if ( tileSource.match( /^\s*<.*>\s*$/ ) ) { + tileSource = $.parseXml( tileSource ); + //json should start with "{" or "[" and end with "}" or "]" + } else if ( tileSource.match(/^\s*[{[].*[}\]]\s*$/ ) ) { + try { + tileSource = $.parseJSON(tileSource); + } catch (e) { + //tileSource = tileSource; + } + } + } + + function waitUntilReady(tileSource, originalTileSource) { + if (tileSource.ready) { + resolve({ + source: tileSource + }); + } else { + tileSource.addHandler('ready', function (event) { + resolve({ + source: event.tileSource + }); + }); + tileSource.addHandler('open-failed', function (event) { + reject({ + message: event.message, + source: originalTileSource + }); + }); + } + } + + setTimeout(() => { + if ( $.type( tileSource ) === 'string' ) { + //If its still a string it means it must be a url at this point + tileSource = new $.TileSource({ + url: tileSource, + crossOriginPolicy: options.crossOriginPolicy !== undefined ? + options.crossOriginPolicy : this.crossOriginPolicy, + ajaxWithCredentials: this.ajaxWithCredentials, + ajaxHeaders: $.extend({}, this.ajaxHeaders, options.ajaxHeaders), + splitHashDataForPost: this.splitHashDataForPost, + }); + waitUntilReady(tileSource, tileSource); + } else if ($.isPlainObject(tileSource) || tileSource.nodeType) { + if (tileSource.crossOriginPolicy === undefined && + (options.crossOriginPolicy !== undefined || this.crossOriginPolicy !== undefined)) { + tileSource.crossOriginPolicy = options.crossOriginPolicy !== undefined ? + options.crossOriginPolicy : this.crossOriginPolicy; + } + if (tileSource.ajaxWithCredentials === undefined) { + tileSource.ajaxWithCredentials = this.ajaxWithCredentials; + } + + if ( $.isFunction( tileSource.getTileUrl ) ) { + //Custom tile source + const customTileSource = new $.TileSource( tileSource ); + customTileSource.getTileUrl = tileSource.getTileUrl; + tileSource.ready = false; + waitUntilReady(customTileSource, tileSource); + } else { + //inline configuration + const $TileSource = $.TileSource.determineType( this, tileSource, null ); + if ( !$TileSource ) { + reject({ + message: "Unable to load TileSource", + source: tileSource, + error: true + }); + return; + } + const tileOptions = $TileSource.prototype.configure.apply( this, [ tileSource ] ); + tileOptions.ready = false; + waitUntilReady(new $TileSource(tileOptions), tileSource); + } + } else { + //can assume it's already a tile source implementation, force inheritance + waitUntilReady(tileSource, tileSource); + } + }); + }); + }, + + /** + * Add a simple image to the viewer. + * The options are the same as the ones in {@link OpenSeadragon.Viewer#addTiledImage} + * except for options.tileSource which is replaced by options.url. + * @function + * @param {Object} options - See {@link OpenSeadragon.Viewer#addTiledImage} + * for all the options + * @param {String} options.url - The URL of the image to add. + * @fires OpenSeadragon.World.event:add-item + * @fires OpenSeadragon.Viewer.event:add-item-failed + */ + addSimpleImage: function(options) { + $.console.assert(options, "[Viewer.addSimpleImage] options is required"); + $.console.assert(options.url, "[Viewer.addSimpleImage] options.url is required"); + + const opts = $.extend({}, options, { + tileSource: { + type: 'image', + url: options.url + } + }); + delete opts.url; + this.addTiledImage(opts); + }, + + // deprecated + addLayer: function( options ) { + const _this = this; + + $.console.error( "[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead." ); + + const optionsClone = $.extend({}, options, { + success: function(event) { + _this.raiseEvent("add-layer", { + options: options, + drawer: event.item + }); + }, + error: function(event) { + _this.raiseEvent("add-layer-failed", event); + } + }); + + this.addTiledImage(optionsClone); + return this; + }, + + // deprecated + getLayerAtLevel: function( level ) { + $.console.error( "[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead." ); + return this.world.getItemAt(level); + }, + + // deprecated + getLevelOfLayer: function( drawer ) { + $.console.error( "[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead." ); + return this.world.getIndexOfItem(drawer); + }, + + // deprecated + getLayersCount: function() { + $.console.error( "[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead." ); + return this.world.getItemCount(); + }, + + // deprecated + setLayerLevel: function( drawer, level ) { + $.console.error( "[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead." ); + return this.world.setItemIndex(drawer, level); + }, + + // deprecated + removeLayer: function( drawer ) { + $.console.error( "[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead." ); + return this.world.removeItem(drawer); + }, + + /** + * Force the viewer to redraw its contents. + * @returns {OpenSeadragon.Viewer} Chainable. + */ + forceRedraw: function() { + THIS[ this.hash ].forceRedraw = true; + return this; + }, + + /** + * Force the viewer to reset its size to match its container. + */ + forceResize: function() { + THIS[this.hash].needsResize = true; + THIS[this.hash].forceResize = true; + }, + + /** + * @function + * @returns {OpenSeadragon.Viewer} Chainable. + */ + bindSequenceControls: function(){ + + ////////////////////////////////////////////////////////////////////////// + // Image Sequence Controls + ////////////////////////////////////////////////////////////////////////// + const onFocusHandler = $.delegate( this, onFocus ); + const onBlurHandler = $.delegate( this, onBlur ); + const onNextHandler = $.delegate( this, this.goToNextPage ); + const onPreviousHandler = $.delegate( this, this.goToPreviousPage ); + const navImages = this.navImages; + let useGroup = true; + + if( this.showSequenceControl ){ + + if( this.previousButton || this.nextButton ){ + //if we are binding to custom buttons then layout and + //grouping is the responsibility of the page author + useGroup = false; + } + + this.previousButton = new $.Button({ + element: this.previousButton ? $.getElement( this.previousButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.PreviousPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ), + onRelease: onPreviousHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + }); + + this.nextButton = new $.Button({ + element: this.nextButton ? $.getElement( this.nextButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.NextPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ), + onRelease: onNextHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + }); + + if( !this.navPrevNextWrap ){ + this.previousButton.disable(); + } + + if (!this.tileSources || !this.tileSources.length) { + this.nextButton.disable(); + } + + if( useGroup ){ + this.paging = new $.ButtonGroup({ + buttons: [ + this.previousButton, + this.nextButton + ], + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold + }); + + this.pagingControl = this.paging.element; + + if( this.toolbar ){ + this.toolbar.addControl( + this.pagingControl, + {anchor: $.ControlAnchor.BOTTOM_RIGHT} + ); + }else{ + this.addControl( + this.pagingControl, + {anchor: this.sequenceControlAnchor || $.ControlAnchor.TOP_LEFT} + ); + } + } + } + return this; + }, + + + /** + * @function + * @returns {OpenSeadragon.Viewer} Chainable. + */ + bindStandardControls: function(){ + ////////////////////////////////////////////////////////////////////////// + // Navigation Controls + ////////////////////////////////////////////////////////////////////////// + const beginZoomingInHandler = $.delegate( this, this.startZoomInAction ); + const endZoomingHandler = $.delegate( this, this.endZoomAction ); + const doSingleZoomInHandler = $.delegate( this, this.singleZoomInAction ); + const beginZoomingOutHandler = $.delegate( this, this.startZoomOutAction ); + const doSingleZoomOutHandler = $.delegate( this, this.singleZoomOutAction ); + const onHomeHandler = $.delegate( this, onHome ); + const onFullScreenHandler = $.delegate( this, onFullScreen ); + const onRotateLeftHandler = $.delegate( this, onRotateLeft ); + const onRotateRightHandler = $.delegate( this, onRotateRight ); + const onFlipHandler = $.delegate( this, onFlip); + const onFocusHandler = $.delegate( this, onFocus ); + const onBlurHandler = $.delegate( this, onBlur ); + const navImages = this.navImages; + const buttons = []; + let useGroup = true; + + + if ( this.showNavigationControl ) { + + if( this.zoomInButton || this.zoomOutButton || + this.homeButton || this.fullPageButton || + this.rotateLeftButton || this.rotateRightButton || + this.flipButton ) { + //if we are binding to custom buttons then layout and + //grouping is the responsibility of the page author + useGroup = false; + } + + if ( this.showZoomControl ) { + buttons.push( this.zoomInButton = new $.Button({ + element: this.zoomInButton ? $.getElement( this.zoomInButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.ZoomIn" ), + srcRest: resolveUrl( this.prefixUrl, navImages.zoomIn.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ), + onPress: beginZoomingInHandler, + onRelease: endZoomingHandler, + onClick: doSingleZoomInHandler, + onEnter: beginZoomingInHandler, + onExit: endZoomingHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + buttons.push( this.zoomOutButton = new $.Button({ + element: this.zoomOutButton ? $.getElement( this.zoomOutButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.ZoomOut" ), + srcRest: resolveUrl( this.prefixUrl, navImages.zoomOut.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ), + onPress: beginZoomingOutHandler, + onRelease: endZoomingHandler, + onClick: doSingleZoomOutHandler, + onEnter: beginZoomingOutHandler, + onExit: endZoomingHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + } + + if ( this.showHomeControl ) { + buttons.push( this.homeButton = new $.Button({ + element: this.homeButton ? $.getElement( this.homeButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.Home" ), + srcRest: resolveUrl( this.prefixUrl, navImages.home.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.home.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.home.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.home.DOWN ), + onRelease: onHomeHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + } + + if ( this.showFullPageControl ) { + buttons.push( this.fullPageButton = new $.Button({ + element: this.fullPageButton ? $.getElement( this.fullPageButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.FullPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.fullpage.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ), + onRelease: onFullScreenHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + } + + if ( this.showRotationControl ) { + buttons.push( this.rotateLeftButton = new $.Button({ + element: this.rotateLeftButton ? $.getElement( this.rotateLeftButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.RotateLeft" ), + srcRest: resolveUrl( this.prefixUrl, navImages.rotateleft.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.rotateleft.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.rotateleft.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.rotateleft.DOWN ), + onRelease: onRotateLeftHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + buttons.push( this.rotateRightButton = new $.Button({ + element: this.rotateRightButton ? $.getElement( this.rotateRightButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.RotateRight" ), + srcRest: resolveUrl( this.prefixUrl, navImages.rotateright.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.rotateright.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.rotateright.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.rotateright.DOWN ), + onRelease: onRotateRightHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + } + + if ( this.showFlipControl ) { + buttons.push( this.flipButton = new $.Button({ + element: this.flipButton ? $.getElement( this.flipButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.Flip" ), + srcRest: resolveUrl( this.prefixUrl, navImages.flip.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.flip.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.flip.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.flip.DOWN ), + onRelease: onFlipHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + } + + if ( useGroup ) { + this.buttonGroup = new $.ButtonGroup({ + buttons: buttons, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold + }); + + this.navControl = this.buttonGroup.element; + this.addHandler( 'open', $.delegate( this, lightUp ) ); + + if( this.toolbar ){ + this.toolbar.addControl( + this.navControl, + {anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT} + ); + } else { + this.addControl( + this.navControl, + {anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT} + ); + } + } else { + this.customButtons = buttons; + } + + } + return this; + }, + + /** + * Gets the active page of a sequence + * @function + * @returns {Number} + */ + currentPage: function() { + return this._sequenceIndex; + }, + + /** + * @function + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:page + */ + goToPage: function( page ){ + if( this.tileSources && page >= 0 && page < this.tileSources.length ){ + this._sequenceIndex = page; + + this._updateSequenceButtons( page ); + + this.open( this.tileSources[ page ] ); + + if( this.referenceStrip ){ + this.referenceStrip.setFocus( page ); + } + + /** + * Raised when the page is changed on a viewer configured with multiple image sources (see {@link OpenSeadragon.Viewer#goToPage}). + * + * @event page + * @memberof OpenSeadragon.Viewer + * @type {Object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {Number} page - The page index. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'page', { page: page } ); + } + + return this; + }, + + /** + * Adds an html element as an overlay to the current viewport. Useful for + * highlighting words or areas of interest on an image or other zoomable + * interface. Unless the viewer has been configured with the preserveOverlays + * option, overlays added via this method are removed when the viewport + * is closed (including in sequence mode when changing page). + * @method + * @param {Element|String|Object} element - A reference to an element or an id for + * the element which will be overlaid. Or an Object specifying the configuration for the overlay. + * If using an object, see {@link OpenSeadragon.Overlay} for a list of + * all available options. + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or + * rectangle which will be overlaid. This is a viewport relative location. + * @param {OpenSeadragon.Placement} [placement=OpenSeadragon.Placement.TOP_LEFT] - The position of the + * viewport which the location coordinates will be treated as relative + * to. + * @param {function} [onDraw] - If supplied the callback is called when the overlay + * needs to be drawn. It is the responsibility of the callback to do any drawing/positioning. + * It is passed position, size and element. + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:add-overlay + */ + addOverlay: function( element, location, placement, onDraw ) { + let options; + if( $.isPlainObject( element ) ){ + options = element; + } else { + options = { + element: element, + location: location, + placement: placement, + onDraw: onDraw + }; + } + + element = $.getElement( options.element ); + + if ( getOverlayIndex( this.currentOverlays, element ) >= 0 ) { + // they're trying to add a duplicate overlay + return this; + } + + const overlay = getOverlayObject( this, options); + this.currentOverlays.push(overlay); + overlay.drawHTML( this.overlaysContainer, this.viewport ); + + /** + * Raised when an overlay is added to the viewer (see {@link OpenSeadragon.Viewer#addOverlay}). + * + * @event add-overlay + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {Element} element - The overlay element. + * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location + * @property {OpenSeadragon.Placement} placement + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'add-overlay', { + element: element, + location: options.location, + placement: options.placement + }); + return this; + }, + + /** + * Updates the overlay represented by the reference to the element or + * element id moving it to the new location, relative to the new placement. + * @method + * @param {Element|String} element - A reference to an element or an id for + * the element which is overlaid. + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or + * rectangle which will be overlaid. This is a viewport relative location. + * @param {OpenSeadragon.Placement} [placement=OpenSeadragon.Placement.TOP_LEFT] - The position of the + * viewport which the location coordinates will be treated as relative + * to. + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:update-overlay + */ + updateOverlay: function( element, location, placement ) { + element = $.getElement( element ); + const i = getOverlayIndex( this.currentOverlays, element ); + + if ( i >= 0 ) { + this.currentOverlays[ i ].update( location, placement ); + THIS[ this.hash ].forceRedraw = true; + /** + * Raised when an overlay's location or placement changes + * (see {@link OpenSeadragon.Viewer#updateOverlay}). + * + * @event update-overlay + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the + * Viewer which raised the event. + * @property {Element} element + * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location + * @property {OpenSeadragon.Placement} placement + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'update-overlay', { + element: element, + location: location, + placement: placement + }); + } + return this; + }, + + /** + * Removes an overlay identified by the reference element or element id + * and schedules an update. + * @method + * @param {Element|String} element - A reference to the element or an + * element id which represent the ovelay content to be removed. + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:remove-overlay + */ + removeOverlay: function( element ) { + element = $.getElement( element ); + const i = getOverlayIndex( this.currentOverlays, element ); + + if ( i >= 0 ) { + this.currentOverlays[ i ].destroy(); + this.currentOverlays.splice( i, 1 ); + THIS[ this.hash ].forceRedraw = true; + /** + * Raised when an overlay is removed from the viewer + * (see {@link OpenSeadragon.Viewer#removeOverlay}). + * + * @event remove-overlay + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the + * Viewer which raised the event. + * @property {Element} element - The overlay element. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'remove-overlay', { + element: element + }); + } + return this; + }, + + /** + * Removes all currently configured Overlays from this Viewer and schedules + * an update. + * @method + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:clear-overlay + */ + clearOverlays: function() { + while ( this.currentOverlays.length > 0 ) { + this.currentOverlays.pop().destroy(); + } + THIS[ this.hash ].forceRedraw = true; + /** + * Raised when all overlays are removed from the viewer (see {@link OpenSeadragon.Drawer#clearOverlays}). + * + * @event clear-overlay + * @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( 'clear-overlay', {} ); + return this; + }, + + /** + * Finds an overlay identified by the reference element or element id + * and returns it as an object, return null if not found. + * @method + * @param {Element|String} element - A reference to the element or an + * element id which represents the overlay content. + * @returns {OpenSeadragon.Overlay} the matching overlay or null if none found. + */ + getOverlayById: function( element ) { + element = $.getElement( element ); + const i = getOverlayIndex( this.currentOverlays, element ); + + if (i >= 0) { + return this.currentOverlays[i]; + } else { + return null; + } + }, + + /** + * Register drawer for shared updates + * @param drawer + * @private + */ + _registerDrawer: function (drawer) { + if (!this._drawerList) { + this._drawerList = []; + } + this._drawerList.push(drawer); + }, + /** + * Unregister drawer from shared updates + * @param drawer + * @private + */ + _unregisterDrawer: function (drawer) { + if (!this._drawerList) { + $.console.warn('Viewer._unregisterDrawer: cannot unregister on viewer that is not meant to share updates.'); + return; + } + this._drawerList.splice(this._drawerList.indexOf(drawer), 1); + }, + + /** + * Updates the sequence buttons. + * @function OpenSeadragon.Viewer.prototype._updateSequenceButtons + * @private + * @param {Number} Sequence Value + */ + _updateSequenceButtons: function( page ) { + + if ( this.nextButton ) { + if(!this.tileSources || this.tileSources.length - 1 === page) { + //Disable next button + if ( !this.navPrevNextWrap ) { + this.nextButton.disable(); + } + } else { + this.nextButton.enable(); + } + } + if ( this.previousButton ) { + if ( page > 0 ) { + //Enable previous button + this.previousButton.enable(); + } else { + if ( !this.navPrevNextWrap ) { + this.previousButton.disable(); + } + } + } + }, + + /** + * Display a message in the viewport + * @function OpenSeadragon.Viewer.prototype._showMessage + * @private + * @param {String} text message + */ + _showMessage: function ( message ) { + this._hideMessage(); + + const div = $.makeNeutralElement( "div" ); + div.appendChild( document.createTextNode( message ) ); + + this.messageDiv = $.makeCenteredNode( div ); + + $.addClass(this.messageDiv, "openseadragon-message"); + + this.container.appendChild( this.messageDiv ); + }, + + /** + * Hide any currently displayed viewport message + * @function OpenSeadragon.Viewer.prototype._hideMessage + * @private + */ + _hideMessage: function () { + const div = this.messageDiv; + if (div) { + div.parentNode.removeChild(div); + delete this.messageDiv; + } + }, + + /** + * Gets this viewer's gesture settings for the given pointer device type. + * @method + * @param {String} type - The pointer device type to get the gesture settings for ("mouse", "touch", "pen", etc.). + * @returns {OpenSeadragon.GestureSettings} + */ + gestureSettingsByDeviceType: function ( type ) { + switch ( type ) { + case 'mouse': + return this.gestureSettingsMouse; + case 'touch': + return this.gestureSettingsTouch; + case 'pen': + return this.gestureSettingsPen; + default: + return this.gestureSettingsUnknown; + } + }, + + // private + _drawOverlays: function() { + const length = this.currentOverlays.length; + for ( let i = 0; i < length; i++ ) { + this.currentOverlays[ i ].drawHTML( this.overlaysContainer, this.viewport ); + } + }, + + /** + * Cancel the "in flight" images. + */ + _cancelPendingImages: function() { + this._loadQueue = []; + }, + + /** + * Removes the reference strip and disables displaying it. + * @function + */ + removeReferenceStrip: function() { + this.showReferenceStrip = false; + + if (this.referenceStrip) { + this.referenceStrip.destroy(); + this.referenceStrip = null; + } + }, + + /** + * Enables and displays the reference strip based on the currently set tileSources. + * Works only when the Viewer has sequenceMode set to true. + * @function + */ + addReferenceStrip: function() { + this.showReferenceStrip = true; + + if (this.sequenceMode) { + if (this.referenceStrip) { + return; + } + + if (this.tileSources.length && this.tileSources.length > 1) { + this.referenceStrip = new $.ReferenceStrip({ + id: this.referenceStripElement, + position: this.referenceStripPosition, + sizeRatio: this.referenceStripSizeRatio, + scroll: this.referenceStripScroll, + height: this.referenceStripHeight, + width: this.referenceStripWidth, + tileSources: this.tileSources, + prefixUrl: this.prefixUrl, + viewer: this + }); + + this.referenceStrip.setFocus( this._sequenceIndex ); + } + } else { + $.console.warn('Attempting to display a reference strip while "sequenceMode" is off.'); + } + }, + + /** + * Adds _updatePixelDensityRatio to the window resize event. + * @private + */ + _addUpdatePixelDensityRatioEvent: function() { + this._updatePixelDensityRatioBind = this._updatePixelDensityRatio.bind(this); + $.addEvent( window, 'resize', this._updatePixelDensityRatioBind ); + }, + + /** + * Removes _updatePixelDensityRatio from the window resize event. + * @private + */ + _removeUpdatePixelDensityRatioEvent: function() { + $.removeEvent( window, 'resize', this._updatePixelDensityRatioBind ); + }, + + /** + * Update pixel density ratio and forces a resize operation. + * @private + */ + _updatePixelDensityRatio: function() { + const previusPixelDensityRatio = $.pixelDensityRatio; + const currentPixelDensityRatio = $.getCurrentPixelDensityRatio(); + if (previusPixelDensityRatio !== currentPixelDensityRatio) { + $.pixelDensityRatio = currentPixelDensityRatio; + this.forceResize(); + } + }, + + /** + * Sets the image source to the source with index equal to + * currentIndex - 1. Changes current image in sequence mode. + * If specified, wraps around (see navPrevNextWrap in + * {@link OpenSeadragon.Options}) + * + * @method + */ + + goToPreviousPage: function () { + let previous = this._sequenceIndex - 1; + if(this.navPrevNextWrap && previous < 0){ + previous += this.tileSources.length; + } + this.goToPage( previous ); + }, + + /** + * Sets the image source to the source with index equal to + * currentIndex + 1. Changes current image in sequence mode. + * If specified, wraps around (see navPrevNextWrap in + * {@link OpenSeadragon.Options}) + * + * @method + */ + goToNextPage: function () { + let next = this._sequenceIndex + 1; + if(this.navPrevNextWrap && next >= this.tileSources.length){ + next = 0; + } + this.goToPage( next ); + }, + + isAnimating: function () { + return THIS[ this.hash ].animating; + }, + + /** + * Starts continuous zoom-in animation (typically bound to mouse-down on the zoom-in button). + * @function + * @memberof OpenSeadragon.Viewer.prototype + */ + startZoomInAction: function () { + THIS[ this.hash ].lastZoomTime = $.now(); + THIS[ this.hash ].zoomFactor = this.zoomPerSecond; + THIS[ this.hash ].zooming = true; + scheduleZoom( this ); + }, + + /** + * Starts continuous zoom-out animation (typically bound to mouse-down on the zoom-out button). + * @function + * @memberof OpenSeadragon.Viewer.prototype + */ + startZoomOutAction: function () { + THIS[ this.hash ].lastZoomTime = $.now(); + THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond; + THIS[ this.hash ].zooming = true; + scheduleZoom( this ); + }, + + /** + * Stops any continuous zoom animation (typically bound to mouse-up/leave events on a button). + * @function + * @memberof OpenSeadragon.Viewer.prototype + */ + endZoomAction: function () { + THIS[ this.hash ].zooming = false; + }, + + /** + * Performs single-step zoom-in operation (typically bound to click/enter on the zoom-in button). + * @function + * @memberof OpenSeadragon.Viewer.prototype + */ + singleZoomInAction: function () { + if ( this.viewport ) { + THIS[ this.hash ].zooming = false; + this.viewport.zoomBy( + this.zoomPerClick / 1.0 + ); + this.viewport.applyConstraints(); + } + }, + + /** + * Performs single-step zoom-out operation (typically bound to click/enter on the zoom-out button). + * @function + * @memberof OpenSeadragon.Viewer.prototype + */ + singleZoomOutAction: function () { + if ( this.viewport ) { + THIS[ this.hash ].zooming = false; + this.viewport.zoomBy( + 1.0 / this.zoomPerClick + ); + this.viewport.applyConstraints(); + } + }, +}); + + +/** + * _getSafeElemSize is like getElementSize(), but refuses to return 0 for x or y, + * which was causing some calling operations to return NaN. + * @returns {Point} + * @private + */ +function _getSafeElemSize (oElement) { + oElement = $.getElement( oElement ); + + return new $.Point( + (oElement.clientWidth === 0 ? 1 : oElement.clientWidth), + (oElement.clientHeight === 0 ? 1 : oElement.clientHeight) + ); +} + +function getOverlayObject( viewer, overlay ) { + if ( overlay instanceof $.Overlay ) { + return overlay; + } + + let element = null; + if ( overlay.element ) { + element = $.getElement( overlay.element ); + } else { + const id = overlay.id ? + overlay.id : + "openseadragon-overlay-" + Math.floor( Math.random() * 10000000 ); + + element = $.getElement( overlay.id ); + if ( !element ) { + element = document.createElement( "a" ); + element.href = "#/overlay/" + id; + } + element.id = id; + $.addClass( element, overlay.className ? + overlay.className : + "openseadragon-overlay" + ); + } + + let location = overlay.location; + let width = overlay.width; + let height = overlay.height; + if (!location) { + let x = overlay.x; + let y = overlay.y; + if (overlay.px !== undefined) { + const rect = viewer.viewport.imageToViewportRectangle(new $.Rect( + overlay.px, + overlay.py, + width || 0, + height || 0)); + x = rect.x; + y = rect.y; + width = width !== undefined ? rect.width : undefined; + height = height !== undefined ? rect.height : undefined; + } + location = new $.Point(x, y); + } + + let placement = overlay.placement; + if (placement && $.type(placement) === "string") { + placement = $.Placement[overlay.placement.toUpperCase()]; + } + + return new $.Overlay({ + element: element, + location: location, + placement: placement, + onDraw: overlay.onDraw, + checkResize: overlay.checkResize, + width: width, + height: height, + rotationMode: overlay.rotationMode + }); +} + +/** + * Determines the index of a specific overlay element within an array of overlays. + * + * @private + * @inner + * @param {Array} overlays - The array of overlay objects, each containing an `element` property. + * @param {Element} element - The DOM element of the overlay to find. + * @returns {number} The index of the matching overlay in the array, or -1 if not found. + */ +function getOverlayIndex( overlays, element ) { + for ( let i = overlays.length - 1; i >= 0; i-- ) { + if ( overlays[ i ].element === element ) { + return i; + } + } + + return -1; +} + +/////////////////////////////////////////////////////////////////////////////// +// Schedulers provide the general engine for animation +/////////////////////////////////////////////////////////////////////////////// +function scheduleUpdate( viewer, updateFunc ){ + return $.requestAnimationFrame( function(){ + updateFunc( viewer ); + } ); +} + + +//provides a sequence in the fade animation +function scheduleControlsFade( viewer ) { + $.requestAnimationFrame( function(){ + updateControlsFade( viewer ); + }); +} + + +//initiates an animation to hide the controls +function beginControlsAutoHide( viewer ) { + if ( !viewer.autoHideControls ) { + return; + } + viewer.controlsShouldFade = true; + viewer.controlsFadeBeginTime = + $.now() + + viewer.controlsFadeDelay; + + window.setTimeout( function(){ + scheduleControlsFade( viewer ); + }, viewer.controlsFadeDelay ); +} + + +//determines if fade animation is done or continues the animation +function updateControlsFade( viewer ) { + if ( viewer.controlsShouldFade ) { + let currentTime = $.now(); + let deltaTime = currentTime - viewer.controlsFadeBeginTime; + let opacity = 1.0 - deltaTime / viewer.controlsFadeLength; + + opacity = Math.min( 1.0, opacity ); + opacity = Math.max( 0.0, opacity ); + + for ( let i = viewer.controls.length - 1; i >= 0; i--) { + if (viewer.controls[ i ].autoFade) { + viewer.controls[ i ].setOpacity( opacity ); + } + } + + if ( opacity > 0 ) { + // fade again + scheduleControlsFade( viewer ); + } + } +} + + +//stop the fade animation on the controls and show them +function abortControlsAutoHide( viewer ) { + viewer.controlsShouldFade = false; + for ( let i = viewer.controls.length - 1; i >= 0; i-- ) { + viewer.controls[ i ].setOpacity( 1.0 ); + } +} + + + +/////////////////////////////////////////////////////////////////////////////// +// Default view event handlers. +/////////////////////////////////////////////////////////////////////////////// +function onFocus(){ + abortControlsAutoHide( this ); +} + +function onBlur(){ + beginControlsAutoHide( this ); + +} + +function onCanvasContextMenu( event ) { + const eventArgs = { + tracker: event.eventSource, + position: event.position, + originalEvent: event.originalEvent, + preventDefault: event.preventDefault + }; + + /** + * Raised when a contextmenu event occurs in the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-contextmenu + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefault - Set to true to prevent the default user-agent's handling of the contextmenu event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-contextmenu', eventArgs ); + + event.preventDefault = eventArgs.preventDefault; +} + +/** + * Maps keyboard events to corresponding navigation actions, + * accounting for Shift modifier state. + * + * @private + * @param {Object} event - Keyboard event object + * Returns string Navigation action name (e.g. 'panUp') or null if unmapped + * + * Handles: + * - Arrow/WASD keys with Shift for zoom + * - Arrow/WASD keys without Shift for panning + * - Equal(=)/Minus(-) keys for zoom + */ +function getActiveActionFromKey(code, shift) { + switch (code) { + case 'ArrowUp': + return shift ? 'zoomIn' : 'panUp'; + case 'ArrowDown': + return shift ? 'zoomOut' : 'panDown'; + case 'ArrowLeft': + return 'panLeft'; + case 'ArrowRight': + return 'panRight'; + case 'Equal': + return 'zoomIn'; + case 'Minus': + return 'zoomOut'; + default: + return null; + } +} + +/** + * Handles the keyup event on the viewer's canvas element. + * + * @private + * For the released key, marks both the shifted and non-shifted navigation actions as inactive in the _activeActions object. + * If either action is released before reaching the minimum frame threshold, sets that action as "virtually held" in _navActionVirtuallyHeld, + * ensuring smooth completion of the minimum pan or zoom distance regardless of modifier key release order. + */ +function onCanvasKeyUp(event) { + + // Using arrow function to inherit 'this' from parent scope + const processCombo = (code, shift) => { + const action = getActiveActionFromKey(code, shift); + + if (action && this._activeActions[action]) { + this._activeActions[action] = false; + // If the action was released before the minimum frame threshold, + // keep it "virtually held" for smoothness + if (this._navActionFrames[action] < this._minNavActionFrames) { + this._navActionVirtuallyHeld[action] = true; + } + } + }; + + // We don't know if the shift key was held down originally, so we check them both. + // Clear both possible actions for this key + const code = event.originalEvent.code; + processCombo(code, true); + processCombo(code, false); +} + + +function onCanvasKeyDown( event ) { + + const canvasKeyDownEventArgs = { + originalEvent: event.originalEvent, + preventDefaultAction: !this.keyboardNavEnabled, + preventVerticalPan: event.preventVerticalPan || !this.panVertical, + preventHorizontalPan: event.preventHorizontalPan || !this.panHorizontal + }; + + /** + * Raised when a keyboard key is pressed and the focus is on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-key + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultAction - Set to true to prevent default keyboard behaviour. Default: false. + * @property {Boolean} preventVerticalPan - Set to true to prevent keyboard vertical panning. Default: false. + * @property {Boolean} preventHorizontalPan - Set to true to prevent keyboard horizontal panning. Default: false. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + + this.raiseEvent('canvas-key', canvasKeyDownEventArgs); + + if ( !canvasKeyDownEventArgs.preventDefaultAction && !event.ctrl && !event.alt && !event.meta ) { + + const code = event.originalEvent.code; + const shift = event.shift; + const action = getActiveActionFromKey(code, shift); + + if (action && !this._activeActions[action]) { + this._activeActions[action] = true; // Mark this action as held down in the viewer's internal tracking object + this._navActionFrames[action] = 0; // Reset action frames + event.preventDefault = true; // prevent browser scroll/zoom, etc + return; + } + + switch( event.keyCode ){ + case 48://0|) + this.viewport.goHome(); + this.viewport.applyConstraints(); + event.preventDefault = true; + break; + case 82: //r - clockwise rotation/R - counterclockwise rotation + if(event.shift){ + if(this.viewport.flipped){ + this.viewport.setRotation(this.viewport.getRotation() + this.rotationIncrement); + } else{ + this.viewport.setRotation(this.viewport.getRotation() - this.rotationIncrement); + } + }else{ + if(this.viewport.flipped){ + this.viewport.setRotation(this.viewport.getRotation() - this.rotationIncrement); + } else{ + this.viewport.setRotation(this.viewport.getRotation() + this.rotationIncrement); + } + } + this.viewport.applyConstraints(); + event.preventDefault = true; + break; + case 70: //f/F + this.viewport.toggleFlip(); + event.preventDefault = true; + break; + case 74: //j - previous image source + this.goToPreviousPage(); + break; + case 75: //k - next image source + this.goToNextPage(); + break; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + event.preventDefault = false; + break; + } + } else { + event.preventDefault = false; + } +} + +function onCanvasKeyPress( event ) { + const canvasKeyPressEventArgs = { + originalEvent: event.originalEvent, + }; + + /** + * Raised when a keyboard key is pressed and the focus is on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-key-press + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + + this.raiseEvent('canvas-key-press', canvasKeyPressEventArgs); +} + +function onCanvasClick( event ) { + let gestureSettings; + + const haveKeyboardFocus = document.activeElement === this.canvas; + + // If we don't have keyboard focus, request it. + if ( !haveKeyboardFocus ) { + this.canvas.focus(); + } + if(this.viewport.flipped){ + event.position.x = this.viewport.getContainerSize().x - event.position.x; + } + + const canvasClickEventArgs = { + tracker: event.eventSource, + position: event.position, + quick: event.quick, + shift: event.shift, + originalEvent: event.originalEvent, + originalTarget: event.originalTarget, + preventDefaultAction: false + }; + + /** + * Raised when a mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-click + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Boolean} quick - True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for differentiating between clicks and drags. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Element} originalTarget - The DOM element clicked on. + * @property {Boolean} preventDefaultAction - Set to true to prevent default click to zoom behaviour. Default: false. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + + this.raiseEvent( 'canvas-click', canvasClickEventArgs); + + + if ( !canvasClickEventArgs.preventDefaultAction && this.viewport && event.quick ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + + if (gestureSettings.clickToZoom === true){ + this.viewport.zoomBy( + event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick, + gestureSettings.zoomToRefPoint ? this.viewport.pointFromPixel( event.position, true ) : null + ); + this.viewport.applyConstraints(); + } + + if( gestureSettings.dblClickDragToZoom){ + if(THIS[ this.hash ].draggingToZoom === true){ + THIS[ this.hash ].lastClickTime = null; + THIS[ this.hash ].draggingToZoom = false; + } + else{ + THIS[ this.hash ].lastClickTime = $.now(); + } + } + + } +} + +function onCanvasDblClick( event ) { + let gestureSettings; + + const canvasDblClickEventArgs = { + tracker: event.eventSource, + position: event.position, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: false + }; + + /** + * Raised when a double mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-double-click + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultAction - Set to true to prevent default double tap to zoom behaviour. Default: false. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-double-click', canvasDblClickEventArgs); + + if ( !canvasDblClickEventArgs.preventDefaultAction && this.viewport ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.dblClickToZoom ) { + this.viewport.zoomBy( + event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick, + gestureSettings.zoomToRefPoint ? this.viewport.pointFromPixel( event.position, true ) : null + ); + this.viewport.applyConstraints(); + } + } +} + +function onCanvasDrag( event ) { + let gestureSettings; + + const canvasDragEventArgs = { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + delta: event.delta, + speed: event.speed, + direction: event.direction, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: false + }; + + /** + * Raised when a mouse or touch drag operation occurs on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-drag + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag. + * @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 {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultAction - Set to true to prevent default drag to pan behaviour. Default: false. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-drag', canvasDragEventArgs); + + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + + if(!canvasDragEventArgs.preventDefaultAction && this.viewport){ + + if (gestureSettings.dblClickDragToZoom && THIS[ this.hash ].draggingToZoom){ + const factor = Math.pow( this.zoomPerDblClickDrag, event.delta.y / 50); + this.viewport.zoomBy(factor); + } + else if (gestureSettings.dragToPan && !THIS[ this.hash ].draggingToZoom) { + if( !this.panHorizontal ){ + event.delta.x = 0; + } + if( !this.panVertical ){ + event.delta.y = 0; + } + if(this.viewport.flipped){ + event.delta.x = -event.delta.x; + } + + if( this.constrainDuringPan ){ + const delta = this.viewport.deltaPointsFromPixels( event.delta.negate() ); + + this.viewport.centerSpringX.target.value += delta.x; + this.viewport.centerSpringY.target.value += delta.y; + + const constrainedBounds = this.viewport.getConstrainedBounds(); + + this.viewport.centerSpringX.target.value -= delta.x; + this.viewport.centerSpringY.target.value -= delta.y; + + if (constrainedBounds.xConstrained) { + event.delta.x = 0; + } + + if (constrainedBounds.yConstrained) { + event.delta.y = 0; + } + } + this.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta.negate() ), gestureSettings.flickEnabled && !this.constrainDuringPan); + } + + } + +} + +function onCanvasDragEnd( event ) { + let gestureSettings; + const canvasDragEndEventArgs = { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + speed: event.speed, + direction: event.direction, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: false + }; + + /** + * Raised when a mouse or touch drag operation ends on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-drag-end + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} speed - Speed at the end of a drag gesture, in pixels per second. + * @property {Number} direction - Direction at the end of a drag gesture, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultAction - Set to true to prevent default drag-end flick behaviour. Default: false. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('canvas-drag-end', canvasDragEndEventArgs); + + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + + if (!canvasDragEndEventArgs.preventDefaultAction && this.viewport) { + if ( !THIS[ this.hash ].draggingToZoom && + gestureSettings.dragToPan && + gestureSettings.flickEnabled && + event.speed >= gestureSettings.flickMinSpeed) { + let amplitudeX = 0; + if (this.panHorizontal) { + amplitudeX = gestureSettings.flickMomentum * event.speed * + Math.cos(event.direction); + } + let amplitudeY = 0; + if (this.panVertical) { + amplitudeY = gestureSettings.flickMomentum * event.speed * + Math.sin(event.direction); + } + const center = this.viewport.pixelFromPoint( + this.viewport.getCenter(true)); + const target = this.viewport.pointFromPixel( + new $.Point(center.x - amplitudeX, center.y - amplitudeY)); + this.viewport.panTo(target, false); + } + this.viewport.applyConstraints(); + } + + + if( gestureSettings.dblClickDragToZoom && THIS[ this.hash ].draggingToZoom === true ){ + THIS[ this.hash ].draggingToZoom = false; + } + + +} + +function onCanvasEnter( event ) { + /** + * Raised when a pointer enters the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-enter + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. + * @property {Number} pointers - Number of pointers (all types) active in the tracked element. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-enter', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + buttons: event.buttons, + pointers: event.pointers, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + +function onCanvasLeave( event ) { + /** + * Raised when a pointer leaves the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-exit + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. + * @property {Number} pointers - Number of pointers (all types) active in the tracked element. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-exit', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + buttons: event.buttons, + pointers: event.pointers, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + +function onCanvasPress( event ) { + + /** + * Raised when the primary mouse button is pressed or touch starts on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-press + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-press', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + insideElementPressed: event.insideElementPressed, + insideElementReleased: event.insideElementReleased, + originalEvent: event.originalEvent + }); + + + const gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.dblClickDragToZoom ){ + const lastClickTime = THIS[ this.hash ].lastClickTime; + const currClickTime = $.now(); + + if ( lastClickTime === null) { + return; + } + + if ((currClickTime - lastClickTime) < this.dblClickTimeThreshold) { + THIS[ this.hash ].draggingToZoom = true; + } + + THIS[ this.hash ].lastClickTime = null; + } + +} + +function onCanvasRelease( event ) { + /** + * Raised when the primary mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-release + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-release', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + insideElementPressed: event.insideElementPressed, + insideElementReleased: event.insideElementReleased, + originalEvent: event.originalEvent + }); +} + +function onCanvasNonPrimaryPress( event ) { + /** + * Raised when any non-primary pointer button is pressed on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-nonprimary-press + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {Number} 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. + * @property {Number} 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. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-nonprimary-press', { + tracker: event.eventSource, + position: event.position, + pointerType: event.pointerType, + button: event.button, + buttons: event.buttons, + originalEvent: event.originalEvent + }); +} + +function onCanvasNonPrimaryRelease( event ) { + /** + * Raised when any non-primary pointer button is released on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-nonprimary-release + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {Number} 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. + * @property {Number} 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. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-nonprimary-release', { + tracker: event.eventSource, + position: event.position, + pointerType: event.pointerType, + button: event.button, + buttons: event.buttons, + originalEvent: event.originalEvent + }); +} + +function onCanvasPinch( event ) { + let centerPt; + let lastCenterPt; + let panByPt; + + const canvasPinchEventArgs = { + tracker: event.eventSource, + pointerType: event.pointerType, + gesturePoints: event.gesturePoints, + lastCenter: event.lastCenter, + center: event.center, + lastDistance: event.lastDistance, + distance: event.distance, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultPanAction: false, + preventDefaultZoomAction: false, + preventDefaultRotateAction: false + }; + + /** + * Raised when a pinch event occurs on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-pinch + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {Array.} gesturePoints - Gesture points associated with the gesture. Velocity data can be found here. + * @property {OpenSeadragon.Point} lastCenter - The previous center point of the two pinch contact points relative to the tracked element. + * @property {OpenSeadragon.Point} center - The center point of the two pinch contact points relative to the tracked element. + * @property {Number} lastDistance - The previous distance between the two pinch contact points in CSS pixels. + * @property {Number} distance - The distance between the two pinch contact points in CSS pixels. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultPanAction - Set to true to prevent default pinch to pan behaviour. Default: false. + * @property {Boolean} preventDefaultZoomAction - Set to true to prevent default pinch to zoom behaviour. Default: false. + * @property {Boolean} preventDefaultRotateAction - Set to true to prevent default pinch to rotate behaviour. Default: false. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('canvas-pinch', canvasPinchEventArgs); + + if ( this.viewport ) { + let gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.pinchToZoom && + (!canvasPinchEventArgs.preventDefaultPanAction || !canvasPinchEventArgs.preventDefaultZoomAction) ) { + centerPt = this.viewport.pointFromPixel( event.center, true ); + if ( gestureSettings.zoomToRefPoint && !canvasPinchEventArgs.preventDefaultPanAction ) { + lastCenterPt = this.viewport.pointFromPixel( event.lastCenter, true ); + panByPt = lastCenterPt.minus( centerPt ); + if( !this.panHorizontal ) { + panByPt.x = 0; + } + if( !this.panVertical ) { + panByPt.y = 0; + } + this.viewport.panBy(panByPt, true); + } + if ( !canvasPinchEventArgs.preventDefaultZoomAction ) { + this.viewport.zoomBy( event.distance / event.lastDistance, centerPt, true ); + } + this.viewport.applyConstraints(); + } + if ( gestureSettings.pinchRotate && !canvasPinchEventArgs.preventDefaultRotateAction ) { + // Pinch rotate + const angle1 = Math.atan2(event.gesturePoints[0].currentPos.y - event.gesturePoints[1].currentPos.y, + event.gesturePoints[0].currentPos.x - event.gesturePoints[1].currentPos.x); + const angle2 = Math.atan2(event.gesturePoints[0].lastPos.y - event.gesturePoints[1].lastPos.y, + event.gesturePoints[0].lastPos.x - event.gesturePoints[1].lastPos.x); + centerPt = this.viewport.pointFromPixel( event.center, true ); + this.viewport.rotateTo(this.viewport.getRotation(true) + ((angle1 - angle2) * (180 / Math.PI)), centerPt, true); + } + } +} + +function onCanvasFocus( event ) { + + /** + * Raised when the {@link OpenSeadragon.Viewer#canvas} element gets keyboard focus. + * + * @event canvas-focus + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-focus', { + tracker: event.eventSource, + originalEvent: event.originalEvent + }); +} + +function onCanvasBlur( event ) { + + // When canvas loses focus, clear all navigation key states. + for (const action in this._activeActions) { + this._activeActions[action] = false; + } + for (const action in this._navActionVirtuallyHeld) { + this._navActionVirtuallyHeld[action] = false; + } + + /** + * Raised when the {@link OpenSeadragon.Viewer#canvas} element loses keyboard focus. + * + * @event canvas-blur + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-blur', { + tracker: event.eventSource, + originalEvent: event.originalEvent + }); +} + +function onCanvasScroll( event ) { + let canvasScrollEventArgs; + let gestureSettings; + let factor; + + /* Certain scroll devices fire the scroll event way too fast so we are injecting a simple adjustment to keep things + * partially normalized. If we have already fired an event within the last 'minScrollDelta' milliseconds we skip + * this one and wait for the next event. */ + const thisScrollTime = $.now(); + const deltaScrollTime = thisScrollTime - this._lastScrollTime; + if (deltaScrollTime > this.minScrollDeltaTime) { + this._lastScrollTime = thisScrollTime; + + canvasScrollEventArgs = { + tracker: event.eventSource, + position: event.position, + scroll: event.scroll, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: false, + preventDefault: true + }; + + /** + * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel). + * + * @event canvas-scroll + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} scroll - The scroll delta for the event. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultAction - Set to true to prevent default scroll to zoom behaviour. Default: false. + * @property {Boolean} preventDefault - Set to true to prevent the default user-agent's handling of the wheel event. Default: true. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('canvas-scroll', canvasScrollEventArgs ); + + if ( !canvasScrollEventArgs.preventDefaultAction && this.viewport ) { + if(this.viewport.flipped){ + event.position.x = this.viewport.getContainerSize().x - event.position.x; + } + + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.scrollToZoom ) { + factor = Math.pow( this.zoomPerScroll, event.scroll ); + this.viewport.zoomBy( + factor, + gestureSettings.zoomToRefPoint ? this.viewport.pointFromPixel( event.position, true ) : null + ); + this.viewport.applyConstraints(); + } + } + + event.preventDefault = canvasScrollEventArgs.preventDefault; + } else { + event.preventDefault = true; + } +} + +function onContainerEnter( event ) { + THIS[ this.hash ].mouseInside = true; + abortControlsAutoHide( this ); + /** + * Raised when the cursor enters the {@link OpenSeadragon.Viewer#container} element. + * + * @event container-enter + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. + * @property {Number} pointers - Number of pointers (all types) active in the tracked element. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'container-enter', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + buttons: event.buttons, + pointers: event.pointers, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + +function onContainerLeave( event ) { + if ( event.pointers < 1 ) { + THIS[ this.hash ].mouseInside = false; + if ( !THIS[ this.hash ].animating ) { + beginControlsAutoHide( this ); + } + } + /** + * Raised when the cursor leaves the {@link OpenSeadragon.Viewer#container} element. + * + * @event container-exit + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. + * @property {Number} pointers - Number of pointers (all types) active in the tracked element. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'container-exit', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + buttons: event.buttons, + pointers: event.pointers, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + + +/////////////////////////////////////////////////////////////////////////////// +// Page update routines ( aka Views - for future reference ) +/////////////////////////////////////////////////////////////////////////////// + +function updateMulti( viewer ) { + updateOnce( viewer ); + + // Request the next frame, unless we've been closed + if ( viewer.isOpen() ) { + viewer._updateRequestId = scheduleUpdate( viewer, updateMulti ); + } else { + viewer._updateRequestId = false; + } +} + +function doViewerResize(viewer, containerSize){ + const viewport = viewer.viewport; + const zoom = viewport.getZoom(); + const center = viewport.getCenter(); + viewport.resize(containerSize, viewer.preserveImageSizeOnResize); + viewport.panTo(center, true); + let resizeRatio; + if (viewer.preserveImageSizeOnResize) { + resizeRatio = THIS[viewer.hash].prevContainerSize.x / containerSize.x; + } else { + const origin = new $.Point(0, 0); + const prevDiag = new $.Point(THIS[viewer.hash].prevContainerSize.x, THIS[viewer.hash].prevContainerSize.y).distanceTo(origin); + const newDiag = new $.Point(containerSize.x, containerSize.y).distanceTo(origin); + resizeRatio = newDiag / prevDiag * THIS[viewer.hash].prevContainerSize.x / containerSize.x; + } + viewport.zoomTo(zoom * resizeRatio, null, true); + THIS[viewer.hash].prevContainerSize = containerSize; + THIS[viewer.hash].forceRedraw = true; + THIS[viewer.hash].needsResize = false; + THIS[viewer.hash].forceResize = false; +} + +function handleNavKeys(viewer) { + // Iterate over all navigation actions. + for (const action in viewer._activeActions) { + if (viewer._activeActions[action] || viewer._navActionVirtuallyHeld[action]) { + viewer._navActionFrames[action]++; + if (viewer._navActionFrames[action] >= viewer._minNavActionFrames) { + viewer._navActionVirtuallyHeld[action] = false; + } + } + } + + // Helper for action state + function isDown(action) { + return viewer._activeActions[action] || viewer._navActionVirtuallyHeld[action]; + } + + // Use the viewer's configured pan amount + const pixels = viewer.pixelsPerArrowPress / 10; + const panDelta = viewer.viewport.deltaPointsFromPixels(new OpenSeadragon.Point(pixels, pixels)); + + // 1. Zoom actions (priority: zoom disables pan) + if (isDown('zoomIn')) { + viewer.viewport.zoomBy(1.01, null, true); + viewer.viewport.applyConstraints(); + return; + } + if (isDown('zoomOut')) { + viewer.viewport.zoomBy(0.99, null, true); + viewer.viewport.applyConstraints(); + return; + } + + // 2. Pan actions + let dx = 0; + let dy = 0; + + if (!viewer.preventVerticalPan) { + if (isDown('panUp')) { + dy -= panDelta.y; + } + if (isDown('panDown')) { + dy += panDelta.y; + } + } + + if (!viewer.preventHorizontalPan) { + if (isDown('panLeft')) { + dx -= panDelta.x; + } + if (isDown('panRight')) { + dx += panDelta.x; + } + } + + if (dx !== 0 || dy !== 0) { + viewer.viewport.panBy(new OpenSeadragon.Point(dx, dy), true); + viewer.viewport.applyConstraints(); + } +} + +function updateOnce( viewer ) { + + handleNavKeys(viewer); + + //viewer.profiler.beginUpdate(); + + if (viewer._opening || !THIS[viewer.hash]) { + return; + } + + let viewerWasResized = false; + if (viewer.autoResize || THIS[viewer.hash].forceResize){ + let containerSize; + if(viewer._autoResizePolling){ + containerSize = _getSafeElemSize(viewer.container); + const prevContainerSize = THIS[viewer.hash].prevContainerSize; + if (!containerSize.equals(prevContainerSize)) { + THIS[viewer.hash].needsResize = true; + } + } + if(THIS[viewer.hash].needsResize){ + doViewerResize(viewer, containerSize || _getSafeElemSize(viewer.container)); + viewerWasResized = true; + } + + } + + + + const viewportChange = viewer.viewport.update() || viewerWasResized; + let animated = viewer.world.update(viewportChange) || viewportChange; + + if (viewportChange) { + /** + * Raised when any spring animation update occurs (zoom, pan, etc.), + * before the viewer has drawn the new location. + * + * @event viewport-change + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + viewer.raiseEvent('viewport-change'); + } + + if( viewer.referenceStrip ){ + animated = viewer.referenceStrip.update( viewer.viewport ) || animated; + } + + const currentAnimating = THIS[ viewer.hash ].animating; + + if ( !currentAnimating && animated ) { + /** + * Raised when any spring animation starts (zoom, pan, etc.). + * + * @event animation-start + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + viewer.raiseEvent( "animation-start" ); + abortControlsAutoHide( viewer ); + } + + const isAnimationFinished = currentAnimating && !animated; + + if ( isAnimationFinished ) { + THIS[ viewer.hash ].animating = false; + } + + if ( animated || isAnimationFinished || THIS[ viewer.hash ].forceRedraw || viewer.world.needsDraw() ) { + drawWorld( viewer ); + viewer._drawOverlays(); + if( viewer.navigator ){ + viewer.navigator.update( viewer.viewport ); + } + + THIS[ viewer.hash ].forceRedraw = false; + + if (animated) { + /** + * Raised when any spring animation update occurs (zoom, pan, etc.), + * after the viewer has drawn the new location. + * + * @event animation + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + viewer.raiseEvent( "animation" ); + } + } + + if ( isAnimationFinished ) { + /** + * Raised when any spring animation ends (zoom, pan, etc.). + * + * @event animation-finish + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + viewer.raiseEvent( "animation-finish" ); + + if ( !THIS[ viewer.hash ].mouseInside ) { + beginControlsAutoHide( viewer ); + } + } + + THIS[ viewer.hash ].animating = animated; + + //viewer.profiler.endUpdate(); +} + +function drawWorld( viewer ) { + viewer.imageLoader.clear(); + viewer.world.draw(); + + /** + * - Needs documentation - + * + * @event update-viewport + * @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. + */ + viewer.raiseEvent( 'update-viewport', {} ); +} + +/////////////////////////////////////////////////////////////////////////////// +// Navigation Controls +/////////////////////////////////////////////////////////////////////////////// +function resolveUrl( prefix, url ) { + return prefix ? prefix + url : url; +} + + +function scheduleZoom( viewer ) { + $.requestAnimationFrame( $.delegate( viewer, doZoom ) ); +} + + +function doZoom() { + if ( THIS[ this.hash ].zooming && this.viewport) { + const currentTime = $.now(); + const deltaTime = currentTime - THIS[ this.hash ].lastZoomTime; + const adjustedFactor = Math.pow( THIS[ this.hash ].zoomFactor, deltaTime / 1000 ); + + this.viewport.zoomBy( adjustedFactor ); + this.viewport.applyConstraints(); + THIS[ this.hash ].lastZoomTime = currentTime; + scheduleZoom( this ); + } +} + + +function lightUp() { + if (this.buttonGroup) { + this.buttonGroup.emulateEnter(); + this.buttonGroup.emulateLeave(); + } +} + + +function onHome() { + if ( this.viewport ) { + this.viewport.goHome(); + } +} + + +function onFullScreen() { + if ( this.isFullPage() && !$.isFullScreen() ) { + // Is fullPage but not fullScreen + this.setFullPage( false ); + } else { + this.setFullScreen( !this.isFullPage() ); + } + // correct for no mouseout event on change + if ( this.buttonGroup ) { + this.buttonGroup.emulateLeave(); + } + this.fullPageButton.element.focus(); + if ( this.viewport ) { + this.viewport.applyConstraints(); + } +} + +function onRotateLeft() { + if ( this.viewport ) { + let currRotation = this.viewport.getRotation(); + + if ( this.viewport.flipped ){ + currRotation += this.rotationIncrement; + } else { + currRotation -= this.rotationIncrement; + } + this.viewport.setRotation(currRotation); + } +} + +function onRotateRight() { + if ( this.viewport ) { + let currRotation = this.viewport.getRotation(); + + if ( this.viewport.flipped ){ + currRotation -= this.rotationIncrement; + } else { + currRotation += this.rotationIncrement; + } + this.viewport.setRotation(currRotation); + } +} +/** + * Note: When pressed flip control button + */ +function onFlip() { + this.viewport.toggleFlip(); +} + +/** + * Return the drawer type string for a candidate (string or DrawerBase constructor). + * Used to normalize drawerCandidates to strings so includes('canvas') is reliable. + * @private + * @param {string|Function} candidate - Drawer type string or constructor + * @returns {string|undefined} Type string, or undefined if not resolvable + */ +function getDrawerTypeString(candidate) { + if (typeof candidate === 'string') { + return candidate; + } + const proto = candidate && candidate.prototype; + if (proto && proto instanceof OpenSeadragon.DrawerBase && $.isFunction(proto.getType)) { + return proto.getType.call(candidate); + } + return undefined; +} + +/** + * Return the list of drawer type strings that 'auto' expands to (platform-dependent). + * Uses the same detection as determineDrawer('auto'): on iOS-like devices, ['canvas'] only; + * on all other platforms, ['webgl', 'canvas'] so webgl is tried first and canvas next if WebGL fails. + * @private + * @returns {string[]} + */ +function getAutoDrawerCandidates() { + // Our WebGL drawer is not as performant on iOS at the moment, so we use canvas there. + // Note that modern iPads report themselves as Mac, so we also check for coarse pointer. + const isPrimaryTouch = window.matchMedia('(pointer: coarse)').matches; + const isIOSDevice = /iPad|iPhone|iPod|Mac/.test(navigator.userAgent) && isPrimaryTouch; + return isIOSDevice ? ['canvas'] : ['webgl', 'canvas']; +} + +/** + * Find drawer + */ +$.determineDrawer = function( id ){ + if (id === 'auto') { + // Same platform detection as getAutoDrawerCandidates(); first entry is the preferred drawer type. + id = getAutoDrawerCandidates()[0]; + } + + for (const property in OpenSeadragon) { + const drawer = OpenSeadragon[ property ]; + const proto = drawer.prototype; + if( proto && + proto instanceof OpenSeadragon.DrawerBase && + $.isFunction( proto.getType ) && + proto.getType.call( drawer ) === id + ){ + return drawer; + } + } + return null; +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Navigator + * + * 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 Navigator + * @classdesc The Navigator provides a small view of the current image as fixed + * while representing the viewport as a moving box serving as a frame + * of reference in the larger viewport as to which portion of the image + * is currently being examined. The navigator's viewport can be interacted + * with using the keyboard or the mouse. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.Viewer + * @extends OpenSeadragon.EventSource + * @param {Object} options - Navigator options + * @param {Element} [options.element] - An element to use for the navigator. + * @param {String} [options.id] - Id of the element to use for the navigator. However, this is ignored if {@link options.element} is provided. + */ +$.Navigator = function( options ){ + + const viewer = options.viewer; + const _this = this; + let viewerSize; + let navigatorSize; + + //We may need to create a new element and id if they did not + //provide the id for the existing element or the element itself + if( options.element || options.id ){ + if ( options.element ) { + if ( options.id ){ + $.console.warn("Given option.id for Navigator was ignored since option.element was provided and is being used instead."); + } + + // Don't overwrite the element's id if it has one already + if ( options.element.id ) { + options.id = options.element.id; + } else { + options.id = 'navigator-' + $.now(); + } + + this.element = options.element; + } else { + this.element = document.getElementById( options.id ); + } + + options.controlOptions = { + anchor: $.ControlAnchor.NONE, + attachToViewer: false, + autoFade: false + }; + } else { + options.id = 'navigator-' + $.now(); + this.element = $.makeNeutralElement( "div" ); + options.controlOptions = { + anchor: $.ControlAnchor.TOP_RIGHT, + attachToViewer: true, + autoFade: options.autoFade + }; + + if( options.position ){ + if( 'BOTTOM_RIGHT' === options.position ){ + options.controlOptions.anchor = $.ControlAnchor.BOTTOM_RIGHT; + } else if( 'BOTTOM_LEFT' === options.position ){ + options.controlOptions.anchor = $.ControlAnchor.BOTTOM_LEFT; + } else if( 'TOP_RIGHT' === options.position ){ + options.controlOptions.anchor = $.ControlAnchor.TOP_RIGHT; + } else if( 'TOP_LEFT' === options.position ){ + options.controlOptions.anchor = $.ControlAnchor.TOP_LEFT; + } else if( 'ABSOLUTE' === options.position ){ + options.controlOptions.anchor = $.ControlAnchor.ABSOLUTE; + options.controlOptions.top = options.top; + options.controlOptions.left = options.left; + options.controlOptions.height = options.height; + options.controlOptions.width = options.width; + } + } + } + this.element.id = options.id; + this.element.className += ' navigator'; + + options = $.extend( true, { + sizeRatio: $.DEFAULT_SETTINGS.navigatorSizeRatio + }, options, { + element: this.element, + tabIndex: -1, // No keyboard navigation, omit from tab order + //These need to be overridden to prevent recursion since + //the navigator is a viewer and a viewer has a navigator + showNavigator: false, + mouseNavEnabled: false, + showNavigationControl: false, + showSequenceControl: false, + immediateRender: true, + blendTime: 0, + animationTime: options.animationTime, + // disable autoResize since resize behavior is implemented differently by the navigator + autoResize: false, + // prevent resizing the navigator from adding unwanted space around the image + minZoomImageRatio: 1.0, + background: options.background, + opacity: options.opacity, + borderColor: options.borderColor, + displayRegionColor: options.displayRegionColor + }); + + options.minPixelRatio = this.minPixelRatio = viewer.minPixelRatio; + + $.setElementTouchActionNone( this.element ); + + this.borderWidth = 2; + //At some browser magnification levels the display regions lines up correctly, but at some there appears to + //be a one pixel gap. + this.fudge = new $.Point(1, 1); + this.totalBorderWidths = new $.Point(this.borderWidth * 2, this.borderWidth * 2).minus(this.fudge); + + + if ( options.controlOptions.anchor !== $.ControlAnchor.NONE ) { + (function( style, borderWidth ){ + style.margin = '0px'; + style.border = borderWidth + 'px solid ' + options.borderColor; + style.padding = '0px'; + style.background = options.background; + style.opacity = options.opacity; + style.overflow = 'hidden'; + }( this.element.style, this.borderWidth)); + } + + this.displayRegion = $.makeNeutralElement( "div" ); + this.displayRegion.id = this.element.id + '-displayregion'; + this.displayRegion.className = 'displayregion'; + + (function( style, borderWidth ){ + style.position = 'relative'; + style.top = '0px'; + style.left = '0px'; + style.fontSize = '0px'; + style.overflow = 'hidden'; + style.border = borderWidth + 'px solid ' + options.displayRegionColor; + style.margin = '0px'; + style.padding = '0px'; + style.background = 'transparent'; + + // We use square bracket notation on the statement below, because float is a keyword. + // This is important for the Google Closure compiler, if nothing else. + /*jshint sub:true */ + style['float'] = 'left'; //Webkit + + style.cssFloat = 'left'; //Firefox + style.zIndex = 999999999; + style.cursor = 'default'; + style.boxSizing = 'content-box'; + }( this.displayRegion.style, this.borderWidth )); + $.setElementPointerEventsNone( this.displayRegion ); + $.setElementTouchActionNone( this.displayRegion ); + + this.displayRegionContainer = $.makeNeutralElement("div"); + this.displayRegionContainer.id = this.element.id + '-displayregioncontainer'; + this.displayRegionContainer.className = "displayregioncontainer"; + this.displayRegionContainer.style.width = "100%"; + this.displayRegionContainer.style.height = "100%"; + $.setElementPointerEventsNone( this.displayRegionContainer ); + $.setElementTouchActionNone( this.displayRegionContainer ); + + viewer.addControl( + this.element, + options.controlOptions + ); + + this._resizeWithViewer = options.controlOptions.anchor !== $.ControlAnchor.ABSOLUTE && + options.controlOptions.anchor !== $.ControlAnchor.NONE; + + if (options.width && options.height) { + this.setWidth(options.width); + this.setHeight(options.height); + } else if ( this._resizeWithViewer ) { + viewerSize = $.getElementSize( viewer.element ); + this.element.style.height = Math.round( viewerSize.y * options.sizeRatio ) + 'px'; + this.element.style.width = Math.round( viewerSize.x * options.sizeRatio ) + 'px'; + this.oldViewerSize = viewerSize; + navigatorSize = $.getElementSize( this.element ); + this.elementArea = navigatorSize.x * navigatorSize.y; + } + + this.oldContainerSize = new $.Point( 0, 0 ); + + $.Viewer.apply( this, [ options ] ); + + this.displayRegionContainer.appendChild(this.displayRegion); + this.element.getElementsByTagName('div')[0].appendChild(this.displayRegionContainer); + + function rotate(degrees, immediately) { + _setTransformRotate(_this.displayRegionContainer, degrees); + _setTransformRotate(_this.displayRegion, -degrees); + _this.viewport.setRotation(degrees, immediately); + } + if (options.navigatorRotate) { + const degrees = options.viewer.viewport ? + options.viewer.viewport.getRotation() : + options.viewer.degrees || 0; + + rotate(degrees, true); + options.viewer.addHandler("rotate", function (args) { + rotate(args.degrees, args.immediately); + }); + } + + + // Remove the base class' (Viewer's) innerTracker and replace it with our own + this.innerTracker.destroy(); + this.innerTracker = new $.MouseTracker({ + userData: 'Navigator.innerTracker', + element: this.element, //this.canvas, + dragHandler: $.delegate( this, onCanvasDrag ), + clickHandler: $.delegate( this, onCanvasClick ), + releaseHandler: $.delegate( this, onCanvasRelease ), + scrollHandler: $.delegate( this, onCanvasScroll ), + preProcessEventHandler: function (eventInfo) { + if (eventInfo.eventType === 'wheel') { + //don't scroll the page up and down if the user is scrolling + //in the navigator + eventInfo.preventDefault = true; + } + } + }); + this.outerTracker.userData = 'Navigator.outerTracker'; + + // this.innerTracker is attached to this.element...we need to allow pointer + // events to pass through this Viewer's canvas/container elements so implicit + // pointer capture works on touch devices + //TODO an alternative is to attach the new MouseTracker to this.canvas...not + // sure why it isn't already (see MouseTracker constructor call above) + $.setElementPointerEventsNone( this.canvas ); + $.setElementPointerEventsNone( this.container ); + + this.addHandler("reset-size", function() { + if (_this.viewport) { + _this.viewport.goHome(true); + } + }); + + viewer.world.addHandler("item-index-change", function(event) { + window.setTimeout(function(){ + const item = _this.world.getItemAt(event.previousIndex); + _this.world.setItemIndex(item, event.newIndex); + }, 1); + }); + + viewer.world.addHandler("remove-item", function(event) { + const theirItem = event.item; + const myItem = _this._getMatchingItem(theirItem); + if (myItem) { + _this.world.removeItem(myItem); + } + }); + + this.update(viewer.viewport); +}; + +$.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /** @lends OpenSeadragon.Navigator.prototype */{ + + /** + * Used to notify the navigator when its size has changed. Especially useful when the navigator is resizable. + * @function + */ + updateSize: function () { + if ( this.viewport ) { + const containerSize = new $.Point( + (this.container.clientWidth === 0 ? 1 : this.container.clientWidth), + (this.container.clientHeight === 0 ? 1 : this.container.clientHeight) + ); + + if ( !containerSize.equals( this.oldContainerSize ) ) { + this.viewport.resize( containerSize, true ); + this.viewport.goHome(true); + this.oldContainerSize = containerSize; + this.world.update(); + this.world.draw(); + this.update(this.viewer.viewport); + } + } + }, + + /** + * Explicitly sets the width of the navigator, in web coordinates. Disables automatic resizing. + * @param {Number|String} width - the new width, either a number of pixels or a CSS string, such as "100%" + */ + setWidth: function(width) { + this.width = width; + this.element.style.width = typeof (width) === "number" ? (width + 'px') : width; + this._resizeWithViewer = false; + this.updateSize(); + }, + + /** + * Explicitly sets the height of the navigator, in web coordinates. Disables automatic resizing. + * @param {Number|String} height - the new height, either a number of pixels or a CSS string, such as "100%" + */ + setHeight: function(height) { + this.height = height; + this.element.style.height = typeof (height) === "number" ? (height + 'px') : height; + this._resizeWithViewer = false; + this.updateSize(); + }, + + /** + * Flip navigator element + * @param {Boolean} state - Flip state to set. + */ + setFlip: function(state) { + this.viewport.setFlip(state); + + this.setDisplayTransform(this.viewer.viewport.getFlip() ? "scale(-1,1)" : "scale(1,1)"); + return this; + }, + + setDisplayTransform: function(rule) { + setElementTransform(this.canvas, rule); + setElementTransform(this.element, rule); + }, + + /** + * Used to update the navigator minimap's viewport rectangle when a change in the viewer's viewport occurs. + * @function + * @param {OpenSeadragon.Viewport} [viewport] The viewport to display. Default: the viewport this navigator is tracking. + */ + update: function( viewport ) { + let viewerSize; + let newWidth; + let newHeight; + let bounds; + let topleft; + let bottomright; + + if(!viewport){ + viewport = this.viewer.viewport; + } + + viewerSize = $.getElementSize( this.viewer.element ); + if ( this._resizeWithViewer && viewerSize.x && viewerSize.y && !viewerSize.equals( this.oldViewerSize ) ) { + this.oldViewerSize = viewerSize; + + if ( this.maintainSizeRatio || !this.elementArea) { + newWidth = viewerSize.x * this.sizeRatio; + newHeight = viewerSize.y * this.sizeRatio; + } else { + newWidth = Math.sqrt(this.elementArea * (viewerSize.x / viewerSize.y)); + newHeight = this.elementArea / newWidth; + } + + this.element.style.width = Math.round( newWidth ) + 'px'; + this.element.style.height = Math.round( newHeight ) + 'px'; + + if (!this.elementArea) { + this.elementArea = newWidth * newHeight; + } + + this.updateSize(); + } + + if (viewport && this.viewport) { + bounds = viewport.getBoundsNoRotate(true); + topleft = this.viewport.pixelFromPointNoRotate(bounds.getTopLeft(), false); + bottomright = this.viewport.pixelFromPointNoRotate(bounds.getBottomRight(), false) + .minus( this.totalBorderWidths ); + + if (!this.navigatorRotate) { + const degrees = viewport.getRotation(true); + _setTransformRotate(this.displayRegion, -degrees); + } + + //update style for navigator-box + const style = this.displayRegion.style; + style.display = this.world.getItemCount() ? 'block' : 'none'; + + style.top = topleft.y.toFixed(2) + "px"; + style.left = topleft.x.toFixed(2) + "px"; + + const width = bottomright.x - topleft.x; + const height = bottomright.y - topleft.y; + // make sure width and height are non-negative so IE doesn't throw + style.width = Math.round( Math.max( width, 0 ) ) + 'px'; + style.height = Math.round( Math.max( height, 0 ) ) + 'px'; + } + + }, + + // overrides Viewer.addTiledImage + addTiledImage: function(options) { + const _this = this; + + const original = options.originalTiledImage; + delete options.original; + + const optionsClone = $.extend({}, options, { + success: function(event) { + const myItem = event.item; + myItem._originalForNavigator = original; + _this._matchBounds(myItem, original, true); + _this._matchOpacity(myItem, original); + _this._matchCompositeOperation(myItem, original); + + function matchBounds() { + _this._matchBounds(myItem, original); + } + + function matchOpacity() { + _this._matchOpacity(myItem, original); + } + + function matchCompositeOperation() { + _this._matchCompositeOperation(myItem, original); + } + + original.addHandler('bounds-change', matchBounds); + original.addHandler('clip-change', matchBounds); + original.addHandler('opacity-change', matchOpacity); + original.addHandler('composite-operation-change', matchCompositeOperation); + } + }); + + return $.Viewer.prototype.addTiledImage.apply(this, [optionsClone]); + }, + + destroy: function() { + return $.Viewer.prototype.destroy.apply(this); + }, + + // private + _getMatchingItem: function(theirItem) { + const count = this.world.getItemCount(); + for (let i = 0; i < count; i++) { + let item = this.world.getItemAt(i); + if (item._originalForNavigator === theirItem) { + return item; + } + } + + return null; + }, + + // private + _matchBounds: function(myItem, theirItem, immediately) { + const bounds = theirItem.getBoundsNoRotate(); + myItem.setPosition(bounds.getTopLeft(), immediately); + myItem.setWidth(bounds.width, immediately); + myItem.setRotation(theirItem.getRotation(), immediately); + myItem.setClip(theirItem.getClip()); + myItem.setFlip(theirItem.getFlip()); + }, + + // private + _matchOpacity: function(myItem, theirItem) { + myItem.setOpacity(theirItem.opacity); + }, + + // private + _matchCompositeOperation: function(myItem, theirItem) { + myItem.setCompositeOperation(theirItem.compositeOperation); + } +}); + + +/** + * @private + * @inner + * @function + */ +function onCanvasClick( event ) { + const canvasClickEventArgs = { + tracker: event.eventSource, + position: event.position, + quick: event.quick, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: false + }; + /** + * Raised when a click event occurs on the {@link OpenSeadragon.Viewer#navigator} element. + * + * @event navigator-click + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Boolean} quick - True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for differentiating between clicks and drags. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + * @property {Boolean} preventDefaultAction - Set to true to prevent default click to zoom behaviour. Default: false. + */ + + this.viewer.raiseEvent('navigator-click', canvasClickEventArgs); + + if ( !canvasClickEventArgs.preventDefaultAction && event.quick && this.viewer.viewport && (this.panVertical || this.panHorizontal)) { + if(this.viewer.viewport.flipped) { + event.position.x = this.viewport.getContainerSize().x - event.position.x; + } + const target = this.viewport.pointFromPixel(event.position); + if (!this.panVertical) { + // perform only horizonal pan + target.y = this.viewer.viewport.getCenter(true).y; + } else if (!this.panHorizontal) { + // perform only vertical pan + target.x = this.viewer.viewport.getCenter(true).x; + } + this.viewer.viewport.panTo(target); + this.viewer.viewport.applyConstraints(); + } + +} + +/** + * @private + * @inner + * @function + */ +function onCanvasDrag( event ) { + const canvasDragEventArgs = { + tracker: event.eventSource, + position: event.position, + delta: event.delta, + speed: event.speed, + direction: event.direction, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: false + }; + /** + * Raised when a drag event occurs on the {@link OpenSeadragon.Viewer#navigator} element. + * + * @event navigator-drag + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag. + * @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 {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + * @property {Boolean} preventDefaultAction - Set to true to prevent default drag to pan behaviour. Default: false. + */ + this.viewer.raiseEvent('navigator-drag', canvasDragEventArgs); + + if ( !canvasDragEventArgs.preventDefaultAction && this.viewer.viewport ) { + if( !this.panHorizontal ){ + event.delta.x = 0; + } + if( !this.panVertical ){ + event.delta.y = 0; + } + + if(this.viewer.viewport.flipped){ + event.delta.x = -event.delta.x; + } + + this.viewer.viewport.panBy( + this.viewport.deltaPointsFromPixels( + event.delta + ) + ); + if( this.viewer.constrainDuringPan ){ + this.viewer.viewport.applyConstraints(); + } + } +} + + +/** + * @private + * @inner + * @function + */ +function onCanvasRelease( event ) { + if ( event.insideElementPressed && this.viewer.viewport ) { + this.viewer.viewport.applyConstraints(); + } +} + + +/** + * @private + * @inner + * @function + */ +function onCanvasScroll( event ) { + const eventArgs = { + tracker: event.eventSource, + position: event.position, + scroll: event.scroll, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefault: event.preventDefault + }; + + /** + * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#navigator} element (mouse wheel, touch pinch, etc.). + * + * @event navigator-scroll + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} scroll - The scroll delta for the event. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefault - Set to true to prevent the default user-agent's handling of the wheel event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'navigator-scroll', eventArgs ); + + event.preventDefault = eventArgs.preventDefault; +} + +/** + * @function + * @private + * @param {Object} element + * @param {Number} degrees + */ +function _setTransformRotate( element, degrees ) { + setElementTransform(element, "rotate(" + degrees + "deg)"); +} + +function setElementTransform( element, rule ) { + element.style.webkitTransform = rule; + element.style.mozTransform = rule; + element.style.msTransform = rule; + element.style.oTransform = rule; + element.style.transform = rule; +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - getString/setString + * + * 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( $ ){ + +//TODO: I guess this is where the i18n needs to be reimplemented. I'll look +// into existing patterns for i18n in javascript but i think that mimicking +// pythons gettext might be a reasonable approach. +const I18N = { + Errors: { + Dzc: "Sorry, we don't support Deep Zoom Collections!", + Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", + Security: "It looks like a security restriction stopped us from " + + "loading this Deep Zoom Image.", + Status: "This space unintentionally left blank ({0} {1}).", + OpenFailed: "Unable to open {0}: {1}" + }, + + Tooltips: { + FullPage: "Toggle full page", + Home: "Go home", + ZoomIn: "Zoom in", + ZoomOut: "Zoom out", + NextPage: "Next page", + PreviousPage: "Previous page", + RotateLeft: "Rotate left", + RotateRight: "Rotate right", + Flip: "Flip Horizontally" + } +}; + +$.extend( $, /** @lends OpenSeadragon */{ + + /** + * @function + * @param {String} property + */ + getString: function( prop ) { + + const props = prop.split('.'); + let string = null; + const args = arguments; + let container = I18N; + let i; + + for (i = 0; i < props.length - 1; i++) { + // in case not a subproperty + container = container[ props[ i ] ] || {}; + } + string = container[ props[ i ] ]; + + if ( typeof ( string ) !== "string" ) { + $.console.error( "Untranslated source string:", prop ); + string = ""; // FIXME: this breaks gettext()-style convention, which would return source + } + + return string.replace(/\{\d+\}/g, function(capture) { + const i = parseInt( capture.match( /\d+/ ), 10 ) + 1; + return i < args.length ? + args[ i ] : + ""; + }); + }, + + /** + * @function + * @param {String} property + * @param {*} value + */ + setString: function( prop, value ) { + + const props = prop.split('.'); + let container = I18N; + let i; + + for ( i = 0; i < props.length - 1; i++ ) { + if ( !container[ props[ i ] ] ) { + container[ props[ i ] ] = {}; + } + container = container[ props[ i ] ]; + } + + container[ props[ i ] ] = value; + } + +}); + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Point + * + * 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 Point + * @classdesc A Point is really used as a 2-dimensional vector, equally useful for + * representing a point on a plane, or the height and width of a plane + * not requiring any other frame of reference. + * + * @memberof OpenSeadragon + * @param {Number} [x] The vector component 'x'. Defaults to the origin at 0. + * @param {Number} [y] The vector component 'y'. Defaults to the origin at 0. + */ +$.Point = function( x, y ) { + /** + * The vector component 'x'. + * @member {Number} x + * @memberof OpenSeadragon.Point# + */ + this.x = typeof ( x ) === "number" ? x : 0; + /** + * The vector component 'y'. + * @member {Number} y + * @memberof OpenSeadragon.Point# + */ + this.y = typeof ( y ) === "number" ? y : 0; +}; + +/** @lends OpenSeadragon.Point.prototype */ +$.Point.prototype = { + /** + * @function + * @returns {OpenSeadragon.Point} a duplicate of this Point + */ + clone: function() { + return new $.Point(this.x, this.y); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + plus: function( point ) { + return new $.Point( + this.x + point.x, + this.y + point.y + ); + }, + + /** + * Subtract another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to subtract vector components. + * @returns {OpenSeadragon.Point} A new point representing the subtraction of the + * vector components + */ + minus: function( point ) { + return new $.Point( + this.x - point.x, + this.y - point.y + ); + }, + + /** + * Multiply this point by a factor and return a new Point. + * @function + * @param {Number} factor The factor to multiply vector components. + * @returns {OpenSeadragon.Point} A new point representing the multiplication + * of the vector components by the factor + */ + times: function( factor ) { + return new $.Point( + this.x * factor, + this.y * factor + ); + }, + + /** + * Divide this point by a factor and return a new Point. + * @function + * @param {Number} factor The factor to divide vector components. + * @returns {OpenSeadragon.Point} A new point representing the division of the + * vector components by the factor + */ + divide: function( factor ) { + return new $.Point( + this.x / factor, + this.y / factor + ); + }, + + /** + * Compute the opposite of this point and return a new Point. + * @function + * @returns {OpenSeadragon.Point} A new point representing the opposite of the + * vector components + */ + negate: function() { + return new $.Point( -this.x, -this.y ); + }, + + /** + * Compute the distance between this point and another point. + * @function + * @param {OpenSeadragon.Point} point The point to compute the distance with. + * @returns {Number} The distance between the 2 points + */ + distanceTo: function( point ) { + return Math.sqrt( + Math.pow( this.x - point.x, 2 ) + + Math.pow( this.y - point.y, 2 ) + ); + }, + + /** + * Compute the squared distance between this point and another point. + * Useful for optimizing things like comparing distances. + * @function + * @param {OpenSeadragon.Point} point The point to compute the squared distance with. + * @returns {Number} The squared distance between the 2 points + */ + squaredDistanceTo: function( point ) { + return Math.pow( this.x - point.x, 2 ) + + Math.pow( this.y - point.y, 2 ); + }, + + /** + * Apply a function to each coordinate of this point and return a new point. + * @function + * @param {function} func The function to apply to each coordinate. + * @returns {OpenSeadragon.Point} A new point with the coordinates computed + * by the specified function + */ + apply: function( func ) { + return new $.Point( func( this.x ), func( this.y ) ); + }, + + /** + * Check if this point is equal to another one. + * @function + * @param {OpenSeadragon.Point} point The point to compare this point with. + * @returns {Boolean} true if they are equal, false otherwise. + */ + equals: function( point ) { + return ( + point instanceof $.Point + ) && ( + this.x === point.x + ) && ( + this.y === point.y + ); + }, + + /** + * Rotates the point around the specified pivot + * From http://stackoverflow.com/questions/4465931/rotate-rectangle-around-a-point + * @function + * @param {Number} degress to rotate around the pivot. + * @param {OpenSeadragon.Point} [pivot=(0,0)] Point around which to rotate. + * Defaults to the origin. + * @returns {OpenSeadragon.Point}. A new point representing the point rotated around the specified pivot + */ + rotate: function (degrees, pivot) { + pivot = pivot || new $.Point(0, 0); + let cos; + let sin; + // Avoid float computations when possible + if (degrees % 90 === 0) { + const d = $.positiveModulo(degrees, 360); + switch (d) { + case 0: + cos = 1; + sin = 0; + break; + case 90: + cos = 0; + sin = 1; + break; + case 180: + cos = -1; + sin = 0; + break; + case 270: + cos = 0; + sin = -1; + break; + } + } else { + const angle = degrees * Math.PI / 180.0; + cos = Math.cos(angle); + sin = Math.sin(angle); + } + const x = cos * (this.x - pivot.x) - sin * (this.y - pivot.y) + pivot.x; + const y = sin * (this.x - pivot.x) + cos * (this.y - pivot.y) + pivot.y; + return new $.Point(x, y); + }, + + /** + * Convert this point to a string in the format (x,y) where x and y are + * rounded to the nearest integer. + * @function + * @returns {String} A string representation of this point. + */ + toString: function() { + return "(" + (Math.round(this.x * 100) / 100) + "," + (Math.round(this.y * 100) / 100) + ")"; + } +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - TileSource + * + * 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.TileSourceOptions + * @property {String} [options.url] + * The URL for the data necessary for this TileSource. + * @property {String} [options.referenceStripThumbnailUrl] + * The URL for a thumbnail image to be used by the reference strip + * @property {Function} [options.success] + * A function to be called upon successful creation. + * @property {Boolean} [options.ajaxWithCredentials] + * If this TileSource needs to make an AJAX call, this specifies whether to set + * the XHR's withCredentials (for accessing secure data). + * @property {Object} [options.ajaxHeaders] + * A set of headers to include in AJAX requests. + * @property {Boolean} [options.splitHashDataForPost] + * First occurrence of '#' in the options.url is used to split URL + * and the latter part is treated as POST data (applies to getImageInfo(...)) + * Does not work if getImageInfo() is overridden and used (see the options description) + * @property {Number} [options.width] + * Width of the source image at max resolution in pixels. + * @property {Number} [options.height] + * Height of the source image at max resolution in pixels. + * @property {Number} [options.tileSize] + * The size of the tiles to assumed to make up each pyramid layer in pixels. + * Tile size determines the point at which the image pyramid must be + * divided into a matrix of smaller images. + * Use options.tileWidth and options.tileHeight to support non-square tiles. + * @property {Number} [options.tileWidth] + * The width of the tiles to assumed to make up each pyramid layer in pixels. + * @property {Number} [options.tileHeight] + * The height of the tiles to assumed to make up each pyramid layer in pixels. + * @property {Number} [options.tileOverlap] + * The number of pixels each tile is expected to overlap touching tiles. + * @property {Number} [options.minLevel] + * The minimum level to attempt to load. + * @property {Number} [options.maxLevel] + * The maximum level to attempt to load. + * @property {Boolean} [options.ready=true] + * If true, the event 'ready' is called immediately after the TileSource is created. + * This is important because some flows rely on immediate initialization, which + * computes additional properties like dimensions or aspect ratio. + * + * + * TODO: could be removed completely: + * - do not use Tiled Image's getImageInfo, implement it separately + * - call getImageInfo as perviously, by default just call raiseEvent('ready', { tileSource: this }) + */ + +/** + * @class TileSource + * @classdesc The TileSource contains the most basic implementation required to create a + * smooth transition between layers in an image pyramid. It has only a single key + * interface that must be implemented to complete its key functionality: + * 'getTileUrl'. It also has several optional interfaces that can be + * implemented if a new TileSource wishes to support configuration via a simple + * object or array ('configure') and if the tile source supports or requires + * configuration via retrieval of a document on the network ala AJAX or JSONP, + * ('getImageInfo'). + *
+ * By default the image pyramid is split into N layers where the image's longest + * side in M (in pixels), where N is the smallest integer which satisfies + * 2^(N+1) >= M. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @param {OpenSeadragon.TileSourceOptions|string} options + * You can either specify a URL, or literally define the TileSource (by specifying + * width, height, tileSize, tileOverlap, minLevel, and maxLevel). For the former, + * the extending class is expected to implement 'supports' and 'configure'. + * Note that _in this case, the child class of getImageInfo() is ignored!_ + * For the latter, the construction is assumed to occur through + * the extending classes implementation of 'configure'. + */ +$.TileSource = function( options ) { + + // NOTE! Manually rewriting this to a class syntax is problematic, since apply(...) would have to be overridden + // static apply( target, args ) {...} + // and check if target inherits TileSource and if not, copy all props to the __proto__ of the target + $.EventSource.apply( this ); + + + /** + * The URL of the image to be loaded. Can be undefined if the configuration happened + * via plain object or class injection + * @member {String} url + * @memberof OpenSeadragon.TileSource# + */ + this.url = null; + /** + * Ratio of width to height + * @member {Number} aspectRatio + * @memberof OpenSeadragon.TileSource# + */ + /** + * Vector storing x and y dimensions ( width and height respectively ). + * @member {OpenSeadragon.Point} dimensions + * @memberof OpenSeadragon.TileSource# + */ + /** + * The overlap in pixels each tile shares with its adjacent neighbors. + * @member {Number} tileOverlap + * @memberof OpenSeadragon.TileSource# + */ + /** + * The minimum pyramid level this tile source supports or should attempt to load. + * @member {Number} minLevel + * @memberof OpenSeadragon.TileSource# + */ + /** + * The maximum pyramid level this tile source supports or should attempt to load. + * @member {Number} maxLevel + * @memberof OpenSeadragon.TileSource# + */ + /** + * + * @member {Boolean} ready + * @memberof OpenSeadragon.TileSource# + */ + + this.addHandler('ready', e => { + const source = e.tileSource; + //explicit configuration via positional args in constructor + //or the more idiomatic 'options' object + this.ready = true; + this.aspectRatio = (source.width && source.height) ? + (source.width / source.height) : 1; + this.dimensions = new $.Point( source.width, source.height ); + + if ( source.tileSize ){ + this._tileWidth = this._tileHeight = source.tileSize; + delete this.tileSize; + } else { + if( source.tileWidth ){ + // We were passed tileWidth in options, but we want to rename it + // with a leading underscore to make clear that it is not safe to directly modify it + this._tileWidth = source.tileWidth; + delete this.tileWidth; + } else { + this._tileWidth = 0; + } + + if( source.tileHeight ){ + // See note above about renaming this.tileWidth + this._tileHeight = source.tileHeight; + delete this.tileHeight; + } else { + this._tileHeight = 0; + } + } + + this.tileOverlap = source.tileOverlap ? source.tileOverlap : 0; + this.minLevel = source.minLevel ? source.minLevel : 0; + this.maxLevel = ( undefined !== source.maxLevel && null !== source.maxLevel ) ? + source.maxLevel : ( + ( source.width && source.height ) ? Math.ceil( + Math.log( Math.max( source.width, source.height ) ) / + Math.log( 2 ) + ) : 0 + ); + if( source.success && $.isFunction( source.success ) ){ + source.success( this ); + } + }, null, Infinity); // important! go first to finish initialization + + if( 'string' === $.type( options ) ){ + this.url = options; + options = undefined; + } else { + //we allow options to override anything we don't treat as + //required via idiomatic options or which is functionally + //set depending on the state of the readiness of this tile + //source + $.extend( true, this, options ); + } + + if (this.url && !this.ready) { + //in case the getImageInfo method is overridden and/or implies an + //async mechanism set some safe defaults first + this.aspectRatio = 1; + this.dimensions = new $.Point( 10, 10 ); + this._tileWidth = 0; + this._tileHeight = 0; + this.tileOverlap = 0; + this.minLevel = 0; + this.maxLevel = 0; + this.ready = false; + this._uniqueIdentifier = this.url; + //configuration via url implies the extending class + //implements and 'configure' + setTimeout(() => this.getImageInfo(this.url)); //needs async in case someone exits immediately + } else { + this._uniqueIdentifier = Math.floor(Math.random() * 1e10).toString(36); + // by default it used to fire immediately, so make the ready default + if (this.ready || this.ready === undefined) { + this.raiseEvent('ready', { tileSource: this }); + } else { + setTimeout(() => this.raiseEvent('ready', { tileSource: this })); + } + } + return this; +}; + +/** @lends OpenSeadragon.TileSource.prototype */ +$.TileSource.prototype = { + + getTileSize: function( level ) { + $.console.error( + "[TileSource.getTileSize] is deprecated. " + + "Use TileSource.getTileWidth() and TileSource.getTileHeight() instead" + ); + return this._tileWidth; + }, + + /** + * Return the tileWidth for a given level. + * Subclasses should override this if tileWidth can be different at different levels + * such as in IIIFTileSource. Code should use this function rather than reading + * from ._tileWidth directly. + * @function + * @param {Number} level + */ + getTileWidth: function( level ) { + if (!this._tileWidth) { + return this.getTileSize(level); + } + return this._tileWidth; + }, + + /** + * Return the tileHeight for a given level. + * Subclasses should override this if tileHeight can be different at different levels + * such as in IIIFTileSource. Code should use this function rather than reading + * from ._tileHeight directly. + * @function + * @param {Number} level + */ + getTileHeight: function( level ) { + if (!this._tileHeight) { + return this.getTileSize(level); + } + return this._tileHeight; + }, + + /** + * Set the maxLevel to the given level, and perform the memoization of + * getLevelScale with the new maxLevel. This function can be useful if the + * memoization is required before the first call of getLevelScale, or both + * memoized getLevelScale and maxLevel should be changed accordingly. + * @function + * @param {Number} level + */ + setMaxLevel: function( level ) { + this.maxLevel = level; + this._memoizeLevelScale(); + }, + + /** + * @function + * @param {Number} level + */ + getLevelScale: function( level ) { + // if getLevelScale is not memoized, we generate the memoized version + // at the first call and return the result + this._memoizeLevelScale(); + return this.getLevelScale( level ); + }, + + // private + _memoizeLevelScale: function() { + // see https://github.com/openseadragon/openseadragon/issues/22 + // we use the tilesources implementation of getLevelScale to generate + // a memoized re-implementation + const levelScaleCache = {}; + let i; + for( i = 0; i <= this.maxLevel; i++ ){ + levelScaleCache[ i ] = 1 / Math.pow(2, this.maxLevel - i); + } + this.getLevelScale = function( _level ){ + return levelScaleCache[ _level ]; + }; + }, + + /** + * @function + * @param {Number} level + */ + getNumTiles: function( level ) { + const scale = this.getLevelScale( level ); + const x = Math.ceil( scale * this.dimensions.x / this.getTileWidth(level) ); + const y = Math.ceil( scale * this.dimensions.y / this.getTileHeight(level) ); + + return new $.Point( x, y ); + }, + + /** + * @function + * @param {Number} level + */ + getPixelRatio: function( level ) { + const imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ); + const rx = 1.0 / imageSizeScaled.x * $.pixelDensityRatio; + const ry = 1.0 / imageSizeScaled.y * $.pixelDensityRatio; + + return new $.Point(rx, ry); + }, + + + /** + * @function + * @returns {Number} The highest level in this tile source that can be contained in a single tile. + */ + getClosestLevel: function() { + let i; + let tiles; + + for (i = this.minLevel + 1; i <= this.maxLevel; i++){ + tiles = this.getNumTiles(i); + if (tiles.x > 1 || tiles.y > 1) { + break; + } + } + + return i - 1; + }, + + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function(level, point) { + const validPoint = point.x >= 0 && point.x <= 1 && + point.y >= 0 && point.y <= 1 / this.aspectRatio; + $.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point."); + + + const widthScaled = this.dimensions.x * this.getLevelScale(level); + const pixelX = point.x * widthScaled; + const pixelY = point.y * widthScaled; + + let x = Math.floor(pixelX / this.getTileWidth(level)); + let y = Math.floor(pixelY / this.getTileHeight(level)); + + // When point.x == 1 or point.y == 1 / this.aspectRatio we want to + // return the last tile of the row/column + if (point.x >= 1) { + x = this.getNumTiles(level).x - 1; + } + const EPSILON = 1e-15; + if (point.y >= 1 / this.aspectRatio - EPSILON) { + y = this.getNumTiles(level).y - 1; + } + + return new $.Point(x, y); + }, + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @param {Boolean} [isSource=false] Whether to return the source bounds of the tile. + * @returns {OpenSeadragon.Rect} Either where this tile fits (in normalized coordinates) or the + * portion of the tile to use as the source of the drawing operation (in pixels), depending on + * the isSource parameter. + */ + getTileBounds: function( level, x, y, isSource ) { + const dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ); + const tileWidth = this.getTileWidth(level); + const tileHeight = this.getTileHeight(level); + const px = ( x === 0 ) ? 0 : tileWidth * x - this.tileOverlap; + const py = ( y === 0 ) ? 0 : tileHeight * y - this.tileOverlap; + let sx = tileWidth + ( x === 0 ? 1 : 2 ) * this.tileOverlap; + let sy = tileHeight + ( y === 0 ? 1 : 2 ) * this.tileOverlap; + const scale = 1.0 / dimensionsScaled.x; + + sx = Math.min( sx, dimensionsScaled.x - px ); + sy = Math.min( sy, dimensionsScaled.y - py ); + + if (isSource) { + return new $.Rect(0, 0, sx, sy); + } + + return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); + }, + + + /** + * Responsible for retrieving, and caching the + * image metadata pertinent to this TileSources implementation. + * There are three scenarios of opening a tile source: providing a parseable string, plain object, or an URL. + * This method is only called by OSD if the TileSource configuration is a non-parseable string (~url). + * + * Note: you can access the properties sent to the TileSource constructor via the options object + * directly on 'this' reference. + * + * The string can contain a hash `#` symbol, followed by + * key=value arguments. If this is the case, this method sends this + * data as a POST body. + * + * @function + * @param {String} url + * @throws {Error} + */ + getImageInfo: function( url ) { + const _this = this; + let callbackName; + let callback; + let readySource; + let options; + let urlParts; + let filename; + let lastDot; + + + if( url ) { + urlParts = url.split( '/' ); + filename = urlParts[ urlParts.length - 1 ]; + lastDot = filename.lastIndexOf( '.' ); + if ( lastDot > -1 ) { + urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); + } + } + + let postData = null; + if (this.splitHashDataForPost) { + const hashIdx = url.indexOf("#"); + if (hashIdx !== -1) { + postData = url.substring(hashIdx + 1); + url = url.substr(0, hashIdx); + } + } + + callback = function( data ){ + if( typeof (data) === "string" ) { + data = $.parseXml( data ); + } + const $TileSource = $.TileSource.determineType( _this, data, url ); + if ( !$TileSource ) { + /** + * Raised when an error occurs loading a TileSource. + * + * @event open-failed + * @memberof OpenSeadragon.TileSource + * @type {object} + * @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event. + * @property {String} message + * @property {String} source + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } ); + return; + } + + options = $TileSource.prototype.configure.apply( _this, [ data, url, postData ]); + if (options.ajaxWithCredentials === undefined) { + options.ajaxWithCredentials = _this.ajaxWithCredentials; + } + + options.ready = true; // force synchronous finish + readySource = new $TileSource( options ); + _this.ready = true; + /** + * Raised when a TileSource is opened and initialized. + * + * @event ready + * @memberof OpenSeadragon.TileSource + * @type {object} + * @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event. + * @property {Object} tileSource + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( 'ready', { tileSource: readySource } ); + }; + + if( url.match(/\.js$/) ){ + //TODO: Its not very flexible to require tile sources to end jsonp + // request for info with a url that ends with '.js' but for + // now it's the only way I see to distinguish uniformly. + callbackName = url.split('/').pop().replace('.js', ''); + $.jsonp({ + url: url, + async: false, + callbackName: callbackName, + callback: callback + }); + } else { + // request info via xhr asynchronously. + $.makeAjaxRequest( { + url: url, + postData: postData, + withCredentials: this.ajaxWithCredentials, + headers: this.ajaxHeaders, + success: function( xhr ) { + const data = processResponse( xhr ); + callback( data ); + }, + error: function ( xhr, exc ) { + let msg; + + /* + IE < 10 will block XHR requests to different origins. Any property access on the request + object will raise an exception which we'll attempt to handle by formatting the original + exception rather than the second one raised when we try to access xhr.status + */ + try { + msg = "HTTP " + xhr.status + " attempting to load TileSource: " + url; + } catch ( e ) { + let formattedExc; + if ( typeof ( exc ) === "undefined" || !exc.toString ) { + formattedExc = "Unknown error"; + } else { + formattedExc = exc.toString(); + } + + msg = formattedExc + " attempting to load TileSource: " + url; + } + + $.console.error(msg); + + /*** + * Raised when an error occurs loading a TileSource. + * + * @event open-failed + * @memberof OpenSeadragon.TileSource + * @type {object} + * @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event. + * @property {String} message + * @property {String} source + * @property {String} postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form, + * see TileSource::getTilePostData) or null + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( 'open-failed', { + message: msg, + source: url, + postData: postData + }); + } + }); + } + + }, + + /** + * Responsible for determining if the particular TileSource supports the + * data format ( and allowed to apply logic against the url the data was + * loaded from, if any ). Overriding implementations are expected to do + * something smart with data and / or url to determine support. Also + * understand that iteration order of TileSources is not guaranteed so + * please make sure your data or url is expressive enough to ensure a simple + * and sufficient mechanism for clear determination. + * @function + * @param {String|Object|Array|Document} data + * @param {String} url - the url the data was loaded + * from if any. + * @returns {Boolean} + */ + supports: function( data, url ) { + return false; + }, + + /** + * Check whether two tileSources are equal. This is used for example + * when replacing tile-sources, which turns on the zombie cache before + * old item removal. + * @param {OpenSeadragon.TileSource} otherSource + * @returns {Boolean} + */ + equals: function (otherSource) { + return this === otherSource; + }, + + /** + * Determines if this tile source data can be batched. + * @return {boolean} + */ + batchEnabled() { + return false; + }, + + /** + * Determines if a tile request from a source (even itself!) can be batched with this source. + * By default, returns false -> in this case, each tile falls to a single bucket alone. + * @param {OpenSeadragon.TileSource} otherSource + * @return {boolean} + */ + batchCompatible(otherSource) { + return false; + }, + + /** + * Maximum batch size. Can, for example, be derived from (average) tile size of the source. + * @return {number} integer, number of max jobs per batch + */ + batchMaxJobs() { + return -1; + }, + + /** + * How long to wait with a batch before processing. Big timeout means larger + * batches with fewer requests, at the cost of slower loading. + * @return {number} milliseconds to wait for tiles to be added to the batch before processing + */ + batchTimeout() { + return 5; + }, + + /** + * Responsible for parsing and configuring the + * image metadata pertinent to this TileSources implementation. + * This method is not implemented by this class other than to throw an Error + * announcing you have to implement it. Because of the variety of tile + * server technologies, and various specifications for building image + * pyramids, this method is here to allow easy integration. + * @function + * @param {String|Object|Array|Document} data + * @param {String} url - the url the data was loaded + * from if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null value obtained from + * the protocol URL after '#' sign if flag splitHashDataForPost set to 'true' + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure the tile source constructor (include all values you want to + * instantiate the TileSource subclass with - what _options_ object should contain). + * @throws {Error} + */ + configure: function( data, url, postData ) { + throw new Error( "Method not implemented." ); + }, + + /** + * Shall this source need to free some objects + * upon unloading, it must be done here. For example, canvas + * size must be set to 0 for safari to free. + * @param {OpenSeadragon.Viewer} viewer + */ + destroy: function ( viewer ) { + //no-op + }, + + /** + * Responsible for retrieving the url which will return an image for the + * region specified by the given x, y, and level components. + * This method is not implemented by this class other than to throw an Error + * announcing you have to implement it. Because of the variety of tile + * server technologies, and various specifications for building image + * pyramids, this method is here to allow easy integration. + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @returns {String|Function} url - A string for the url or a function that returns a url string. + * @throws {Error} + */ + getTileUrl: function( level, x, y ) { + throw new Error( "Method not implemented." ); + }, + + /** + * Must use AJAX in order to work, i.e. loadTilesWithAjax = true is set. + * If a value is returned, ajax issues POST request to the tile url. + * If null is returned, ajax issues GET request. + * The return value must comply to the header 'content type'. + * + * Examples (USED HEADER --> getTilePostData CODE): + * 'Content-type': 'application/x-www-form-urlencoded' --> + * return "key1=value=1&key2=value2"; + * + * 'Content-type': 'application/x-www-form-urlencoded' --> + * return JSON.stringify({key: "value", number: 5}); + * + * 'Content-type': 'multipart/form-data' --> + * let result = new FormData(); + * result.append("data", myData); + * return result; + * + * IMPORTANT: in case you move all the logic on image fetching + * to post data, you must re-define 'getTileHashKey(...)' to + * stay unique for different tile images. + * + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @returns {*|null} post data to send with tile configuration request + */ + getTilePostData: function( level, x, y ) { + return null; + }, + + /** + * Responsible for retrieving the headers which will be attached to the image request for the + * region specified by the given x, y, and level components. + * This option is only relevant if {@link OpenSeadragon.Options}.loadTilesWithAjax is set to true. + * The headers returned here will override headers specified at the Viewer or TiledImage level. + * Specifying a falsy value for a header will clear its existing value set at the Viewer or + * TiledImage level (if any). + * + * Note that the headers of existing tiles don't automatically change when this function + * returns updated headers. To do that, you need to call {@link OpenSeadragon.Viewer#setAjaxHeaders} + * and propagate the changes. + * + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @returns {Object} + */ + getTileAjaxHeaders: function( level, x, y ) { + return {}; + }, + + /** + * The tile cache object is uniquely determined by this key and used to lookup + * the image data in cache: keys should be different if images are different. + * + * You can return falsey tile cache key, in which case the tile will + * be created without invoking ImageJob --- but with data=null. Then, + * you are responsible for manually creating the cache data. This is useful + * particularly if you want to use empty TiledImage with client-side derived data + * only. The default tile-cache key is then called "" - an empty string. + * + * Note: default behaviour does not take into account post data. + * @param {Number} level tile level it was fetched with + * @param {Number} x x-coordinate in the pyramid level + * @param {Number} y y-coordinate in the pyramid level + * @param {String} url the tile was fetched with + * @param {Object} ajaxHeaders the tile was fetched with + * @param {*} postData data the tile was fetched with (type depends on getTilePostData(..) return type) + * @return {?String} can return the cache key or null, in that case an empty cache is initialized + * without downloading any data for internal use: user has to define the cache contents manually, via + * the cache interface of this class. + */ + getTileHashKey: function(level, x, y, url, ajaxHeaders, postData) { + function withHeaders(hash) { + return ajaxHeaders ? hash + "+" + JSON.stringify(ajaxHeaders) : hash; + } + + if (typeof url !== "string") { + return withHeaders(this._uniqueIdentifier + ":" + level + "/" + x + "_" + y); + } + return withHeaders(url); + }, + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + tileExists: function( level, x, y ) { + const numTiles = this.getNumTiles( level ); + return level >= this.minLevel && + level <= this.maxLevel && + x >= 0 && + y >= 0 && + x < numTiles.x && + y < numTiles.y; + }, + + /** + * Decide whether tiles have transparency: this is crucial for correct images blending. + * Overriden on a tile level by setting tile.hasTransparency = true; + * @param context2D unused, deprecated argument + * @param url tile.getUrl() value for given tile + * @param ajaxHeaders tile.ajaxHeaders value for given tile + * @param post tile.post value for given tile + * @returns {boolean} true if the image has transparency + */ + hasTransparency: function(context2D, url, ajaxHeaders, post) { + return url.match('.png'); + }, + + /** + * Download tile data. The context attribute is the reference to the job object itself, which is extended + * by ImageLoader.addJob(options) options object, so there are also properties like context.source (reference to self). + * + * Note that if you override this function, you should override also downloadTileAbort(). + * @param {OpenSeadragon.ImageJob} context job context that you have to call finish(...) on. + */ + downloadTileStart: function (context) { + // Load the tile with an AJAX request if the loadWithAjax option is + // set. Otherwise load the image by setting the source property of the image object. + + // TODO: the cors/creds is not optimal here: + // - XMLHttpRequest can only setup credentials flag, so `ajaxWithCredentials` is a boolean + // - item can turn on/off cors, and include credentials if cors on, therefore `crossOriginPolicy` can have three values (one is null) + // --> we should merge these flags to a single value to avoid confusion with usage, and use modern fetch that can setup also cors to have consistent behavior + if (context.loadWithAjax) { + context.userData.request = $.makeAjaxRequest({ + url: context.src, + withCredentials: context.ajaxWithCredentials, + headers: context.ajaxHeaders, + responseType: "arraybuffer", + postData: context.postData, + success: function(request) { + let blb; + // Make the raw data into a blob. + // BlobBuilder fallback adapted from + // http://stackoverflow.com/questions/15293694/blob-constructor-browser-compatibility + try { + blb = new window.Blob([request.response]); + } catch (e) { + const BlobBuilder = ( + window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder + ); + if (e.name === 'TypeError' && BlobBuilder) { + const bb = new BlobBuilder(); + bb.append(request.response); + blb = bb.getBlob(); + } + } + // If the blob is empty for some reason consider the image load a failure. + if (blb.size === 0) { + context.fail("[downloadTileStart] Empty image response.", request); + } else { + context.finish(blb, request, "rasterBlob"); + } + }, + error: function(request) { + context.fail("[downloadTileStart] Image load aborted - XHR error", request); + } + }); + } else { + // While we could just do this one-liner, we found out that downloading the data _before_ a cache is initialized + // works better in general cases. Network access is the most error-prone part, and this scenario better supports + // all default use-cases, including the fact that retry logic works only at this stage, not on the cache level. + // context.finish(context.src, null, "__private__imageUrl"); + + const image = new Image(); + context.userData.imageRequest = image; + image.onload = function () { + image.onload = image.onerror = image.onabort = null; + context.finish(image, null, "image"); + }; + image.onabort = image.onerror = function() { + image.onload = image.onerror = image.onabort = null; + context.fail("[downloadTileStart] Image load aborted or errored out.", null); + }; + if (typeof context.crossOriginPolicy === "string") { + image.crossOrigin = context.crossOriginPolicy; + } + image.src = context.src; + } + }, + + /** + * Provide means of aborting the execution. + * Note that if you override this function, you should override also downloadTileStart(). + * Note that calling job.abort() would create an infinite loop! + * + * @param {OpenSeadragon.ImageJob} context job, the same object as with downloadTileStart(..) + * @param {*} [context.userData] - Empty object to attach (and mainly read) your own data. + */ + downloadTileAbort: function (context) { + if (context.userData.request) { + context.userData.request.abort(); + } + if (context.userData.imageRequest) { + const image = context.userData.imageRequest; + image.onload = image.onerror = image.onabort = null; + image.src = ""; + } + }, + + /** + * Handles the fetching of multiple tiles in a single operation. + * The TileSource is responsible for calling finish/fail on each of the individual job items + * carried by batchJob.jobs. Avoid calling finish/fail on `batchJob` itself. + * + * Note that failed batch jobs are retried in non-batched mode. You should therefore + * have a valid downloadTileStart implementation in any case. + * + * @param {OpenSeadragon.BatchImageJob} batchJob - The batch job containing .jobs array + */ + downloadTileBatchStart(batchJob) { + // Fallback default implementation: process individually. + // Real implementations (e.g. for sprite sheets) should override this and use true batched approach. + for (let i = 0; i < batchJob.jobs.length; i++) { + this.downloadTileStart(batchJob.jobs[i]); + } + }, + + /** + * Handles abortion of the fetching of multiple tiles. + * @param {OpenSeadragon.BatchImageJob} batchJob + */ + downloadTileBatchAbort(batchJob) { + for (let i = 0; i < batchJob.jobs.length; i++) { + this.downloadTileAbort(batchJob.jobs[i]); + } + }, + + /** + * Create cache object from the result of the download process. The + * cacheObject parameter should be used to attach the data to, there are no + * conventions on how it should be stored - all the logic is implemented within *TileCache() functions. + * + * Note that + * - data is cached automatically as cacheObject.data + * - if you override any of *TileCache() functions, you should override all of them. + * - these functions might be called over shared cache object managed by other TileSources simultaneously. + * @param {OpenSeadragon.CacheRecord} cacheObject context cache object + * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object + * @param {OpenSeadragon.Tile} tile instance the cache was created with + * @deprecated + */ + createTileCache: function(cacheObject, data, tile) { + $.console.error("[TileSource.createTileCache] has been deprecated. Use cache API of a tile instead."); + //no-op, we create the cache automatically + }, + + /** + * Cache object destructor, unset all properties you created to allow GC collection. + * Note that if you override any of *TileCache() functions, you should override all of them. + * Note that these functions might be called over shared cache object managed by other TileSources simultaneously. + * Original cache data is cacheObject.data, but do not delete it manually! It is taken care for, + * you might break things. + * @param {OpenSeadragon.CacheRecord} cacheObject context cache object + * @deprecated + */ + destroyTileCache: function (cacheObject) { + $.console.error("[TileSource.destroyTileCache] has been deprecated. Use cache API of a tile instead."); + //no-op, handled internally + }, + + /** + * Raw data getter, should return anything that is compatible with the system, or undefined + * if the system can handle it. + * @param {OpenSeadragon.CacheRecord} cacheObject context cache object + * @returns {OpenSeadragon.Promise} cache data + * @deprecated + */ + getTileCacheData: function(cacheObject) { + $.console.error("[TileSource.getTileCacheData] has been deprecated. Use cache API of a tile instead."); + return cacheObject.getDataAs(undefined, false); + }, + + /** + * Compatibility image element getter + * - plugins might need image representation of the data + * - div HTML rendering relies on image element presence + * Note that if you override any of *TileCache() functions, you should override all of them. + * Note that these functions might be called over shared cache object managed by other TileSources simultaneously. + * @param {OpenSeadragon.CacheRecord} cacheObject context cache object + * @returns {Image} cache data as an Image + * @deprecated + */ + getTileCacheDataAsImage: function(cacheObject) { + $.console.error("[TileSource.getTileCacheDataAsImage] has been deprecated. Use cache API of a tile instead."); + return cacheObject.getImage(); + }, + + /** + * Compatibility context 2D getter + * - most heavily used rendering method is a canvas-based approach, + * convert the data to a canvas and return it's 2D context + * Note that if you override any of *TileCache() functions, you should override all of them. + * @param {OpenSeadragon.CacheRecord} cacheObject context cache object + * @returns {CanvasRenderingContext2D} context of the canvas representation of the cache data + * @deprecated + */ + getTileCacheDataAsContext2D: function(cacheObject) { + $.console.error("[TileSource.getTileCacheDataAsContext2D] has been deprecated. Use cache API of a tile instead."); + return cacheObject.getRenderedContext(); + } +}; + + +$.extend( true, $.TileSource.prototype, $.EventSource.prototype ); + + +/** + * Decides whether to try to process the response as xml, json, or hand back + * the text + * @private + * @inner + * @function + * @param {XMLHttpRequest} xhr - the completed network request + */ +function processResponse( xhr ){ + const responseText = xhr.responseText; + let status = xhr.status; + let statusText; + let data; + + if ( !xhr ) { + throw new Error( $.getString( "Errors.Security" ) ); + } else if ( xhr.status !== 200 && xhr.status !== 0 ) { + status = xhr.status; + statusText = ( status === 404 ) ? + "Not Found" : + xhr.statusText; + throw new Error( $.getString( "Errors.Status", status, statusText ) ); + } + + if( responseText.match(/^\s*<.*/) ){ + try{ + data = ( xhr.responseXML && xhr.responseXML.documentElement ) ? + xhr.responseXML : + $.parseXml( responseText ); + } catch (e){ + data = xhr.responseText; + } + }else if( responseText.match(/\s*[{[].*/) ){ + try{ + data = $.parseJSON(responseText); + } catch(e){ + data = responseText; + } + }else{ + data = responseText; + } + return data; +} + + +/** + * Determines the TileSource Implementation by introspection of OpenSeadragon + * namespace, calling each TileSource implementation of 'isType' + * @private + * @inner + * @function + * @param {Object|Array|Document} data - the tile source configuration object + * @param {String} url - the url where the tile source configuration object was + * loaded from, if any. + */ +$.TileSource.determineType = function( tileSource, data, url ){ + for( const property in OpenSeadragon ){ + if( property.match(/.+TileSource$/) && + $.isFunction( OpenSeadragon[ property ] ) && + $.isFunction( OpenSeadragon[ property ].prototype.supports ) && + OpenSeadragon[ property ].prototype.supports.call( tileSource, data, url ) + ){ + return OpenSeadragon[ property ]; + } + } + + $.console.error( "No TileSource was able to open %s %s", url, data ); + + return null; +}; + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - DziTileSource + * + * 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 DziTileSource + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Number|Object} width - the pixel width of the image or the idiomatic + * options object which is used instead of positional arguments. + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + * @param {String} fileFormat + * @param {OpenSeadragon.DisplayRect[]} displayRects + * @property {String} tilesUrl + * @property {String} fileFormat + * @property {OpenSeadragon.DisplayRect[]} displayRects + */ +$.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects, minLevel, maxLevel ) { + let level; + let options; + + if( $.isPlainObject( width ) ){ + options = width; + }else{ + options = { + width: arguments[ 0 ], + height: arguments[ 1 ], + tileSize: arguments[ 2 ], + tileOverlap: arguments[ 3 ], + tilesUrl: arguments[ 4 ], + fileFormat: arguments[ 5 ], + displayRects: arguments[ 6 ], + minLevel: arguments[ 7 ], + maxLevel: arguments[ 8 ] + }; + } + + this._levelRects = {}; + this.tilesUrl = options.tilesUrl; + this.fileFormat = options.fileFormat; + this.displayRects = options.displayRects; + this.queryParams = options.queryParams || ""; + + if ( this.displayRects ) { + for ( let i = this.displayRects.length - 1; i >= 0; i-- ) { + const rect = this.displayRects[ i ]; + for ( level = rect.minLevel; level <= rect.maxLevel; level++ ) { + if ( !this._levelRects[ level ] ) { + this._levelRects[ level ] = []; + } + this._levelRects[ level ].push( rect ); + } + } + } + + $.TileSource.apply( this, [ options ] ); + +}; + +$.extend( $.DziTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.DziTileSource.prototype */{ + + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] + */ + supports: function( data, url ){ + let ns; + if ( data.Image ) { + ns = data.Image.xmlns; + } else if ( data.documentElement) { + if ("Image" === data.documentElement.localName || "Image" === data.documentElement.tagName) { + ns = data.documentElement.namespaceURI; + } + } + + ns = (ns || '').toLowerCase(); + + return (ns.indexOf('schemas.microsoft.com/deepzoom/2008') !== -1 || + ns.indexOf('schemas.microsoft.com/deepzoom/2009') !== -1); + }, + + /** + * + * @function + * @param {Object|XMLDocument} data - the raw configuration + * @param {String} url - the url the data was retrieved from if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( data, url, postData ){ + + let options; + + if( !$.isPlainObject(data) ){ + + options = configureFromXML( this, data ); + + }else{ + + options = configureFromObject( this, data ); + } + + if (url && !options.tilesUrl) { + options.tilesUrl = url.replace( + /([^/]+?)(\.(dzi|xml|js)?(\?[^/]*)?)?\/?$/, '$1_files/'); + + if (url.search(/\.(dzi|xml|js)\?/) !== -1) { + options.queryParams = url.match(/\?.*/); + }else{ + options.queryParams = ''; + } + } + + return options; + }, + + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function( level, x, y ) { + return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat, this.queryParams ].join( '' ); + }, + + + /** + * Equality comparator + */ + equals: function(otherSource) { + return otherSource && this.tilesUrl === otherSource.tilesUrl; + }, + + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + tileExists: function( level, x, y ) { + const rects = this._levelRects[ level ]; + let scale; + let xMin; + let yMin; + let xMax; + let yMax; + + if ((this.minLevel && level < this.minLevel) || (this.maxLevel && level > this.maxLevel)) { + return false; + } + + if ( !rects || !rects.length ) { + return true; + } + + for (let i = rects.length - 1; i >= 0; i-- ) { + const rect = rects[ i ]; + + if ( level < rect.minLevel || level > rect.maxLevel ) { + continue; + } + + scale = this.getLevelScale( level ); + xMin = rect.x * scale; + yMin = rect.y * scale; + xMax = xMin + rect.width * scale; + yMax = yMin + rect.height * scale; + + xMin = Math.floor( xMin / this._tileWidth ); + yMin = Math.floor( yMin / this._tileWidth ); // DZI tiles are square, so we just use _tileWidth + xMax = Math.ceil( xMax / this._tileWidth ); + yMax = Math.ceil( yMax / this._tileWidth ); + + if ( xMin <= x && x < xMax && yMin <= y && y < yMax ) { + return true; + } + } + + return false; + } +}); + + +/** + * @private + * @inner + * @function + */ +function configureFromXML( tileSource, xmlDoc ){ + + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + const root = xmlDoc.documentElement; + const rootName = root.localName || root.tagName; + const ns = xmlDoc.documentElement.namespaceURI; + let configuration = null; + const displayRects = []; + let dispRectNodes; + let dispRectNode; + let rectNode; + let sizeNode; + let i; + + if ( rootName === "Image" ) { + + try { + sizeNode = root.getElementsByTagName("Size" )[ 0 ]; + if (sizeNode === undefined) { + sizeNode = root.getElementsByTagNameNS(ns, "Size" )[ 0 ]; + } + + configuration = { + Image: { + xmlns: "http://schemas.microsoft.com/deepzoom/2008", + Url: root.getAttribute( "Url" ), + Format: root.getAttribute( "Format" ), + DisplayRect: null, + Overlap: parseInt( root.getAttribute( "Overlap" ), 10 ), + TileSize: parseInt( root.getAttribute( "TileSize" ), 10 ), + Size: { + Height: parseInt( sizeNode.getAttribute( "Height" ), 10 ), + Width: parseInt( sizeNode.getAttribute( "Width" ), 10 ) + } + } + }; + + if ( !$.imageFormatSupported( configuration.Image.Format ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", configuration.Image.Format.toUpperCase() ) + ); + } + + dispRectNodes = root.getElementsByTagName("DisplayRect" ); + if (dispRectNodes === undefined) { + dispRectNodes = root.getElementsByTagNameNS(ns, "DisplayRect" )[ 0 ]; + } + + for ( i = 0; i < dispRectNodes.length; i++ ) { + dispRectNode = dispRectNodes[ i ]; + rectNode = dispRectNode.getElementsByTagName("Rect" )[ 0 ]; + if (rectNode === undefined) { + rectNode = dispRectNode.getElementsByTagNameNS(ns, "Rect" )[ 0 ]; + } + + displayRects.push({ + Rect: { + X: parseInt( rectNode.getAttribute( "X" ), 10 ), + Y: parseInt( rectNode.getAttribute( "Y" ), 10 ), + Width: parseInt( rectNode.getAttribute( "Width" ), 10 ), + Height: parseInt( rectNode.getAttribute( "Height" ), 10 ), + MinLevel: parseInt( dispRectNode.getAttribute( "MinLevel" ), 10 ), + MaxLevel: parseInt( dispRectNode.getAttribute( "MaxLevel" ), 10 ) + } + }); + } + + if( displayRects.length ){ + configuration.Image.DisplayRect = displayRects; + } + + return configureFromObject( tileSource, configuration ); + + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( $.getString("Errors.Dzi") ); + } + } else if ( rootName === "Collection" ) { + throw new Error( $.getString( "Errors.Dzc" ) ); + } else if ( rootName === "Error" ) { + const messageNode = root.getElementsByTagName("Message")[0]; + const message = messageNode.firstChild.nodeValue; + throw new Error(message); + } + + throw new Error( $.getString( "Errors.Dzi" ) ); +} + +/** + * @private + * @inner + * @function + */ +function configureFromObject( tileSource, configuration ){ + const imageData = configuration.Image; + const tilesUrl = imageData.Url; + const fileFormat = imageData.Format; + const sizeData = imageData.Size; + const dispRectData = imageData.DisplayRect || []; + const width = parseInt( sizeData.Width, 10 ); + const height = parseInt( sizeData.Height, 10 ); + const tileSize = parseInt( imageData.TileSize, 10 ); + const tileOverlap = parseInt( imageData.Overlap, 10 ); + const displayRects = []; + let rectData; + + //TODO: need to figure out out to better handle image format compatibility + // which actually includes additional file formats like xml and pdf + // and plain text for various tilesource implementations to avoid low + // level errors. + // + // For now, just don't perform the check. + // + /*if ( !imageFormatSupported( fileFormat ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) + ); + }*/ + + for (let i = 0; i < dispRectData.length; i++ ) { + rectData = dispRectData[ i ].Rect; + + displayRects.push( new $.DisplayRect( + parseInt( rectData.X, 10 ), + parseInt( rectData.Y, 10 ), + parseInt( rectData.Width, 10 ), + parseInt( rectData.Height, 10 ), + parseInt( rectData.MinLevel, 10 ), + parseInt( rectData.MaxLevel, 10 ) + )); + } + + return $.extend(true, { + width: width, /* width *required */ + height: height, /* height *required */ + tileSize: tileSize, /* tileSize *required */ + tileOverlap: tileOverlap, /* tileOverlap *required */ + minLevel: null, /* minLevel */ + maxLevel: null, /* maxLevel */ + tilesUrl: tilesUrl, /* tilesUrl */ + fileFormat: fileFormat, /* fileFormat */ + displayRects: displayRects /* displayRects */ + }, configuration ); + +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - IIIFTileSource + * + * 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 IIIFTileSource + * @classdesc A client implementation of the International Image Interoperability Framework + * Format: Image API 1.0 - 3.0 + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @see http://iiif.io/api/image/ + * @param {String} [options.tileFormat='jpg'] + * The extension that will be used when requiring tiles. + */ +$.IIIFTileSource = function( options ){ + + /* eslint-disable camelcase */ + + $.extend( true, this, options ); + + /* Normalizes v3-style 'id' keys to an "_id" internal property */ + this._id = this["@id"] || this["id"] || this['identifier'] || null; + + if ( !( this.height && this.width && this._id) ) { + throw new Error( 'IIIF required parameters (width, height, or id) not provided.' ); + } + + options.tileSizePerScaleFactor = {}; + + this.tileFormat = this.tileFormat || 'jpg'; + + this.version = options.version; + + this.isLevel0 = checkLevel0( options ); + + // N.B. 2.0 renamed scale_factors to scaleFactors + if ( this.tile_width && this.tile_height ) { + options.tileWidth = this.tile_width; + options.tileHeight = this.tile_height; + } else if ( this.tile_width ) { + options.tileSize = this.tile_width; + } else if ( this.tile_height ) { + options.tileSize = this.tile_height; + } else if ( this.tiles ) { + // Version 2.0 forwards + if ( this.tiles.length === 1 ) { + options.tileWidth = this.tiles[0].width; + // Use height if provided, otherwise assume square tiles and use width. + options.tileHeight = this.tiles[0].height || this.tiles[0].width; + this.scale_factors = this.tiles[0].scaleFactors; + } else { + // Multiple tile sizes at different levels + this.scale_factors = []; + for (let t = 0; t < this.tiles.length; t++ ) { + for (let sf = 0; sf < this.tiles[t].scaleFactors.length; sf++) { + const scaleFactor = this.tiles[t].scaleFactors[sf]; + this.scale_factors.push(scaleFactor); + options.tileSizePerScaleFactor[scaleFactor] = { + width: this.tiles[t].width, + height: this.tiles[t].height || this.tiles[t].width + }; + } + } + } + } else if ( canBeTiled(options) ) { + // use the largest of tileOptions that is smaller than the short dimension + const shortDim = Math.min( this.height, this.width ); + const tileOptions = [256, 512, 1024]; + const smallerTiles = []; + + for ( let c = 0; c < tileOptions.length; c++ ) { + if ( tileOptions[c] <= shortDim ) { + smallerTiles.push( tileOptions[c] ); + } + } + + if ( smallerTiles.length > 0 ) { + options.tileSize = Math.max.apply( null, smallerTiles ); + } else { + // If we're smaller than 256, just use the short side. + options.tileSize = shortDim; + } + } else if (this.sizes && this.sizes.length > 0) { + // This info.json can't be tiled, but we can still construct a legacy pyramid from the sizes array. + // In this mode, IIIFTileSource will call functions from the abstract baseTileSource or the + // LegacyTileSource instead of performing IIIF tiling. + this.emulateLegacyImagePyramid = true; + + options.levels = constructLevels( this ); + // use the largest available size to define tiles + $.extend( true, options, { + width: options.levels[ options.levels.length - 1 ].width, + height: options.levels[ options.levels.length - 1 ].height, + tileSize: Math.max( options.height, options.width ), + tileOverlap: 0, + minLevel: 0, + maxLevel: options.levels.length - 1 + }); + this.levels = options.levels; + } else { + $.console.error("Nothing in the info.json to construct image pyramids from"); + } + + if (!options.maxLevel && !this.emulateLegacyImagePyramid) { + if (!this.scale_factors) { + options.maxLevel = Number(Math.round(Math.log(Math.max(this.width, this.height), 2))); + } else { + const maxScaleFactor = Math.max.apply(null, this.scale_factors); + options.maxLevel = Math.round(Math.log(maxScaleFactor) * Math.LOG2E); + } + } + + // Create an array with precise resolution sizes if these have been supplied through the 'sizes' object + if( this.sizes ) { + let sizeLength = this.sizes.length; + + // Create a copy of the sizes list and sort in ascending order + const sortedSizes = this.sizes.slice().sort(( size1, size2 ) => size1.width - size2.width); + + // List may or may not include the full resolution size (should be last after sorting): add it if necessary + if( sortedSizes[sizeLength - 1].width < this.width && sortedSizes[sizeLength - 1].height < this.height ) { + sortedSizes.push( {width: this.width, height: this.height} ); + sizeLength++; + } + + // Only try to use 'sizes' if the number of dimensions within exactly matches the number of resolution levels (maxLevel+1) + if ( sizeLength === options.maxLevel + 1 ) { + + // If we have a list of scaleFactors, make sure each of our sizes really corresponds to the listed scales + let isResolutionList = 1; + if ( this.scale_factors && this.scale_factors.length === sizeLength ) { + for ( let i = 0; i < sizeLength; i++ ) { + const factor = this.scale_factors[sizeLength - i - 1]; // Scale factor order is inverted + if ( Math.round( this.width / sortedSizes[i].width ) !== factor || + Math.round( this.height / sortedSizes[i].height ) !== factor ) { + isResolutionList = 0; + break; + } + } + } + // The 'sizes' array does indeed contain a list of resolution levels, so assign our sorted array + if ( isResolutionList === 1 ) { + this.levelSizes = sortedSizes; + } + } + } + + $.TileSource.apply( this, [ options ] ); +}; + +$.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.IIIFTileSource.prototype */{ + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] - url + */ + + supports: function( data, url ) { + // Version 2.0 and forwards + if (data.protocol && data.protocol === 'http://iiif.io/api/image') { + return true; + // Version 1.1 + } else if ( data['@context'] && ( + data['@context'] === "http://library.stanford.edu/iiif/image-api/1.1/context.json" || + data['@context'] === "http://iiif.io/api/image/1/context.json") ) { + // N.B. the iiif.io context is wrong, but where the representation lives so likely to be used + return true; + + // Version 1.0 + } else if ( data.profile && + data.profile.indexOf("http://library.stanford.edu/iiif/image-api/compliance.html") === 0) { + return true; + } else if ( data.identifier && data.width && data.height ) { + return true; + } else if ( data.documentElement && + "info" === data.documentElement.tagName && + "http://library.stanford.edu/iiif/image-api/ns/" === + data.documentElement.namespaceURI) { + return true; + + // Not IIIF + } else { + return false; + } + }, + + /** + * A static function used to prepare an incoming IIIF Image API info.json + * response for processing by the tile handler. Normalizes data for all + * versions of IIIF (1.0, 1.1, 2.x, 3.x) and returns a data object that + * may be passed to the IIIFTileSource. + * + * @function + * @static + * @param {Object} data - the raw configuration + * @param {String} url - the url configuration was retrieved from + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null + * @returns {Object} A normalized IIIF data object + * @example IIIF 2.x Info Looks like this + * { + * "@context": "http://iiif.io/api/image/2/context.json", + * "@id": "http://iiif.example.com/prefix/1E34750D-38DB-4825-A38A-B60A345E591C", + * "protocol": "http://iiif.io/api/image", + * "height": 1024, + * "width": 775, + * "tiles" : [{"width":256, "scaleFactors":[1,2,4,8]}], + * "profile": ["http://iiif.io/api/image/2/level1.json", { + * "qualities": [ "native", "bitonal", "grey", "color" ], + * "formats": [ "jpg", "png", "gif" ] + * }] + * } + */ + configure: function( data, url, postData ){ + // Try to deduce our version and fake it upwards if needed + if ( !$.isPlainObject(data) ) { + const options = configureFromXml10( data ); + options['@context'] = "http://iiif.io/api/image/1.0/context.json"; + options["@id"] = url.replace('/info.xml', ''); + options.version = 1; + return options; + } else { + if ( !data['@context'] ) { + data['@context'] = 'http://iiif.io/api/image/1.0/context.json'; + data["@id"] = url.replace('/info.json', ''); + data.version = 1; + } else { + let context = data['@context']; + if (Array.isArray(context)) { + for (let i = 0; i < context.length; i++) { + if (typeof context[i] === 'string' && + ( /^http:\/\/iiif\.io\/api\/image\/[1-3]\/context\.json$/.test(context[i]) || + context[i] === 'http://library.stanford.edu/iiif/image-api/1.1/context.json' ) ) { + context = context[i]; + break; + } + } + } + switch (context) { + case 'http://iiif.io/api/image/1/context.json': + case 'http://library.stanford.edu/iiif/image-api/1.1/context.json': + data.version = 1; + break; + case 'http://iiif.io/api/image/2/context.json': + data.version = 2; + break; + case 'http://iiif.io/api/image/3/context.json': + data.version = 3; + break; + default: + $.console.error('Data has a @context property which contains no known IIIF context URI.'); + } + } + + if (data.preferredFormats) { + for (let f = 0; f < data.preferredFormats.length; f++ ) { + if ( $.imageFormatSupported(data.preferredFormats[f]) ) { + data.tileFormat = data.preferredFormats[f]; + break; + } + } + } + return data; + } + }, + + /** + * Return the tileWidth for the given level. + * @function + * @param {Number} level + */ + getTileWidth: function( level ) { + + if(this.emulateLegacyImagePyramid) { + return $.TileSource.prototype.getTileWidth.call(this, level); + } + + const scaleFactor = Math.pow(2, this.maxLevel - level); + + if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { + return this.tileSizePerScaleFactor[scaleFactor].width; + } + return this._tileWidth; + }, + + /** + * Return the tileHeight for the given level. + * @function + * @param {Number} level + */ + getTileHeight: function( level ) { + + if(this.emulateLegacyImagePyramid) { + return $.TileSource.prototype.getTileHeight.call(this, level); + } + + const scaleFactor = Math.pow(2, this.maxLevel - level); + + if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { + return this.tileSizePerScaleFactor[scaleFactor].height; + } + return this._tileHeight; + }, + + /** + * @function + * @param {Number} level + */ + getLevelScale: function ( level ) { + + if(this.emulateLegacyImagePyramid) { + let levelScale = NaN; + if (this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel) { + levelScale = + this.levels[level].width / + this.levels[this.maxLevel].width; + } + return levelScale; + } + + return $.TileSource.prototype.getLevelScale.call(this, level); + }, + + /** + * @function + * @param {Number} level + */ + getNumTiles: function( level ) { + + if(this.emulateLegacyImagePyramid) { + const scale = this.getLevelScale(level); + if (scale) { + return new $.Point(1, 1); + } else { + return new $.Point(0, 0); + } + } + + // Use supplied list of scaled resolution sizes if these exist + if( this.levelSizes ) { + const levelSize = this.levelSizes[level]; + const x = Math.ceil( levelSize.width / this.getTileWidth(level) ); + const y = Math.ceil( levelSize.height / this.getTileHeight(level) ); + return new $.Point( x, y ); + } + // Otherwise call default TileSource->getNumTiles() function + else { + return $.TileSource.prototype.getNumTiles.call(this, level); + } + }, + + + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function( level, point ) { + + if(this.emulateLegacyImagePyramid) { + return new $.Point(0, 0); + } + + // Use supplied list of scaled resolution sizes if these exist + if( this.levelSizes ) { + + const validPoint = point.x >= 0 && point.x <= 1 && + point.y >= 0 && point.y <= 1 / this.aspectRatio; + $.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point."); + + const widthScaled = this.levelSizes[level].width; + const pixelX = point.x * widthScaled; + const pixelY = point.y * widthScaled; + + let x = Math.floor(pixelX / this.getTileWidth(level)); + let y = Math.floor(pixelY / this.getTileHeight(level)); + + // When point.x == 1 or point.y == 1 / this.aspectRatio we want to + // return the last tile of the row/column + if (point.x >= 1) { + x = this.getNumTiles(level).x - 1; + } + const EPSILON = 1e-15; + if (point.y >= 1 / this.aspectRatio - EPSILON) { + y = this.getNumTiles(level).y - 1; + } + + return new $.Point(x, y); + } + + // Otherwise call default TileSource->getTileAtPoint() function + return $.TileSource.prototype.getTileAtPoint.call(this, level, point); + }, + + + /** + * Responsible for retrieving the url which will return an image for the + * region specified by the given x, y, and level components. + * @function + * @param {Number} level - z index + * @param {Number} x + * @param {Number} y + * @throws {Error} + */ + getTileUrl: function( level, x, y ){ + + if(this.emulateLegacyImagePyramid) { + let url = null; + if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) { + url = this.levels[ level ].url; + } + return url; + } + + //# constants + const IIIF_ROTATION = '0'; + //## get the scale (level as a decimal) + const scale = Math.pow( 0.5, this.maxLevel - level ); + //# image dimensions at this level + let levelWidth; + let levelHeight; + + //## iiif region + let tileWidth; + let tileHeight; + let iiifTileSizeWidth; + let iiifTileSizeHeight; + let iiifRegion; + let iiifTileX; + let iiifTileY; + let iiifTileW; + let iiifTileH; + let iiifSize; + let iiifSizeW; + let iiifSizeH; + let iiifQuality; + + // Use supplied list of scaled resolution sizes if these exist + if( this.levelSizes ) { + levelWidth = this.levelSizes[level].width; + levelHeight = this.levelSizes[level].height; + } + // Otherwise calculate the sizes ourselves + else { + levelWidth = Math.ceil( this.width * scale ); + levelHeight = Math.ceil( this.height * scale ); + } + + tileWidth = this.getTileWidth(level); + tileHeight = this.getTileHeight(level); + iiifTileSizeWidth = Math.round( tileWidth / scale ); + iiifTileSizeHeight = Math.round( tileHeight / scale ); + if (this.version === 1) { + iiifQuality = "native." + this.tileFormat; + } else { + iiifQuality = "default." + this.tileFormat; + } + if ( levelWidth < tileWidth && levelHeight < tileHeight ){ + if ( this.version === 2 && levelWidth === this.width ) { + iiifSize = "full"; + } else if ( this.version === 3 && levelWidth === this.width && levelHeight === this.height ) { + iiifSize = "max"; + } else if ( this.version === 3 ) { + iiifSize = levelWidth + "," + levelHeight; + } else { + iiifSize = levelWidth + ","; + } + iiifRegion = 'full'; + } else { + iiifTileX = x * iiifTileSizeWidth; + iiifTileY = y * iiifTileSizeHeight; + iiifTileW = Math.min( iiifTileSizeWidth, this.width - iiifTileX ); + iiifTileH = Math.min( iiifTileSizeHeight, this.height - iiifTileY ); + if ( x === 0 && y === 0 && iiifTileW === this.width && iiifTileH === this.height ) { + iiifRegion = "full"; + } else { + iiifRegion = [ iiifTileX, iiifTileY, iiifTileW, iiifTileH ].join( ',' ); + } + iiifSizeW = Math.min( tileWidth, levelWidth - (x * tileWidth) ); + iiifSizeH = Math.min( tileHeight, levelHeight - (y * tileHeight) ); + if ( this.version === 2 && iiifSizeW === this.width ) { + iiifSize = "full"; + } else if ( this.version === 3 && iiifSizeW === this.width && iiifSizeH === this.height ) { + iiifSize = "max"; + } else if (this.isLevel0 && this.version < 3) { + iiifSize = iiifSizeW + ","; + } else { + iiifSize = iiifSizeW + "," + iiifSizeH; + } + } + const uri = [ this._id, iiifRegion, iiifSize, IIIF_ROTATION, iiifQuality ].join( '/' ); + + return uri; + }, + + /** + * Equality comparator + */ + equals: function(otherSource) { + return otherSource && this._id === otherSource._id; + }, + + __testonly__: { + canBeTiled: canBeTiled, + constructLevels: constructLevels + } + + }); + + /** + * Determine whether we have a level 0 compliance profile + * @function + * @param {Object} options + * @param {Array|String} options.profile + * @returns {Boolean} + */ + function checkLevel0 ( options ) { + const level0Profiles = [ + "http://library.stanford.edu/iiif/image-api/compliance.html#level0", + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0", + "http://iiif.io/api/image/2/level0.json", + "level0", + "https://iiif.io/api/image/3/level0.json" + ]; + const profileLevel = Array.isArray(options.profile) ? options.profile[0] : options.profile; + const isLevel0 = (level0Profiles.indexOf(profileLevel) !== -1); + return isLevel0; + } + + + /** + * Determine whether arbitrary tile requests can be made against a service with the given profile + * @function + * @param {Object} options + * @param {Array|String} options.profile + * @param {Number} options.version + * @param {String[]} options.extraFeatures + * @returns {Boolean} + */ + function canBeTiled ( options ) { + const isLevel0 = checkLevel0( options ); + let hasCanonicalSizeFeature = false; + if ( options.version === 2 && options.profile.length > 1 && options.profile[1].supports ) { + hasCanonicalSizeFeature = options.profile[1].supports.indexOf( "sizeByW" ) !== -1; + } + if ( options.version === 3 && options.extraFeatures ) { + hasCanonicalSizeFeature = options.extraFeatures.indexOf( "sizeByWh" ) !== -1; + } + return !isLevel0 || hasCanonicalSizeFeature; + } + + + /** + * Build the legacy pyramid URLs (one tile per level) + * @function + * @param {object} options - infoJson + * @throws {Error} + */ + function constructLevels(options) { + const levels = []; + for(let i = 0; i < options.sizes.length; i++) { + levels.push({ + url: options._id + '/full/' + options.sizes[i].width + ',' + + (options.version === 3 ? options.sizes[i].height : '') + + '/0/default.' + options.tileFormat, + width: options.sizes[i].width, + height: options.sizes[i].height + }); + } + return levels.sort(function(a, b) { + return a.width - b.width; + }); + } + + + function configureFromXml10(xmlDoc) { + //parse the xml + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + const root = xmlDoc.documentElement; + const rootName = root.tagName; + let configuration = null; + + if ( rootName === "info" ) { + try { + configuration = {}; + parseXML10( root, configuration ); + return configuration; + + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( $.getString("Errors.IIIF") ); + } + } + throw new Error( $.getString( "Errors.IIIF" ) ); + } + + function parseXML10( node, configuration, property ) { + if ( node.nodeType === 3 && property ) {//text node + let value = node.nodeValue.trim(); + if( value.match(/^\d*$/)){ + value = Number( value ); + } + if( !configuration[ property ] ){ + configuration[ property ] = value; + }else{ + if( !$.isArray( configuration[ property ] ) ){ + configuration[ property ] = [ configuration[ property ] ]; + } + configuration[ property ].push( value ); + } + } else if( node.nodeType === 1 ){ + for( let i = 0; i < node.childNodes.length; i++ ){ + parseXML10( node.childNodes[ i ], configuration, node.nodeName ); + } + } + } + + + +}( OpenSeadragon )); + +/** + * OpenSeadragon - IIPTileSource + * + * 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 IIPTileSource + * @classdesc A tilesource implementation for the Internet Imaging Protocol (IIP). + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @see https://iipimage.sourceforge.io + * + * @param {String} iipsrv - IIPImage host server path (ex: "https://host/fcgi-bin/iipsrv.fcgi" or "/fcgi-bin/iipsrv.fcgi") + * @param {String} image - Image path and name on server (ex: "image.tif") + * @param {String} [format] - Tile output format (default: "jpg") + * @param {Object} [transform] - Object containing image processing transforms + * (supported transform: "stack","quality","contrast","color","invert", + * "colormap," "gamma","minmax","twist","hillshade". + * See https://iipimage.sourceforge.io/documentation/protocol for how to use) + * + * Example: tileSources: { + * iipsrv: "/fcgi-bin/iipsrv.fcgi", + * image: "test.tif", + * transform: { + * gamma: 1.5, + * invert: true + * } + * } + */ + + $.IIPTileSource = function(options) { + + $.EventSource.call( this ); + + if( options && options.iipsrv && options.image ){ + $.extend( this, options ); + this.aspectRatio = 1; + this.dimensions = new $.Point( 10, 10 ); + this._tileWidth = 0; + this._tileHeight = 0; + this.tileOverlap = 0; + this.minLevel = 0; + this.maxLevel = 0; + this.ready = false; + + // Query server for image metadata + const url = this.getMetadataUrl(); + this.getImageInfo( url ); + } + }; + + + $.extend($.IIPTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.IIPTileSource.prototype */ { + + /** + * Return URL string for image metadata + * @function + * @returns {String} url - The IIP URL needed for image metadata + */ + getMetadataUrl: function() { + return this.iipsrv + '?FIF=' + this.image + '&obj=IIP,1.0&obj=Max-size&obj=Tile-size&obj=Resolution-number&obj=Resolutions'; + }, + + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] + */ + supports: function(data, url) { + // Configuration must supply the IIP server endpoint and the image name + return ( data && ("iipsrv" in data) && ("image" in data) ); + }, + + + /** + * Parse IIP protocol response + * @function + * @param {Object|Array} data - raw metadata from an IIP server + */ + parseIIP: function( data ) { + + // Full image size + let tmp = data.split( "Max-size:" ); + if(!tmp[1]){ + throw new Error( "No Max-size returned" ); + } + let size = tmp[1].split(" "); + this.width = parseInt( size[0], 10 ); + this.height = parseInt( size[1], 10 ); + this.dimensions = new $.Point( this.width, this.height ); + + // Calculate aspect ratio + this.aspectRatio = this.width / this.height; + + // Tile size + tmp = data.split( "Tile-size:" ); + if(!tmp[1]){ + throw new Error( "No Tile-size returned" ); + } + size = tmp[1].split(" "); + this._tileWidth = parseInt(size[0], 10); + this._tileHeight = parseInt(size[1], 10); + + // Number of resolution levels + tmp = data.split( "Resolution-number:" ); + const numRes = parseInt(tmp[1], 10); + this.minLevel = 0; + this.maxLevel = numRes - 1; + this.tileOverlap = 0; + + // Size of each resolution + tmp = data.split( "Resolutions:" ); + size = tmp[1].split(","); + const len = size.length; + this.levelSizes = new Array(len); + for( let n = 0; n < len; n++ ) { + const res = size[n].split(" "); + const w = parseInt(res[0], 10); + const h = parseInt(res[1], 10); + this.levelSizes[n] = {width: w, height: h}; + } + }, + + + /** + * Retrieve image metadata from an IIP-compatible server + * + * @function + * @param {String} url + * @throws {Error} + */ + getImageInfo: function( url ) { + + const _this = this; + + $.makeAjaxRequest( { + url: url, + type: "GET", + async: false, + withCredentials: this.ajaxWithCredentials, + headers: this.ajaxHeaders, + success: function( xhr ) { + try { + OpenSeadragon[ "IIPTileSource" ].prototype.parseIIP.call( _this, xhr.responseText ); + _this.ready = true; + _this.raiseEvent( 'ready', { tileSource: _this } ); + } + catch( e ) { + const msg = "IIPTileSource: Error parsing IIP metadata: " + e.message; + _this.raiseEvent( 'open-failed', { message: msg, source: url } ); + } + }, + error: function ( xhr, exc ) { + const msg = "IIPTileSource: Unable to get IIP metadata from " + url; + $.console.error( msg ); + _this.raiseEvent( 'open-failed', { message: msg, source: url }); + } + }); + }, + + + /** + * Parse and configure the image metadata + * @function + * @param {String|Object|Array|Document} data + * @param {String} url - the url the data was loaded + * from if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null value obtained from + * the protocol URL after '#' sign if flag splitHashDataForPost set to 'true' + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure the tile source constructor (include all values you want to + * instantiate the TileSource subclass with - what _options_ object should contain). + * @throws {Error} + */ + configure: function( options, url, postData ) { + return options; + }, + + + /** + * @function + * @param {Number} level + */ + getNumTiles: function( level ) { + const levelSize = this.levelSizes[level]; + let x = Math.ceil( levelSize.width / this._tileWidth ); + let y = Math.ceil( levelSize.height / this._tileHeight ); + return new $.Point( x, y ); + }, + + + /** + * Determine the url which will return an image for the region specified by the given x, y, and level components. + * Takes into account image processing parameters that have been set in constructor + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function(level, x, y) { + + // Get the exact size of this level and calculate the number of tiles across + const levelSize = this.levelSizes[level]; + const ntlx = Math.ceil( levelSize.width / this._tileWidth ); + + // Set the base URL + let url = this.iipsrv + '?FIF=' + this.image + '&'; + + // Apply any image procesing transform + if( this.transform ){ + + if( this.transform.stack ) { + url += 'SDS=' + this.transform.stack + '&'; + } + if( this.transform.contrast ) { + url += 'CNT=' + this.transform.contrast + '&'; + } + if( this.transform.gamma ) { + url += 'GAM=' + this.transform.gamma + '&'; + } + if( this.transform.invert && this.transform.invert === true ) { + url += 'INV&'; + } + if( this.transform.color ) { + url += 'COL=' + this.transform.color + '&'; + } + if( this.transform.twist ) { + url += 'CTW=' + this.transform.twist + '&'; + } + if( this.transform.convolution ) { + url += 'CNV=' + this.transform.convolution + '&'; + } + if( this.transform.quality ) { + url += 'QLT=' + this.transform.quality + '&'; + } + if( this.transform.colormap ) { + url += 'CMP=' + this.transform.colormap + '&'; + } + if( this.transform.minmax ) { + url += 'MINMAX=' + this.transform.minmax + '&'; + } + if( this.transform.hillshade ) { + url += 'SHD=' + this.transform.hillshade + '&'; + } + } + + // Our output command depends on the requested image format + let format = "JTL"; + if (this.format === "png") { + format = "PTL"; + } else if (this.format === "webp" ) { + format = "WTL"; + } else if (this.format === "avif" ) { + format = "ATL"; + } + + // Calculate the tile index for this resolution + const tile = (y * ntlx) + x; + + return url + format + '=' + level + ',' + tile; + } + + }); + + $.extend( true, $.IIPTileSource.prototype, $.EventSource.prototype ); + +}(OpenSeadragon)); + +/** + * OpenSeadragon - IrisTileSource + * + * 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 IrisTileSource + * @classdesc A tilesource implementation for use with Iris images. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * + * @param {String} type - iris + * @param {String} serverUrl - Iris host server path (ex: "http://localhost:3000") + * @param {String} slideId - Image id (ex: "12345" for 12345.iris) + * @param {Object} [metadata] - Optional metadata object to use instead of fetching + * + * Example: tileSources: { + * type: "iris", + * serverUrl: "http://localhost:3000", + * slideId: "12345" + * } + */ + + $.IrisTileSource = function(options) { + + $.TileSource.apply(this, [options]); + if (!options.serverUrl || !options.slideId) { + throw new Error("IrisTileSource requires serverUrl and slideId"); + } + this.serverUrl = options.serverUrl; + this.slideId = options.slideId; + this.ready = false; + + if (options.metadata) { + this.parseMetadata(options.metadata); + this.ready = true; + this.raiseEvent('ready', { tileSource: this }); + } else { + const url = this.getMetadataUrl(); + this.getImageInfo(url); + } + }; + + $.extend($.IrisTileSource.prototype, $.TileSource.prototype, { + /** + * Return URL string for image metadata + * @function + * @returns {String} url - The Iris metadata URL + */ + getMetadataUrl: function() { + return this.serverUrl + '/slides/' + this.slideId + '/metadata'; + }, + + /** + * Determine if the data implies the image service is supported by this tile source. + * @function + * @param {Object} data - The raw metadata object to check + * @returns {Boolean} - True if supported, false otherwise + */ + supports: function(data) { + return (data && data.type === "iris" && data.serverUrl && data.slideId); + }, + + /** + * Parse Iris protocol metadata response + * @function + * @param {Object} data - Raw metadata from Iris server + */ + parseMetadata: function(data) { + this._tileWidth = 256; + this._tileHeight = 256; + + this.tileSize = this._tileWidth; + this.tileOverlap = 0; + + const layers = data.extent.layers; + + const maxLayer = layers.length - 1; + const maxScale = layers[maxLayer].scale; + + this.width = Math.ceil(data.extent.width * maxScale); + this.height = Math.ceil(data.extent.height * maxScale); + + this.dimensions = new $.Point(this.width, this.height); + this.aspectRatio = this.width / this.height; + this.levelSizes = layers.map(level => ({ + width: Math.ceil(level.x_tiles * this._tileWidth), + height: Math.ceil(level.y_tiles * this._tileHeight), + xTiles: Math.ceil(level.x_tiles), + yTiles: Math.ceil(level.y_tiles) + })); + + this.levelScales = layers.map(level => level.scale / maxScale); + + this.minLevel = 0; + this.maxLevel = Math.ceil(this.levelSizes.length - 1); + }, + + /** + * Retrieve image metadata from an Iris-compatible server + * @function + * @param {String} url - The metadata URL + */ + getImageInfo: function(url) { + const _this = this; + + $.makeAjaxRequest({ + url: url, + type: "GET", + async: true, + success: function(xhr) { + try { + const data = JSON.parse(xhr.responseText); + _this.parseMetadata(data); + _this.ready = true; + _this.raiseEvent('ready', { tileSource: _this }); + } + catch (e) { + const msg = "IrisTileSource: Error parsing metadata: " + e.message; + $.console.error(msg); + _this.raiseEvent('open-failed', { message: msg, source: url }); + } + }, + error: function(xhr, exc) { + const msg = "IrisTileSource: Unable to get metadata from " + url; + $.console.error(msg); + _this.raiseEvent('open-failed', { message: msg, source: url }); + } + }); + }, + + /** + * Get the number of tiles at a given level + * @function + * @param {Number} level - The image depth level + * @returns {OpenSeadragon.Point} - Number of tiles in x and y directions + */ + getNumTiles: function(level) { + if (level < this.minLevel || level > this.maxLevel || !this.levelSizes[level]) { + return new $.Point(0, 0); + } + return new $.Point( + Math.ceil(this.levelSizes[level].xTiles), + Math.ceil(this.levelSizes[level].yTiles) + ); + }, + + /** + * Determine the URL which will return an image for the region specified by the given x, y, and level components. + * @function + * @param {Number} level - The zoom level + * @param {Number} x - The x tile index + * @param {Number} y - The y tile index + * @returns {String} - The tile URL + */ + getTileUrl: function(level, x, y) { + const pos = y * this.levelSizes[level].xTiles + x; + return `${this.serverUrl}/slides/${this.slideId}/layers/${level}/tiles/${pos}`; + }, + + /** + * Get the scale for a given level + * @function + * @param {Number} level - The image depth level + * @returns {Number} - The scale for the level + */ + getLevelScale: function(level) { + return this.levelScales[level]; + }, + + /** + * Retrieve and immediately return the options object + * @function + * @param {Object} options - Options object + * @returns {Object} - The options object + */ + configure: function (options) { + return options; + } + }); + + $.extend(true, $.IrisTileSource.prototype, $.EventSource.prototype); + +}(OpenSeadragon)); + +/* + * OpenSeadragon - OsmTileSource + * + * 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. + */ + +/* + * Derived from the OSM tile source in Rainer Simon's seajax-utils project + * . Rainer Simon has contributed + * the included code to the OpenSeadragon project under the New BSD license; + * see . + */ + + +(function( $ ){ + +/** + * @class OsmTileSource + * @classdesc A tilesource implementation for OpenStreetMap.

+ * + * Note 1. Zoomlevels. Deep Zoom and OSM define zoom levels differently. In Deep + * Zoom, level 0 equals an image of 1x1 pixels. In OSM, level 0 equals an image of + * 256x256 levels (see http://gasi.ch/blog/inside-deep-zoom-2). I.e. there is a + * difference of log2(256)=8 levels.

+ * + * Note 2. Image dimension. According to the OSM Wiki + * (http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels) + * the highest Mapnik zoom level has 262.144x262.144 tiles, with a 256x256 + * pixel size. I.e. the Deep Zoom image dimension is 67.108.864x67.108.864 + * pixels. + * OSM now supports higher max zoom (e.g. 19), but this default is + * based on zoom level 18: 2^18 tiles * 256px. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Number|Object} width - the pixel width of the image or the idiomatic + * options object which is used instead of positional arguments. + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + */ +$.OsmTileSource = function( width, height, tileSize, tileOverlap, tilesUrl ) { + let options; + + if( $.isPlainObject( width ) ){ + options = width; + }else{ + options = { + width: arguments[0], + height: arguments[1], + tileSize: arguments[2], + tileOverlap: arguments[3], + tilesUrl: arguments[4] + }; + } + //apply default setting for standard public OpenStreatMaps service + //but allow them to be specified so fliks can host there own instance + //or apply against other services supportting the same standard + if( !options.width || !options.height ){ + options.width = 67108864; + options.height = 67108864; + } + if( !options.tileSize ){ + options.tileSize = 256; + options.tileOverlap = 0; + } + if( !options.tilesUrl ){ + options.tilesUrl = "http://tile.openstreetmap.org/"; + } + options.minLevel = 8; + + $.TileSource.apply( this, [ options ] ); + +}; + +$.extend( $.OsmTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.OsmTileSource.prototype */{ + + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] + */ + supports: function( data, url ){ + return ( + data.type && + "openstreetmaps" === data.type + ); + }, + + /** + * + * @function + * @param {Object} data - the raw configuration + * @param {String} url - the url the data was retrieved from if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( data, url, postData ){ + return data; + }, + + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function( level, x, y ) { + return this.tilesUrl + (level - 8) + "/" + x + "/" + y + ".png"; + }, + + /** + * Equality comparator + */ + equals: function(otherSource) { + return otherSource && this.tilesUrl === otherSource.tilesUrl; + } +}); + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - TmsTileSource + * + * 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. + */ + +/* + * Derived from the TMS tile source in Rainer Simon's seajax-utils project + * . Rainer Simon has contributed + * the included code to the OpenSeadragon project under the New BSD license; + * see . + */ + + +(function( $ ){ + +/** + * @class TmsTileSource + * @classdesc A tilesource implementation for Tiled Map Services (TMS). + * TMS tile scheme ( [ as supported by OpenLayers ] is described here + * ( http://openlayers.org/dev/examples/tms.html ). + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Number|Object} width - the pixel width of the image or the idiomatic + * options object which is used instead of positional arguments. + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + */ +$.TmsTileSource = function( width, height, tileSize, tileOverlap, tilesUrl ) { + let options; + + if( $.isPlainObject( width ) ){ + options = width; + }else{ + options = { + width: arguments[0], + height: arguments[1], + tileSize: arguments[2], + tileOverlap: arguments[3], + tilesUrl: arguments[4] + }; + } + // TMS has integer multiples of 256 for width/height and adds buffer + // if necessary -> account for this! + const bufferedWidth = Math.ceil(options.width / 256) * 256; + const bufferedHeight = Math.ceil(options.height / 256) * 256; + let max; + + // Compute number of zoomlevels in this tileset + if (bufferedWidth > bufferedHeight) { + max = bufferedWidth / 256; + } else { + max = bufferedHeight / 256; + } + options.maxLevel = Math.ceil(Math.log(max) / Math.log(2)) - 1; + options.tileSize = 256; + options.width = bufferedWidth; + options.height = bufferedHeight; + + $.TileSource.apply( this, [ options ] ); + +}; + +$.extend( $.TmsTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.TmsTileSource.prototype */{ + + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] + */ + supports: function( data, url ){ + return ( data.type && "tiledmapservice" === data.type ); + }, + + /** + * + * @function + * @param {Object} data - the raw configuration + * @param {String} url - the url the data was retrieved from if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( data, url, postData ){ + return data; + }, + + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function( level, x, y ) { + // Convert from Deep Zoom definition to TMS zoom definition + const yTiles = this.getNumTiles( level ).y - 1; + + return this.tilesUrl + level + "/" + x + "/" + (yTiles - y) + ".png"; + }, + + /** + * Equality comparator + */ + equals: function (otherSource) { + return otherSource && this.tilesUrl === otherSource.tilesUrl; + } +}); + + +}( OpenSeadragon )); + +(function($) { + + /** + * @class ZoomifyTileSource + * @classdesc A tilesource implementation for the zoomify format. + * + * A description of the format can be found here: + * https://ecommons.cornell.edu/bitstream/handle/1813/5410/Introducing_Zoomify_Image.pdf + * + * There are two ways of creating a zoomify tilesource for openseadragon + * + * 1) Supplying all necessary information in the tilesource object. A minimal example object for this method looks like this: + * + * { + * type: "zoomifytileservice", + * width: 1000, + * height: 1000, + * tilesUrl: "/test/data/zoomify/" + * } + * + * The tileSize is set to 256 (the usual Zoomify default) when it is not defined. The tileUrl must the path to the image _directory_. + * + * 2) Loading image metadata from xml file: (CURRENTLY NOT SUPPORTED) + * + * When creating zoomify formatted images one "xml" like file with name ImageProperties.xml + * will be created as well. Here is an example of such a file: + * + * + * + * To use this xml file as metadata source you must supply the path to the ImageProperties.xml file and leave out all other parameters: + * As stated above, this method of loading a zoomify tilesource is currently not supported + * + * { + * type: "zoomifytileservice", + * tilesUrl: "/test/data/zoomify/ImageProperties.xml" + * } + + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Number} width - the pixel width of the image. + * @param {Number} height + * @param {Number} tileSize + * @param {String} tilesUrl + */ + $.ZoomifyTileSource = function(options) { + if(typeof options.tileSize === 'undefined'){ + options.tileSize = 256; + } + + if(typeof options.fileFormat === 'undefined'){ + options.fileFormat = 'jpg'; + this.fileFormat = options.fileFormat; + } + + const currentImageSize = { + x: options.width, + y: options.height + }; + options.imageSizes = [{ + x: options.width, + y: options.height + }]; + options.gridSize = [this._getGridSize(options.width, options.height, options.tileSize)]; + + while (parseInt(currentImageSize.x, 10) > options.tileSize || parseInt(currentImageSize.y, 10) > options.tileSize) { + currentImageSize.x = Math.floor(currentImageSize.x / 2); + currentImageSize.y = Math.floor(currentImageSize.y / 2); + options.imageSizes.push({ + x: currentImageSize.x, + y: currentImageSize.y + }); + options.gridSize.push(this._getGridSize(currentImageSize.x, currentImageSize.y, options.tileSize)); + } + options.imageSizes.reverse(); + options.gridSize.reverse(); + options.minLevel = 0; + options.maxLevel = options.gridSize.length - 1; + + $.TileSource.apply(this, [options]); + }; + + $.extend($.ZoomifyTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ZoomifyTileSource.prototype */ { + + //private + _getGridSize: function(width, height, tileSize) { + return { + x: Math.ceil(width / tileSize), + y: Math.ceil(height / tileSize) + }; + }, + + //private + _calculateAbsoluteTileNumber: function(level, x, y) { + let num = 0; + let size = {}; + + //Sum up all tiles below the level we want the number of tiles + for (let z = 0; z < level; z++) { + size = this.gridSize[z]; + num += size.x * size.y; + } + //Add the tiles of the level + size = this.gridSize[level]; + num += size.x * y + x; + return num; + }, + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] + */ + supports: function(data, url) { + return (data.type && "zoomifytileservice" === data.type); + }, + + /** + * + * @function + * @param {Object} data - the raw configuration + * @param {String} url - the url the data was retrieved from if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function(data, url, postData) { + return data; + }, + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function(level, x, y) { + //console.log(level); + let result = 0; + const num = this._calculateAbsoluteTileNumber(level, x, y); + result = Math.floor(num / 256); + return this.tilesUrl + 'TileGroup' + result + '/' + level + '-' + x + '-' + y + '.' + this.fileFormat; + + }, + + /** + * Equality comparator + */ + equals: function (otherSource) { + return otherSource && this.tilesUrl === otherSource.tilesUrl; + } + }); + +}(OpenSeadragon)); + + +/* + * OpenSeadragon - LegacyTileSource + * + * 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 LegacyTileSource + * @classdesc The LegacyTileSource allows simple, traditional image pyramids to be loaded + * into an OpenSeadragon Viewer. Basically, this translates to the historically + * common practice of starting with a 'master' image, maybe a tiff for example, + * and generating a set of 'service' images like one or more thumbnails, a medium + * resolution image and a high resolution image in standard web formats like + * png or jpg. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Array} levels An array of file descriptions, each is an object with + * a 'url', a 'width', and a 'height'. Overriding classes can expect more + * properties but these properties are sufficient for this implementation. + * Additionally, the levels are required to be listed in order from + * smallest to largest. + * @property {Number} aspectRatio + * @property {Number} dimensions + * @property {Number} tileSize + * @property {Number} tileOverlap + * @property {Number} minLevel + * @property {Number} maxLevel + * @property {Array} levels + */ +$.LegacyTileSource = function( levels ) { + + let options; + let width; + let height; + + if( $.isArray( levels ) ){ + options = { + type: 'legacy-image-pyramid', + levels: levels + }; + } + + //clean up the levels to make sure we support all formats + options.levels = filterFiles( options.levels ); + + if ( options.levels.length > 0 ) { + width = options.levels[ options.levels.length - 1 ].width; + height = options.levels[ options.levels.length - 1 ].height; + } + else { + width = 0; + height = 0; + $.console.error( "No supported image formats found" ); + } + + $.extend( true, options, { + width: width, + height: height, + tileSize: Math.max( height, width ), + tileOverlap: 0, + minLevel: 0, + maxLevel: options.levels.length > 0 ? options.levels.length - 1 : 0 + } ); + + $.TileSource.apply( this, [ options ] ); + + this.levels = options.levels; +}; + +$.extend( $.LegacyTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.LegacyTileSource.prototype */{ + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] + */ + supports: function( data, url ){ + return ( + data.type && + "legacy-image-pyramid" === data.type + ) || ( + data.documentElement && + "legacy-image-pyramid" === data.documentElement.getAttribute('type') + ); + }, + + + /** + * + * @function + * @param {Object|XMLDocument} configuration - the raw configuration + * @param {String} dataUrl - the url the data was retrieved from if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( configuration, dataUrl, postData ){ + + let options; + + if( !$.isPlainObject(configuration) ){ + + options = configureFromXML( this, configuration ); + + }else{ + + options = configureFromObject( this, configuration ); + } + + return options; + + }, + + /** + * @function + * @param {Number} level + */ + getLevelScale: function ( level ) { + let levelScale = NaN; + if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) { + levelScale = + this.levels[ level ].width / + this.levels[ this.maxLevel ].width; + } + return levelScale; + }, + + /** + * @function + * @param {Number} level + */ + getNumTiles: function( level ) { + const scale = this.getLevelScale( level ); + if ( scale ){ + return new $.Point( 1, 1 ); + } else { + return new $.Point( 0, 0 ); + } + }, + + /** + * This method is not implemented by this class other than to throw an Error + * announcing you have to implement it. Because of the variety of tile + * server technologies, and various specifications for building image + * pyramids, this method is here to allow easy integration. + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @throws {Error} + */ + getTileUrl: function ( level, x, y ) { + let url = null; + if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) { + url = this.levels[ level ].url; + } + return url; + }, + + /** + * Equality comparator + */ + equals: function (otherSource) { + if (!otherSource || !otherSource.levels || otherSource.levels.length !== this.levels.length) { + return false; + } + for (let i = this.minLevel; i <= this.maxLevel; i++) { + if (this.levels[i].url !== otherSource.levels[i].url) { + return false; + } + } + return true; + } +} ); + +/** + * This method removes any files from the Array which don't conform to our + * basic requirements for a 'level' in the LegacyTileSource. + * @private + * @inner + * @function + */ +function filterFiles( files ){ + const filtered = []; + let file; + + for( let i = 0; i < files.length; i++ ){ + file = files[ i ]; + if( file.height && + file.width && + file.url ){ + //This is sufficient to serve as a level + filtered.push({ + url: file.url, + width: Number( file.width ), + height: Number( file.height ) + }); + } + else { + $.console.error( 'Unsupported image format: %s', file.url ? file.url : '' ); + } + } + + return filtered.sort(function(a, b) { + return a.height - b.height; + }); + +} + +/** + * @private + * @inner + * @function + */ +function configureFromXML( tileSource, xmlDoc ){ + + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + const root = xmlDoc.documentElement; + const rootName = root.tagName; + let conf = null; + let levels = []; + let level; + + if ( rootName === "image" ) { + + try { + conf = { + type: root.getAttribute( "type" ), + levels: [] + }; + + levels = root.getElementsByTagName( "level" ); + for ( let i = 0; i < levels.length; i++ ) { + level = levels[ i ]; + + conf.levels.push({ + url: level.getAttribute( "url" ), + width: parseInt( level.getAttribute( "width" ), 10 ), + height: parseInt( level.getAttribute( "height" ), 10 ) + }); + } + + return configureFromObject( tileSource, conf ); + + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( 'Unknown error parsing Legacy Image Pyramid XML.' ); + } + } else if ( rootName === "collection" ) { + throw new Error( 'Legacy Image Pyramid Collections not yet supported.' ); + } else if ( rootName === "error" ) { + throw new Error( 'Error: ' + xmlDoc ); + } + + throw new Error( 'Unknown element ' + rootName ); +} + +/** + * @private + * @inner + * @function + */ +function configureFromObject( tileSource, configuration ){ + + return configuration.levels; + +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - ImageTileSource + * + * 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 ImageTileSource + * @classdesc The ImageTileSource allows a simple image to be loaded + * into an OpenSeadragon Viewer. + * There are 2 ways to open an ImageTileSource: + * 1. viewer.open({type: 'image', url: fooUrl}); + * 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl})); + * + * With the first syntax, the crossOriginPolicy, ajaxWithCredentials and + * useCanvas options are inherited from the viewer if they are not + * specified directly in the options object. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Object} options Options object. + * @param {String} options.url URL of the image + * @param {Boolean} [options.buildPyramid=true] If set to true (default), a + * pyramid will be built internally to provide a better downsampling. + * @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are + * 'Anonymous', 'use-credentials', and false. If false, image requests will + * not use CORS preventing internal pyramid building for images from other + * domains. + * @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set + * the withCredentials XHR flag for AJAX requests (when loading tile sources). + * @param {Boolean} [options.useCanvas=true] Set to false to prevent any use + * of the canvas API. + */ +$.ImageTileSource = class extends $.TileSource { + + constructor(props) { + super($.extend({ + buildPyramid: true, + crossOriginPolicy: false, + ajaxWithCredentials: false, + }, props)); + } + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} [url] + */ + supports(data, url) { + return data.type && data.type === "image"; + } + /** + * + * @function + * @param {Object} options - the options + * @param {String} dataUrl - the url the image was retrieved from, if any. + * @param {String} postData - HTTP POST data in k=v&k2=v2... form or null + * @returns {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure(options, dataUrl, postData) { + return options; + } + /** + * Responsible for retrieving, and caching the + * image metadata pertinent to this TileSources implementation. + * @function + * @param {String} url + * @throws {Error} + */ + getImageInfo(url) { + const image = new Image(), + _this = this; + + if (this.crossOriginPolicy) { + image.crossOrigin = this.crossOriginPolicy; + } + + $.addEvent(image, 'load', function () { + _this.width = image.naturalWidth; + _this.height = image.naturalHeight; + _this.tileWidth = _this.width; + _this.tileHeight = _this.height; + _this.tileOverlap = 0; + _this.minLevel = 0; + _this.image = image; + _this.levels = _this._buildLevels(image); + _this.maxLevel = _this.levels.length - 1; + + // Note: this event is documented elsewhere, in TileSource + _this.raiseEvent('ready', {tileSource: _this}); + }); + + $.addEvent(image, 'error', function () { + _this.image = null; + // Note: this event is documented elsewhere, in TileSource + _this.raiseEvent('open-failed', { + message: "Error loading image at " + url, + source: url + }); + }); + + image.src = url; + } + /** + * @function + * @param {Number} level + */ + getLevelScale(level) { + let levelScale = NaN; + if (level >= this.minLevel && level <= this.maxLevel) { + levelScale = + this.levels[level].width / + this.levels[this.maxLevel].width; + } + return levelScale; + } + /** + * @function + * @param {Number} level + */ + getNumTiles(level) { + if (this.getLevelScale(level)) { + return new $.Point(1, 1); + } + return new $.Point(0, 0); + } + /** + * Retrieves a tile url + * @function + * @param {Number} level Level of the tile + * @param {Number} x x coordinate of the tile + * @param {Number} y y coordinate of the tile + */ + getTileUrl(level, x, y) { + if (level === this.maxLevel) { + return this.url; //for original image, preserve url + } + //make up url by positional args + return `${this.url}?l=${level}&x=${x}&y=${y}`; + } + + /** + * Equality comparator + */ + equals(otherSource) { + return this.url === otherSource.url; + } + + getTilePostData(level, x, y) { + return {level: level, x: x, y: y}; + } + + /** + * Retrieves a tile context 2D + * @deprecated + */ + getContext2D(level, x, y) { + $.console.error('Using [TiledImage.getContext2D] (for plain images only) is deprecated. ' + + 'Use overridden downloadTileStart (https://openseadragon.github.io/examples/advanced-data-model/) instead.'); + return this._createContext2D(); + } + + downloadTileStart(job) { + const tileData = job.postData; + if (tileData.level === this.maxLevel) { + job.finish(this.image, null, "image"); + return; + } + + if (tileData.level >= this.minLevel && tileData.level <= this.maxLevel) { + const levelData = this.levels[tileData.level]; + const context = this._createContext2D(this.image, levelData.width, levelData.height); + job.finish(context, null, "context2d"); + return; + } + job.fail(`Invalid level ${tileData.level} for plain image source. Did you forget to set buildPyramid=true?`); + } + + downloadTileAbort(job) { + //no-op + } + + // private + // + // Builds the different levels of the pyramid if possible + // (i.e. if canvas API enabled and no canvas tainting issue). + _buildLevels(image) { + const levels = [{ + url: image.src, + width: image.naturalWidth, + height: image.naturalHeight + }]; + + if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) { + return levels; + } + + let currentWidth = image.naturalWidth, + currentHeight = image.naturalHeight; + // We build smaller levels until either width or height becomes + // 2 pixel wide. + while (currentWidth >= 2 && currentHeight >= 2) { + currentWidth = Math.floor(currentWidth / 2); + currentHeight = Math.floor(currentHeight / 2); + + levels.push({ + width: currentWidth, + height: currentHeight, + }); + } + return levels.reverse(); + } + + + _createContext2D(data, w, h) { + const canvas = document.createElement("canvas"), + context = canvas.getContext("2d"); + + + canvas.width = w; + canvas.height = h; + context.drawImage(data, 0, 0, w, h); + return context; + } +}; + +}(OpenSeadragon)); + +/* + * OpenSeadragon - TileSourceCollection + * + * 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($) { + +// deprecated +$.TileSourceCollection = function(tileSize, tileSources, rows, layout) { + $.console.error('TileSourceCollection is deprecated; use World instead'); +}; + +}(OpenSeadragon)); + +/* + * OpenSeadragon - Queue + * + * Copyright (C) 2024 OpenSeadragon contributors (modified) + * Copyright (C) Google Inc., The Closure Library Authors. + * https://github.com/google/closure-library + * + * SPDX-License-Identifier: Apache-2.0 + */ + +(function($) { + +const OpenSeadragon = $; // alias for JSDoc + +/** + * @class OpenSeadragon.PriorityQueue + * @classdesc Fast priority queue. Implemented as a Heap. + */ +OpenSeadragon.PriorityQueue = class PriorityQueue { + + /** + * @param {?OpenSeadragon.PriorityQueue} optHeap Optional Heap or + * Object to initialize heap with. + */ + constructor(optHeap = undefined) { + /** + * The nodes of the heap. + * + * This is a densely packed array containing all nodes of the heap, using + * the standard flat representation of a tree as an array (i.e. element [0] + * at the top, with [1] and [2] as the second row, [3] through [6] as the + * third, etc). Thus, the children of element `i` are `2i+1` and `2i+2`, and + * the parent of element `i` is `⌊(i-1)/2⌋`. + * + * The only invariant is that children's keys must be greater than parents'. + * + * @private + */ + this.nodes_ = []; + + if (optHeap) { + this.insertAll(optHeap); + } + } + + /** + * Insert the given value into the heap with the given key. + * @param {K} key The key. + * @param {V} value The value. + */ + insert(key, value) { + this.insertNode(new Node(key, value)); + } + + /** + * Insert node item. + * @param node + */ + insertNode(node) { + const nodes = this.nodes_; + node.index = nodes.length; + nodes.push(node); + this.moveUp_(node.index); + } + + /** + * Adds multiple key-value pairs from another Heap or Object + * @param {?OpenSeadragon.PriorityQueue} heap Object containing the data to add. + */ + insertAll(heap) { + let keys, values; + if (heap instanceof $.PriorityQueue) { + keys = heap.getKeys(); + values = heap.getValues(); + + // If it is a heap and the current heap is empty, I can rely on the fact + // that the keys/values are in the correct order to put in the underlying + // structure. + if (this.getCount() <= 0) { + const nodes = this.nodes_; + for (let i = 0; i < keys.length; i++) { + const node = new Node(keys[i], values[i]); + node.index = nodes.length; + nodes.push(node); + } + return; + } + } else { + throw "insertAll supports only OpenSeadragon.PriorityQueue object!"; + } + + for (let i = 0; i < keys.length; i++) { + this.insert(keys[i], values[i]); + } + } + + /** + * Retrieves and removes the root value of this heap. + * @return {Node} The root node item removed from the root of the heap. Returns + * undefined if the heap is empty. + */ + remove() { + const nodes = this.nodes_; + const count = nodes.length; + const rootNode = nodes[0]; + if (count <= 0) { + return undefined; + } else if (count == 1) { // eslint-disable-line + nodes.length = 0; + } else { + nodes[0] = nodes.pop(); + if (nodes[0]) { + nodes[0].index = 0; + } + this.moveDown_(0); + } + if (rootNode) { + delete rootNode.index; + } + return rootNode; + } + + /** + * Retrieves but does not remove the root value of this heap. + * @return {V} The value at the root of the heap. Returns + * undefined if the heap is empty. + */ + peek() { + const nodes = this.nodes_; + if (nodes.length == 0) { // eslint-disable-line + return undefined; + } + return nodes[0].value; + } + + /** + * Retrieves but does not remove the key of the root node of this heap. + * @return {string} The key at the root of the heap. Returns undefined if the + * heap is empty. + */ + peekKey() { + return this.nodes_[0] && this.nodes_[0].key; + } + + /** + * Move the node up in hierarchy + * @param {Node} node the node + * @param {K} key new ley, must be smaller than current key + */ + decreaseKey(node, key) { + if (node.index === undefined) { + node.key = key; + this.insertNode(node); + } else { + node.key = key; + this.moveUp_(node.index); + } + } + + /** + * Moves the node at the given index down to its proper place in the heap. + * @param {number} index The index of the node to move down. + * @private + */ + moveDown_(index) { + const nodes = this.nodes_; + const count = nodes.length; + + // Save the node being moved down. + const node = nodes[index]; + // While the current node has a child. + while (index < (count >> 1)) { + const leftChildIndex = this.getLeftChildIndex_(index); + const rightChildIndex = this.getRightChildIndex_(index); + + // Determine the index of the smaller child. + const smallerChildIndex = rightChildIndex < count && + nodes[rightChildIndex].key < nodes[leftChildIndex].key ? + rightChildIndex : + leftChildIndex; + + // If the node being moved down is smaller than its children, the node + // has found the correct index it should be at. + if (nodes[smallerChildIndex].key > node.key) { + break; + } + + // If not, then take the smaller child as the current node. + nodes[index] = nodes[smallerChildIndex]; + nodes[index].index = index; + index = smallerChildIndex; + } + nodes[index] = node; + if (node) { + node.index = index; + } + } + + /** + * Moves the node at the given index up to its proper place in the heap. + * @param {number} index The index of the node to move up. + * @private + */ + moveUp_(index) { + const nodes = this.nodes_; + const node = nodes[index]; + + // While the node being moved up is not at the root. + while (index > 0) { + // If the parent is greater than the node being moved up, move the parent + // down. + const parentIndex = this.getParentIndex_(index); + if (nodes[parentIndex].key > node.key) { + nodes[index] = nodes[parentIndex]; + nodes[index].index = index; + index = parentIndex; + } else { + break; + } + } + nodes[index] = node; + if (node) { + node.index = index; + } + } + + /** + * Gets the index of the left child of the node at the given index. + * @param {number} index The index of the node to get the left child for. + * @return {number} The index of the left child. + * @private + */ + getLeftChildIndex_(index) { + return index * 2 + 1; + } + + /** + * Gets the index of the right child of the node at the given index. + * @param {number} index The index of the node to get the right child for. + * @return {number} The index of the right child. + * @private + */ + getRightChildIndex_(index) { + return index * 2 + 2; + } + + /** + * Gets the index of the parent of the node at the given index. + * @param {number} index The index of the node to get the parent for. + * @return {number} The index of the parent. + * @private + */ + getParentIndex_(index) { + return (index - 1) >> 1; + } + + /** + * Gets the values of the heap. + * @return {!Array<*>} The values in the heap. + */ + getValues() { + return this.nodes_.map(n => n.value); + } + + /** + * Gets the keys of the heap. + * @return {!Array} The keys in the heap. + */ + getKeys() { + return this.nodes_.map(n => n.key); + } + + /** + * Whether the heap contains the given value. + * @param {V} val The value to check for. + * @return {boolean} Whether the heap contains the value. + */ + containsValue(val) { + return this.nodes_.some((node) => node.value == val); // eslint-disable-line + } + + /** + * Whether the heap contains the given key. + * @param {string} key The key to check for. + * @return {boolean} Whether the heap contains the key. + */ + containsKey(key) { + return this.nodes_.some((node) => node.value == key); // eslint-disable-line + } + + /** + * Clones a heap and returns a new heap + * @return {!OpenSeadragon.PriorityQueue} A new Heap with the same key-value pairs. + */ + clone() { + return new $.PriorityQueue(this); + } + + /** + * The number of key-value pairs in the map + * @return {number} The number of pairs. + */ + getCount() { + return this.nodes_.length; + } + + /** + * Returns true if this heap contains no elements. + * @return {boolean} Whether this heap contains no elements. + */ + isEmpty() { + return this.nodes_.length === 0; + } + + /** + * Removes all elements from the heap. + */ + clear() { + this.nodes_.length = 0; + } +}; + +/** + * @private + */ +OpenSeadragon.PriorityQueue.Node = class Node { + constructor(key, value) { + /** + * The key. + * @type {K} + * @private + */ + this.key = key; + + /** + * The value. + * @type {V} + * @private + */ + this.value = value; + + /** + * The node index value. Updated in the heap. + * @type {number} + * @private + */ + this.index = 0; + } + + clone() { + return new Node(this.key, this.value); + } +}; + +}(OpenSeadragon)); + +/* + * OpenSeadragon.converter (static property) + * + * 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($){ + +const OpenSeadragon = $; // alias for JSDoc + +/** + * modified from https://gist.github.com/Prottoy2938/66849e04b0bac459606059f5f9f3aa1a + * @private + */ +class WeightedGraph { + constructor() { + this.adjacencyList = {}; + this.vertices = {}; + } + + /** + * Add vertex to graph + * @param vertex unique vertex ID + * @return {boolean} true if inserted, false if exists (no-op) + */ + addVertex(vertex) { + if (!this.vertices[vertex]) { + this.vertices[vertex] = new $.PriorityQueue.Node(0, vertex); + this.adjacencyList[vertex] = []; + return true; + } + return false; + } + + /** + * Add edge to graph + * @param vertex1 id, must exist by calling addVertex() + * @param vertex2 id, must exist by calling addVertex() + * @param weight + * @param transform function that transforms on path vertex1 -> vertex2 + * @return {boolean} true if new edge, false if replaced existing + */ + addEdge(vertex1, vertex2, weight, transform) { + if (weight < 0) { + $.console.error("WeightedGraph: negative weights will make for invalid shortest path computation!"); + } + const outgoingPaths = this.adjacencyList[vertex1], + replacedEdgeIndex = outgoingPaths.findIndex(edge => edge.target === this.vertices[vertex2]), + newEdge = { target: this.vertices[vertex2], origin: this.vertices[vertex1], weight, transform }; + if (replacedEdgeIndex < 0) { + this.adjacencyList[vertex1].push(newEdge); + return true; + } + this.adjacencyList[vertex1][replacedEdgeIndex] = newEdge; + return false; + } + + /** + * @return {{path: ConversionStep[], cost: number}|undefined} cheapest path from start to finish + */ + dijkstra(start, finish) { + const path = []; //to return at end + if (start === finish) { + return { path: path, cost: 0 }; + } + const nodes = new OpenSeadragon.PriorityQueue(); + let smallestNode; + //build up initial state + for (let vertex in this.vertices) { + vertex = this.vertices[vertex]; + if (vertex.value === start) { + vertex.key = 0; //keys are known distances + nodes.insertNode(vertex); + } else { + vertex.key = Infinity; + delete vertex.index; + } + vertex._previous = null; + } + // as long as there is something to visit + while (nodes.getCount() > 0) { + smallestNode = nodes.remove(); + if (smallestNode.value === finish) { + break; + } + const neighbors = this.adjacencyList[smallestNode.value]; + for (const neighborKey in neighbors) { + const edge = neighbors[neighborKey]; + //relax node + const newCost = smallestNode.key + edge.weight; + const nextNeighbor = edge.target; + if (newCost < nextNeighbor.key) { + nextNeighbor._previous = smallestNode; + //key change + nodes.decreaseKey(nextNeighbor, newCost); + } + } + } + + if (!smallestNode || !smallestNode._previous || smallestNode.value !== finish) { + return undefined; //no path + } + + const finalCost = smallestNode.key; //final weight last node + + // done, build the shortest path + while (smallestNode._previous) { + //backtrack + const to = smallestNode.value, + parent = smallestNode._previous, + from = parent.value; + + path.push(this.adjacencyList[from].find(x => x.target.value === to)); + smallestNode = parent; + } + + return { + path: path.reverse(), + cost: finalCost + }; + } +} + +let _imageConversionWorker; +let _conversionId = 0; +// id -> { resolve, reject, timer? } +const _pendingConversions = new Map(); +let __warnedNoSAB = false; +const __hasSAB = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated === true; + +function getIBWorker() { + if (_imageConversionWorker) { + return _imageConversionWorker; + } + + const code = ` +self.onmessage = async (e) => { + const { id, op, } = e.data; + let error; + try { + if (op === 'decodeFromBlob') { + const bmp = await createImageBitmap(e.data.blob, { colorSpaceConversion: 'none' }); + postMessage({ id, ok: true, bmp }, [bmp]); + return; + } + if (op === 'decodeFromBytes') { + const u8 = new Uint8Array(e.data.bytes); + const b = new Blob([u8], { type: e.data.mime || '' }); + const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' }); + postMessage({ id, ok: true, bmp }, [bmp]); + return; + } + if (op === 'fetchDecode') { + const res = await fetch(e.data.url, e.data.setup); + if (!res.ok) throw new Error('HTTP ' + res.status); + const b = await res.blob(); + const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' }); + postMessage({ id, ok: true, bmp }, [bmp]); + return; + } + error = 'Unknown op: ' + op; + } catch (err) { + error = String(err && err.message || err); + } + postMessage({ id, ok: false, err: error }); +}; +`; + // eslint-disable-next-line compat/compat + const url = URL.createObjectURL(new Blob([code], { type: 'text/javascript' })); + _imageConversionWorker = new Worker(url); + + _imageConversionWorker.onmessage = (e) => { + const { id, ok, bmp, err } = e.data || {}; + const entry = _pendingConversions.get(id); + if (!entry) { + return; + } + _pendingConversions.delete(id); + if (entry.timer) { + clearTimeout(entry.timer); + entry.timer = null; + } + if (ok) { + entry.resolve(bmp); + } else { + entry.reject(new Error(err)); + } + }; + + _imageConversionWorker.onerror = (e) => { + for (const [, entry] of _pendingConversions) { + if (entry.timer) { + clearTimeout(entry.timer); + entry.timer = null; + } + entry.reject(new Error('Worker error')); + } + _pendingConversions.clear(); + }; + return _imageConversionWorker; +} + +function postWorker(op, payload, { timeoutMs = 15000 } = {}) { + const worker = getIBWorker(); + const id = ++_conversionId; + + return new $.Promise((resolve, reject) => { + // possibly test $.supportsPromise here as well... + payload.id = id; + payload.op = op; + + const entry = { resolve, reject, timer: null }; + if (timeoutMs > 0) { + entry.timer = setTimeout(() => { + entry.timer = null; + _pendingConversions.delete(id); + reject(new Error(`Worker timeout (${op})`)); + }, timeoutMs); + } + _pendingConversions.set(id, entry); + + if (op === 'decodeFromBytes') { + if (__hasSAB) { + const u8 = payload.u8; + // eslint-disable-next-line no-undef + const sab = new SharedArrayBuffer(u8.byteLength); + new Uint8Array(sab).set(u8); + worker.postMessage({ id, op, bytes: sab, mime: payload.mime }); + } else { + if (!__warnedNoSAB) { + __warnedNoSAB = true; + console.warn('[Converter] SharedArrayBuffer unavailable; falling back to ArrayBuffer.'); + } + const u8 = payload.u8; + const tight = (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength) ? u8 : u8.slice(); + worker.postMessage({ id, op, bytes: tight.buffer, mime: payload.mime }, [tight.buffer]); + } + return; + } + + worker.postMessage(payload); + }); +} + +/** + * Edge.transform function on the conversion path in OpenSeadragon.converter.getConversionPath(). + * It can be also conversion to undefined if used as destructor implementation. + * + * @callback TypeConverter + * @memberof OpenSeadragon + * @param {OpenSeadragon.Tile} tile reference tile that owns the data + * @param {any} data data in the input format + * @returns {any} data in the output format + */ + +/** + * Destructor called every time a data type is to be destroyed or converted to another type. + * + * @callback TypeDestructor + * @memberof OpenSeadragon + * @param {any} data data in the format the destructor is registered for + * @returns {any} can return any value that is carried over to the caller if desirable. + * Note: not used by the OSD cache system. + */ + +/** + * Node on the conversion path in OpenSeadragon.converter.getConversionPath(). + * + * @typedef {Object} ConversionStep + * @memberof OpenSeadragon + * @param {OpenSeadragon.PriorityQueue.Node} target - Target node of the conversion step. + * Its value is the target format. + * @param {OpenSeadragon.PriorityQueue.Node} origin - Origin node of the conversion step. + * Its value is the origin format. + * @param {number} weight cost of the conversion + * @param {TypeConverter} transform the conversion itself + */ + +/** + * Class that orchestrates automated data types conversion. Do not instantiate + * this class, use OpenSeadragon.converter - a global instance, instead. + * + * Types are defined to closely describe the data type, e.g. "url" is insufficient, + * because url can point to many different data types. Another bad example is 'canvas' + * as canvas can have different underlying rendering implementations and thus differ + * in behavior. The following data types supported by + * OpenSeadragon core are: + * - "image" - HTMLImageElement, an object + * - "context2d" - HtmlRenderingContext2D, a 2D canvas context + * - "rasterBlob" - Blob, a binary file-like object carrying image data + * - "imageBitmap" - an ImageBitmap object + * + * The system uses these to deliver desired data from TileSource (which implements fetching logics) + * through plugins to the renderer with preserving data type compatibility. Typical example is: + * TiledImage downloads and creates Image object with type 'image'. It submits + * to the system object of data type 'image'. The system runs this object through + * possible plugins integrated into the invalidation routine (by default none), + * and finishes by conversion for the WebGL renderer, which would most likely be "image" + * object, because the conversion in this case is not even necessary, as the drawer publishes + * the image type as one of its supported ones. + * If some plugin required context2d type, the pipeline would deliver this type and used + * it also for WebGL, as texture loading function accepts canvas object as well as image. + * + * @class OpenSeadragon.DataTypeConverter + * @memberOf OpenSeadragon + */ +OpenSeadragon.DataTypeConverter = class DataTypeConverter { + + constructor() { + this.graph = new WeightedGraph(); + this.destructors = {}; + this.copyings = {}; + + // Teaching OpenSeadragon built-in conversions: + const imageCreator = (tile, url) => new $.Promise((resolve, reject) => { + if (!$.supportsAsync) { + return reject("Not supported in sync mode!"); + } + const img = new Image(); + img.onerror = img.onabort = e => reject(`Failed to load image: ${url}`); + img.onload = () => resolve(img); + if (tile.tiledImage && tile.tiledImage.crossOriginPolicy) { + img.crossOrigin = tile.tiledImage.crossOriginPolicy; + } + img.src = url; + return undefined; + }); + const canvasContextCreator = (tile, imageData) => { + const canvas = document.createElement('canvas'); + canvas.width = imageData.width; + canvas.height = imageData.height; + const context = canvas.getContext('2d', { willReadFrequently: true }); + context.drawImage(imageData, 0, 0); + return context; + }; + + this.learn("rasterBlob", "image", (tile, blob) => new $.Promise((resolve, reject) => { + // eslint-disable-next-line compat/compat + const url = (window.URL || window.webkitURL).createObjectURL(blob); + if (!$.supportsAsync) { + return reject("Not supported in sync mode!"); + } + const img = new Image(); + img.onerror = img.onabort = e => { + // eslint-disable-next-line compat/compat + (window.URL || window.webkitURL).revokeObjectURL(blob); + reject(e); + }; + img.onload = () => { + // eslint-disable-next-line compat/compat + (window.URL || window.webkitURL).revokeObjectURL(blob); + resolve(img); + }; + img.decoding = 'async'; + img.src = url; + return undefined; + }), 1, 2); + + this.learn("context2d", "rasterBlob", (tile, ctx) => new $.Promise((resolve, reject) => { + if (!$.supportsAsync) { + return reject("Not supported in sync mode!"); + } + ctx.canvas.toBlob(resolve); + return undefined; + }), 1, 2); + + // rasterBlob -> imageBitmap (preferred fast path) + this.learn("rasterBlob", "imageBitmap", (tile, blob) => new $.Promise((resolve, reject) => { + if (!$.supportsAsync) { + return reject("Not supported in sync mode!"); + } + if (_imageConversionWorker) { + postWorker('decodeFromBlob', { blob }).then(resolve).catch(reject); + } else { + // Fallback main thread + createImageBitmap(blob, { colorSpaceConversion: 'none' }).then(resolve).catch(reject); + } + return undefined; + }), 1, 1); + + this.learn("imageBitmap", "context2d", (tile, bmp) => { + const canvas = document.createElement('canvas'); + canvas.width = bmp.width; + canvas.height = bmp.height; + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + ctx.drawImage(bmp, 0, 0); + return ctx; + }, 1, 2); + + this.learn("image", "imageBitmap", (tile, img) => { + return createImageBitmap(img, { colorSpaceConversion: 'none' }); + }, 1, 2); + this.learn("image", "context2d", canvasContextCreator, 1, 2); + + //Copies + this.learn("image", "image", (tile, image) => imageCreator(tile, image.src), 1, 1); + this.learn("context2d", "context2d", (tile, ctx) => canvasContextCreator(tile, ctx.canvas)); + this.learn("rasterBlob", "rasterBlob", (tile, blob) => blob, 0, 1); //blobs are immutable, no need to copy + this.learn("imageBitmap", "imageBitmap", (tile, bmp) => new $.Promise((resolve, reject) => { + try { + if (!$.supportsAsync) { + return reject("Not supported in sync mode!"); + } + if (!bmp) { + return reject(new Error("No ImageBitmap to copy")); + } + + if (typeof OffscreenCanvas !== 'undefined' && bmp.width && bmp.height) { + const oc = new OffscreenCanvas(bmp.width, bmp.height); + const ctx = oc.getContext('2d', { willReadFrequently: false }); + ctx.drawImage(bmp, 0, 0); + + if (typeof oc.transferToImageBitmap === 'function') { + const copy = oc.transferToImageBitmap(); + return resolve(copy); + } + return createImageBitmap(oc, { colorSpaceConversion: 'none' }).then(resolve); + } + // Fallback + return createImageBitmap(bmp, { colorSpaceConversion: 'none' }).then(resolve); + } catch (e) { + return reject(e); + } + }), 1, 1); + /** + * Free up canvas memory + * (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory, + * and Safari keeps canvas until its height and width will be set to 0). + */ + this.learnDestroy("context2d", ctx => { + ctx.canvas.width = 0; + ctx.canvas.height = 0; + }); + } + + /** + * Unique identifier (unlike toString.call(x)) to be guessed + * from the data value. This type guess is more strict than + * OpenSeadragon.type() implementation, but for most type recognition + * this test relies on the output of OpenSeadragon.type(). + * + * Note: although we try to implement the type guessing, do + * not rely on this functionality! Prefer explicit type declaration. + * + * @function guessType + * @param x object to get unique identifier for + * - can be array, in that case, alphabetically-ordered list of inner unique types + * is returned (null, undefined are ignored) + * - if $.isPlainObject(x) is true, then the object can define + * getType function to specify its type + * - otherwise, toString.call(x) is applied to get the parameter description + * @return {string} unique variable descriptor + */ + guessType(x) { + if (Array.isArray(x)) { + const types = []; + for (const item of x) { + if (item === undefined || item === null) { + continue; + } + + const type = this.guessType(item); + if (!types.includes(type)) { + types.push(type); + } + } + types.sort(); + return `Array [${types.join(",")}]`; + } + + const guessType = $.type(x); + if (guessType === "dom-node") { + //distinguish nodes + return guessType.nodeName.toLowerCase(); + } + + if (guessType === "object") { + if ($.isFunction(x.getType)) { + return x.getType(); + } + } + return guessType; + } + + /** + * Teach the system to convert data type 'from' -> 'to' + * @param {string} from unique ID of the data item 'from' + * @param {string} to unique ID of the data item 'to' + * @param {OpenSeadragon.TypeConverter} callback converter that takes two arguments: a tile reference, and + * a data object of a type 'from'; and converts this data object to type 'to'. It can return also the value + * wrapped in a Promise (returned in resolve) or it can be async function. + * @param {Number} [costPower=0] positive cost class of the conversion, smaller or equal than 7. + * Should reflect the actual cost of the conversion: + * - if nothing must be done and only reference is retrieved (or a constant operation done), + * return 0 (default) + * - if a linear amount of work is necessary, + * return 1 + * ... and so on, basically the number in O() complexity power exponent (for simplification) + * @param {Number} [costMultiplier=1] multiplier of the cost class, e.g. O(3n^2) would + * use costPower=2, costMultiplier=3; can be between 1 and 10^5 + */ + learn(from, to, callback, costPower = 0, costMultiplier = 1) { + $.console.assert(costPower >= 0 && costPower <= 7, "[DataTypeConverter] Conversion costPower must be between <0, 7>."); + $.console.assert($.isFunction(callback), "[DataTypeConverter:learn] Callback must be a valid function!"); + + if (from === to) { + this.copyings[to] = callback; + } else { + //we won't know if somebody added multiple edges, though it will choose some edge anyway + costPower++; + costMultiplier = Math.min(Math.max(costMultiplier, 1), 10 ^ 5); + this.graph.addVertex(from); + this.graph.addVertex(to); + this.graph.addEdge(from, to, costPower * 10 ^ 5 + costMultiplier, callback); + this._known = {}; //invalidate precomputed paths :/ + } + } + + /** + * Teach the system to destroy data type 'type' + * for example, textures loaded to GPU have to be also manually removed when not needed anymore. + * Needs to be defined only when the created object has extra deletion process. + * @param {string} type + * @param {OpenSeadragon.TypeDestructor} callback destructor, receives the object created, + * it is basically a type conversion to 'undefined' - thus the type. + */ + learnDestroy(type, callback) { + this.destructors[type] = callback; + } + + /** + * Convert data item x of type 'from' to any of the 'to' types, chosen is the cheapest known conversion. + * Data is destroyed upon conversion. For different behavior, implement your conversion using the + * path rules obtained from getConversionPath(). + * Note: conversion DOES NOT COPY data if [to] contains type 'from' (e.g., the cheapest conversion is no conversion). + * It automatically calls destructor on immediate types, but NOT on the x and the result. You should call these + * manually if these should be destroyed. + * @param {OpenSeadragon.Tile} tile + * @param {any} data data item to convert + * @param {string} from data item type + * @param {string} to desired type(s) + * @return {OpenSeadragon.Promise} promise resolution with type 'to', or rejection if conversion failed. + */ + convert(tile, data, from, ...to) { + const conversionPath = this.getConversionPath(from, to); + if (!conversionPath) { + $.console.error(`[OpenSeadragon.converter.convert] Conversion ${from} ---> ${to} cannot be done!`); + return $.Promise.resolve(); + } + + const stepCount = conversionPath.length; + const _this = this; + const step = (x, i, destroy = true) => { + if (i >= stepCount) { + return $.Promise.resolve(x); + } + const edge = conversionPath[i]; + let y; + try { + y = edge.transform(tile, x); + } catch (err) { + if (destroy) { + _this.destroy(x, edge.origin.value); + } + return $.Promise.reject(`[OpenSeadragon.converter.convert] sync failure (while converting using ${edge.origin.value} -> ${edge.target.value})`); + } + if (y === undefined) { + if (destroy) { + _this.destroy(x, edge.origin.value); + } + return $.Promise.reject(`[OpenSeadragon.converter.convert] data mid result undefined value (while converting using ${edge.origin.value} -> ${edge.target.value})`); + } + //node.value holds the type string + if (destroy) { + _this.destroy(x, edge.origin.value); + } + const result = $.type(y) === "promise" ? y : $.Promise.resolve(y); + return result.then(res => step(res, i + 1)); + }; + //destroy only mid-results, but not the original value + return step(data, 0, false); + } + + /** + * Copy the data item given. + * @param {OpenSeadragon.Tile} tile + * @param {any} data data item to convert + * @param {string} type data type + * @return {OpenSeadragon.Promise|undefined} promise resolution with data passed from constructor + */ + copy(tile, data, type) { + const copyTransform = this.copyings[type]; + if (copyTransform) { + const y = copyTransform(tile, data); + return $.type(y) === "promise" ? y : $.Promise.resolve(y); + } + $.console.warn(`[OpenSeadragon.converter.copy] is not supported with type %s`, type); + return $.Promise.resolve(undefined); + } + + /** + * Destroy the data item given. + * @param {string} type data type + * @param {any} data + * @return {OpenSeadragon.Promise|undefined} promise resolution with data passed from constructor, or undefined + * if not such conversion exists + */ + destroy(data, type) { + const destructor = this.destructors[type]; + if (destructor) { + const y = destructor(data); + return $.type(y) === "promise" ? y : $.Promise.resolve(y); + } + return undefined; + } + + /** + * Get possible system type conversions and cache result. + * @param {string} from data item type + * @param {string|string[]} to array of accepted types + * @return {ConversionStep[]|undefined} array of required conversions (returns empty array + * for from===to), or undefined if the system cannot convert between given types. + * Each object has 'transform' function that converts between neighbouring types, such + * that x = arr[i].transform(x) is valid input for converter arr[i+1].transform(), e.g. + * arr[i+1].transform(arr[i].transform( ... )) is a valid conversion procedure. + * + * Note: if a function is returned, it is a callback called once the data is ready. + */ + getConversionPath(from, to) { + let bestConverterPath; + let knownFrom = this._known[from]; + if (!knownFrom) { + this._known[from] = knownFrom = {}; + } + + if (Array.isArray(to)) { + $.console.assert(to.length > 0, "[getConversionPath] conversion 'to' type must be defined."); + let bestCost = Infinity; + + for (const outType of to) { + let conversion = knownFrom[outType]; + if (conversion === undefined) { + knownFrom[outType] = conversion = this.graph.dijkstra(from, outType); + } + if (conversion && bestCost > conversion.cost) { + bestConverterPath = conversion; + bestCost = conversion.cost; + } + } + } else { + $.console.assert(typeof to === "string", "[getConversionPath] conversion 'to' type must be defined."); + bestConverterPath = knownFrom[to]; + if (bestConverterPath === undefined) { + bestConverterPath = this.graph.dijkstra(from, to); + this._known[from][to] = bestConverterPath; + } + } + + return bestConverterPath ? bestConverterPath.path : undefined; + } + + /** + * Get the final type of the conversion path. + * @param {ConversionStep[]} path + * @return {undefined|string} undefined if invalid path + */ + getConversionPathFinalType(path) { + if (!path || !path.length) { + return undefined; + } + return path[path.length - 1].target.value; + } + + /** + * Return a list of known conversion types + * @return {string[]} + */ + getKnownTypes() { + return Object.keys(this.graph.vertices); + } + + /** + * Check whether given type is known to the converter + * @param {string} type type to test + * @return {boolean} + */ + existsType(type) { + return !!this.graph.vertices[type]; + } +}; + +/** + * Static converter available throughout OpenSeadragon. + * + * Built-in conversions include types: + * - context2d canvas 2d context + * - image HTMLImage element + * - url url string carrying or pointing to 2D raster data + * - canvas HTMLCanvas element + * + * @type OpenSeadragon.DataTypeConverter + * @memberOf OpenSeadragon + */ +$.converter = new $.DataTypeConverter(); + +// Image URL -> image private conversion, used in tests (was public originally, but made private to +// discourage bad practices by forcing conversion API to deal with URLs that download data +$.converter.learn("__private__imageUrl", "imageBitmap", (tile, url) => new $.Promise((resolve, reject) => { + if (!$.supportsAsync) { + return reject("Not supported in sync mode!"); + } + let setup; + if (tile.tiledImage && tile.tiledImage.crossOriginPolicy) { + const policy = tile.tiledImage.crossOriginPolicy; + if (policy === 'anonymous') { + setup = { + mode: 'cors', + credentials: 'omit', + }; + } else if (policy === 'use-credentials') { + setup = { + mode: 'cors', + credentials: 'include', + }; + } else if (policy) { + $.console.error(`Unsupported crossOriginPolicy ${policy}. Ignoring the property.`); + } + } + if (_imageConversionWorker) { + return postWorker('fetchDecode', { url, setup }).then(resolve).catch(reject); + } + // Fallback to the main thread + // eslint-disable-next-line compat/compat + return fetch(url, setup).then(res => { + if (!res.ok) { + throw new Error(`HTTP ${res.status} loading ${url}`); + } + return res.blob(); + }).then(blob => createImageBitmap(blob, { colorSpaceConversion: 'none' }) + ).then(resolve).catch(reject); +}), 1, 1); +$.converter.learn("__private__imageUrl", "__private__imageUrl", (tile, url) => url, 0, 1); //strings are immutable, no need to copy +}(OpenSeadragon)); + +/* + * OpenSeadragon - Button + * + * 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 button states + * @member ButtonState + * @memberof OpenSeadragon + * @static + * @type {Object} + * @property {Number} REST + * @property {Number} GROUP + * @property {Number} HOVER + * @property {Number} DOWN + */ +$.ButtonState = { + REST: 0, + GROUP: 1, + HOVER: 2, + DOWN: 3 +}; + +/** + * @class Button + * @classdesc Manages events, hover states for individual buttons, tool-tips, as well + * as fading the buttons out when the user has not interacted with them + * for a specified period. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @param {Object} options + * @param {Element} [options.element=null] Element to use as the button. If not specified, an HTML <div> element is created. + * @param {String} [options.tooltip=null] Provides context help for the button when the + * user hovers over it. + * @param {String} [options.srcRest=null] URL of image to use in 'rest' state. + * @param {String} [options.srcGroup=null] URL of image to use in 'up' state. + * @param {String} [options.srcHover=null] URL of image to use in 'hover' state. + * @param {String} [options.srcDown=null] URL of image to use in 'down' state. + * @param {Number} [options.fadeDelay=0] How long to wait before fading. + * @param {Number} [options.fadeLength=2000] How long should it take to fade the button. + * @param {OpenSeadragon.EventHandler} [options.onPress=null] Event handler callback for {@link OpenSeadragon.Button.event:press}. + * @param {OpenSeadragon.EventHandler} [options.onRelease=null] Event handler callback for {@link OpenSeadragon.Button.event:release}. + * @param {OpenSeadragon.EventHandler} [options.onClick=null] Event handler callback for {@link OpenSeadragon.Button.event:click}. + * @param {OpenSeadragon.EventHandler} [options.onEnter=null] Event handler callback for {@link OpenSeadragon.Button.event:enter}. + * @param {OpenSeadragon.EventHandler} [options.onExit=null] Event handler callback for {@link OpenSeadragon.Button.event:exit}. + * @param {OpenSeadragon.EventHandler} [options.onFocus=null] Event handler callback for {@link OpenSeadragon.Button.event:focus}. + * @param {OpenSeadragon.EventHandler} [options.onBlur=null] Event handler callback for {@link OpenSeadragon.Button.event:blur}. + * @param {Object} [options.userData=null] Arbitrary object to be passed unchanged to any attached handler methods. + */ +$.Button = function( options ) { + + const _this = this; + + $.EventSource.call( this ); + + $.extend( true, this, { + + tooltip: null, + srcRest: null, + srcGroup: null, + srcHover: null, + srcDown: null, + clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, + clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold, + /** + * How long to wait before fading. + * @member {Number} fadeDelay + * @memberof OpenSeadragon.Button# + */ + fadeDelay: 0, + /** + * How long should it take to fade the button. + * @member {Number} fadeLength + * @memberof OpenSeadragon.Button# + */ + fadeLength: 2000, + onPress: null, + onRelease: null, + onClick: null, + onEnter: null, + onExit: null, + onFocus: null, + onBlur: null, + userData: null + + }, options ); + + /** + * The button element. + * @member {Element} element + * @memberof OpenSeadragon.Button# + */ + this.element = options.element || $.makeNeutralElement("div"); + + //if the user has specified the element to bind the control to explicitly + //then do not add the default control images + if ( !options.element ) { + this.imgRest = $.makeTransparentImage( this.srcRest ); + this.imgGroup = $.makeTransparentImage( this.srcGroup ); + this.imgHover = $.makeTransparentImage( this.srcHover ); + this.imgDown = $.makeTransparentImage( this.srcDown ); + + this.imgRest.alt = + this.imgGroup.alt = + this.imgHover.alt = + this.imgDown.alt = + this.tooltip; + + // Allow pointer events to pass through the img elements so implicit + // pointer capture works on touch devices + $.setElementPointerEventsNone( this.imgRest ); + $.setElementPointerEventsNone( this.imgGroup ); + $.setElementPointerEventsNone( this.imgHover ); + $.setElementPointerEventsNone( this.imgDown ); + + this.element.style.position = "relative"; + $.setElementTouchActionNone( this.element ); + + this.imgGroup.style.position = + this.imgHover.style.position = + this.imgDown.style.position = + "absolute"; + + this.imgGroup.style.top = + this.imgHover.style.top = + this.imgDown.style.top = + "0px"; + + this.imgGroup.style.left = + this.imgHover.style.left = + this.imgDown.style.left = + "0px"; + + this.imgHover.style.visibility = + this.imgDown.style.visibility = + "hidden"; + + this.element.appendChild( this.imgRest ); + this.element.appendChild( this.imgGroup ); + this.element.appendChild( this.imgHover ); + this.element.appendChild( this.imgDown ); + } + + + this.addHandler("press", this.onPress); + this.addHandler("release", this.onRelease); + this.addHandler("click", this.onClick); + this.addHandler("enter", this.onEnter); + this.addHandler("exit", this.onExit); + this.addHandler("focus", this.onFocus); + this.addHandler("blur", this.onBlur); + + /** + * The button's current state. + * @member {OpenSeadragon.ButtonState} currentState + * @memberof OpenSeadragon.Button# + */ + this.currentState = $.ButtonState.GROUP; + + // When the button last began to fade. + this.fadeBeginTime = null; + // Whether this button should fade after user stops interacting with the viewport. + this.shouldFade = false; + + this.element.style.display = "inline-block"; + this.element.style.position = "relative"; + this.element.title = this.tooltip; + + /** + * Tracks mouse/touch/key events on the button. + * @member {OpenSeadragon.MouseTracker} tracker + * @memberof OpenSeadragon.Button# + */ + this.tracker = new $.MouseTracker({ + + userData: 'Button.tracker', + element: this.element, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + + enterHandler: function( event ) { + if ( event.insideElementPressed ) { + inTo( _this, $.ButtonState.DOWN ); + /** + * Raised when the cursor enters the Button element. + * + * @event enter + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "enter", { originalEvent: event.originalEvent } ); + } else if ( !event.buttonDownAny ) { + inTo( _this, $.ButtonState.HOVER ); + } + }, + + focusHandler: function ( event ) { + _this.tracker.enterHandler( event ); + /** + * Raised when the Button element receives focus. + * + * @event focus + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "focus", { originalEvent: event.originalEvent } ); + }, + + leaveHandler: function( event ) { + outTo( _this, $.ButtonState.GROUP ); + if ( event.insideElementPressed ) { + /** + * Raised when the cursor leaves the Button element. + * + * @event exit + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "exit", { originalEvent: event.originalEvent } ); + } + }, + + blurHandler: function ( event ) { + _this.tracker.leaveHandler( event ); + /** + * Raised when the Button element loses focus. + * + * @event blur + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "blur", { originalEvent: event.originalEvent } ); + }, + + pressHandler: function ( event ) { + inTo( _this, $.ButtonState.DOWN ); + /** + * Raised when a mouse button is pressed or touch occurs in the Button element. + * + * @event press + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "press", { originalEvent: event.originalEvent } ); + }, + + releaseHandler: function( event ) { + if ( event.insideElementPressed && event.insideElementReleased ) { + outTo( _this, $.ButtonState.HOVER ); + /** + * Raised when the mouse button is released or touch ends in the Button element. + * + * @event release + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "release", { originalEvent: event.originalEvent } ); + } else if ( event.insideElementPressed ) { + outTo( _this, $.ButtonState.GROUP ); + } else { + inTo( _this, $.ButtonState.HOVER ); + } + }, + + clickHandler: function( event ) { + if ( event.quick ) { + /** + * Raised when a mouse button is pressed and released or touch is initiated and ended in the Button element within the time and distance threshold. + * + * @event click + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent("click", { originalEvent: event.originalEvent }); + } + }, + + keyHandler: function( event ){ + //console.log( "%s : handling key %s!", _this.tooltip, event.keyCode); + if( 13 === event.keyCode ){ + /*** + * Raised when a mouse button is pressed and released or touch is initiated and ended in the Button element within the time and distance threshold. + * + * @event click + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "click", { originalEvent: event.originalEvent } ); + /*** + * Raised when the mouse button is released or touch ends in the Button element. + * + * @event release + * @memberof OpenSeadragon.Button + * @type {object} + * @property {OpenSeadragon.Button} eventSource - A reference to the Button which raised the event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( "release", { originalEvent: event.originalEvent } ); + + event.preventDefault = true; + } else{ + event.preventDefault = false; + } + } + + }); + + outTo( this, $.ButtonState.REST ); +}; + +$.extend( $.Button.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.Button.prototype */{ + + /** + * Used by a button container element (e.g. a ButtonGroup) to transition the button state + * to ButtonState.GROUP. + * @function + */ + notifyGroupEnter: function() { + inTo( this, $.ButtonState.GROUP ); + }, + + /** + * Used by a button container element (e.g. a ButtonGroup) to transition the button state + * to ButtonState.REST. + * @function + */ + notifyGroupExit: function() { + outTo( this, $.ButtonState.REST ); + }, + + /** + * @function + */ + disable: function(){ + this.notifyGroupExit(); + this.element.disabled = true; + this.tracker.setTracking(false); + $.setElementOpacity( this.element, 0.2, true ); + }, + + /** + * @function + */ + enable: function(){ + this.element.disabled = false; + this.tracker.setTracking(true); + $.setElementOpacity( this.element, 1.0, true ); + this.notifyGroupEnter(); + }, + + destroy: function() { + if (this.imgRest) { + this.element.removeChild(this.imgRest); + this.imgRest = null; + } + if (this.imgGroup) { + this.element.removeChild(this.imgGroup); + this.imgGroup = null; + } + if (this.imgHover) { + this.element.removeChild(this.imgHover); + this.imgHover = null; + } + if (this.imgDown) { + this.element.removeChild(this.imgDown); + this.imgDown = null; + } + this.removeAllHandlers(); + this.tracker.destroy(); + this.element = null; + } + +}); + + +function scheduleFade( button ) { + $.requestAnimationFrame(function(){ + updateFade( button ); + }); +} + +function updateFade( button ) { + let currentTime, + deltaTime, + opacity; + + if ( button.shouldFade ) { + currentTime = $.now(); + deltaTime = currentTime - button.fadeBeginTime; + opacity = 1.0 - deltaTime / button.fadeLength; + opacity = Math.min( 1.0, opacity ); + opacity = Math.max( 0.0, opacity ); + + if( button.imgGroup ){ + $.setElementOpacity( button.imgGroup, opacity, true ); + } + if ( opacity > 0 ) { + // fade again + scheduleFade( button ); + } + } +} + +function beginFading( button ) { + button.shouldFade = true; + button.fadeBeginTime = $.now() + button.fadeDelay; + window.setTimeout( function(){ + scheduleFade( button ); + }, button.fadeDelay ); +} + +function stopFading( button ) { + button.shouldFade = false; + if( button.imgGroup ){ + $.setElementOpacity( button.imgGroup, 1.0, true ); + } +} + +function inTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + + if ( newState >= $.ButtonState.GROUP && + button.currentState === $.ButtonState.REST ) { + stopFading( button ); + button.currentState = $.ButtonState.GROUP; + } + + if ( newState >= $.ButtonState.HOVER && + button.currentState === $.ButtonState.GROUP ) { + if( button.imgHover ){ + button.imgHover.style.visibility = ""; + } + button.currentState = $.ButtonState.HOVER; + } + + if ( newState >= $.ButtonState.DOWN && + button.currentState === $.ButtonState.HOVER ) { + if( button.imgDown ){ + button.imgDown.style.visibility = ""; + } + button.currentState = $.ButtonState.DOWN; + } +} + + +function outTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + + if ( newState <= $.ButtonState.HOVER && + button.currentState === $.ButtonState.DOWN ) { + if( button.imgDown ){ + button.imgDown.style.visibility = "hidden"; + } + button.currentState = $.ButtonState.HOVER; + } + + if ( newState <= $.ButtonState.GROUP && + button.currentState === $.ButtonState.HOVER ) { + if( button.imgHover ){ + button.imgHover.style.visibility = "hidden"; + } + button.currentState = $.ButtonState.GROUP; + } + + if ( newState <= $.ButtonState.REST && + button.currentState === $.ButtonState.GROUP ) { + beginFading( button ); + button.currentState = $.ButtonState.REST; + } +} + + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - ButtonGroup + * + * 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 ButtonGroup + * @classdesc Manages events on groups of buttons. + * + * @memberof OpenSeadragon + * @param {Object} options - A dictionary of settings applied against the entire group of buttons. + * @param {Array} options.buttons Array of buttons + * @param {Element} [options.element] Element to use as the container + **/ +$.ButtonGroup = function( options ) { + + $.extend( true, this, { + /** + * An array containing the buttons themselves. + * @member {Array} buttons + * @memberof OpenSeadragon.ButtonGroup# + */ + buttons: [], + clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, + clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold, + labelText: "" + }, options ); + + // copy the button elements TODO: Why? + let buttons = this.buttons.concat([]), + _this = this, + i; + + /** + * The shared container for the buttons. + * @member {Element} element + * @memberof OpenSeadragon.ButtonGroup# + */ + this.element = options.element || $.makeNeutralElement( "div" ); + + // TODO What if there IS an options.group specified? + if( !options.group ){ + this.element.style.display = "inline-block"; + //this.label = $.makeNeutralElement( "label" ); + //TODO: support labels for ButtonGroups + //this.label.innerHTML = this.labelText; + //this.element.appendChild( this.label ); + for ( i = 0; i < buttons.length; i++ ) { + this.element.appendChild( buttons[ i ].element ); + } + } + + $.setElementTouchActionNone( this.element ); + + /** + * Tracks mouse/touch/key events across the group of buttons. + * @member {OpenSeadragon.MouseTracker} tracker + * @memberof OpenSeadragon.ButtonGroup# + */ + this.tracker = new $.MouseTracker({ + userData: 'ButtonGroup.tracker', + element: this.element, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + enterHandler: function ( event ) { + let i; + for ( i = 0; i < _this.buttons.length; i++ ) { + _this.buttons[ i ].notifyGroupEnter(); + } + }, + leaveHandler: function ( event ) { + let i; + if ( !event.insideElementPressed ) { + for ( i = 0; i < _this.buttons.length; i++ ) { + _this.buttons[ i ].notifyGroupExit(); + } + } + }, + }); +}; + +/** @lends OpenSeadragon.ButtonGroup.prototype */ +$.ButtonGroup.prototype = { + + /** + * Adds the given button to this button group. + * + * @function + * @param {OpenSeadragon.Button} button + */ + addButton: function( button ){ + this.buttons.push(button); + this.element.appendChild(button.element); + }, + + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @private + */ + emulateEnter: function() { + this.tracker.enterHandler( { eventSource: this.tracker } ); + }, + + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @private + */ + emulateLeave: function() { + this.tracker.leaveHandler( { eventSource: this.tracker } ); + }, + + destroy: function() { + while (this.buttons.length) { + const button = this.buttons.pop(); + this.element.removeChild(button.element); + button.destroy(); + } + this.tracker.destroy(); + this.element = null; + } +}; + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Rect + * + * 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 Rect + * @classdesc A Rectangle is described by it top left coordinates (x, y), width, + * height and degrees of rotation around (x, y). + * Note that the coordinate system used is the one commonly used with images: + * x increases when going to the right + * y increases when going to the bottom + * degrees increases clockwise with 0 being the horizontal + * + * The constructor normalizes the rectangle to always have 0 <= degrees < 90 + * + * @memberof OpenSeadragon + * @param {Number} [x=0] The vector component 'x'. + * @param {Number} [y=0] The vector component 'y'. + * @param {Number} [width=0] The vector component 'width'. + * @param {Number} [height=0] The vector component 'height'. + * @param {Number} [degrees=0] Rotation of the rectangle around (x,y) in degrees. + */ +$.Rect = function(x, y, width, height, degrees) { + /** + * The vector component 'x'. + * @member {Number} x + * @memberof OpenSeadragon.Rect# + */ + this.x = typeof (x) === "number" ? x : 0; + /** + * The vector component 'y'. + * @member {Number} y + * @memberof OpenSeadragon.Rect# + */ + this.y = typeof (y) === "number" ? y : 0; + /** + * The vector component 'width'. + * @member {Number} width + * @memberof OpenSeadragon.Rect# + */ + this.width = typeof (width) === "number" ? width : 0; + /** + * The vector component 'height'. + * @member {Number} height + * @memberof OpenSeadragon.Rect# + */ + this.height = typeof (height) === "number" ? height : 0; + + /** + * The rotation of the rectangle, in degrees. + * @member {Number} degrees + * @memberof OpenSeadragon.Rect# + */ + this.degrees = typeof (degrees) === "number" ? degrees : 0; + + // Normalizes the rectangle. + this.degrees = $.positiveModulo(this.degrees, 360); + let newTopLeft, newWidth; + if (this.degrees >= 270) { + newTopLeft = this.getTopRight(); + this.x = newTopLeft.x; + this.y = newTopLeft.y; + newWidth = this.height; + this.height = this.width; + this.width = newWidth; + this.degrees -= 270; + } else if (this.degrees >= 180) { + newTopLeft = this.getBottomRight(); + this.x = newTopLeft.x; + this.y = newTopLeft.y; + this.degrees -= 180; + } else if (this.degrees >= 90) { + newTopLeft = this.getBottomLeft(); + this.x = newTopLeft.x; + this.y = newTopLeft.y; + newWidth = this.height; + this.height = this.width; + this.width = newWidth; + this.degrees -= 90; + } +}; + +/** + * Builds a rectangle having the 3 specified points as summits. + * @static + * @memberof OpenSeadragon.Rect + * @param {OpenSeadragon.Point} topLeft + * @param {OpenSeadragon.Point} topRight + * @param {OpenSeadragon.Point} bottomLeft + * @returns {OpenSeadragon.Rect} + */ +$.Rect.fromSummits = function(topLeft, topRight, bottomLeft) { + const width = topLeft.distanceTo(topRight); + const height = topLeft.distanceTo(bottomLeft); + const diff = topRight.minus(topLeft); + let radians = Math.atan(diff.y / diff.x); + if (diff.x < 0) { + radians += Math.PI; + } else if (diff.y < 0) { + radians += 2 * Math.PI; + } + return new $.Rect( + topLeft.x, + topLeft.y, + width, + height, + radians / Math.PI * 180); +}; + +/** @lends OpenSeadragon.Rect.prototype */ +$.Rect.prototype = { + /** + * @function + * @returns {OpenSeadragon.Rect} a duplicate of this Rect + */ + clone: function() { + return new $.Rect( + this.x, + this.y, + this.width, + this.height, + this.degrees); + }, + + /** + * The aspect ratio is simply the ratio of width to height. + * @function + * @returns {Number} The ratio of width to height. + */ + getAspectRatio: function() { + return this.width / this.height; + }, + + /** + * Provides the coordinates of the upper-left corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of + * the rectangle. + */ + getTopLeft: function() { + return new $.Point( + this.x, + this.y + ); + }, + + /** + * Provides the coordinates of the bottom-right corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of + * the rectangle. + */ + getBottomRight: function() { + return new $.Point(this.x + this.width, this.y + this.height) + .rotate(this.degrees, this.getTopLeft()); + }, + + /** + * Provides the coordinates of the top-right corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the top-right corner of + * the rectangle. + */ + getTopRight: function() { + return new $.Point(this.x + this.width, this.y) + .rotate(this.degrees, this.getTopLeft()); + }, + + /** + * Provides the coordinates of the bottom-left corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the bottom-left corner of + * the rectangle. + */ + getBottomLeft: function() { + return new $.Point(this.x, this.y + this.height) + .rotate(this.degrees, this.getTopLeft()); + }, + + /** + * Computes the center of the rectangle. + * @function + * @returns {OpenSeadragon.Point} The center of the rectangle as represented + * as represented by a 2-dimensional vector (x,y) + */ + getCenter: function() { + return new $.Point( + this.x + this.width / 2.0, + this.y + this.height / 2.0 + ).rotate(this.degrees, this.getTopLeft()); + }, + + /** + * Returns the width and height component as a vector OpenSeadragon.Point + * @function + * @returns {OpenSeadragon.Point} The 2 dimensional vector representing the + * width and height of the rectangle. + */ + getSize: function() { + return new $.Point(this.width, this.height); + }, + + /** + * Determines if two Rectangles have equivalent components. + * @function + * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to. + * @returns {Boolean} 'true' if all components are equal, otherwise 'false'. + */ + equals: function(other) { + return (other instanceof $.Rect) && + this.x === other.x && + this.y === other.y && + this.width === other.width && + this.height === other.height && + this.degrees === other.degrees; + }, + + /** + * Multiply all dimensions (except degrees) in this Rect by a factor and + * return a new Rect. + * @function + * @param {Number} factor The factor to multiply vector components. + * @returns {OpenSeadragon.Rect} A new rect representing the multiplication + * of the vector components by the factor + */ + times: function(factor) { + return new $.Rect( + this.x * factor, + this.y * factor, + this.width * factor, + this.height * factor, + this.degrees); + }, + + /** + * Translate/move this Rect by a vector and return new Rect. + * @function + * @param {OpenSeadragon.Point} delta The translation vector. + * @returns {OpenSeadragon.Rect} A new rect with altered position + */ + translate: function(delta) { + return new $.Rect( + this.x + delta.x, + this.y + delta.y, + this.width, + this.height, + this.degrees); + }, + + /** + * Returns the smallest rectangle that will contain this and the given + * rectangle bounding boxes. + * @param {OpenSeadragon.Rect} rect + * @returns {OpenSeadragon.Rect} The new rectangle. + */ + union: function(rect) { + const thisBoundingBox = this.getBoundingBox(); + const otherBoundingBox = rect.getBoundingBox(); + + const left = Math.min(thisBoundingBox.x, otherBoundingBox.x); + const top = Math.min(thisBoundingBox.y, otherBoundingBox.y); + const right = Math.max( + thisBoundingBox.x + thisBoundingBox.width, + otherBoundingBox.x + otherBoundingBox.width); + const bottom = Math.max( + thisBoundingBox.y + thisBoundingBox.height, + otherBoundingBox.y + otherBoundingBox.height); + + return new $.Rect( + left, + top, + right - left, + bottom - top); + }, + + /** + * Returns the bounding box of the intersection of this rectangle with the + * given rectangle. + * @param {OpenSeadragon.Rect} rect + * @returns {OpenSeadragon.Rect} the bounding box of the intersection + * or null if the rectangles don't intersect. + */ + intersection: function(rect) { + // Simplified version of Weiler Atherton clipping algorithm + // https://en.wikipedia.org/wiki/Weiler%E2%80%93Atherton_clipping_algorithm + // Because we just want the bounding box of the intersection, + // we can just compute the bounding box of: + // 1. all the summits of this which are inside rect + // 2. all the summits of rect which are inside this + // 3. all the intersections of rect and this + const EPSILON = 0.0000000001; + + const intersectionPoints = []; + + const thisTopLeft = this.getTopLeft(); + if (rect.containsPoint(thisTopLeft, EPSILON)) { + intersectionPoints.push(thisTopLeft); + } + const thisTopRight = this.getTopRight(); + if (rect.containsPoint(thisTopRight, EPSILON)) { + intersectionPoints.push(thisTopRight); + } + const thisBottomLeft = this.getBottomLeft(); + if (rect.containsPoint(thisBottomLeft, EPSILON)) { + intersectionPoints.push(thisBottomLeft); + } + const thisBottomRight = this.getBottomRight(); + if (rect.containsPoint(thisBottomRight, EPSILON)) { + intersectionPoints.push(thisBottomRight); + } + + const rectTopLeft = rect.getTopLeft(); + if (this.containsPoint(rectTopLeft, EPSILON)) { + intersectionPoints.push(rectTopLeft); + } + const rectTopRight = rect.getTopRight(); + if (this.containsPoint(rectTopRight, EPSILON)) { + intersectionPoints.push(rectTopRight); + } + const rectBottomLeft = rect.getBottomLeft(); + if (this.containsPoint(rectBottomLeft, EPSILON)) { + intersectionPoints.push(rectBottomLeft); + } + const rectBottomRight = rect.getBottomRight(); + if (this.containsPoint(rectBottomRight, EPSILON)) { + intersectionPoints.push(rectBottomRight); + } + + const thisSegments = this._getSegments(); + const rectSegments = rect._getSegments(); + for (let i = 0; i < thisSegments.length; i++) { + const thisSegment = thisSegments[i]; + for (let j = 0; j < rectSegments.length; j++) { + const rectSegment = rectSegments[j]; + const intersect = getIntersection(thisSegment[0], thisSegment[1], + rectSegment[0], rectSegment[1]); + if (intersect) { + intersectionPoints.push(intersect); + } + } + } + + // Get intersection point of segments [a,b] and [c,d] + function getIntersection(a, b, c, d) { + // http://stackoverflow.com/a/1968345/1440403 + const abVector = b.minus(a); + const cdVector = d.minus(c); + + const denom = -cdVector.x * abVector.y + abVector.x * cdVector.y; + if (denom === 0) { + return null; + } + + const s = (abVector.x * (a.y - c.y) - abVector.y * (a.x - c.x)) / denom; + const t = (cdVector.x * (a.y - c.y) - cdVector.y * (a.x - c.x)) / denom; + + if (-EPSILON <= s && s <= 1 - EPSILON && + -EPSILON <= t && t <= 1 - EPSILON) { + return new $.Point(a.x + t * abVector.x, a.y + t * abVector.y); + } + return null; + } + + if (intersectionPoints.length === 0) { + return null; + } + + let minX = intersectionPoints[0].x; + let maxX = intersectionPoints[0].x; + let minY = intersectionPoints[0].y; + let maxY = intersectionPoints[0].y; + for (let k = 1; k < intersectionPoints.length; k++) { + const point = intersectionPoints[k]; + if (point.x < minX) { + minX = point.x; + } + if (point.x > maxX) { + maxX = point.x; + } + if (point.y < minY) { + minY = point.y; + } + if (point.y > maxY) { + maxY = point.y; + } + } + return new $.Rect(minX, minY, maxX - minX, maxY - minY); + }, + + // private + _getSegments: function() { + const topLeft = this.getTopLeft(); + const topRight = this.getTopRight(); + const bottomLeft = this.getBottomLeft(); + const bottomRight = this.getBottomRight(); + return [[topLeft, topRight], + [topRight, bottomRight], + [bottomRight, bottomLeft], + [bottomLeft, topLeft]]; + }, + + /** + * Rotates a rectangle around a point. + * @function + * @param {Number} degrees The angle in degrees to rotate. + * @param {OpenSeadragon.Point} [pivot] The point about which to rotate. + * Defaults to the center of the rectangle. + * @returns {OpenSeadragon.Rect} + */ + rotate: function(degrees, pivot) { + degrees = $.positiveModulo(degrees, 360); + if (degrees === 0) { + return this.clone(); + } + + pivot = pivot || this.getCenter(); + const newTopLeft = this.getTopLeft().rotate(degrees, pivot); + const newTopRight = this.getTopRight().rotate(degrees, pivot); + + let diff = newTopRight.minus(newTopLeft); + // Handle floating point error + diff = diff.apply(function(x) { + const EPSILON = 1e-15; + return Math.abs(x) < EPSILON ? 0 : x; + }); + let radians = Math.atan(diff.y / diff.x); + if (diff.x < 0) { + radians += Math.PI; + } else if (diff.y < 0) { + radians += 2 * Math.PI; + } + return new $.Rect( + newTopLeft.x, + newTopLeft.y, + this.width, + this.height, + radians / Math.PI * 180); + }, + + /** + * Retrieves the smallest horizontal (degrees=0) rectangle which contains + * this rectangle. + * @returns {OpenSeadragon.Rect} + */ + getBoundingBox: function() { + if (this.degrees === 0) { + return this.clone(); + } + const topLeft = this.getTopLeft(); + const topRight = this.getTopRight(); + const bottomLeft = this.getBottomLeft(); + const bottomRight = this.getBottomRight(); + const minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); + const maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); + const minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); + const maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); + return new $.Rect( + minX, + minY, + maxX - minX, + maxY - minY); + }, + + /** + * Retrieves the smallest horizontal (degrees=0) rectangle which contains + * this rectangle and has integers x, y, width and height + * @returns {OpenSeadragon.Rect} + */ + getIntegerBoundingBox: function() { + const boundingBox = this.getBoundingBox(); + const x = Math.floor(boundingBox.x); + const y = Math.floor(boundingBox.y); + const width = Math.ceil(boundingBox.width + boundingBox.x - x); + const height = Math.ceil(boundingBox.height + boundingBox.y - y); + return new $.Rect(x, y, width, height); + }, + + /** + * Determines whether a point is inside this rectangle (edge included). + * @function + * @param {OpenSeadragon.Point} point + * @param {Number} [epsilon=0] the margin of error allowed + * @returns {Boolean} true if the point is inside this rectangle, false + * otherwise. + */ + containsPoint: function(point, epsilon) { + epsilon = epsilon || 0; + + // See http://stackoverflow.com/a/2752754/1440403 for explanation + const topLeft = this.getTopLeft(); + const topRight = this.getTopRight(); + const bottomLeft = this.getBottomLeft(); + const topDiff = topRight.minus(topLeft); + const leftDiff = bottomLeft.minus(topLeft); + + return ((point.x - topLeft.x) * topDiff.x + + (point.y - topLeft.y) * topDiff.y >= -epsilon) && + + ((point.x - topRight.x) * topDiff.x + + (point.y - topRight.y) * topDiff.y <= epsilon) && + + ((point.x - topLeft.x) * leftDiff.x + + (point.y - topLeft.y) * leftDiff.y >= -epsilon) && + + ((point.x - bottomLeft.x) * leftDiff.x + + (point.y - bottomLeft.y) * leftDiff.y <= epsilon); + }, + + /** + * Provides a string representation of the rectangle which is useful for + * debugging. + * @function + * @returns {String} A string representation of the rectangle. + */ + toString: function() { + return "[" + + (Math.round(this.x * 100) / 100) + ", " + + (Math.round(this.y * 100) / 100) + ", " + + (Math.round(this.width * 100) / 100) + "x" + + (Math.round(this.height * 100) / 100) + ", " + + (Math.round(this.degrees * 100) / 100) + "deg" + + "]"; + } +}; + + +}(OpenSeadragon)); + +/* + * OpenSeadragon - ReferenceStrip + * + * 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 id to private properties +const THIS = {}; + +/** + * The CollectionDrawer is a reimplementation if the Drawer API that + * focuses on allowing a viewport to be redefined as a collection + * of smaller viewports, defined by a clear number of rows and / or + * columns of which each item in the matrix of viewports has its own + * source. + * + * This idea is a reexpression of the idea of dzi collections + * which allows a clearer algorithm to reuse the tile sources already + * supported by OpenSeadragon, in heterogeneous or homogeneous + * sequences just like mixed groups already supported by the viewer + * for the purpose of image sequnces. + * + * TODO: The difficult part of this feature is figuring out how to express + * this functionality as a combination of the functionality already + * provided by Drawer, Viewport, TileSource, and Navigator. It may + * require better abstraction at those points in order to efficiently + * reuse those paradigms. + */ +/** + * @class ReferenceStrip + * @memberof OpenSeadragon + * @param {Object} options + */ +$.ReferenceStrip = function ( options ) { + + const _this = this; + const viewer = options.viewer; + const viewerSize = $.getElementSize( viewer.element ); + let element; + let i; + + //We may need to create a new element and id if they did not + //provide the id for the existing element + if ( !options.id ) { + options.id = 'referencestrip-' + $.now(); + this.element = $.makeNeutralElement( "div" ); + this.element.id = options.id; + this.element.className = 'referencestrip'; + } + + options = $.extend( true, { + sizeRatio: $.DEFAULT_SETTINGS.referenceStripSizeRatio, + position: $.DEFAULT_SETTINGS.referenceStripPosition, + scroll: $.DEFAULT_SETTINGS.referenceStripScroll, + clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold + }, options, { + element: this.element + } ); + + $.extend( this, options ); + //Private state properties + THIS[this.id] = { + animating: false + }; + + this.minPixelRatio = this.viewer.minPixelRatio; + + this.element.tabIndex = 0; + + const style = this.element.style; + style.marginTop = '0px'; + style.marginRight = '0px'; + style.marginBottom = '0px'; + style.marginLeft = '0px'; + style.left = '0px'; + style.bottom = '0px'; + style.border = '0px'; + style.background = '#000'; + style.position = 'relative'; + + $.setElementTouchActionNone( this.element ); + + $.setElementOpacity( this.element, 0.8 ); + + this.viewer = viewer; + this.tracker = new $.MouseTracker( { + userData: 'ReferenceStrip.tracker', + element: this.element, + clickHandler: $.delegate( this, onStripClick ), + dragHandler: $.delegate( this, onStripDrag ), + scrollHandler: $.delegate( this, onStripScroll ), + enterHandler: $.delegate( this, onStripEnter ), + leaveHandler: $.delegate( this, onStripLeave ), + keyDownHandler: $.delegate( this, onKeyDown ), + keyHandler: $.delegate( this, onKeyPress ), + preProcessEventHandler: function (eventInfo) { + if (eventInfo.eventType === 'wheel') { + eventInfo.preventDefault = true; + } + } + } ); + + //Controls the position and orientation of the reference strip and sets the + //appropriate width and height + if ( options.width && options.height ) { + this.element.style.width = options.width + 'px'; + this.element.style.height = options.height + 'px'; + viewer.addControl( + this.element, + { anchor: $.ControlAnchor.BOTTOM_LEFT } + ); + } else { + if ( "horizontal" === options.scroll ) { + this.element.style.width = ( + viewerSize.x * + options.sizeRatio * + viewer.tileSources.length + ) + ( 12 * viewer.tileSources.length ) + 'px'; + + this.element.style.height = ( + viewerSize.y * + options.sizeRatio + ) + 'px'; + + viewer.addControl( + this.element, + { anchor: $.ControlAnchor.BOTTOM_LEFT } + ); + } else { + this.element.style.height = ( + viewerSize.y * + options.sizeRatio * + viewer.tileSources.length + ) + ( 12 * viewer.tileSources.length ) + 'px'; + + this.element.style.width = ( + viewerSize.x * + options.sizeRatio + ) + 'px'; + + viewer.addControl( + this.element, + { anchor: $.ControlAnchor.TOP_LEFT } + ); + + } + } + + this.panelWidth = ( viewerSize.x * this.sizeRatio ) + 8; + this.panelHeight = ( viewerSize.y * this.sizeRatio ) + 8; + this.panels = []; + this.miniViewers = {}; + + /*jshint loopfunc:true*/ + for ( i = 0; i < viewer.tileSources.length; i++ ) { + + element = $.makeNeutralElement( 'div' ); + element.id = this.element.id + "-" + i; + + element.style.width = _this.panelWidth + 'px'; + element.style.height = _this.panelHeight + 'px'; + element.style.display = 'inline'; + element.style['float'] = 'left'; //Webkit + element.style.cssFloat = 'left'; //Firefox + element.style.padding = '2px'; + $.setElementTouchActionNone( element ); + $.setElementPointerEventsNone( element ); + + this.element.appendChild( element ); + + element.activePanel = false; + + this.panels.push( element ); + + } + loadPanels( this, this.scroll === 'vertical' ? viewerSize.y : viewerSize.x, 0 ); + this.setFocus( 0 ); + +}; + +/** @lends OpenSeadragon.ReferenceStrip.prototype */ +$.ReferenceStrip.prototype = { + + /** + * @function + */ + setFocus: function ( page ) { + const element = this.element.querySelector('#' + this.element.id + '-' + page ); + const viewerSize = $.getElementSize( this.viewer.canvas ); + const scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ); + const scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ); + const offsetLeft = -Number( this.element.style.marginLeft.replace( 'px', '' ) ); + const offsetTop = -Number( this.element.style.marginTop.replace( 'px', '' ) ); + let offset; + + if ( this.currentSelected !== element ) { + if ( this.currentSelected ) { + this.currentSelected.style.background = '#000'; + } + this.currentSelected = element; + this.currentSelected.style.background = '#999'; + + if ( 'horizontal' === this.scroll ) { + //right left + offset = ( Number( page ) ) * ( this.panelWidth + 3 ); + if ( offset > offsetLeft + viewerSize.x - this.panelWidth ) { + offset = Math.min( offset, ( scrollWidth - viewerSize.x ) ); + this.element.style.marginLeft = -offset + 'px'; + loadPanels( this, viewerSize.x, -offset ); + } else if ( offset < offsetLeft ) { + offset = Math.max( 0, offset - viewerSize.x / 2 ); + this.element.style.marginLeft = -offset + 'px'; + loadPanels( this, viewerSize.x, -offset ); + } + } else { + offset = ( Number( page ) ) * ( this.panelHeight + 3 ); + if ( offset > offsetTop + viewerSize.y - this.panelHeight ) { + offset = Math.min( offset, ( scrollHeight - viewerSize.y ) ); + this.element.style.marginTop = -offset + 'px'; + loadPanels( this, viewerSize.y, -offset ); + } else if ( offset < offsetTop ) { + offset = Math.max( 0, offset - viewerSize.y / 2 ); + this.element.style.marginTop = -offset + 'px'; + loadPanels( this, viewerSize.y, -offset ); + } + } + + this.currentPage = page; + onStripEnter.call( this, { eventSource: this.tracker } ); + } + }, + + /** + * @function + */ + update: function () { + if ( THIS[this.id].animating ) { + // $.console.log( 'image reference strip update' ); + return true; + } + return false; + }, + + destroy: function() { + if (this.miniViewers) { + for (const key in this.miniViewers) { + this.miniViewers[key].destroy(); + } + } + + this.tracker.destroy(); + + if (this.element) { + this.viewer.removeControl( this.element ); + } + } + +}; + + +/** + * @private + * @inner + * @function + */ +function onStripClick( event ) { + if ( event.quick ) { + let page; + + if ( 'horizontal' === this.scroll ) { + // +4px fix to solve problem with precision on thumbnail selection if there is a lot of them + page = Math.floor(event.position.x / (this.panelWidth + 4)); + } else { + page = Math.floor(event.position.y / this.panelHeight); + } + + this.viewer.goToPage( page ); + } + + this.element.focus(); +} + + +/** + * @private + * @inner + * @function + */ +function onStripDrag( event ) { + + this.dragging = true; + if ( this.element ) { + const offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ); + const offsetTop = Number( this.element.style.marginTop.replace( 'px', '' ) ); + const scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ); + const scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ); + const viewerSize = $.getElementSize( this.viewer.canvas ); + + if ( 'horizontal' === this.scroll ) { + if ( -event.delta.x > 0 ) { + //forward + if ( offsetLeft > -( scrollWidth - viewerSize.x ) ) { + this.element.style.marginLeft = ( offsetLeft + ( event.delta.x * 2 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft + ( event.delta.x * 2 ) ); + } + } else if ( -event.delta.x < 0 ) { + //reverse + if ( offsetLeft < 0 ) { + this.element.style.marginLeft = ( offsetLeft + ( event.delta.x * 2 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft + ( event.delta.x * 2 ) ); + } + } + } else { + if ( -event.delta.y > 0 ) { + //forward + if ( offsetTop > -( scrollHeight - viewerSize.y ) ) { + this.element.style.marginTop = ( offsetTop + ( event.delta.y * 2 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.delta.y * 2 ) ); + } + } else if ( -event.delta.y < 0 ) { + //reverse + if ( offsetTop < 0 ) { + this.element.style.marginTop = ( offsetTop + ( event.delta.y * 2 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.delta.y * 2 ) ); + } + } + } + } + +} + + + +/** + * @private + * @inner + * @function + */ +function onStripScroll( event ) { + if ( this.element ) { + const offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ); + const offsetTop = Number( this.element.style.marginTop.replace( 'px', '' ) ); + const scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ); + const scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ); + const viewerSize = $.getElementSize( this.viewer.canvas ); + + if ( 'horizontal' === this.scroll ) { + if ( event.scroll > 0 ) { + //forward + if ( offsetLeft > -( scrollWidth - viewerSize.x ) ) { + this.element.style.marginLeft = ( offsetLeft - ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft - ( event.scroll * 60 ) ); + } + } else if ( event.scroll < 0 ) { + //reverse + if ( offsetLeft < 0 ) { + this.element.style.marginLeft = ( offsetLeft - ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft - ( event.scroll * 60 ) ); + } + } + } else { + if ( event.scroll < 0 ) { + //scroll up + if ( offsetTop > viewerSize.y - scrollHeight ) { + this.element.style.marginTop = ( offsetTop + ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.scroll * 60 ) ); + } + } else if ( event.scroll > 0 ) { + //scroll dowm + if ( offsetTop < 0 ) { + this.element.style.marginTop = ( offsetTop + ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.scroll * 60 ) ); + } + } + } + + event.preventDefault = true; + } +} + + +function loadPanels( strip, viewerSize, scroll ) { + let panelSize; + let activePanelsStart; + let activePanelsEnd; + let miniViewer; + let i; + let element; + + if ( 'horizontal' === strip.scroll ) { + panelSize = strip.panelWidth; + } else { + panelSize = strip.panelHeight; + } + activePanelsStart = Math.ceil( viewerSize / panelSize ) + 5; + activePanelsEnd = Math.ceil( ( Math.abs( scroll ) + viewerSize ) / panelSize ) + 1; + activePanelsStart = activePanelsEnd - activePanelsStart; + activePanelsStart = activePanelsStart < 0 ? 0 : activePanelsStart; + + for ( i = activePanelsStart; i < activePanelsEnd && i < strip.panels.length; i++ ) { + element = strip.panels[i]; + if ( !element.activePanel ) { + let miniTileSource; + const originalTileSource = strip.viewer.tileSources[i]; + if (originalTileSource.referenceStripThumbnailUrl) { + miniTileSource = { + type: 'image', + url: originalTileSource.referenceStripThumbnailUrl + }; + } else { + miniTileSource = originalTileSource; + } + miniViewer = new $.Viewer( { + id: element.id, + tileSources: [miniTileSource], + element: element, + navigatorSizeRatio: strip.sizeRatio, + showNavigator: false, + mouseNavEnabled: false, + showNavigationControl: false, + showSequenceControl: false, + immediateRender: true, + blendTime: 0, + animationTime: 0, + loadTilesWithAjax: strip.viewer.loadTilesWithAjax, + ajaxHeaders: strip.viewer.ajaxHeaders, + viewer: strip.viewer, + // TODO: make possible for users to ensure the sub-drawer is the same type as the base parent drawer + drawer: 'canvas', //always use canvas for the reference strip + } ); + // Allow pointer events to pass through miniViewer's canvas/container + // elements so implicit pointer capture works on touch devices + $.setElementPointerEventsNone( miniViewer.canvas ); + $.setElementPointerEventsNone( miniViewer.container ); + // We'll use event delegation from the reference strip element instead of + // handling events on every miniViewer + miniViewer.innerTracker.setTracking( false ); + miniViewer.outerTracker.setTracking( false ); + + strip.miniViewers[element.id] = miniViewer; + + element.activePanel = true; + } + } +} + + +/** + * @private + * @inner + * @function + */ +function onStripEnter( event ) { + const element = event.eventSource.element; + + //$.setElementOpacity(element, 0.8); + + //element.style.border = '1px solid #555'; + //element.style.background = '#000'; + + if ( 'horizontal' === this.scroll ) { + + //element.style.paddingTop = "0px"; + element.style.marginBottom = "0px"; + + } else { + + //element.style.paddingRight = "0px"; + element.style.marginLeft = "0px"; + + } +} + + +/** + * @private + * @inner + * @function + */ +function onStripLeave( event ) { + const element = event.eventSource.element; + + if ( 'horizontal' === this.scroll ) { + + //element.style.paddingTop = "10px"; + element.style.marginBottom = "-" + ( $.getElementSize( element ).y / 2 ) + "px"; + + } else { + + //element.style.paddingRight = "10px"; + element.style.marginLeft = "-" + ( $.getElementSize( element ).x / 2 ) + "px"; + + } +} + + +/** + * @private + * @inner + * @function + */ +function onKeyDown( event ) { + //console.log( event.keyCode ); + + if ( !event.ctrl && !event.alt && !event.meta ) { + switch ( event.keyCode ) { + case 38: //up arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + event.preventDefault = true; + break; + case 40: //down arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + event.preventDefault = true; + break; + case 37: //left arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + event.preventDefault = true; + break; + case 39: //right arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + event.preventDefault = true; + break; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + event.preventDefault = false; + break; + } + } else { + event.preventDefault = false; + } +} + + +/** + * @private + * @inner + * @function + */ +function onKeyPress( event ) { + //console.log( event.keyCode ); + + if ( !event.ctrl && !event.alt && !event.meta ) { + switch ( event.keyCode ) { + case 61: //=|+ + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + event.preventDefault = true; + break; + case 45: //-|_ + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + event.preventDefault = true; + break; + case 48: //0|) + default: + //console.log( 'navigator keycode %s', event.keyCode ); + event.preventDefault = false; + break; + } + } else { + event.preventDefault = false; + } +} + +}(OpenSeadragon)); + +/* + * OpenSeadragon - DisplayRect + * + * 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 DisplayRect + * @classdesc A display rectangle is very similar to {@link OpenSeadragon.Rect} but adds two + * fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels + * for this rectangle. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.Rect + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {Number} width The vector component 'height'. + * @param {Number} height The vector component 'width'. + * @param {Number} minLevel The lowest zoom level supported. + * @param {Number} maxLevel The highest zoom level supported. + */ +$.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) { + $.Rect.apply( this, [ x, y, width, height ] ); + + /** + * The lowest zoom level supported. + * @member {Number} minLevel + * @memberof OpenSeadragon.DisplayRect# + */ + this.minLevel = minLevel; + /** + * The highest zoom level supported. + * @member {Number} maxLevel + * @memberof OpenSeadragon.DisplayRect# + */ + this.maxLevel = maxLevel; +}; + +$.extend( $.DisplayRect.prototype, $.Rect.prototype ); + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Spring + * + * 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 Spring + * @memberof OpenSeadragon + * @param {Object} options - Spring configuration settings. + * @param {Number} options.springStiffness - Spring stiffness. Must be greater than zero. + * The closer to zero, the closer to linear animation. + * @param {Number} options.animationTime - Animation duration per spring, in seconds. + * Must be zero or greater. + * @param {Number} [options.initial=0] - Initial value of spring. + * @param {Boolean} [options.exponential=false] - Whether this spring represents + * an exponential scale (such as zoom) and should be animated accordingly. Note that + * exponential springs must have non-zero values. + */ +$.Spring = function( options ) { + const args = arguments; + + if( typeof ( options ) !== 'object' ){ + //allows backward compatible use of ( initialValue, config ) as + //constructor parameters + options = { + initial: args.length && typeof ( args[ 0 ] ) === "number" ? + args[ 0 ] : + undefined, + /** + * Spring stiffness. + * @member {Number} springStiffness + * @memberof OpenSeadragon.Spring# + */ + springStiffness: args.length > 1 ? + args[ 1 ].springStiffness : + 5.0, + /** + * Animation duration per spring. + * @member {Number} animationTime + * @memberof OpenSeadragon.Spring# + */ + animationTime: args.length > 1 ? + args[ 1 ].animationTime : + 1.5 + }; + } + + $.console.assert(typeof options.springStiffness === "number" && options.springStiffness !== 0, + "[OpenSeadragon.Spring] options.springStiffness must be a non-zero number"); + + $.console.assert(typeof options.animationTime === "number" && options.animationTime >= 0, + "[OpenSeadragon.Spring] options.animationTime must be a number greater than or equal to 0"); + + if (options.exponential) { + this._exponential = true; + delete options.exponential; + } + + $.extend( true, this, options); + + /** + * @member {Object} current + * @memberof OpenSeadragon.Spring# + * @property {Number} value + * @property {Number} time + */ + this.current = { + value: typeof ( this.initial ) === "number" ? + this.initial : + (this._exponential ? 0 : 1), + time: $.now() // always work in milliseconds + }; + + $.console.assert(!this._exponential || this.current.value !== 0, + "[OpenSeadragon.Spring] value must be non-zero for exponential springs"); + + /** + * @member {Object} start + * @memberof OpenSeadragon.Spring# + * @property {Number} value + * @property {Number} time + */ + this.start = { + value: this.current.value, + time: this.current.time + }; + + /** + * @member {Object} target + * @memberof OpenSeadragon.Spring# + * @property {Number} value + * @property {Number} time + */ + this.target = { + value: this.current.value, + time: this.current.time + }; + + if (this._exponential) { + this.start._logValue = Math.log(this.start.value); + this.target._logValue = Math.log(this.target.value); + this.current._logValue = Math.log(this.current.value); + } +}; + +/** @lends OpenSeadragon.Spring.prototype */ +$.Spring.prototype = { + + /** + * @function + * @param {Number} target + */ + resetTo: function( target ) { + $.console.assert(!this._exponential || target !== 0, + "[OpenSeadragon.Spring.resetTo] target must be non-zero for exponential springs"); + + this.start.value = this.target.value = this.current.value = target; + this.start.time = this.target.time = this.current.time = $.now(); + + if (this._exponential) { + this.start._logValue = Math.log(this.start.value); + this.target._logValue = Math.log(this.target.value); + this.current._logValue = Math.log(this.current.value); + } + }, + + /** + * @function + * @param {Number} target + */ + springTo: function( target ) { + $.console.assert(!this._exponential || target !== 0, + "[OpenSeadragon.Spring.springTo] target must be non-zero for exponential springs"); + + this.start.value = this.current.value; + this.start.time = this.current.time; + this.target.value = target; + this.target.time = this.start.time + 1000 * this.animationTime; + + if (this._exponential) { + this.start._logValue = Math.log(this.start.value); + this.target._logValue = Math.log(this.target.value); + } + }, + + /** + * @function + * @param {Number} delta + */ + shiftBy: function( delta ) { + this.start.value += delta; + this.target.value += delta; + + if (this._exponential) { + $.console.assert(this.target.value !== 0 && this.start.value !== 0, + "[OpenSeadragon.Spring.shiftBy] spring value must be non-zero for exponential springs"); + + this.start._logValue = Math.log(this.start.value); + this.target._logValue = Math.log(this.target.value); + } + }, + + setExponential: function(value) { + this._exponential = value; + + if (this._exponential) { + $.console.assert(this.current.value !== 0 && this.target.value !== 0 && this.start.value !== 0, + "[OpenSeadragon.Spring.setExponential] spring value must be non-zero for exponential springs"); + + this.start._logValue = Math.log(this.start.value); + this.target._logValue = Math.log(this.target.value); + this.current._logValue = Math.log(this.current.value); + } + }, + + /** + * @function + * @returns true if the spring is still updating its value, false if it is + * already at the target value. + */ + update: function() { + this.current.time = $.now(); + + let startValue, targetValue; + if (this._exponential) { + startValue = this.start._logValue; + targetValue = this.target._logValue; + } else { + startValue = this.start.value; + targetValue = this.target.value; + } + + if(this.current.time >= this.target.time){ + this.current.value = this.target.value; + } else { + let currentValue = startValue + + ( targetValue - startValue ) * + transform( + this.springStiffness, + ( this.current.time - this.start.time ) / + ( this.target.time - this.start.time ) + ); + + if (this._exponential) { + this.current.value = Math.exp(currentValue); + } else { + this.current.value = currentValue; + } + } + + return this.current.value !== this.target.value; + }, + + /** + * Returns whether the spring is at the target value + * @function + * @returns {Boolean} True if at target value, false otherwise + */ + isAtTargetValue: function() { + return this.current.value === this.target.value; + } +}; + +/** + * @private + */ +function transform( stiffness, x ) { + return ( 1.0 - Math.exp( stiffness * -x ) ) / + ( 1.0 - Math.exp( -stiffness ) ); +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - ImageLoader + * + * 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 ImageJob + * @classdesc Handles downloading of a single image. + * + * @memberof OpenSeadragon + * @param {Object} options - Options for this ImageJob. + * @param {String} [options.src] - URL of image to download. + * @param {Tile} [options.tile] - Tile that belongs the data to. + * @param {TileSource} [options.source] - Image loading strategy + * @param {String} [options.loadWithAjax] - Whether to load this image with AJAX. + * @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX. + * @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX requests. + * @param {String} [options.crossOriginPolicy] - CORS policy to use for downloads + * @param {String} [options.postData] - HTTP POST data (usually but not necessarily in k=v&k2=v2... form, + * see TileSource::getTilePostData) or null + * @param {Function} [options.callback] - Called once image has been downloaded. + * @param {Function} [options.abort] - Called when this image job is aborted. + * @param {Number} [options.timeout] - The max number of milliseconds that this image job may take to complete. + * @param {Number} [options.tries] - Actual number of the current try. + */ +$.ImageJob = function(options) { + + /** + * Private parameter. Called automatically once image has been downloaded + * (triggered by finish). + * @member {function} callback + * @memberof OpenSeadragon.ImageJob# + * @private + */ + + /** + * URL of image (or other data item that will be rendered) to download. + * @member {string} src + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * Tile that owns the load. Note the data might be shared between tiles. + * @member {OpenSeadragon.Tile} tile + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * TileSource that initiated the load and owns the tile. Note the data might be shared between tiles and tile sources. + * @member {OpenSeadragon.TileSource} source + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * Whether to load this image with AJAX. + * @member {boolean} loadWithAjax + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * Headers to add to the image request if using AJAX. + * @member {Object.} ajaxHeaders + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * Whether to set withCredentials on AJAX requests. + * @member {boolean} ajaxWithCredentials + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * CORS policy to use for downloads + * @member {String} crossOriginPolicy + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * HTTP POST data to send with the request + * @member {(String|Object)} [postData] - HTTP POST data (usually but not necessarily + * in k=v&k2=v2... form, see TileSource::getTilePostData) or null + * @memberof OpenSeadragon.ImageJob# + */ + + /** + * Data object which will contain downloaded image data. + * @member {Image|*} data data object, by default an Image object (depends on TileSource) + * @memberof OpenSeadragon.ImageJob# + */ + this.data = null; + + /** + * User workspace to populate with helper variables + * @member {*} userData to append custom data and avoid namespace collision + * @memberof OpenSeadragon.ImageJob# + */ + this.userData = {}; + + /** + * Error message holder. The final error message, default null (set by finish). + * @member {string} error message + * @memberof OpenSeadragon.ImageJob# + * @private + */ + this.errorMsg = null; + + /** + * Private parameter. The max number of milliseconds that + * this image job may take to complete. + * @member {number} timeout + * @memberof OpenSeadragon.ImageJob# + * @private + */ + this.timeout = $.DEFAULT_SETTINGS.timeout; + + /** + * Flag if part of batch query. + * @member {boolean} isBatched + * @memberof OpenSeadragon.ImageJob# + * @private + */ + this.isBatched = false; + + + $.extend(true, this, { + jobId: null, + tries: 0, + }, options); +}; + +$.ImageJob.prototype = { + /** + * Starts the image job. + * @method + * @private + * @memberof OpenSeadragon.ImageJob# + */ + start: function() { + this.tries++; + + const self = this; + const selfAbort = this.abort; + + this.jobId = window.setTimeout(function () { + self.fail("Image load exceeded timeout (" + self.timeout + " ms)", null); + }, this.timeout); + + /** + * Called automatically when the job times out. + * Usage: if you decide to abort the request (no fail/finish will be called), call context.abort(). + * @member {function} abort + * @memberof OpenSeadragon.ImageJob# + */ + this.abort = function() { + // this should call finish or fail + self.source.downloadTileAbort(self); + if (typeof selfAbort === "function") { + selfAbort(); + } + self.fail("Image load aborted.", null); + }; + + this.source.downloadTileStart(this); + }, + + /** + * Prepares the image job to be part of batched mode. It does not override abort + * callback and does not set timeout, nor call any tile source APIs. Managed by parent batch. + * @method + * @private + * @memberof OpenSeadragon.ImageJob# + */ + prepareForBatch: function() { + this.tries++; + this.jobId = -1; // ensures methods above work, calling clearTimeout is noop + }, + + /** + * Finish this job. Should be called unless abort() was executed upon successful data retrieval. + * Usage: context.finish(data, request, dataType=undefined). Pass the downloaded data object + * add also reference to an ajax request if used. Optionally, specify what data type the data is. + * @param {*} data data that has been downloaded + * @param {XMLHttpRequest} request reference to the request if used + * @param {string} dataType data type identifier + * fallback compatibility behavior: dataType treated as errorMessage if data is falsey value + * @memberof OpenSeadragon.ImageJob# + */ + finish: function(data, request, dataType) { + if (!this.jobId) { + return; + } + // old behavior, no deprecation due to possible finish calls with invalid data item (e.g. different error) + if (isInvalidData(data)) { + this.fail(dataType || "[downloadTileStart->finish()] Retrieved data is invalid!", request); + return; + } + + this.data = data; + this.request = request; + this.errorMsg = null; + this.dataType = dataType; + + window.clearTimeout(this.jobId); + this.jobId = null; + + this.callback(this); + }, + + /** + * Finish this job as a failure. Should be called unless abort() was executed upon unsuccessful request. + * Usage: context.fail(errMessage, request). Provide error message in case of failure, + * add also reference to an ajax request if used. + * @param {string} errorMessage description upon failure + * @param {XMLHttpRequest} request reference to the request if used + */ + fail: function(errorMessage, request) { + this.data = null; + this.request = request; + this.errorMsg = errorMessage; + this.dataType = null; + + if (this.jobId) { + window.clearTimeout(this.jobId); + this.jobId = null; + } + + this.callback(this); + } +}; + +/** + * @class BatchImageJob + * @memberof OpenSeadragon + * @classdesc Wraps a group of ImageJobs as a single unit of work for the ImageLoader queue. + * It mimics the ImageJob API so it can be managed in a similar way. + * @param {Object} options + * @param {TileSource} options.source + * @param {Array} options.jobs + * @param {Function} [options.callback] + * @param {Function} [options.abort] + */ +$.BatchImageJob = function(options) { + $.extend(true, this, { + timeout: $.DEFAULT_SETTINGS.timeout, + jobId: null, + data: null, + dataType: null, + errorMsg: null + }, options); + + this.jobs = options.jobs || []; + this.source = options.source; +}; + +$.BatchImageJob.prototype = { + /** + * Starts the batch job. + */ + start: function() { + this._finishedJobs = 0; + const self = this; + + // Set timeout for the whole batch + this.jobId = window.setTimeout(function () { + self.fail("Batch image load exceeded timeout (" + self.timeout + " ms)", null); + }, this.timeout); + + this.abort = function() { + // we don't call job.start() for each job, so abort is callable here + self.source.downloadTileBatchAbort(self); + for (let j of this.jobs) { + // Abort only running jobs by checking jobId. In theory, all should finish at once, + // but we cannot enforce the logic executed by each batch job. + if (j.jobId && j.abort) { + j.abort(); + } + } + }; + + const wrap = (fn, job) => { + return (...args) => { + if (!this.jobId) { + return; + } + this._finishedJobs++; + fn.call(job, ...args); + if (this._finishedJobs === this.jobs.length) { + window.clearTimeout(this.jobId); + this.jobId = null; + if (this.callback) { + this.callback(this); + } + } + }; + }; + + for (let j of this.jobs) { + // Handle timeout securely + j.finish = wrap(j.finish, j); + j.fail = wrap(j.fail, j); + j.prepareForBatch(); + } + + this.source.downloadTileBatchStart(this); + }, + + /** + * Finish is defined as not to throw when accidentally used, but should not be called. + */ + finish: function(data, request, dataType) { + $.console.error('Finish call on batch job is not desirable: call finish on individual child jobs!', data, request); + }, + + /** + * Finish all batched jobs as a failure. This is available mainly for ImageLoader class logics, + * implementations should fail/finish/abort individual jobs directly. + * @param {string} errorMessage description upon failure + * @param {XMLHttpRequest} request reference to the request if used + */ + fail: function(errorMessage, request) { + this.data = null; + this.request = request; + this.errorMsg = errorMessage; + this.dataType = null; + + // Fail before setting jobId to null, which is checked for in wrapped fail call. + for (let i = 0; i < this.jobs.length; i++) { + if (this.jobs[i].jobId) { // If still running + this.jobs[i].fail(errorMessage || "Batch failed", request); + } + } + + if (this.jobId) { + window.clearTimeout(this.jobId); + this.jobId = null; + } + + if (this.callback) { + this.callback(this); + } + } +}; + +/** + * @class ImageLoader + * @memberof OpenSeadragon + * @classdesc Handles downloading of a set of images using asynchronous queue pattern. + * You generally won't have to interact with the ImageLoader directly. + * @param {Object} options - Options for this ImageLoader. + * @param {Number} [options.jobLimit] - The number of concurrent image requests. See imageLoaderLimit in {@link OpenSeadragon.Options} for details. + * @param {Number} [options.timeout] - The max number of milliseconds that an image job may take to complete. + */ +$.ImageLoader = function(options) { + + $.extend(true, this, { + jobLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, + timeout: $.DEFAULT_SETTINGS.timeout, + jobQueue: [], + failedTiles: [], + jobsInProgress: 0 + }, options); + + this._batchBuckets = []; +}; + +/** @lends OpenSeadragon.ImageLoader.prototype */ +$.ImageLoader.prototype = { + + /** + * Add an unloaded image to the loader queue. + * @method + * @param {Object} options - Options for this job. + * @param {TileSource} options.source - Image loading strategy definition + * @param {String} [options.src] - URL of image to download. + * @param {Tile} [options.tile] - Tile that belongs the data to. The tile instance + * is not internally used and serves for custom TileSources implementations. + * @param {String} [options.loadWithAjax] - Whether to load this image with AJAX. + * @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX. + * @param {String|Boolean} [options.crossOriginPolicy] - CORS policy to use for downloads + * @param {String} [options.postData] - POST parameters (usually but not necessarily in k=v&k2=v2... form, + * see TileSource::getTilePostData) or null + * @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX + * requests. + * @param {Function} [options.callback] - Called once image has been downloaded. + * @param {Function} [options.abort] - Called when this image job is aborted. + * @returns {boolean} true if job was immediatelly started, false if queued + */ + addJob: function(options) { + if (!options.source) { + $.console.error('ImageLoader.prototype.addJob() requires [options.source]...'); + options.source = $.TileSource.prototype; + } + + const _this = this, + jobOptions = { + src: options.src, + tile: options.tile || {}, + source: options.source, + loadWithAjax: options.loadWithAjax, + ajaxHeaders: options.loadWithAjax ? options.ajaxHeaders : null, + crossOriginPolicy: options.crossOriginPolicy, + ajaxWithCredentials: options.ajaxWithCredentials, + postData: options.postData, + callback: (job) => completeJob(_this, job, options.callback), + abort: options.abort, + timeout: this.timeout + }, + newJob = new $.ImageJob(jobOptions); + + const sourceWantsBatching = options.source && options.source.batchEnabled(); + if (sourceWantsBatching) { + // Mark job as batched so completeJob knows not to decrement global counters + newJob.isBatched = true; + this._stageJobForBatching(newJob, options.source); + return false; + } + + if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) { + newJob.start(); + this.jobsInProgress++; + return true; + } + this.jobQueue.push( newJob ); + return false; + }, + + /** + * Internal method to group jobs. + * @private + */ + _stageJobForBatching: function(newJob, source) { + let bucket = null; + for (let i = 0; i < this._batchBuckets.length; i++) { + if (this._batchBuckets[i].source.batchCompatible(source)) { + bucket = this._batchBuckets[i]; + break; + } + } + + if (bucket && !bucket.timer) { + $.console.error( + 'Attempted to add a new job to a batch bucket that has already been flushed. ' + + 'Creating a new batch bucket for this source. ' + + 'Check batch logic and timing if this happens frequently. ' + + 'Bucket source:', source, 'Job ID:', newJob && newJob.jobId + ); + bucket = null; + } + + if (!bucket) { + bucket = { + source: source, + jobs: [], + timer: null, + waitTimeout: source.batchTimeout(), + maxJobs: source.batchMaxJobs() + }; + bucket.timer = setTimeout(() => this._flushBatchBucket(bucket), bucket.waitTimeout); + this._batchBuckets.push(bucket); + } + + bucket.jobs.push(newJob); + + if (bucket.maxJobs >= 1 && bucket.jobs.length >= bucket.maxJobs) { + clearTimeout(bucket.timer); + this._flushBatchBucket(bucket); + } + }, + + /** + * Flushes a specific bucket, creating a BatchJob and submitting it to the main queue logic. + * @private + */ + _flushBatchBucket: function(bucket) { + bucket.timer = null; + const index = this._batchBuckets.indexOf(bucket); + if (index > -1) { + this._batchBuckets.splice(index, 1); + } + + if (bucket.jobs.length === 0) { + return; + } + + const _this = this; + const batchJob = new $.BatchImageJob({ + source: bucket.source, + jobs: bucket.jobs, + timeout: this.timeout, + callback: (job) => completeBatchJob(_this, job), + // no abort here + }); + + if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) { + batchJob.start(); + this.jobsInProgress++; + } else { + this.jobQueue.push(batchJob); + } + }, + + /** + * @returns {boolean} true if a job can be submitted + */ + canAcceptNewJob() { + return !this.jobLimit || this.jobsInProgress < this.jobLimit; + }, + + /** + * Clear any unstarted image loading jobs from the queue. + * @method + */ + clear: function() { + for( let i = 0; i < this.jobQueue.length; i++ ) { + const job = this.jobQueue[i]; + if ( typeof job.abort === "function" ) { + job.abort(); + } + } + this.jobQueue = []; + + if (this._batchBuckets) { + for (let i = 0; i < this._batchBuckets.length; i++) { + const bucket = this._batchBuckets[i]; + clearTimeout(bucket.timer); + bucket.timer = null; + // Jobs in buckets haven't started, no abort needed typically, just drop refs + } + this._batchBuckets = []; + } + } +}; + +/** + * Cleans up ImageJob once completed. Restarts job after tileRetryDelay seconds if failed + * but max tileRetryMax times + * @method + * @private + * @param loader - ImageLoader used to start job. + * @param {OpenSeadragon.ImageJob} job - The ImageJob that has completed. + * @param callback - Called once cleanup is finished. + */ +function completeJob(loader, job, callback) { + if (job.errorMsg && job.data === null && job.tries < 1 + loader.tileRetryMax) { + // Retries are ran separately. + job.isBatched = false; + loader.failedTiles.push(job); + } + + // CRITICAL: Child batch job items are marked as batched - do NOT decrement. + if (!job.isBatched) { + loader.jobsInProgress--; + } + + if (loader.canAcceptNewJob() && loader.jobQueue.length > 0) { + let nextJob = loader.jobQueue.shift(); + nextJob.start(); + loader.jobsInProgress++; + } + + if (loader.tileRetryMax > 0 && loader.jobQueue.length === 0) { + if (loader.canAcceptNewJob() && loader.failedTiles.length > 0) { + let nextJob = loader.failedTiles.shift(); + setTimeout(function () { + nextJob.start(); + }, loader.tileRetryDelay); + loader.jobsInProgress++; + } + } + + if (callback) { + callback(job.data, job.errorMsg, job.request, job.dataType, job.tries); + } +} + +/** + * Cleans up BatchImageJob once completed. Explicit here so it's easier to debug, + * In fact batch job does not need to do anything except decrementing counter. + * @method + * @private + * @param loader - ImageLoader used to start job. + * @param {BatchImageJob} job - The ImageJob that has completed. + */ +function completeBatchJob(loader, job) { + loader.jobsInProgress--; + job.jobs.length = 0; // make sure items are detached +} + +// Consistent data validity checker +function isInvalidData(dataItem) { + return dataItem === null || dataItem === undefined || dataItem === false; +} + +}(OpenSeadragon)); + +/* + * OpenSeadragon - Tile + * + * 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 Tile + * @memberof OpenSeadragon + * @param {Number} level The zoom level this tile belongs to. + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {OpenSeadragon.Rect} bounds Where this tile fits, in normalized + * coordinates. + * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has + * this tile failed to load? ) + * @param {String|Function} url The URL of this tile's image or a function that returns a url. + * @param {CanvasRenderingContext2D} [context2D=undefined] The context2D of this tile if it + * * is provided directly by the tile source. Deprecated: use Tile::addCache(...) instead. + * @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request . + * @param {Object} ajaxHeaders The headers to send with this tile's AJAX request (if applicable). + * @param {OpenSeadragon.Rect} sourceBounds The portion of the tile to use as the source of the + * drawing operation, in pixels. Note that this only works when drawing with canvas; when drawing + * with HTML the entire tile is always used. + * @param {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form, + * see TileSource::getTilePostData) or null + * @param {String} cacheKey key to act as a tile cache, must be unique for tiles with unique image data + */ +$.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, ajaxHeaders, sourceBounds, postData, cacheKey) { + /** + * The zoom level this tile belongs to. + * @member {Number} level + * @memberof OpenSeadragon.Tile# + */ + this.level = level; + /** + * The vector component 'x'. + * @member {Number} x + * @memberof OpenSeadragon.Tile# + */ + this.x = x; + /** + * The vector component 'y'. + * @member {Number} y + * @memberof OpenSeadragon.Tile# + */ + this.y = y; + /** + * Where this tile fits, in normalized coordinates + * @member {OpenSeadragon.Rect} bounds + * @memberof OpenSeadragon.Tile# + */ + this.bounds = bounds; + /** + * Where this tile fits, in normalized coordinates, after positioning + * @member {OpenSeadragon.Rect} positionedBounds + * @memberof OpenSeadragon.Tile# + */ + this.positionedBounds = new OpenSeadragon.Rect(bounds.x, bounds.y, bounds.width, bounds.height); + /** + * The portion of the tile to use as the source of the drawing operation, in pixels. Note that + * this property is ignored with HTML drawer where the whole tile is always drawn. + * @member {OpenSeadragon.Rect} sourceBounds + * @memberof OpenSeadragon.Tile# + */ + this.sourceBounds = sourceBounds; + /** + * Is this tile a part of a sparse image? Also has this tile failed to load? + * @member {Boolean} exists + * @memberof OpenSeadragon.Tile# + */ + this.exists = exists; + /** + * Private property to hold string url or url retriever function. + * Consumers should access via Tile.getUrl() + * @member {String|Function} url + * @memberof OpenSeadragon.Tile# + * @private + */ + this._url = url; + /** + * Post parameters for this tile. For example, it can be an URL-encoded string + * in k1=v1&k2=v2... format, or a JSON, or a FormData instance... or null if no POST request used + * @member {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form, + * see TileSource::getTilePostData) or null + * @memberof OpenSeadragon.Tile# + */ + this.postData = postData; + /** + * The context2D of this tile if it is provided directly by the tile source. + * @member {CanvasRenderingContext2D} context2D + * @memberOf OpenSeadragon.Tile# + */ + if (context2D) { + this.context2D = context2D; + } + /** + * Whether to load this tile's image with an AJAX request. + * @member {Boolean} loadWithAjax + * @memberof OpenSeadragon.Tile# + */ + this.loadWithAjax = loadWithAjax; + /** + * The headers to be used in requesting this tile's image. + * Only used if loadWithAjax is set to true. + * @member {Object} ajaxHeaders + * @memberof OpenSeadragon.Tile# + */ + this.ajaxHeaders = ajaxHeaders; + + if (cacheKey === undefined) { + $.console.warn("Tile constructor needs 'cacheKey' variable: creation tile cache" + + " in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used."); + cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData); + } + + this._cKey = cacheKey || ""; + this._ocKey = cacheKey || ""; + + /** + * Is this tile loaded? + * @member {Boolean} loaded + * @memberof OpenSeadragon.Tile# + */ + this.loaded = false; + /** + * Is this tile loading? + * @member {Boolean} loading + * @memberof OpenSeadragon.Tile# + */ + this.loading = false; + /** + * This tile's position on screen, in pixels. + * @member {OpenSeadragon.Point} position + * @memberof OpenSeadragon.Tile# + */ + this.position = null; + /** + * This tile's size on screen, in pixels. + * @member {OpenSeadragon.Point} size + * @memberof OpenSeadragon.Tile# + */ + this.size = null; + /** + * Whether to flip the tile when rendering. + * @member {Boolean} flipped + * @memberof OpenSeadragon.Tile# + */ + this.flipped = false; + /** + * The start time of this tile's blending. + * @member {Number} blendStart + * @memberof OpenSeadragon.Tile# + */ + this.blendStart = null; + /** + * The current opacity this tile should be. + * @member {Number} opacity + * @memberof OpenSeadragon.Tile# + */ + this.opacity = null; + /** + * The squared distance of this tile to the viewport center. + * Use for comparing tiles. + * @member {Number} squaredDistance + * @memberof OpenSeadragon.Tile# + * @private + */ + this.squaredDistance = null; + /** + * The visibility score of this tile. + * @member {Number} visibility + * @memberof OpenSeadragon.Tile# + */ + this.visibility = null; + + /** + * The transparency indicator of this tile. + * @member {Boolean} hasTransparency true if tile contains transparency for correct rendering + * @memberof OpenSeadragon.Tile# + */ + this.hasTransparency = false; + + /** + * Whether this tile is currently being drawn. + * @member {Boolean} beingDrawn + * @memberof OpenSeadragon.Tile# + */ + this.beingDrawn = false; + + /** + * Timestamp the tile was last touched. + * @member {Number} lastTouchTime + * @memberof OpenSeadragon.Tile# + */ + this.lastTouchTime = 0; + + /** + * Whether this tile is in the right-most column for its level. + * @member {Boolean} isRightMost + * @memberof OpenSeadragon.Tile# + */ + this.isRightMost = false; + + /** + * Whether this tile is in the bottom-most row for its level. + * @member {Boolean} isBottomMost + * @memberof OpenSeadragon.Tile# + */ + this.isBottomMost = false; + + /** + * Owner of this tile. Do not change this property manually. + * @member {OpenSeadragon.TiledImage} + * @memberof OpenSeadragon.Tile# + */ + this.tiledImage = null; + /** + * Array of cached tile data associated with the tile. + * @member {Object} + * @private + */ + this._caches = {}; + /** + * Processing flag, exempt the tile from removal when there are ongoing updates + * @member {Boolean|Number} + * @private + */ + this.processing = false; + /** + * Processing promise, resolves when the tile exits processing, or + * resolves immediatelly if not in the processing state. + * @member {OpenSeadragon.Promise} + * @private + */ + this.processingPromise = $.Promise.resolve(this); +}; + +/** @lends OpenSeadragon.Tile.prototype */ +$.Tile.prototype = { + + /** + * Provides a string representation of this tiles level and (x,y) + * components. + * @function + * @returns {String} + */ + toString: function() { + return this.level + "/" + this.x + "_" + this.y; + }, + + /** + * The unique main cache key for this tile. Created automatically + * from the given tiledImage.source.getTileHashKey(...) implementation. + * @member {String} cacheKey + * @memberof OpenSeadragon.Tile# + * @private + */ + get cacheKey() { + return this._cKey; + }, + set cacheKey(value) { + if (value === this.cacheKey) { + return; + } + const cache = this.getCache(value); + if (!cache) { + // It's better to first set cache, then change the key to existing one. Warn if otherwise. + $.console.warn("[Tile.cacheKey] should not be set manually. Use addCache() with setAsMain=true."); + } + this._updateMainCacheKey(value); + }, + + /** + * By default equal to tile.cacheKey, marks a cache associated with this tile + * that holds the cache original data (it was loaded with). In case you + * change the tile data, the tile original data should be left with the cache + * 'originalCacheKey' and the new, modified data should be stored in cache 'cacheKey'. + * This key is used in cache resolution: in case new tile data is requested, if + * this cache key exists in the cache it is loaded. + * @member {String} originalCacheKey + * @memberof OpenSeadragon.Tile# + * @private + */ + set originalCacheKey(value) { + throw "Original Cache Key cannot be managed manually!"; + }, + get originalCacheKey() { + return this._ocKey; + }, + + /** + * The Image object for this tile. + * @member {Object} image + * @memberof OpenSeadragon.Tile# + * @deprecated + * @returns {Image} + */ + get image() { + $.console.error("[Tile.image] property has been deprecated. Use [Tile.getData] instead."); + return this.getImage(); + }, + + /** + * The URL of this tile's image. + * @member {String} url + * @memberof OpenSeadragon.Tile# + * @deprecated + * @returns {String} + */ + get url() { + $.console.error("[Tile.url] property has been deprecated. Use [Tile.getUrl] instead."); + return this.getUrl(); + }, + + /** + * The HTML div element for this tile + * @member {Element} element + * @memberof OpenSeadragon.Tile# + * @deprecated + */ + get element() { + $.console.error("Tile::element property is deprecated. Use cache API instead. Moreover, this property might be unstable."); + const cache = this.getCache(); + if (!cache || !cache.loaded) { + return null; + } + if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) { + $.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!"); + return null; + } + return cache.data.element; + }, + + /** + * The HTML img element for this tile. + * @member {Element} imgElement + * @memberof OpenSeadragon.Tile# + * @deprecated + */ + get imgElement() { + $.console.error("Tile::imgElement property is deprecated. Use cache API instead. Moreover, this property might be unstable."); + const cache = this.getCache(); + if (!cache || !cache.loaded) { + return null; + } + if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) { + $.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!"); + return null; + } + return cache.data.imgElement; + }, + + /** + * The alias of this.element.style. + * @member {String} style + * @memberof OpenSeadragon.Tile# + * @deprecated + */ + get style() { + $.console.error("Tile::style property is deprecated. Use cache API instead. Moreover, this property might be unstable."); + const cache = this.getCache(); + if (!cache || !cache.loaded) { + return null; + } + if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) { + $.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!"); + return null; + } + return cache.data.style; + }, + + /** + * Get the Image object for this tile. + * @returns {?Image} + * @deprecated + */ + getImage: function() { + $.console.error("[Tile.getImage] property has been deprecated. Use 'tile-invalidated' routine event instead."); + //this method used to ensure the underlying data model conformed to given type - convert instead of getData() + const cache = this.getCache(this.cacheKey); + if (!cache) { + return undefined; + } + cache.transformTo("image"); + return cache.data; + }, + + /** + * Get the url string for this tile. + * @returns {String} + */ + getUrl: function() { + if (typeof this._url === 'function') { + return this._url(); + } + + return this._url; + }, + + /** + * Get the CanvasRenderingContext2D instance for tile image data drawn + * onto Canvas if enabled and available + * @deprecated + * @returns {CanvasRenderingContext2D|undefined} + */ + getCanvasContext: function() { + $.console.error("[Tile.getCanvasContext] property has been deprecated. Use 'tile-invalidated' routine event instead."); + //this method used to ensure the underlying data model conformed to given type - convert instead of getData() + const cache = this.getCache(this.cacheKey); + if (!cache) { + return undefined; + } + cache.transformTo("context2d"); + return cache.data; + }, + + /** + * The context2D of this tile if it is provided directly by the tile source. + * @deprecated + * @type {CanvasRenderingContext2D} + */ + get context2D() { + $.console.error("[Tile.context2D] property has been deprecated. Use 'tile-invalidated' routine event instead."); + return this.getCanvasContext(); + }, + + /** + * The context2D of this tile if it is provided directly by the tile source. + * @deprecated + */ + set context2D(value) { + $.console.error("[Tile.context2D] property has been deprecated. Use 'tile-invalidated' routine event instead."); + const cache = this._caches[this.cacheKey]; + if (cache) { + this.removeCache(this.cacheKey); + } + this.addCache(this.cacheKey, value, 'context2d', true, false); + }, + + /** + * The default cache for this tile. + * @deprecated + * @type OpenSeadragon.CacheRecord + */ + get cacheImageRecord() { + $.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::getCache."); + return this.getCache(this.cacheKey); + }, + + /** + * The default cache for this tile. + * @deprecated + */ + set cacheImageRecord(value) { + $.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::addCache."); + const cache = this._caches[this.cacheKey]; + + if (cache) { + this.removeCache(this.cacheKey); + } + + if (value) { + if (value.loaded) { + this.addCache(this.cacheKey, value.data, value.type, true, false); + } else { + value.await().then(x => this.addCache(this.cacheKey, x, value.type, true, false)); + } + } + }, + + /** + * Cache key for main cache that is 'cache-equal', but different from original cache key + * @return {string} + * @private + */ + buildDistinctMainCacheKey: function () { + return this.cacheKey === this.originalCacheKey ? "mod://" + this.originalCacheKey : this.cacheKey; + }, + + /** + * Read tile cache data object (CacheRecord) + * @param {string} [key=this.cacheKey] cache key to read that belongs to this tile + * @return {OpenSeadragon.CacheRecord} + * @private + */ + getCache: function(key = this._cKey) { + const cache = this._caches[key]; + if (cache) { + cache.withTileReference(this); + } + return cache; + }, + + /** + * Create tile cache for given data object. + * + * Using `setAsMain` updates also main tile cache key - the main cache key used to draw this tile. + * In that case, the cache should be ready to be rendered immediatelly (converted to one of the supported formats + * of the currently employed drawer). + * + * NOTE: if the existing cache already exists, + * data parameter is ignored and inherited from the existing cache object. + * WARNING: if you override main tile cache key to point to a different cache, the invalidation routine + * will no longer work. If you need to modify tile main data, prefer to use invalidation routine instead. + * + * @param {string} key cache key, if unique, new cache object is created, else existing cache attached + * @param {*} data this data will be IGNORED if cache already exists; therefore if + * `typeof data === 'function'` holds (both async and normal functions), the data is called to obtain + * the data item: this is an optimization to load data only when necessary. + * @param {string} [type=undefined] data type, will be guessed if not provided (not recommended), + * if data is a callback the type is a mandatory field, not setting it results in undefined behaviour + * @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey, + * no effect if key === this.cacheKey + * @param [_safely=true] private + * @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to. + * @private + */ + addCache: function(key, data, type = undefined, setAsMain = false, _safely = true) { + const tiledImage = this.tiledImage; + if (!tiledImage) { + return null; //async can access outside its lifetime + } + + if (!type) { + if (!this.__typeWarningReported) { + $.console.warn(this, "[Tile.addCache] called without type specification. " + + "Automated deduction is potentially unsafe: prefer specification of data type explicitly."); + this.__typeWarningReported = true; + } + if (typeof data === 'function') { + $.console.error("[TileCache.cacheTile] options.data as a callback requires type argument! Current is " + type); + } + type = $.converter.guessType(data); + } + + const overwritesMainCache = key === this.cacheKey; + if (_safely && (overwritesMainCache || setAsMain)) { + // Need to get the supported type for rendering out of the active drawer. + const supportedTypes = tiledImage.getDrawer().getSupportedDataFormats(); + const conversion = $.converter.getConversionPath(type, supportedTypes); + $.console.assert(conversion, "[Tile.addCache] data was set for the default tile cache we are unable" + + `to render. Make sure OpenSeadragon.converter was taught to convert ${type} to (one of): ${conversion.toString()}`); + } + + const cachedItem = tiledImage._tileCache.cacheTile({ + data: data, + dataType: type, + tile: this, + cacheKey: key, + cutoff: tiledImage.source.getClosestLevel(), + }); + const havingRecord = this._caches[key]; + if (havingRecord !== cachedItem) { + this._caches[key] = cachedItem; + if (havingRecord) { + havingRecord.removeTile(this); + tiledImage._tileCache.safeUnloadCache(havingRecord); + } + } + + // Update cache key if differs and main requested + if (!overwritesMainCache && setAsMain) { + this._updateMainCacheKey(key); + } + return cachedItem; + }, + + + /** + * Add cache object to the tile + * + * @param {string} key cache key, if unique, new cache object is created, else existing cache attached + * @param {OpenSeadragon.CacheRecord} cache the cache object to attach to this tile + * @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey, + * no effect if key === this.cacheKey + * @param [_safely=true] private + * @returns {OpenSeadragon.CacheRecord|null} - Returns cache parameter reference if attached. + * @private + */ + setCache(key, cache, setAsMain = false, _safely = true) { + const tiledImage = this.tiledImage; + if (!tiledImage) { + return null; //async can access outside its lifetime + } + + const overwritesMainCache = key === this.cacheKey; + if (_safely) { + $.console.assert(cache instanceof $.CacheRecord, "[Tile.setCache] cache must be a CacheRecord object!"); + if (overwritesMainCache || setAsMain) { + // Need to get the supported type for rendering out of the active drawer. + const supportedTypes = tiledImage.getDrawer().getSupportedDataFormats(); + const conversion = $.converter.getConversionPath(cache.type, supportedTypes); + $.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" + + `to render. Make sure OpenSeadragon.converter was taught to convert ${cache.type} to (one of): ${conversion.toString()}`); + } + } + + const havingRecord = this._caches[key]; + if (havingRecord !== cache) { + this._caches[key] = cache; + cache.addTile(this); // keep reference bidirectional + if (havingRecord) { + havingRecord.removeTile(this); + tiledImage._tileCache.safeUnloadCache(havingRecord); + } + } + + // Update cache key if differs and main requested + if (!overwritesMainCache && setAsMain) { + this._updateMainCacheKey(key); + } + return cache; + }, + + /** + * Sets the main cache key for this tile and + * performs necessary updates + * @param value + * @private + */ + _updateMainCacheKey: function(value) { + let ref = this._caches[this._cKey]; + if (ref) { + // make sure we free drawer internal cache if people change cache key externally + ref.destroyInternalCache(); + } + this._cKey = value; + }, + + /** + * Get the number of caches available to this tile + * @returns {number} number of caches + */ + getCacheSize: function() { + return Object.keys(this._caches).length; + }, + + /** + * Free tile cache. Removes by default the cache record if no other tile uses it. + * @param {string} key cache key, required + * @param {boolean} [freeIfUnused=true] set to false if zombie should be created + * @return {OpenSeadragon.CacheRecord|undefined} reference to the cache record if it was removed, + * undefined if removal was refused to perform (e.g. does not exist, it is an original data target etc.) + * @private + */ + removeCache: function(key, freeIfUnused = true) { + const deleteTarget = this._caches[key]; + if (!deleteTarget) { + // try to erase anyway in case the cache got stuck in memory + this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, true); + return undefined; + } + + const currentMainKey = this.cacheKey, + originalDataKey = this.originalCacheKey, + sameBuiltinKeys = currentMainKey === originalDataKey; + + if (!sameBuiltinKeys && originalDataKey === key) { + $.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!", + "If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData()."); + return undefined; + } + + if (currentMainKey === key) { + if (!sameBuiltinKeys && this._caches[originalDataKey]) { + // if we have original data let's revert back + this._updateMainCacheKey(originalDataKey); + } else { + $.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!", + "If you want to remove the main cache, first set different cache as main with tile.addCache()"); + return undefined; + } + } + if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, false)) { + //if we managed to free tile from record, we are sure we decreased cache count + delete this._caches[key]; + } + return deleteTarget; + }, + + /** + * Get the ratio between current and original size. + * @function + * @deprecated + * @returns {number} + */ + getScaleForEdgeSmoothing: function() { + // getCanvasContext is deprecated and so should be this method. + $.console.warn("[Tile.getScaleForEdgeSmoothing] is deprecated, the following error is the consequence:"); + const context = this.getCanvasContext(); + if (!context) { + $.console.warn( + '[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached', + this.toString()); + return 1; + } + return context.canvas.width / (this.size.x * $.pixelDensityRatio); + }, + + /** + * Get a translation vector that when applied to the tile position produces integer coordinates. + * Needed to avoid swimming and twitching. + * @function + * @param {Number} [scale=1] - Scale to be applied to position. + * @returns {OpenSeadragon.Point} + */ + getTranslationForEdgeSmoothing: function(scale, canvasSize, sketchCanvasSize) { + // The translation vector must have positive values, otherwise the image goes a bit off + // the sketch canvas to the top and left and we must use negative coordinates to repaint it + // to the main canvas. In that case, some browsers throw: + // INDEX_SIZE_ERR: DOM Exception 1: Index or size was negative, or greater than the allowed value. + const x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2)); + const y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2)); + return new $.Point(x, y).minus( + this.position + .times($.pixelDensityRatio) + .times(scale || 1) + .apply(function(x) { + return x % 1; + }) + ); + }, + + /** + * Reflect that a cache object was renamed. Called internally from TileCache. + * Do NOT call manually. + * @function + * @private + */ + reflectCacheRenamed: function (oldKey, newKey) { + let cache = this._caches[oldKey]; + if (!cache) { + return; // nothing to fix + } + // Do update via private refs, old key no longer exists in cache + if (oldKey === this._ocKey) { + this._ocKey = newKey; + } + if (oldKey === this._cKey) { + this._cKey = newKey; + } + // Working key is never updated, it will be invalidated (but do not dereference cache, just fix the pointers) + this._caches[newKey] = cache; + delete this._caches[oldKey]; + }, + + /** + * Check if two tiles are data-equal + * @param {OpenSeadragon.Tile} tile + */ + equals(tile) { + return this._ocKey === tile._ocKey; + }, + + /** + * Removes tile from the system: it will still be present in the + * OSD memory, but marked as loaded=false, and its data will be erased if erase set to true. + * @param {boolean} [erase=false] + */ + unload: function(erase = false) { + if (!this.loaded) { + return; + } + this.tiledImage._tileCache.unloadTile(this, erase); + }, + + /** + * this method shall be called only by cache system when the tile is already empty of data + * @private + */ + _unload: function () { + this.tiledImage = null; + this._caches = {}; + this.loaded = false; + this.loading = false; + this._cKey = this._ocKey; + } +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Overlay + * + * 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 positions that an overlay may be assigned relative to + * the viewport. + * It is identical to OpenSeadragon.Placement but is kept for backward + * compatibility. + * @member OverlayPlacement + * @memberof OpenSeadragon + * @see OpenSeadragon.Placement + * @static + * @readonly + * @type {Object} + * @property {Number} CENTER + * @property {Number} TOP_LEFT + * @property {Number} TOP + * @property {Number} TOP_RIGHT + * @property {Number} RIGHT + * @property {Number} BOTTOM_RIGHT + * @property {Number} BOTTOM + * @property {Number} BOTTOM_LEFT + * @property {Number} LEFT + */ + $.OverlayPlacement = $.Placement; + + /** + * An enumeration of possible ways to handle overlays rotation + * @member OverlayRotationMode + * @memberOf OpenSeadragon + * @static + * @readonly + * @property {Number} NO_ROTATION The overlay ignore the viewport rotation. + * @property {Number} EXACT The overlay use CSS 3 transforms to rotate with + * the viewport. If the overlay contains text, it will get rotated as well. + * @property {Number} BOUNDING_BOX The overlay adjusts for rotation by + * taking the size of the bounding box of the rotated bounds. + * Only valid for overlays with Rect location and scalable in both directions. + */ + $.OverlayRotationMode = $.freezeObject({ + NO_ROTATION: 1, + EXACT: 2, + BOUNDING_BOX: 3 + }); + + /** + * @class Overlay + * @classdesc Provides a way to float an HTML element on top of the viewer element. + * + * @memberof OpenSeadragon + * @param {Object} options + * @param {Element} options.element + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} options.location - The + * location of the overlay on the image. If a {@link OpenSeadragon.Point} + * is specified, the overlay will be located at this location with respect + * to the placement option. If a {@link OpenSeadragon.Rect} is specified, + * the overlay will be placed at this location with the corresponding width + * and height and placement TOP_LEFT. + * @param {OpenSeadragon.Placement} [options.placement=OpenSeadragon.Placement.TOP_LEFT] + * Defines what part of the overlay should be at the specified options.location + * @param {OpenSeadragon.Overlay.OnDrawCallback} [options.onDraw] + * @param {Boolean} [options.checkResize=true] Set to false to avoid to + * check the size of the overlay every time it is drawn in the directions + * which are not scaled. It will improve performances but will cause a + * misalignment if the overlay size changes. + * @param {Number} [options.width] The width of the overlay in viewport + * coordinates. If specified, the width of the overlay will be adjusted when + * the zoom changes. + * @param {Number} [options.height] The height of the overlay in viewport + * coordinates. If specified, the height of the overlay will be adjusted when + * the zoom changes. + * @param {Boolean} [options.rotationMode=OpenSeadragon.OverlayRotationMode.EXACT] + * How to handle the rotation of the viewport. + */ + $.Overlay = function(element, location, placement) { + + /** + * onDraw callback signature used by {@link OpenSeadragon.Overlay}. + * + * @callback OnDrawCallback + * @memberof OpenSeadragon.Overlay + * @param {OpenSeadragon.Point} position + * @param {OpenSeadragon.Point} size + * @param {Element} element + */ + + let options; + if ($.isPlainObject(element)) { + options = element; + } else { + options = { + element: element, + location: location, + placement: placement + }; + } + + this.elementWrapper = document.createElement('div'); + this.element = options.element; + this.elementWrapper.appendChild(this.element); + + if (this.element.id) { + this.elementWrapper.id = "overlay-wrapper-" + this.element.id; // Unique ID if element has one + } + + // Always add a class for styling & selection + this.elementWrapper.classList.add("openseadragon-overlay-wrapper"); + + this.style = this.elementWrapper.style; + this._init(options); + }; + + /** @lends OpenSeadragon.Overlay.prototype */ + $.Overlay.prototype = { + + // private + _init: function(options) { + this.location = options.location; + this.placement = options.placement === undefined ? + $.Placement.TOP_LEFT : options.placement; + this.onDraw = options.onDraw; + this.checkResize = options.checkResize === undefined ? + true : options.checkResize; + + // When this.width is not null, the overlay get scaled horizontally + this.width = options.width === undefined ? null : options.width; + + // When this.height is not null, the overlay get scaled vertically + this.height = options.height === undefined ? null : options.height; + + this.rotationMode = options.rotationMode || $.OverlayRotationMode.EXACT; + + // Having a rect as location is a syntactic sugar + if (this.location instanceof $.Rect) { + this.width = this.location.width; + this.height = this.location.height; + this.location = this.location.getTopLeft(); + this.placement = $.Placement.TOP_LEFT; + } + + // Deprecated properties kept for backward compatibility. + this.scales = this.width !== null && this.height !== null; + this.bounds = new $.Rect( + this.location.x, this.location.y, this.width, this.height); + this.position = this.location; + }, + + /** + * Internal function to adjust the position of an overlay + * depending on it size and placement. + * @function + * @param {OpenSeadragon.Point} position + * @param {OpenSeadragon.Point} size + */ + adjust: function(position, size) { + const properties = $.Placement.properties[this.placement]; + if (!properties) { + return; + } + if (properties.isHorizontallyCentered) { + position.x -= size.x / 2; + } else if (properties.isRight) { + position.x -= size.x; + } + if (properties.isVerticallyCentered) { + position.y -= size.y / 2; + } else if (properties.isBottom) { + position.y -= size.y; + } + }, + + /** + * @function + */ + destroy: function() { + const element = this.elementWrapper; + const style = this.style; + + if (element.parentNode) { + element.parentNode.removeChild(element); + //this should allow us to preserve overlays when required between + //pages + if (element.prevElementParent) { + style.display = 'none'; + //element.prevElementParent.insertBefore( + // element, + // element.prevNextSibling + //); + document.body.appendChild(element); + } + } + + // clear the onDraw callback + this.onDraw = null; + + style.top = ""; + style.left = ""; + style.position = ""; + + if (this.width !== null) { + style.width = ""; + } + if (this.height !== null) { + style.height = ""; + } + const transformOriginProp = $.getCssPropertyWithVendorPrefix( + 'transformOrigin'); + const transformProp = $.getCssPropertyWithVendorPrefix( + 'transform'); + if (transformOriginProp && transformProp) { + style[transformOriginProp] = ""; + style[transformProp] = ""; + } + }, + + /** + * @function + * @param {Element} container + */ + drawHTML: function(container, viewport) { + const element = this.elementWrapper; + if (element.parentNode !== container) { + //save the source parent for later if we need it + element.prevElementParent = element.parentNode; + element.prevNextSibling = element.nextSibling; + container.appendChild(element); + + // have to set position before calculating size, fix #1116 + this.style.position = "absolute"; + // this.size is used by overlays which don't get scaled in at + // least one direction when this.checkResize is set to false. + this.size = $.getElementSize(this.elementWrapper); + } + const positionAndSize = this._getOverlayPositionAndSize(viewport); + const position = positionAndSize.position; + const size = this.size = positionAndSize.size; + let outerScale = ""; + if (viewport.overlayPreserveContentDirection) { + outerScale = viewport.flipped ? " scaleX(-1)" : " scaleX(1)"; + } + const rotate = viewport.flipped ? -positionAndSize.rotate : positionAndSize.rotate; + const scale = viewport.flipped ? " scaleX(-1)" : ""; + // call the onDraw callback if it exists to allow one to overwrite + // the drawing/positioning/sizing of the overlay + if (this.onDraw) { + this.onDraw(position, size, this.element); + } else { + const style = this.style; + const innerStyle = this.element.style; + innerStyle.display = "block"; + style.left = position.x + "px"; + style.top = position.y + "px"; + if (this.width !== null) { + innerStyle.width = size.x + "px"; + } + if (this.height !== null) { + innerStyle.height = size.y + "px"; + } + const transformOriginProp = $.getCssPropertyWithVendorPrefix( + 'transformOrigin'); + const transformProp = $.getCssPropertyWithVendorPrefix( + 'transform'); + if (transformOriginProp && transformProp) { + if (rotate && !viewport.flipped) { + innerStyle[transformProp] = ""; + style[transformOriginProp] = this._getTransformOrigin(); + style[transformProp] = "rotate(" + rotate + "deg)"; + } else if (!rotate && viewport.flipped) { + innerStyle[transformProp] = outerScale; + style[transformOriginProp] = this._getTransformOrigin(); + style[transformProp] = scale; + } else if (rotate && viewport.flipped){ + innerStyle[transformProp] = outerScale; + style[transformOriginProp] = this._getTransformOrigin(); + style[transformProp] = "rotate(" + rotate + "deg)" + scale; + } else { + innerStyle[transformProp] = ""; + style[transformOriginProp] = ""; + style[transformProp] = ""; + } + } + style.display = 'flex'; + } + }, + + // private + _getOverlayPositionAndSize: function(viewport) { + let position = viewport.pixelFromPoint(this.location, true); + let size = this._getSizeInPixels(viewport); + this.adjust(position, size); + + let rotate = 0; + if (viewport.getRotation(true) && + this.rotationMode !== $.OverlayRotationMode.NO_ROTATION) { + // BOUNDING_BOX is only valid if both directions get scaled. + // Get replaced by EXACT otherwise. + if (this.rotationMode === $.OverlayRotationMode.BOUNDING_BOX && + this.width !== null && this.height !== null) { + const rect = new $.Rect(position.x, position.y, size.x, size.y); + const boundingBox = this._getBoundingBox(rect, viewport.getRotation(true)); + position = boundingBox.getTopLeft(); + size = boundingBox.getSize(); + } else { + rotate = viewport.getRotation(true); + } + } + + if (viewport.flipped) { + position.x = (viewport.getContainerSize().x - position.x); + } + return { + position: position, + size: size, + rotate: rotate + }; + }, + + // private + _getSizeInPixels: function(viewport) { + let width = this.size.x; + let height = this.size.y; + if (this.width !== null || this.height !== null) { + const scaledSize = viewport.deltaPixelsFromPointsNoRotate( + new $.Point(this.width || 0, this.height || 0), true); + if (this.width !== null) { + width = scaledSize.x; + } + if (this.height !== null) { + height = scaledSize.y; + } + } + if (this.checkResize && + (this.width === null || this.height === null)) { + const eltSize = this.size = $.getElementSize(this.elementWrapper); + if (this.width === null) { + width = eltSize.x; + } + if (this.height === null) { + height = eltSize.y; + } + } + return new $.Point(width, height); + }, + + // private + _getBoundingBox: function(rect, degrees) { + const refPoint = this._getPlacementPoint(rect); + return rect.rotate(degrees, refPoint).getBoundingBox(); + }, + + // private + _getPlacementPoint: function(rect) { + const result = new $.Point(rect.x, rect.y); + const properties = $.Placement.properties[this.placement]; + if (properties) { + if (properties.isHorizontallyCentered) { + result.x += rect.width / 2; + } else if (properties.isRight) { + result.x += rect.width; + } + if (properties.isVerticallyCentered) { + result.y += rect.height / 2; + } else if (properties.isBottom) { + result.y += rect.height; + } + } + return result; + }, + + // private + _getTransformOrigin: function() { + let result = ""; + const properties = $.Placement.properties[this.placement]; + if (!properties) { + return result; + } + if (properties.isLeft) { + result = "left"; + } else if (properties.isRight) { + result = "right"; + } + if (properties.isTop) { + result += " top"; + } else if (properties.isBottom) { + result += " bottom"; + } + return result; + }, + + /** + * Changes the overlay settings. + * @function + * @param {OpenSeadragon.Point|OpenSeadragon.Rect|Object} location + * If an object is specified, the options are the same than the constructor + * except for the element which can not be changed. + * @param {OpenSeadragon.Placement} placement + */ + update: function(location, placement) { + const options = $.isPlainObject(location) ? location : { + location: location, + placement: placement + }; + this._init({ + location: options.location || this.location, + placement: options.placement !== undefined ? + options.placement : this.placement, + onDraw: options.onDraw || this.onDraw, + checkResize: options.checkResize || this.checkResize, + width: options.width !== undefined ? options.width : this.width, + height: options.height !== undefined ? options.height : this.height, + rotationMode: options.rotationMode || this.rotationMode + }); + }, + + /** + * Returns the current bounds of the overlay in viewport coordinates + * @function + * @param {OpenSeadragon.Viewport} viewport the viewport + * @returns {OpenSeadragon.Rect} overlay bounds + */ + getBounds: function(viewport) { + $.console.assert(viewport, + 'A viewport must now be passed to Overlay.getBounds.'); + let width = this.width; + let height = this.height; + if (width === null || height === null) { + const size = viewport.deltaPointsFromPixelsNoRotate(this.size, true); + if (width === null) { + width = size.x; + } + if (height === null) { + height = size.y; + } + } + const location = this.location.clone(); + this.adjust(location, new $.Point(width, height)); + return this._adjustBoundsForRotation( + viewport, new $.Rect(location.x, location.y, width, height)); + }, + + // private + _adjustBoundsForRotation: function(viewport, bounds) { + if (!viewport || + viewport.getRotation(true) === 0 || + this.rotationMode === $.OverlayRotationMode.EXACT) { + return bounds; + } + if (this.rotationMode === $.OverlayRotationMode.BOUNDING_BOX) { + // If overlay not fully scalable, BOUNDING_BOX falls back to EXACT + if (this.width === null || this.height === null) { + return bounds; + } + // It is easier to just compute the position and size and + // convert to viewport coordinates. + const positionAndSize = this._getOverlayPositionAndSize(viewport); + return viewport.viewerElementToViewportRectangle(new $.Rect( + positionAndSize.position.x, + positionAndSize.position.y, + positionAndSize.size.x, + positionAndSize.size.y)); + } + + // NO_ROTATION case + return bounds.rotate(-viewport.getRotation(true), + this._getPlacementPoint(bounds)); + } + }; + +}(OpenSeadragon)); + +/* + * OpenSeadragon - DrawerBase + * + * 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 OpenSeadragon.BaseDrawerOptions + * @memberOf OpenSeadragon + * @property {boolean} [usePrivateCache=false] specify whether the drawer should use + * detached (=internal) cache object in case it has to perform custom type conversion atop + * what cache performs. In that case, drawer must implement internalCacheCreate() which gets data in one + * of formats the drawer declares as supported. This method must return object to be used during drawing. + * You should probably implement also internalCacheFree() to provide cleanup logics. + * + * @property {boolean} [preloadCache=true] When internalCacheCreate is used, it can be applied offline + * (asynchronously) during data processing = preloading, or just in time before rendering (if necessary). + * Preloading supports async handlers, and can use promises. If preloadCache=false, no async (e.g. cache conversion) + * logics can be used! + * + * @property {boolean} [offScreen=false] When true, the drawer is not attached to DOM. This must be false + * for all drawers created and used for rendering, particularly the main viewer drawer. However, + * if you need to use particular viewer API for rendering an offscreen images for further processing, + * you can set this to true. + * + * @property {boolean} [broadCastTileInvalidation=true] When true, the drawer will reflect changes done to the viewer's + * base drawer instance. For example, navigator will reflect data updates of the main viewport. + */ + +const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc +/** + * @class OpenSeadragon.DrawerBase + * @classdesc Base class for Drawers that handle rendering of tiles for an {@link OpenSeadragon.Viewer}. + * More viewers can be implemented even as plugins if they are attached to the OpenSeadragon namespace. + * Then you can employ the newly defined type as you would do with built-in drawers. + * @param {Object} options - Options for this Drawer. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. + * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. + * @param {HTMLElement} options.element - Parent element. + * @abstract + */ + +OpenSeadragon.DrawerBase = class DrawerBase { + constructor(options){ + $.console.assert( options.viewer, "[Drawer] options.viewer is required" ); + $.console.assert( options.viewport, "[Drawer] options.viewport is required" ); + $.console.assert( options.element, "[Drawer] options.element is required" ); + + this._id = this.getType() + $.now(); + this.viewer = options.viewer; + this.viewport = options.viewport; + this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor; + + /** + * @memberof OpenSeadragon.DrawerBase# + * @type OpenSeadragon.BaseDrawerOptions + */ + this.options = $.extend({ + usePrivateCache: false, + preloadCache: true, + offScreen: false, + broadCastTileInvalidation: true, + }, this.defaultOptions, options.options); + + this.container = $.getElement( options.element ); + + this._renderingTarget = this._createDrawingElement(); + + if (!this.options.offScreen) { + this.canvas.style.width = "100%"; + this.canvas.style.height = "100%"; + this.canvas.style.position = "absolute"; + // set canvas.style.left = 0 so the canvas is positioned properly in ltr and rtl html + this.canvas.style.left = "0"; + $.setElementOpacity( this.canvas, this.viewer.opacity, true ); + + // Allow pointer events to pass through the canvas element so implicit + // pointer capture works on touch devices + $.setElementPointerEventsNone( this.canvas ); + $.setElementTouchActionNone( this.canvas ); + + // explicit left-align + this.container.style.textAlign = "left"; + this.container.appendChild( this.canvas ); + + if (this.options.broadCastTileInvalidation) { + let parentViewer = this.viewer; + while (parentViewer.viewer) { + parentViewer = parentViewer.viewer; + } + this._parentViewer = parentViewer; + parentViewer._registerDrawer(this); + } else { + this.viewer._registerDrawer(this); + this._parentViewer = this.viewer; + } + } + + this._checkInterfaceImplementation(); + this.setInternalCacheNeedsRefresh(); // initializes timestamp + } + + /** + * Retrieve default options for the current drawer. + * The base implementation provides default shared options. + * Overrides should enumerate all defaults or extend from this implementation. + * return $.extend({}, super.options, { ... custom drawer instance options ... }); + * @memberof {OpenSeadragon.DrawerBase} + * @returns {OpenSeadragon.BaseDrawerOptions} common options + */ + get defaultOptions() { + // defaults are defined in constructor to avoid overriding issues + return {}; + } + + /** + * @memberof {OpenSeadragon.DrawerBase} + * @return {Element} + */ + get canvas(){ + return this._renderingTarget; + } + + get element(){ + $.console.error('Drawer.element is deprecated. Use Drawer.container instead.'); + return this.container; + } + + /** + * Get unique drawer ID + * @return {string} + */ + getId() { + return this._id; + } + + /** + * @abstract + * @memberof {OpenSeadragon.DrawerBase} + * @returns {String | undefined} What type of drawer this is. Must be overridden by extending classes. + */ + getType(){ + $.console.error('Drawer.getType must be implemented by child class'); + return undefined; + } + + /** + * Retrieve required data formats the data must be converted to. + * This list MUST BE A VALID SUBSET OF getSupportedDataFormats() + * @memberof {OpenSeadragon.DrawerBase} + * @return {string[]} + */ + getRequiredDataFormats() { + return this.getSupportedDataFormats(); + } + + /** + * Retrieve data types + * @abstract + * @memberof {OpenSeadragon.DrawerBase} + * @return {string[]} + */ + getSupportedDataFormats() { + throw "Drawer.getSupportedDataFormats must define its supported rendering data types!"; + } + + /** + * Check a particular cache record is compatible. + * This function _MUST_ be called: if it returns a falsey + * value, the rendering _MUST NOT_ proceed. It should + * await next animation frames and check again for availability. + * @param {OpenSeadragon.Tile} tile + * @memberof {OpenSeadragon.DrawerBase} + * @return {any|undefined} undefined if cache not available, compatible data otherwise. + */ + getDataToDraw(tile) { + const cache = tile.getCache(tile.cacheKey); + if (!cache) { + $.console.warn("Attempt to draw tile %s when not cached!", tile); + return undefined; + } + const dataCache = cache.getDataForRendering(this, tile); + return dataCache && dataCache.data; + } + + /** + * @abstract + * @returns {Boolean} Whether the drawer implementation is supported by the browser. Must be overridden by extending classes. + */ + static isSupported() { + $.console.error('Drawer.isSupported must be implemented by child class'); + } + + /** + * @abstract + * @returns {Element} the element to draw into + * @private + */ + _createDrawingElement() { + $.console.error('Drawer._createDrawingElement must be implemented by child class'); + return null; + } + + /** + * @abstract + * @param {Array} tiledImages - An array of TiledImages that are ready to be drawn. + * @private + */ + draw(tiledImages) { + $.console.error('Drawer.draw must be implemented by child class'); + } + + /** + * @abstract + * @returns {Boolean} True if rotation is supported. + */ + canRotate() { + $.console.error('Drawer.canRotate must be implemented by child class'); + } + + /** + * Destroy the drawer. Child classes must call this super class. + */ + destroy() { + // how to force child classes to call this? + // we could force destroy methods to return some unique value that is obtainable only from this method... + this._parentViewer._unregisterDrawer(this); + } + + /** + * Destroy internal cache. Should be called within destroy() when + * usePrivateCache is set to true. Ensures cleanup of anything created + * by internalCacheCreate(...). + */ + destroyInternalCache() { + this.viewer.tileCache.clearDrawerInternalCache(this); + } + + /** + * @param {TiledImage} tiledImage the tiled image that is calling the function + * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams. + * @private + */ + minimumOverlapRequired(tiledImage) { + return false; + } + + /** + * @abstract + * @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is + * drawn smoothly on the canvas; see imageSmoothingEnabled in + * {@link OpenSeadragon.Options} for more explanation. + */ + setImageSmoothingEnabled(imageSmoothingEnabled){ + $.console.error('Drawer.setImageSmoothingEnabled must be implemented by child class'); + } + + /** + * Optional public API to draw a rectangle (e.g. for debugging purposes) + * Child classes can override this method if they wish to support this + * @param {OpenSeadragon.Rect} rect + */ + drawDebuggingRect(rect) { + $.console.warn('[drawer].drawDebuggingRect is not implemented by this drawer'); + } + + // Deprecated functions + clear(){ + $.console.warn('[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.'); + } + + /** + * If options.usePrivateCache is true, this method MUST RETURN the private cache content + * @param {OpenSeadragon.CacheRecord} cache + * @param {OpenSeadragon.Tile} tile + * @return any + */ + internalCacheCreate(cache, tile) {} + + /** + * It is possible to perform any necessary cleanup on internal cache, necessary if you + * need to clean up some memory (e.g. destroy canvas by setting with & height to 0). + * @param {*} data object returned by internalCacheCreate(...) + */ + internalCacheFree(data) {} + + /** + * Call to invalidate internal cache. It will be rebuilt. With synchronous converions, + * it will be rebuilt immediatelly. With asynchronous, it will be rebuilt once invalidation + * routine happens, e.g. you should call also requestInvalidate() if you need to happen + * it as soon as possible. + */ + setInternalCacheNeedsRefresh() { + this._dataNeedsRefresh = $.now(); + } + + /** + * When a Tiled Image is initialized and ready, this method is called. + * Unlike with events, here it is guaranteed that all external code has finished + * processing (under normal circumstances) and the tiled image should not change. + * @param {OpenSeadragon.TiledImage} tiledImage target image that has been created + */ + tiledImageCreated(tiledImage) { + // pass + } + + // Private functions + + /** + * Ensures that child classes have provided implementations for public API methods + * draw, canRotate, destroy, and setImageSmoothinEnabled. Throws an exception if the original + * placeholder methods are still in place. + * @private + * + */ + _checkInterfaceImplementation(){ + if (this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement) { + throw(new Error("[drawer]._createDrawingElement must be implemented by child class")); + } + if (this.draw === $.DrawerBase.prototype.draw) { + throw(new Error("[drawer].draw must be implemented by child class")); + } + if (this.canRotate === $.DrawerBase.prototype.canRotate) { + throw(new Error("[drawer].canRotate must be implemented by child class")); + } + if (this.destroy === $.DrawerBase.prototype.destroy) { + throw(new Error("[drawer].destroy must be implemented by child class")); + } + if (this.setImageSmoothingEnabled === $.DrawerBase.prototype.setImageSmoothingEnabled) { + throw(new Error("[drawer].setImageSmoothingEnabled must be implemented by child class")); + } + } + + + // Utility functions + + /** + * Scale from OpenSeadragon viewer rectangle to drawer rectangle + * (ignoring rotation) + * @param {OpenSeadragon.Rect} rectangle - The rectangle in viewport coordinate system. + * @returns {OpenSeadragon.Rect} Rectangle in drawer coordinate system. + */ + viewportToDrawerRectangle(rectangle) { + const topLeft = this.viewport.pixelFromPointNoRotate(rectangle.getTopLeft(), true); + const size = this.viewport.deltaPixelsFromPointsNoRotate(rectangle.getSize(), true); + + return new $.Rect( + topLeft.x * $.pixelDensityRatio, + topLeft.y * $.pixelDensityRatio, + size.x * $.pixelDensityRatio, + size.y * $.pixelDensityRatio + ); + } + + /** + * This function converts the given point from to the drawer coordinate by + * multiplying it with the pixel density. + * This function does not take rotation into account, thus assuming provided + * point is at 0 degree. + * @param {OpenSeadragon.Point} point - the pixel point to convert + * @returns {OpenSeadragon.Point} Point in drawer coordinate system. + */ + viewportCoordToDrawerCoord(point) { + const vpPoint = this.viewport.pixelFromPointNoRotate(point, true); + return new $.Point( + vpPoint.x * $.pixelDensityRatio, + vpPoint.y * $.pixelDensityRatio + ); + } + + + // Internal utility functions + + /** + * Calculate width and height of the canvas based on viewport dimensions + * and pixelDensityRatio + * @private + * @returns {OpenSeadragon.Point} {x, y} size of the canvas + */ + _calculateCanvasSize() { + const pixelDensityRatio = $.pixelDensityRatio; + const viewportSize = this.viewport.getContainerSize(); + return new OpenSeadragon.Point( Math.round(viewportSize.x * pixelDensityRatio), Math.round(viewportSize.y * pixelDensityRatio)); + } + + /** + * Called by implementations to fire the tiled-image-drawn event (used by tests) + * @private + */ + _raiseTiledImageDrawnEvent(tiledImage, tiles){ + if(!this.viewer) { + return; + } + + /** + * Raised when a tiled image is drawn to the canvas. Used internally for testing. + * The update-viewport event is preferred if you want to know when a frame has been drawn. + * + * @event tiled-image-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {Array} tiles - An array of Tile objects that were drawn. + * @property {?Object} userData - Arbitrary subscriber-defined object. + * @private + */ + this.viewer.raiseEvent( 'tiled-image-drawn', { + tiledImage: tiledImage, + tiles: tiles, + }); + } + + /** + * Called by implementations to fire the drawer-error event + * @private + */ + _raiseDrawerErrorEvent(tiledImage, errorMessage){ + if(!this.viewer) { + return; + } + + /** + * Raised when a tiled image is drawn to the canvas. Used internally for testing. + * The update-viewport event is preferred if you want to know when a frame has been drawn. + * + * @event drawer-error + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.DrawerBase} drawer - The drawer that raised the error. + * @property {String} error - A message describing the error. + * @property {?Object} userData - Arbitrary subscriber-defined object. + * @protected + */ + this.viewer.raiseEvent( 'drawer-error', { + tiledImage: tiledImage, + drawer: this, + error: errorMessage, + }); + } + + +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - HTMLDrawer + * + * 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( $ ){ + +const OpenSeadragon = $; // alias back for JSDoc + +/** + * @class OpenSeadragon.HTMLDrawer + * @extends OpenSeadragon.DrawerBase + * @classdesc HTML-based implementation of DrawerBase for an {@link OpenSeadragon.Viewer}. + * @param {Object} options - Options for this Drawer. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. + * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. + * @param {Element} options.element - Parent element. + * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. + */ + +class HTMLDrawer extends OpenSeadragon.DrawerBase{ + constructor(options){ + super(options); + + /** + * The HTML element (div) that this drawer uses for drawing + * @member {Element} canvas + * @memberof OpenSeadragon.HTMLDrawer# + */ + + /** + * The parent element of this Drawer instance, passed in when the Drawer was created. + * The parent of {@link OpenSeadragon.WebGLDrawer#canvas}. + * @member {Element} container + * @memberof OpenSeadragon.HTMLDrawer# + */ + + // Reject listening for the tile-drawing event, which this drawer does not fire + this.viewer.rejectEventHandler("tile-drawing", "The HTMLDrawer does not raise the tile-drawing event"); + // Since the tile-drawn event is fired by this drawer, make sure handlers can be added for it + this.viewer.allowEventHandler("tile-drawn"); + + // works with canvas & image objects + function _prepareTile(tile, data) { + const element = $.makeNeutralElement( "div" ); + const imgElement = data.cloneNode(); + imgElement.style.msInterpolationMode = "nearest-neighbor"; + imgElement.style.width = "100%"; + imgElement.style.height = "100%"; + + const style = element.style; + style.position = "absolute"; + + return { + element, imgElement, style, data + }; + } + + // In theory, HTML drawer should cope well with canvas node type too, + // but tests fail - if this conversion is used, it outputs uninitialized zeroed data + // (data manipulation test module). + + // The actual placing logics will not happen at draw event, but when the cache is created: + // $.converter.learn("context2d", HTMLDrawer.canvasCacheType, (t, d) => _prepareTile(t, d.canvas), 1, 1); + $.converter.learn("image", HTMLDrawer.imageCacheType, _prepareTile, 1, 1); + // Also learn how to move back, since these elements can be just used as-is + // $.converter.learn(HTMLDrawer.canvasCacheType, "context2d", (t, d) => d.data.getContext('2d'), 1, 3); + $.converter.learn(HTMLDrawer.imageCacheType, "image", (t, d) => d.data, 1, 3); + + function _freeTile(data) { + if ( data.imgElement && data.imgElement.parentNode ) { + data.imgElement.parentNode.removeChild( data.imgElement ); + } + if ( data.element && data.element.parentNode ) { + data.element.parentNode.removeChild( data.element ); + } + } + + // $.converter.learnDestroy(HTMLDrawer.canvasCacheType, _freeTile); + $.converter.learnDestroy(HTMLDrawer.imageCacheType, _freeTile); + } + + static get imageCacheType() { + return 'htmlDrawer[image]'; + } + + static get canvasCacheType() { + return 'htmlDrawer[canvas]'; + } + + /** + * @returns {Boolean} always true + */ + static isSupported() { + return true; + } + + /** + * + * @returns 'html' + */ + getType(){ + return 'html'; + } + + getSupportedDataFormats() { + return [HTMLDrawer.imageCacheType, HTMLDrawer.canvasCacheType]; + } + + /** + * @param {TiledImage} tiledImage the tiled image that is calling the function + * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams. + * @private + */ + minimumOverlapRequired(tiledImage) { + return true; + } + + /** + * create the HTML element (e.g. canvas, div) that the image will be drawn into + * @returns {Element} the div to draw into + */ + _createDrawingElement(){ + return $.makeNeutralElement("div"); + } + + /** + * Draws the TiledImages + */ + draw(tiledImages) { + const _this = this; + this._prepareNewFrame(); // prepare to draw a new frame + tiledImages.forEach(function(tiledImage){ + if (tiledImage.opacity !== 0) { + _this._drawTiles(tiledImage); + } + }); + + } + + /** + * @returns {Boolean} False - rotation is not supported. + */ + canRotate() { + return false; + } + + /** + * Destroy the drawer (unload current loaded tiles) + */ + destroy() { + super.destroy(); + this.container.removeChild(this.canvas); + } + + /** + * This function is ignored by the HTML Drawer. Implementing it is required by DrawerBase. + * @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is + * drawn smoothly on the canvas; see imageSmoothingEnabled in + * {@link OpenSeadragon.Options} for more explanation. + */ + setImageSmoothingEnabled(){ + // noop - HTML Drawer does not deal with this property + } + + /** + * Clears the Drawer so it's ready to draw another frame. + * @private + * + */ + _prepareNewFrame() { + this.canvas.innerHTML = ""; + } + + /** + * Draws a TiledImage. + * @private + * + */ + _drawTiles( tiledImage ) { + const lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile); + if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) { + return; + } + + // Iterate over the tiles to draw, and draw them + for (let i = lastDrawn.length - 1; i >= 0; i--) { + const tile = lastDrawn[ i ]; + this._drawTile( tile ); + + if( this.viewer ){ + /** + * Raised when a tile is drawn to the canvas. Only valid for + * context2d and html drawers. + * + * @event tile-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'tile-drawn', { + tiledImage: tiledImage, + tile: tile + }); + } + } + + } + + /** + * Draws the given tile. + * @private + * @param {OpenSeadragon.Tile} tile - The tile to draw. + * @param {Function} drawingHandler - Method for firing the drawing event if using canvas. + * drawingHandler({context, tile, rendered}) + */ + _drawTile( tile ) { + $.console.assert(tile, '[Drawer._drawTile] tile is required'); + + let container = this.canvas; + + if ( !tile.loaded ) { + $.console.warn( + "Attempting to draw tile %s when it's not yet loaded.", + tile.toString() + ); + return; + } + + //EXPERIMENTAL - trying to figure out how to scale the container + // content during animation of the container size. + + const dataObject = this.getDataToDraw(tile); + if (!dataObject) { + return; + } + + if ( dataObject.element.parentNode !== container ) { + container.appendChild( dataObject.element ); + } + if ( dataObject.imgElement.parentNode !== dataObject.element ) { + dataObject.element.appendChild( dataObject.imgElement ); + } + + dataObject.style.top = tile.position.y + "px"; + dataObject.style.left = tile.position.x + "px"; + dataObject.style.height = tile.size.y + "px"; + dataObject.style.width = tile.size.x + "px"; + + if (tile.flipped) { + dataObject.style.transform = "scaleX(-1)"; + } + + $.setElementOpacity( dataObject.element, tile.opacity ); + } +} + +$.HTMLDrawer = HTMLDrawer; + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - CanvasDrawer + * + * 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( $ ){ + + const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc +/** + * @class OpenSeadragon.CanvasDrawer + * @extends OpenSeadragon.DrawerBase + * @classdesc Default implementation of CanvasDrawer for an {@link OpenSeadragon.Viewer}. + * @param {Object} options - Options for this Drawer. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. + * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. + * @param {Element} options.element - Parent element. + * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. + */ + +class CanvasDrawer extends OpenSeadragon.DrawerBase{ + constructor(options) { + super(options); + + /** + * The HTML element (canvas) that this drawer uses for drawing + * @member {Element} canvas + * @memberof OpenSeadragon.CanvasDrawer# + */ + + /** + * The parent element of this Drawer instance, passed in when the Drawer was created. + * The parent of {@link OpenSeadragon.WebGLDrawer#canvas}. + * @member {Element} container + * @memberof OpenSeadragon.CanvasDrawer# + */ + + /** + * 2d drawing context for {@link OpenSeadragon.CanvasDrawer#canvas}. + * @member {Object} context + * @memberof OpenSeadragon.CanvasDrawer# + * @private + */ + this.context = this.canvas.getContext('2d'); + + // Sketch canvas used to temporarily draw tiles which cannot be drawn directly + // to the main canvas due to opacity. Lazily initialized. + this.sketchCanvas = null; + this.sketchContext = null; + + // Image smoothing for canvas rendering (only if canvas is used). + // Canvas default is "true", so this will only be changed if user specifies "false" in the options or via setImageSmoothinEnabled. + this._imageSmoothingEnabled = true; + + // Since the tile-drawn and tile-drawing events are fired by this drawer, make sure handlers can be added for them + this.viewer.allowEventHandler("tile-drawn"); + this.viewer.allowEventHandler("tile-drawing"); + + } + + /** + * @returns {Boolean} true if canvas is supported by the browser, otherwise false + */ + static isSupported(){ + return $.supportsCanvas; + } + + getType(){ + return 'canvas'; + } + + getSupportedDataFormats() { + return ["context2d"]; + } + + /** + * create the HTML element (e.g. canvas, div) that the image will be drawn into + * @returns {Element} the canvas to draw into + */ + _createDrawingElement(){ + const canvas = $.makeNeutralElement("canvas"); + const viewportSize = this._calculateCanvasSize(); + canvas.width = viewportSize.x; + canvas.height = viewportSize.y; + return canvas; + } + + /** + * Draws the TiledImages + */ + draw(tiledImages) { + this._prepareNewFrame(); // prepare to draw a new frame + if(this.viewer.viewport.getFlip() !== this._viewportFlipped){ + this._flip(); + } + for(const tiledImage of tiledImages){ + if (tiledImage.opacity !== 0) { + this._drawTiles(tiledImage); + } + } + } + + /** + * @returns {Boolean} True - rotation is supported. + */ + canRotate() { + return true; + } + + /** + * Destroy the drawer (unload current loaded tiles) + */ + destroy() { + super.destroy(); + //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed) + this.canvas.width = 1; + this.canvas.height = 1; + this.sketchCanvas = null; + this.sketchContext = null; + this.container.removeChild(this.canvas); + } + + /** + * @param {TiledImage} tiledImage the tiled image that is calling the function + * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams. + * @private + */ + minimumOverlapRequired(tiledImage) { + return true; + } + + + /** + * Turns image smoothing on or off for this viewer. Note: Ignored in some (especially older) browsers that do not support this property. + * + * @function + * @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is + * drawn smoothly on the canvas; see imageSmoothingEnabled in + * {@link OpenSeadragon.Options} for more explanation. + */ + setImageSmoothingEnabled(imageSmoothingEnabled){ + this._imageSmoothingEnabled = !!imageSmoothingEnabled; + this._updateImageSmoothingEnabled(this.context); + this.viewer.forceRedraw(); + } + + /** + * Draw a rectangle onto the canvas + * @param {OpenSeadragon.Rect} rect + */ + drawDebuggingRect(rect) { + const context = this.context; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.strokeStyle = this.debugGridColor[0]; + context.fillStyle = this.debugGridColor[0]; + + context.strokeRect( + rect.x * $.pixelDensityRatio, + rect.y * $.pixelDensityRatio, + rect.width * $.pixelDensityRatio, + rect.height * $.pixelDensityRatio + ); + + context.restore(); + } + + /** + * Test whether the current context is flipped or not + * @private + */ + get _viewportFlipped(){ + return this.context.getTransform().a < 0; + } + + /** + * Fires the tile-drawing event. + * @private + */ + _raiseTileDrawingEvent(tiledImage, context, tile, rendered){ + /** + * This event is fired just before the tile is drawn giving the application a chance to alter the image. + * + * NOTE: This event is only fired when the 'canvas' drawer is being used + * + * @event tile-drawing + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.Tile} tile - The Tile being drawn. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {CanvasRenderingContext2D} context - The HTML canvas context being drawn into. + * @property {CanvasRenderingContext2D} rendered - The HTML canvas context containing the tile imagery. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('tile-drawing', { + tiledImage: tiledImage, + context: context, + tile: tile, + rendered: rendered + }); + } + + /** + * Clears the Drawer so it's ready to draw another frame. + * @private + * + */ + _prepareNewFrame() { + const viewportSize = this._calculateCanvasSize(); + if( this.canvas.width !== viewportSize.x || + this.canvas.height !== viewportSize.y ) { + this.canvas.width = viewportSize.x; + this.canvas.height = viewportSize.y; + this._updateImageSmoothingEnabled(this.context); + if ( this.sketchCanvas !== null ) { + const sketchCanvasSize = this._calculateSketchCanvasSize(); + this.sketchCanvas.width = sketchCanvasSize.x; + this.sketchCanvas.height = sketchCanvasSize.y; + this._updateImageSmoothingEnabled(this.sketchContext); + } + } + this._clear(); + } + + /** + * @private + * @param {Boolean} useSketch Whether to clear sketch canvas or main canvas + * @param {OpenSeadragon.Rect} [bounds] The rectangle to clear + */ + _clear(useSketch, bounds){ + const context = this._getContext(useSketch); + if (bounds) { + context.clearRect(bounds.x, bounds.y, bounds.width, bounds.height); + } else { + const canvas = context.canvas; + context.clearRect(0, 0, canvas.width, canvas.height); + } + } + + /** + * Draws a TiledImage. + * @private + * + */ + _drawTiles( tiledImage ) { + const lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile); + if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) { + return; + } + + let tile = lastDrawn[0]; + let useSketch; + + if (tile) { + useSketch = tiledImage.opacity < 1 || + (tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over') || + (!tiledImage._isBottomItem() && + tiledImage.source.hasTransparency(null, tile.getUrl(), tile.ajaxHeaders, tile.postData)); + } + + let sketchScale; + let sketchTranslate; + + const zoom = this.viewport.getZoom(true); + const imageZoom = tiledImage.viewportToImageZoom(zoom); + + if (lastDrawn.length > 1 && + imageZoom > tiledImage.smoothTileEdgesMinZoom && + !tiledImage.iOSDevice && + tiledImage.getRotation(true) % 360 === 0 ){ // TODO: support tile edge smoothing with tiled image rotation. + // When zoomed in a lot (>100%) the tile edges are visible. + // So we have to composite them at ~100% and scale them up together. + // Note: Disabled on iOS devices per default as it causes a native crash + useSketch = true; + + const context = tile.length && this.getDataToDraw(tile); + if (context) { + sketchScale = context.canvas.width / (tile.size.x * $.pixelDensityRatio); + } else { + sketchScale = 1; + } + sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, + this._getCanvasSize(false), + this._getCanvasSize(true)); + } + + let bounds; + if (useSketch) { + if (!sketchScale) { + // Except when edge smoothing, we only clean the part of the + // sketch canvas we are going to use for performance reasons. + bounds = this.viewport.viewportToViewerElementRectangle( + tiledImage.getClippedBounds(true)) + .getIntegerBoundingBox(); + + bounds = bounds.times($.pixelDensityRatio); + } + this._clear(true, bounds); + } + + // When scaling, we must rotate only when blending the sketch canvas to + // avoid interpolation + if (!sketchScale) { + this._setRotations(tiledImage, useSketch); + } + + let usedClip = false; + if ( tiledImage._clip ) { + this._saveContext(useSketch); + + let box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); + box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true)); + let clipRect = this.viewportToDrawerRectangle(box); + if (sketchScale) { + clipRect = clipRect.times(sketchScale); + } + if (sketchTranslate) { + clipRect = clipRect.translate(sketchTranslate); + } + this._setClip(clipRect, useSketch); + + usedClip = true; + } + + if (tiledImage._croppingPolygons) { + const self = this; + if(!usedClip){ + this._saveContext(useSketch); + } + try { + const polygons = tiledImage._croppingPolygons.map(function (polygon) { + return polygon.map(function (coord) { + const point = tiledImage + .imageToViewportCoordinates(coord.x, coord.y, true) + .rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true)); + let clipPoint = self.viewportCoordToDrawerCoord(point); + if (sketchScale) { + clipPoint = clipPoint.times(sketchScale); + } + if (sketchTranslate) { // mostly fixes #2312 + clipPoint = clipPoint.plus(sketchTranslate); + } + return clipPoint; + }); + }); + this._clipWithPolygons(polygons, useSketch); + } catch (e) { + $.console.error(e); + } + usedClip = true; + } + tiledImage._hasOpaqueTile = false; + if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) { + let placeholderRect = this.viewportToDrawerRectangle(tiledImage.getBoundsNoRotate(true)); + if (sketchScale) { + placeholderRect = placeholderRect.times(sketchScale); + } + if (sketchTranslate) { + placeholderRect = placeholderRect.translate(sketchTranslate); + } + + let fillStyle = null; + if ( typeof tiledImage.placeholderFillStyle === "function" ) { + fillStyle = tiledImage.placeholderFillStyle(tiledImage, this.context); + } + else { + fillStyle = tiledImage.placeholderFillStyle; + } + + this._drawRectangle(placeholderRect, fillStyle, useSketch); + } + + const subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency); + + let shouldRoundPositionAndSize = false; + + if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) { + shouldRoundPositionAndSize = true; + } else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) { + shouldRoundPositionAndSize = !(this.viewer && this.viewer.isAnimating()); + } + + // Iterate over the tiles to draw, and draw them + for (let i = 0; i < lastDrawn.length; i++) { + tile = lastDrawn[ i ]; + this._drawTile( tile, tiledImage, useSketch, sketchScale, + sketchTranslate, shouldRoundPositionAndSize, tiledImage.source ); + + if( this.viewer ){ + /** + * Raised when a tile is drawn to the canvas. Only valid for + * context2d and html drawers. + * + * @event tile-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'tile-drawn', { + tiledImage: tiledImage, + tile: tile + }); + } + } + + if ( usedClip ) { + this._restoreContext( useSketch ); + } + + if (!sketchScale) { + if (tiledImage.getRotation(true) % 360 !== 0) { + this._restoreRotationChanges(useSketch); + } + if (this.viewport.getRotation(true) % 360 !== 0) { + this._restoreRotationChanges(useSketch); + } + } + + if (useSketch) { + if (sketchScale) { + this._setRotations(tiledImage); + } + this.blendSketch({ + opacity: tiledImage.opacity, + scale: sketchScale, + translate: sketchTranslate, + compositeOperation: tiledImage.compositeOperation, + bounds: bounds + }); + if (sketchScale) { + if (tiledImage.getRotation(true) % 360 !== 0) { + this._restoreRotationChanges(false); + } + if (this.viewport.getRotation(true) % 360 !== 0) { + this._restoreRotationChanges(false); + } + } + } + + this._drawDebugInfo( tiledImage, lastDrawn ); + + // Fire tiled-image-drawn event. + this._raiseTiledImageDrawnEvent(tiledImage, lastDrawn); + } + + /** + * Draws special debug information for a TiledImage if in debug mode. + * @private + * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame. + */ + _drawDebugInfo( tiledImage, lastDrawn ) { + if( tiledImage.debugMode ) { + for ( let i = lastDrawn.length - 1; i >= 0; i-- ) { + const tile = lastDrawn[ i ]; + try { + this._drawDebugInfoOnTile(tile, lastDrawn.length, i, tiledImage); + } catch(e) { + $.console.error(e); + } + } + } + } + + /** + * This function will create multiple polygon paths on the drawing context by provided polygons, + * then clip the context to the paths. + * @private + * @param {OpenSeadragon.Point[][]} polygons - an array of polygons. A polygon is an array of OpenSeadragon.Point + * @param {Boolean} useSketch - Whether to use the sketch canvas or not. + */ + _clipWithPolygons (polygons, useSketch) { + const context = this._getContext(useSketch); + context.beginPath(); + for(const polygon of polygons){ + for(const [i, coord] of polygon.entries() ){ + context[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y); + } + } + + context.clip(); + } + + /** + * Draws the given tile. + * @private + * @param {OpenSeadragon.Tile} tile - The tile to draw. + * @param {OpenSeadragon.TiledImage} tiledImage - The tiled image being drawn. + * @param {Boolean} useSketch - Whether to use the sketch canvas or not. + * where rendered is the context with the pre-drawn image. + * @param {Float} [scale=1] - Apply a scale to tile position and size. Defaults to 1. + * @param {OpenSeadragon.Point} [translate] A translation vector to offset tile position + * @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round + * position and size of tiles supporting alpha channel in non-transparency + * context. + * @param {OpenSeadragon.TileSource} source - The source specification of the tile. + */ + _drawTile( tile, tiledImage, useSketch, scale, translate, shouldRoundPositionAndSize, source) { + $.console.assert(tile, '[Drawer._drawTile] tile is required'); + $.console.assert(tiledImage, '[Drawer._drawTile] drawingHandler is required'); + + if ( !tile.loaded ){ + $.console.warn( + "Attempting to draw tile %s when it's not yet loaded.", + tile.toString() + ); + return; + } + + const rendered = this.getDataToDraw(tile); + if (!rendered) { + return; + } + + const context = this._getContext(useSketch); + scale = scale || 1; + + let position = tile.position.times($.pixelDensityRatio), + size = tile.size.times($.pixelDensityRatio); + + context.save(); + + if (typeof scale === 'number' && scale !== 1) { + // draw tile at a different scale + position = position.times(scale); + size = size.times(scale); + } + + if (translate instanceof $.Point) { + // shift tile position slightly + position = position.plus(translate); + } + + //if we are supposed to be rendering fully opaque rectangle, + //ie its done fading or fading is turned off, and if we are drawing + //an image with an alpha channel, then the only way + //to avoid seeing the tile underneath is to clear the rectangle + if (context.globalAlpha === 1 && tile.hasTransparency) { + if (shouldRoundPositionAndSize) { + // Round to the nearest whole pixel so we don't get seams from overlap. + position.x = Math.round(position.x); + position.y = Math.round(position.y); + size.x = Math.round(size.x); + size.y = Math.round(size.y); + } + + //clearing only the inside of the rectangle occupied + //by the png prevents edge flikering + context.clearRect( + position.x, + position.y, + size.x, + size.y + ); + } + + this._raiseTileDrawingEvent(tiledImage, context, tile, rendered); + + let sourceWidth, sourceHeight; + if (tile.sourceBounds) { + sourceWidth = Math.min(tile.sourceBounds.width, rendered.canvas.width); + sourceHeight = Math.min(tile.sourceBounds.height, rendered.canvas.height); + } else { + sourceWidth = rendered.canvas.width; + sourceHeight = rendered.canvas.height; + } + + context.translate(position.x + size.x / 2, 0); + if (tile.flipped) { + context.scale(-1, 1); + } + context.drawImage( + rendered.canvas, + 0, + 0, + sourceWidth, + sourceHeight, + -size.x / 2, + position.y, + size.x, + size.y + ); + + context.restore(); + } + + + + /** + * Get the context of the main or sketch canvas + * @private + * @param {Boolean} useSketch + * @returns {CanvasRenderingContext2D} + */ + _getContext( useSketch ) { + let context = this.context; + if ( useSketch ) { + if (this.sketchCanvas === null) { + this.sketchCanvas = document.createElement( "canvas" ); + const sketchCanvasSize = this._calculateSketchCanvasSize(); + this.sketchCanvas.width = sketchCanvasSize.x; + this.sketchCanvas.height = sketchCanvasSize.y; + this.sketchContext = this.sketchCanvas.getContext( "2d" ); + + // If the viewport is not currently rotated, the sketchCanvas + // will have the same size as the main canvas. However, if + // the viewport get rotated later on, we will need to resize it. + if (this.viewport.getRotation() === 0) { + const self = this; + this.viewer.addHandler('rotate', function resizeSketchCanvas() { + if (self.viewport.getRotation() === 0) { + return; + } + self.viewer.removeHandler('rotate', resizeSketchCanvas); + const sketchCanvasSize = self._calculateSketchCanvasSize(); + self.sketchCanvas.width = sketchCanvasSize.x; + self.sketchCanvas.height = sketchCanvasSize.y; + }); + } + this._updateImageSmoothingEnabled(this.sketchContext); + } + context = this.sketchContext; + } + return context; + } + + /** + * Save the context of the main or sketch canvas + * @private + * @param {Boolean} useSketch + */ + _saveContext( useSketch ) { + this._getContext( useSketch ).save(); + } + + /** + * Restore the context of the main or sketch canvas + * @private + * @param {Boolean} useSketch + */ + _restoreContext( useSketch ) { + this._getContext( useSketch ).restore(); + } + + // private + _setClip(rect, useSketch) { + const context = this._getContext( useSketch ); + context.beginPath(); + context.rect(rect.x, rect.y, rect.width, rect.height); + context.clip(); + } + + // private + // used to draw a placeholder rectangle + _drawRectangle(rect, fillStyle, useSketch) { + const context = this._getContext( useSketch ); + context.save(); + context.fillStyle = fillStyle; + context.fillRect(rect.x, rect.y, rect.width, rect.height); + context.restore(); + } + + /** + * Blends the sketch canvas in the main canvas. + * @param {Object} options The options + * @param {Float} options.opacity The opacity of the blending. + * @param {Float} [options.scale=1] The scale at which tiles were drawn on + * the sketch. Default is 1. + * Use scale to draw at a lower scale and then enlarge onto the main canvas. + * @param {OpenSeadragon.Point} [options.translate] A translation vector + * that was used to draw the tiles + * @param {String} [options.compositeOperation] - How the image is + * composited onto other images; see compositeOperation in + * {@link OpenSeadragon.Options} for possible values. + * @param {OpenSeadragon.Rect} [options.bounds] The part of the sketch + * canvas to blend in the main canvas. If specified, options.scale and + * options.translate get ignored. + */ + blendSketch(opacity, scale, translate, compositeOperation) { + let options = opacity; + if (!$.isPlainObject(options)) { + options = { + opacity: opacity, + scale: scale, + translate: translate, + compositeOperation: compositeOperation + }; + } + + opacity = options.opacity; + compositeOperation = options.compositeOperation; + const bounds = options.bounds; + + this.context.save(); + this.context.globalAlpha = opacity; + if (compositeOperation) { + this.context.globalCompositeOperation = compositeOperation; + } + if (bounds) { + // Internet Explorer, Microsoft Edge, and Safari have problems + // when you call context.drawImage with negative x or y + // or x + width or y + height greater than the canvas width or height respectively. + if (bounds.x < 0) { + bounds.width += bounds.x; + bounds.x = 0; + } + if (bounds.x + bounds.width > this.canvas.width) { + bounds.width = this.canvas.width - bounds.x; + } + if (bounds.y < 0) { + bounds.height += bounds.y; + bounds.y = 0; + } + if (bounds.y + bounds.height > this.canvas.height) { + bounds.height = this.canvas.height - bounds.y; + } + + this.context.drawImage( + this.sketchCanvas, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + bounds.x, + bounds.y, + bounds.width, + bounds.height + ); + } else { + scale = options.scale || 1; + translate = options.translate; + const position = translate instanceof $.Point ? + translate : new $.Point(0, 0); + + let widthExt = 0; + let heightExt = 0; + if (translate) { + const widthDiff = this.sketchCanvas.width - this.canvas.width; + const heightDiff = this.sketchCanvas.height - this.canvas.height; + widthExt = Math.round(widthDiff / 2); + heightExt = Math.round(heightDiff / 2); + } + this.context.drawImage( + this.sketchCanvas, + position.x - widthExt * scale, + position.y - heightExt * scale, + (this.canvas.width + 2 * widthExt) * scale, + (this.canvas.height + 2 * heightExt) * scale, + -widthExt, + -heightExt, + this.canvas.width + 2 * widthExt, + this.canvas.height + 2 * heightExt + ); + } + this.context.restore(); + } + + // private + _drawDebugInfoOnTile(tile, count, i, tiledImage) { + + const colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length; + const context = this.context; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial'; + context.strokeStyle = this.debugGridColor[colorIndex]; + context.fillStyle = this.debugGridColor[colorIndex]; + + this._setRotations(tiledImage); + + if(this._viewportFlipped){ + this._flip({point: tile.position.plus(tile.size.divide(2))}); + } + + context.strokeRect( + tile.position.x * $.pixelDensityRatio, + tile.position.y * $.pixelDensityRatio, + tile.size.x * $.pixelDensityRatio, + tile.size.y * $.pixelDensityRatio + ); + + const tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio; + const tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio; + + // Rotate the text the right way around. + context.translate( tileCenterX, tileCenterY ); + const angleInDegrees = this.viewport.getRotation(true); + context.rotate( Math.PI / 180 * -angleInDegrees ); + context.translate( -tileCenterX, -tileCenterY ); + + if( tile.x === 0 && tile.y === 0 ){ + context.fillText( + "Zoom: " + this.viewport.getZoom(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 30) * $.pixelDensityRatio + ); + context.fillText( + "Pan: " + this.viewport.getBounds().toString(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 20) * $.pixelDensityRatio + ); + } + context.fillText( + "Level: " + tile.level, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 20) * $.pixelDensityRatio + ); + context.fillText( + "Column: " + tile.x, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 30) * $.pixelDensityRatio + ); + context.fillText( + "Row: " + tile.y, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 40) * $.pixelDensityRatio + ); + context.fillText( + "Order: " + i + " of " + count, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 50) * $.pixelDensityRatio + ); + context.fillText( + "Size: " + tile.size.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 60) * $.pixelDensityRatio + ); + context.fillText( + "Position: " + tile.position.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 70) * $.pixelDensityRatio + ); + + if (this.viewport.getRotation(true) % 360 !== 0 ) { + this._restoreRotationChanges(); + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._restoreRotationChanges(); + } + + context.restore(); + } + + // private + _updateImageSmoothingEnabled(context){ + context.msImageSmoothingEnabled = this._imageSmoothingEnabled; + context.imageSmoothingEnabled = this._imageSmoothingEnabled; + } + + /** + * Get the canvas size + * @private + * @param {Boolean} sketch If set to true return the size of the sketch canvas + * @returns {OpenSeadragon.Point} The size of the canvas + */ + _getCanvasSize(sketch) { + const canvas = this._getContext(sketch).canvas; + return new $.Point(canvas.width, canvas.height); + } + + /** + * Get the canvas center + * @private + * @param {Boolean} sketch If set to true return the center point of the sketch canvas + * @returns {OpenSeadragon.Point} The center point of the canvas + */ + _getCanvasCenter() { + return new $.Point(this.canvas.width / 2, this.canvas.height / 2); + } + + /** + * Set rotations for viewport & tiledImage + * @private + * @param {OpenSeadragon.TiledImage} tiledImage + * @param {Boolean} [useSketch=false] + */ + _setRotations(tiledImage, useSketch = false) { + let saveContext = false; + if (this.viewport.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: this.viewport.getRotation(true), + useSketch: useSketch, + saveContext: saveContext + }); + saveContext = false; + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: tiledImage.getRotation(true), + point: this.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + useSketch: useSketch, + saveContext: saveContext + }); + } + } + + // private + _offsetForRotation(options) { + const point = options.point ? + options.point.times($.pixelDensityRatio) : + this._getCanvasCenter(); + + const context = this._getContext(options.useSketch); + context.save(); + + context.translate(point.x, point.y); + context.rotate(Math.PI / 180 * options.degrees); + context.translate(-point.x, -point.y); + } + + // private + _flip(options) { + options = options || {}; + const point = options.point ? + options.point.times($.pixelDensityRatio) : + this._getCanvasCenter(); + const context = this._getContext(options.useSketch); + + context.translate(point.x, 0); + context.scale(-1, 1); + context.translate(-point.x, 0); + } + + // private + _restoreRotationChanges(useSketch) { + const context = this._getContext(useSketch); + context.restore(); + } + + // private + _calculateCanvasSize() { + const pixelDensityRatio = $.pixelDensityRatio; + const viewportSize = this.viewport.getContainerSize(); + return { + // canvas width and height are integers + x: Math.round(viewportSize.x * pixelDensityRatio), + y: Math.round(viewportSize.y * pixelDensityRatio) + }; + } + + // private + _calculateSketchCanvasSize() { + const canvasSize = this._calculateCanvasSize(); + if (this.viewport.getRotation() === 0) { + return canvasSize; + } + // If the viewport is rotated, we need a larger sketch canvas in order + // to support edge smoothing. + const sketchCanvasSize = Math.ceil(Math.sqrt( + canvasSize.x * canvasSize.x + + canvasSize.y * canvasSize.y)); + return { + x: sketchCanvasSize, + y: sketchCanvasSize + }; + } +} +$.CanvasDrawer = CanvasDrawer; + + +/** + * Defines the value for subpixel rounding to fallback to in case of missing or + * invalid value. + * @private + */ +const DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER; + +/** + * Checks whether the input value is an invalid subpixel rounding enum value. + * @private + * + * @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to check. + * @returns {Boolean} Returns true if the input value is none of the expected + * {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} value. + */ +function isSubPixelRoundingRuleUnknown(value) { + return value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS && + value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST && + value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER; +} + +/** + * Ensures the returned value is always a valid subpixel rounding enum value, + * defaulting to {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} if input is missing or invalid. + * @private + * @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to normalize. + * @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns a valid subpixel rounding enum value. + */ +function normalizeSubPixelRoundingRule(value) { + if (isSubPixelRoundingRuleUnknown(value)) { + return DEFAULT_SUBPIXEL_ROUNDING_RULE; + } + return value; +} + +/** + * Ensures the returned value is always a valid subpixel rounding enum value, + * defaulting to 'NEVER' if input is missing or invalid. + * @private + * + * @param {Object} subPixelRoundingRules - A subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}. + * @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns the determined subpixel rounding enum value for the + * current browser. + */ +function determineSubPixelRoundingRule(subPixelRoundingRules) { + if (typeof subPixelRoundingRules === 'number') { + return normalizeSubPixelRoundingRule(subPixelRoundingRules); + } + + if (!subPixelRoundingRules || !$.Browser) { + return DEFAULT_SUBPIXEL_ROUNDING_RULE; + } + + let subPixelRoundingRule = subPixelRoundingRules[$.Browser.vendor]; + + if (isSubPixelRoundingRuleUnknown(subPixelRoundingRule)) { + subPixelRoundingRule = subPixelRoundingRules['*']; + } + + return normalizeSubPixelRoundingRule(subPixelRoundingRule); +} + +}( OpenSeadragon )); + + +/* + * OpenSeadragon - WebGLDrawer + * + * 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( $ ){ + + const OpenSeadragon = $; // alias for JSDoc + + /** + * @class WebglContextManager + * @classdesc Handles the webgl context, isolating it from the rest of the DrawerBase API. + * Manages WebGL context lifecycle, shaders, textures, framebuffers, and other WebGL resources. + * @param {Object} options - Options for the context manager + * @param {HTMLCanvasElement} options.renderingCanvas - The canvas element to use for WebGL context + * @param {Boolean} [options.unpackWithPremultipliedAlpha=false] - Whether to enable gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL + * @param {Boolean} [options.imageSmoothingEnabled=true] - Whether image smoothing is enabled + */ + class WebglContextManager { + constructor(options) { + this._renderingCanvas = options.renderingCanvas; + this._unpackWithPremultipliedAlpha = !!options.unpackWithPremultipliedAlpha; + this._imageSmoothingEnabled = options.imageSmoothingEnabled !== undefined ? options.imageSmoothingEnabled : true; + this._initShaderProgram = options.initShaderProgram; + + this._gl = null; + this._isWebGL2 = false; + this._extTextureFilterAnisotropic = null; + this._maxAnisotropy = 0; + + this._firstPass = null; + this._secondPass = null; + this._glFrameBuffer = null; + this._renderToTexture = null; + this._glNumTextures = 0; + this._unitQuad = null; + + this._destroyed = false; + + // Create WebGL context + this._gl = this._renderingCanvas.getContext('webgl2'); + if (this._gl) { + this._isWebGL2 = true; + this._setupWebGLExtensions(); + } else { + this._gl = this._renderingCanvas.getContext('webgl'); + this._isWebGL2 = false; + if (this._gl) { + this._setupWebGLExtensions(); + } + } + + if (this._gl) { + this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this._unpackWithPremultipliedAlpha); + } + } + + /** + * Get the WebGL context + * @returns {WebGLRenderingContext|WebGL2RenderingContext|null} The WebGL context + */ + getContext() { + return this._gl; + } + + /** + * Check if using WebGL2 + * @returns {Boolean} true if WebGL2, false if WebGL1 + */ + isWebGL2() { + return this._isWebGL2; + } + + /** + * Get the maximum number of texture units + * @returns {Number} MAX_TEXTURE_IMAGE_UNITS value + */ + getMaxTextures() { + if (!this._gl) { + return 0; + } + return this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS); + } + + /** + * Get the rendering canvas element + * @returns {HTMLCanvasElement} The canvas element + */ + getRenderingCanvas() { + return this._renderingCanvas; + } + + /** + * Get the first pass shader program and resources + * @returns {Object|null} The first pass object with shader program, buffers, and uniforms + */ + getFirstPass() { + return this._firstPass; + } + + /** + * Get the second pass shader program and resources + * @returns {Object|null} The second pass object with shader program, buffers, and uniforms + */ + getSecondPass() { + return this._secondPass; + } + + /** + * Get the render-to-texture framebuffer + * @returns {WebGLFramebuffer|null} The framebuffer + */ + getFrameBuffer() { + return this._glFrameBuffer; + } + + /** + * Get the render-to-texture texture + * @returns {WebGLTexture|null} The texture + */ + getRenderToTexture() { + return this._renderToTexture; + } + + /** + * Get the unit quad vertex buffer + * @returns {Float32Array} The unit quad buffer + */ + getUnitQuad() { + return this._unitQuad; + } + + /** + * Set up WebGL extensions (works for both WebGL1 and WebGL2) + * @private + */ + _setupWebGLExtensions() { + const gl = this._gl; + + // Anisotropic filtering extension (available in both WebGL1 and WebGL2) + this._extTextureFilterAnisotropic = + gl.getExtension('EXT_texture_filter_anisotropic') || + gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic') || + gl.getExtension('MOZ_EXT_texture_filter_anisotropic'); + + if (this._extTextureFilterAnisotropic) { + this._maxAnisotropy = gl.getParameter( + this._extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT + ); + } + } + + /** + * Get the texture filter constant (LINEAR or NEAREST) + * @returns {Number} gl.LINEAR or gl.NEAREST + */ + getTextureFilter() { + const gl = this._gl; + return this._imageSmoothingEnabled ? gl.LINEAR : gl.NEAREST; + } + + /** + * Apply anisotropic filtering to the currently bound texture if available + * @private + */ + _applyAnisotropy() { + if (!this._imageSmoothingEnabled || !this._extTextureFilterAnisotropic || this._maxAnisotropy <= 0) { + return; + } + const gl = this._gl; + gl.texParameterf( + gl.TEXTURE_2D, + this._extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, + Math.min(4, this._maxAnisotropy) + ); + } + + /** + * Set up the renderer: create shaders, textures, and framebuffers + * @param {Number} width - Canvas width + * @param {Number} height - Canvas height + */ + setupRenderer(width, height) { + const gl = this._gl; + if (!gl) { + $.console.error('WebGL context not available for setupRenderer'); + return; + } + + // Create unit quad once + this._unitQuad = this.makeQuadVertexBuffer(0, 1, 0, 1); + + this._makeFirstPassShaderProgram(); + this._makeSecondPassShaderProgram(); + + // set up the texture to render to in the first pass, and which will be used for rendering the second pass + this._renderToTexture = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.getTextureFilter()); + this._applyAnisotropy(); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + // set up the framebuffer for render-to-texture + this._glFrameBuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + this._renderToTexture, + 0 + ); + + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + } + + /** + * Resize the render-to-texture when canvas size changes + * @param {Number} width - New canvas width + * @param {Number} height - New canvas height + */ + resizeRenderer(width, height) { + const gl = this._gl; + if (!gl) { + return; + } + gl.viewport(0, 0, width, height); + + //release the old texture + gl.deleteTexture(this._renderToTexture); + //create a new texture and set it up + this._renderToTexture = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.getTextureFilter()); + this._applyAnisotropy(); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + //bind the frame buffer to the new texture + gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0); + } + + /** + * Create and upload a texture for a tile + * @param {HTMLImageElement|HTMLCanvasElement|ImageData} data - Image data to upload + * @param {Object} options - Texture options + * @param {Boolean} [options.unpackWithPremultipliedAlpha] - Override default unpack setting + * @returns {WebGLTexture|null} The created texture, or null on error + */ + createTexture(data, options = {}) { + const gl = this._gl; + if (!gl) { + return null; + } + + const texture = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.getTextureFilter()); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.getTextureFilter()); + this._applyAnisotropy(); + + try { + const unpackPremultipliedAlpha = options.unpackWithPremultipliedAlpha !== undefined ? + options.unpackWithPremultipliedAlpha : + this._unpackWithPremultipliedAlpha; + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, unpackPremultipliedAlpha); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); + return texture; + } catch (e) { + gl.deleteTexture(texture); + return null; + } + } + + /** + * Delete a texture + * @param {WebGLTexture} texture - The texture to delete + */ + deleteTexture(texture) { + if (this._gl && texture) { + this._gl.deleteTexture(texture); + } + } + + /** + * Set image smoothing enabled state + * @param {Boolean} enabled - Whether image smoothing is enabled + */ + setImageSmoothingEnabled(enabled) { + this._imageSmoothingEnabled = !!enabled; + } + + /** + * Set unpack with premultiplied alpha state + * @param {Boolean} enabled - Whether to use premultiplied alpha + */ + setUnpackWithPremultipliedAlpha(enabled) { + this._unpackWithPremultipliedAlpha = !!enabled; + if (this._gl) { + this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this._unpackWithPremultipliedAlpha); + } + } + + /** + * Make a quad vertex buffer + * @param {Number} left - Left coordinate + * @param {Number} right - Right coordinate + * @param {Number} top - Top coordinate + * @param {Number} bottom - Bottom coordinate + * @returns {Float32Array} Vertex buffer + */ + makeQuadVertexBuffer(left, right, top, bottom) { + return new Float32Array([ + left, bottom, + right, bottom, + left, top, + left, top, + right, bottom, + right, top]); + } + + /** + * Create the first pass shader program + * @private + */ + _makeFirstPassShaderProgram() { + const numTextures = this._glNumTextures = this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS); + const makeMatrixUniforms = () => { + return [...Array(numTextures).keys()].map(index => `uniform mat3 u_matrix_${index};`).join('\n'); + }; + const makeConditionals = () => { + return [...Array(numTextures).keys()].map(index => `${index > 0 ? 'else ' : ''}if(int(a_index) == ${index}) { transform_matrix = u_matrix_${index}; }`).join('\n'); + }; + + const vertexShaderProgram = ` + attribute vec2 a_output_position; + attribute vec2 a_texture_position; + attribute float a_index; + + ${makeMatrixUniforms()} // create a uniform mat3 for each potential tile to draw + + varying vec2 v_texture_position; + varying float v_image_index; + + void main() { + + mat3 transform_matrix; // value will be set by the if/elses in makeConditional() + + ${makeConditionals()} + + gl_Position = vec4(transform_matrix * vec3(a_output_position, 1), 1); + + v_texture_position = a_texture_position; + v_image_index = a_index; + } + `; + + const fragmentShaderProgram = ` + precision mediump float; + + // our textures + uniform sampler2D u_images[${numTextures}]; + // our opacities + uniform float u_opacities[${numTextures}]; + + // the varyings passed in from the vertex shader. + varying vec2 v_texture_position; + varying float v_image_index; + + void main() { + // can't index directly with a variable, need to use a loop iterator hack + for(int i = 0; i < ${numTextures}; ++i){ + if(i == int(v_image_index)){ + gl_FragColor = texture2D(u_images[i], v_texture_position) * u_opacities[i]; + } + } + } + `; + + const gl = this._gl; + + const program = this._initShaderProgram(gl, vertexShaderProgram, fragmentShaderProgram); + gl.useProgram(program); + + // get locations of attributes and uniforms, and create buffers for each attribute + this._firstPass = { + shaderProgram: program, + aOutputPosition: gl.getAttribLocation(program, 'a_output_position'), + aTexturePosition: gl.getAttribLocation(program, 'a_texture_position'), + aIndex: gl.getAttribLocation(program, 'a_index'), + uTransformMatrices: [...Array(this._glNumTextures).keys()].map(i=>gl.getUniformLocation(program, `u_matrix_${i}`)), + uImages: gl.getUniformLocation(program, 'u_images'), + uOpacities: gl.getUniformLocation(program, 'u_opacities'), + bufferOutputPosition: gl.createBuffer(), + bufferTexturePosition: gl.createBuffer(), + bufferIndex: gl.createBuffer(), + }; + + gl.uniform1iv(this._firstPass.uImages, [...Array(numTextures).keys()]); + + // provide coordinates for the rectangle in output space, i.e. a unit quad for each one. + const outputQuads = new Float32Array(numTextures * 12); + for(let i = 0; i < numTextures; ++i){ + outputQuads.set(Float32Array.from(this._unitQuad), i * 12); + } + gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferOutputPosition); + gl.bufferData(gl.ARRAY_BUFFER, outputQuads, gl.STATIC_DRAW); // bind data statically here, since it's unchanging + gl.enableVertexAttribArray(this._firstPass.aOutputPosition); + + // provide texture coordinates for the rectangle in image (texture) space. Data will be set later. + gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition); + gl.enableVertexAttribArray(this._firstPass.aTexturePosition); + + // for each vertex, provide an index into the array of textures/matrices to use for the correct tile + gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferIndex); + const indices = [...Array(this._glNumTextures).keys()].map(i => Array(6).fill(i)).flat(); // repeat each index 6 times, for the 6 vertices per tile (2 triangles) + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(indices), gl.STATIC_DRAW); // bind data statically here, since it's unchanging + gl.enableVertexAttribArray(this._firstPass.aIndex); + } + + /** + * Create the second pass shader program + * @private + */ + _makeSecondPassShaderProgram() { + const vertexShaderProgram = ` + attribute vec2 a_output_position; + attribute vec2 a_texture_position; + + varying vec2 v_texture_position; + + void main() { + // Transform to clip space (0:1 --> -1:1) + gl_Position = vec4(vec3(a_output_position * 2.0 - 1.0, 1), 1); + + v_texture_position = a_texture_position; + } + `; + + const fragmentShaderProgram = ` + precision mediump float; + + // our texture + uniform sampler2D u_image; + + // the texCoords passed in from the vertex shader. + varying vec2 v_texture_position; + + // the opacity multiplier for the image + uniform float u_opacity_multiplier; + + void main() { + gl_FragColor = texture2D(u_image, v_texture_position); + gl_FragColor *= u_opacity_multiplier; + } + `; + + const gl = this._gl; + + const program = this._initShaderProgram(gl, vertexShaderProgram, fragmentShaderProgram); + gl.useProgram(program); + + // get locations of attributes and uniforms, and create buffers for each attribute + this._secondPass = { + shaderProgram: program, + aOutputPosition: gl.getAttribLocation(program, 'a_output_position'), + aTexturePosition: gl.getAttribLocation(program, 'a_texture_position'), + uImage: gl.getUniformLocation(program, 'u_image'), + uOpacityMultiplier: gl.getUniformLocation(program, 'u_opacity_multiplier'), + bufferOutputPosition: gl.createBuffer(), + bufferTexturePosition: gl.createBuffer(), + }; + + // provide coordinates for the rectangle in output space, i.e. a unit quad for each one. + gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferOutputPosition); + gl.bufferData(gl.ARRAY_BUFFER, this._unitQuad, gl.STATIC_DRAW); // bind data statically here since it's unchanging + gl.enableVertexAttribArray(this._secondPass.aOutputPosition); + + // provide texture coordinates for the rectangle in image (texture) space. + gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferTexturePosition); + gl.bufferData(gl.ARRAY_BUFFER, this._unitQuad, gl.DYNAMIC_DRAW); // bind data statically here since it's unchanging + gl.enableVertexAttribArray(this._secondPass.aTexturePosition); + } + + /** + * Destroy the WebGL context and all resources + */ + destroy() { + if (this._destroyed) { + return; + } + this._destroyed = true; + + const gl = this._gl; + if (gl) { + try { + // adapted from https://stackoverflow.com/a/23606581/1214731 + const numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); + if (numTextureUnits && numTextureUnits > 0) { + for (let unit = 0; unit < numTextureUnits; ++unit) { + gl.activeTexture(gl.TEXTURE0 + unit); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.bindTexture(gl.TEXTURE_CUBE_MAP, null); + } + } + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + // Delete all our created resources + if (this._secondPass && this._secondPass.bufferOutputPosition) { + gl.deleteBuffer(this._secondPass.bufferOutputPosition); + } + if (this._glFrameBuffer) { + gl.deleteFramebuffer(this._glFrameBuffer); + } + } catch (e) { + // Context may already be lost, continue with cleanup + $.console.warn('Error during WebGL cleanup in WebglContextManager.destroy():', e); + } + + const ext = gl.getExtension('WEBGL_lose_context'); + if (ext) { + ext.loseContext(); + } + } + + // Clean up references + this._gl = null; + this._firstPass = null; + this._secondPass = null; + this._glFrameBuffer = null; + this._renderToTexture = null; + this._unitQuad = null; + } + + /** + * Check if this context manager has been destroyed + * @returns {Boolean} true if destroyed, false otherwise + */ + isDestroyed() { + return this._destroyed; + } + } + + /** + * @class OpenSeadragon.WebGLDrawer + * @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. The WebGLDrawer + * defines its own data type that ensures textures are correctly loaded to and deleted from the GPU memory. + * The drawer utilizes a context-dependent two pass drawing pipeline. For the first pass, tile composition + * for a given TiledImage is always done using a canvas with a WebGL context. This allows tiles to be stitched + * together without seams or artifacts, without requiring a tile source with overlap. If overlap is present, + * overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output + * canvas with a Context2d context. This allows applications to have access to pixel data and other functionality + * provided by Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options, + * including compositeOperation, clip, croppingPolygons, and debugMode are implemented using Context2d operations; + * in these scenarios, each TiledImage is drawn onto the output canvas immediately after the tile composition step + * (pass 1). Otherwise, for efficiency, all TiledImages are copied over to the output canvas at once, after all + * tiles have been composited for all images. + * @param {Object} options - Options for this Drawer. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. + * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. + * @param {Element} options.element - Parent element. + * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. + * @param {Boolean} [options.unpackWithPremultipliedAlpha=false] - Whether to enable gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL when uploading textures. + */ + OpenSeadragon.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{ + constructor(options){ + super(options); + + /** + * The HTML element (canvas) that this drawer uses for drawing + * @member {Element} canvas + * @memberof OpenSeadragon.WebGLDrawer# + */ + + /** + * The parent element of this Drawer instance, passed in when the Drawer was created. + * The parent of {@link OpenSeadragon.WebGLDrawer#canvas}. + * @member {Element} container + * @memberof OpenSeadragon.WebGLDrawer# + */ + + // private members + this._destroyed = false; + /** + * WebGL context manager instance + * @member {WebglContextManager} _glContext + * @memberof OpenSeadragon.WebGLDrawer# + * @private + */ + this._glContext = null; + /** + * Flag to enable/disable automatic WebGL context re-initialization on context loss. + * When enabled, the drawer will attempt to recover from context exhaustion errors. + * @member {Boolean} _enableContextRecovery + * @memberof OpenSeadragon.WebGLDrawer# + * @private + */ + this._enableContextRecovery = true; + this._outputCanvas = null; + this._outputContext = null; + this._clippingCanvas = null; + this._clippingContext = null; + this._renderingCanvas = null; + this._backupCanvasDrawer = null; + this._canvasFallbackAllowed = this.viewer.drawerCandidates && this.viewer.drawerCandidates.includes('canvas'); + + this._imageSmoothingEnabled = true; // will be updated by setImageSmoothingEnabled + this._unpackWithPremultipliedAlpha = !!this.options.unpackWithPremultipliedAlpha; + + // Reject listening for the tile-drawing and tile-drawn events, which this drawer does not fire + this.viewer.rejectEventHandler("tile-drawn", "The WebGLDrawer does not raise the tile-drawn event"); + this.viewer.rejectEventHandler("tile-drawing", "The WebGLDrawer does not raise the tile-drawing event"); + + // this.viewer and this.canvas are part of the public DrawerBase API + // and are defined by the parent DrawerBase class. Additional setup is done by + // the private _setupCanvases and _setupRenderer functions. + this._setupCanvases(); + this._setupRenderer(); + + this._supportedFormats = ["context2d", "image"]; + this.context = this._outputContext; // API required by tests + } + + get defaultOptions() { + return { + // use detached cache: our type conversion will not collide (and does not have to preserve CPU data ref) + usePrivateCache: true, + preloadCache: false, + unpackWithPremultipliedAlpha: false, + }; + } + + getSupportedDataFormats() { + return this._supportedFormats; + } + + // Public API required by all Drawer implementations + /** + * Clean up the renderer, removing all resources + */ + destroy(){ + if(this._destroyed){ + return; + } + super.destroy(); + // Remove the resize handler to prevent memory leaks + if (this._resizeHandler) { + this.viewer.removeHandler("resize", this._resizeHandler); + this._resizeHandler = null; + } + + // Destroy WebGL context manager + if (this._glContext) { + this._glContext.destroy(); + this._glContext = null; + } + + // make canvases 1 x 1 px and delete references + if (this._renderingCanvas) { + this._renderingCanvas.width = this._renderingCanvas.height = 1; + } + if (this._clippingCanvas) { + this._clippingCanvas.width = this._clippingCanvas.height = 1; + } + if (this._outputCanvas) { + this._outputCanvas.width = this._outputCanvas.height = 1; + } + this._renderingCanvas = null; + this._clippingCanvas = this._clippingContext = null; + this._outputCanvas = this._outputContext = null; + + if(this._backupCanvasDrawer){ + this._backupCanvasDrawer.destroy(); + this._backupCanvasDrawer = null; + } + + this.container.removeChild(this.canvas); + if(this.viewer.drawer === this){ + this.viewer.drawer = null; + } + + this.destroyInternalCache(); + + // set our destroyed flag to true + this._destroyed = true; + } + + // Public API required by all Drawer implementations + /** + * + * @returns {Boolean} true + */ + canRotate(){ + return true; + } + + // Public API required by all Drawer implementations + /** + * Functional test: true if WebGL is supported and the real first-pass shader pipeline + * can render (same shaders/context path used at runtime). Uses a temp context and + * WebglContextManager, draws known non-black pixels to an FBO, then readPixels. + * @returns {Boolean} true if WebGL is supported and the pipeline renders successfully + */ + static isSupported(){ + let contextManager = null; + let testTexture = null; + let gl = null; + try { + const size = 4; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + if (!$.isFunction(canvas.getContext)) { + return false; + } + gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); + if (!gl) { + return false; + } + contextManager = new WebglContextManager({ + renderingCanvas: canvas, + unpackWithPremultipliedAlpha: false, + imageSmoothingEnabled: true, + initShaderProgram: WebGLDrawer.initShaderProgram + }); + if (!contextManager.getContext()) { + return false; + } + contextManager.setupRenderer(size, size); + + const firstPass = contextManager.getFirstPass(); + const glFrameBuffer = contextManager.getFrameBuffer(); + if (!firstPass || !glFrameBuffer) { + return false; + } + + const maxTextures = contextManager.getMaxTextures(); + if (!maxTextures || maxTextures <= 0) { + return false; + } + + const imageData = new ImageData(size, size); + imageData.data[0] = 255; + imageData.data[1] = 0; + imageData.data[2] = 0; + imageData.data[3] = 255; + testTexture = contextManager.createTexture(imageData); + if (!testTexture) { + return false; + } + + const unitQuad = contextManager.makeQuadVertexBuffer(0, 1, 0, 1); + gl.viewport(0, 0, size, size); + gl.bindFramebuffer(gl.FRAMEBUFFER, glFrameBuffer); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.useProgram(firstPass.shaderProgram); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, testTexture); + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferTexturePosition); + gl.bufferData(gl.ARRAY_BUFFER, unitQuad, gl.DYNAMIC_DRAW); + const ndcMatrix = new Float32Array([2, 0, 0, 0, 2, 0, -1, -1, 1]); + gl.uniformMatrix3fv(firstPass.uTransformMatrices[0], false, ndcMatrix); + gl.uniform1fv(firstPass.uOpacities, new Float32Array([1])); + + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferOutputPosition); + gl.vertexAttribPointer(firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferTexturePosition); + gl.vertexAttribPointer(firstPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferIndex); + gl.vertexAttribPointer(firstPass.aIndex, 1, gl.FLOAT, false, 0, 0); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + + const pixels = new Uint8Array(size * size * 4); + gl.readPixels(0, 0, size, size, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + const hasNonZero = pixels.some(v => v !== 0); + if (!hasNonZero) { + $.console.warn('[WebGLDrawer.isSupported] Functional test failed: no non-zero pixels read back.'); + return false; + } + return true; + } catch (e) { + $.console.warn('[WebGLDrawer.isSupported] Functional test failed:', e && e.message ? e.message : e); + return false; + } finally { + try { + if (testTexture && contextManager) { + contextManager.deleteTexture(testTexture); + } + if (contextManager) { + contextManager.destroy(); + } else if (gl) { + const ext = gl.getExtension('WEBGL_lose_context'); + if (ext) { + ext.loseContext(); + } + } + } catch (cleanupErr) { + // ignore cleanup errors so we preserve the test result + } + } + } + + /** + * + * @returns {string} 'webgl' + */ + getType(){ + return 'webgl'; + } + + /** + * Check if the drawer is using WebGL2 + * @returns {Boolean} true if WebGL2 is being used, false if WebGL1 + */ + isWebGL2(){ + return this._glContext ? this._glContext.isWebGL2() : false; + } + + /** + * Enable or disable automatic WebGL context re-initialization on context loss. + * When enabled, the drawer will attempt to recover from context exhaustion errors + * by re-initializing the WebGL context. + * @param {Boolean} enabled - true to enable recovery, false to disable + */ + setContextRecoveryEnabled(enabled) { + this._enableContextRecovery = !!enabled; + } + + /** + * Check if context recovery is enabled + * @returns {Boolean} true if recovery is enabled, false otherwise + */ + isContextRecoveryEnabled() { + return this._enableContextRecovery; + } + + /** + * @param {TiledImage} tiledImage the tiled image that is calling the function + * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams. + * @private + */ + minimumOverlapRequired(tiledImage) { + // return true if we cannot render with webgl, since the backup canvas drawer will be used. + return tiledImage.hasIssue('webgl'); + } + + /** + * create the HTML element (canvas in this case) that the image will be drawn into + * @private + * @returns {Element} the canvas to draw into + */ + _createDrawingElement(){ + const canvas = $.makeNeutralElement("canvas"); + const viewportSize = this._calculateCanvasSize(); + canvas.width = viewportSize.x; + canvas.height = viewportSize.y; + return canvas; + } + + /** + * Get the backup renderer (CanvasDrawer) to use if data cannot be used by webgl + * Lazy loaded + * @private + * @returns {CanvasDrawer} + */ + _getBackupCanvasDrawer(){ + if(!this._backupCanvasDrawer){ + this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false}); + this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden'); + this._backupCanvasDrawer.getSupportedDataFormats = () => this._supportedFormats; + this._backupCanvasDrawer.getDataToDraw = this.getDataToDraw.bind(this); + } + + return this._backupCanvasDrawer; + } + + // + /** + * Internal draw method, wrapped in a try/catch within draw() + * @param {Array} tiledImages Array of TiledImage objects to draw + * @param {Boolean} [isRetry=false] Internal flag to prevent infinite retry loops + * @private + */ + _draw(tiledImages, isRetry = false){ + const gl = this._glContext ? this._glContext.getContext() : null; + if (!gl) { + return; + } + const firstPass = this._glContext.getFirstPass(); + const secondPass = this._glContext.getSecondPass(); + const glFrameBuffer = this._glContext.getFrameBuffer(); + const renderToTexture = this._glContext.getRenderToTexture(); + const bounds = this.viewport.getBoundsNoRotateWithMargins(true); + const view = { + bounds: bounds, + center: new OpenSeadragon.Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2), + rotation: this.viewport.getRotation(true) * Math.PI / 180 + }; + + const flipMultiplier = this.viewport.flipped ? -1 : 1; + // calculate view matrix for viewer + const posMatrix = $.Mat3.makeTranslation(-view.center.x, -view.center.y); + const scaleMatrix = $.Mat3.makeScaling(2 / view.bounds.width * flipMultiplier, -2 / view.bounds.height); + const rotMatrix = $.Mat3.makeRotation(-view.rotation); + const viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer + + // clear the output canvas + this._outputContext.clearRect(0, 0, this._outputCanvas.width, this._outputCanvas.height); + + + let renderingBufferHasImageData = false; + + //iterate over tiled images and draw each one using a two-pass rendering pipeline if needed + tiledImages.forEach( (tiledImage, tiledImageIndex) => { + + if(tiledImage.getIssue('webgl')){ + // first, draw any data left in the rendering buffer onto the output canvas + if(renderingBufferHasImageData){ + this._outputContext.drawImage(this._renderingCanvas, 0, 0); + // clear the buffer + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer + renderingBufferHasImageData = false; + } + + // next, use the backup canvas drawer to draw the tiled image (if allowed) + if(this._canvasFallbackAllowed){ + const canvasDrawer = this._getBackupCanvasDrawer(); + canvasDrawer.draw([tiledImage]); + this._outputContext.drawImage(canvasDrawer.canvas, 0, 0); + } + + } else { + const tilesToDraw = tiledImage.getTilesToDraw(); + + if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) { + this._drawPlaceholder(tiledImage); + } + + if(tilesToDraw.length === 0 || tiledImage.getOpacity() === 0){ + return; + } + const firstTile = tilesToDraw[0]; + + const useContext2dPipeline = ( tiledImage.compositeOperation || + this.viewer.compositeOperation || + tiledImage._clip || + tiledImage._croppingPolygons || + tiledImage.debugMode + ); + + const useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1) || firstTile.tile.hasTransparency; + + // using the context2d pipeline requires a clean rendering (back) buffer to start + if(useContext2dPipeline){ + // if the rendering buffer has image data currently, write it to the output canvas now and clear it + + if(renderingBufferHasImageData){ + this._outputContext.drawImage(this._renderingCanvas, 0, 0); + } + + // clear the buffer + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer + } + + // First rendering pass: compose tiles that make up this tiledImage + gl.useProgram(firstPass.shaderProgram); + + // bind to the framebuffer for render-to-texture if using two-pass rendering, otherwise back buffer (null) + if(useTwoPassRendering){ + gl.bindFramebuffer(gl.FRAMEBUFFER, glFrameBuffer); + // clear the buffer to draw a new image + gl.clear(gl.COLOR_BUFFER_BIT); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + // no need to clear, just draw on top of the existing pixels + } + + let overallMatrix = viewMatrix; + + const imageRotation = tiledImage.getRotation(true); + // if needed, handle the tiledImage being rotated + if( imageRotation % 360 !== 0){ + const imageRotationMatrix = $.Mat3.makeRotation(-imageRotation * Math.PI / 180); + const imageCenter = tiledImage.getBoundsNoRotate(true).getCenter(); + const t1 = $.Mat3.makeTranslation(imageCenter.x, imageCenter.y); + const t2 = $.Mat3.makeTranslation(-imageCenter.x, -imageCenter.y); + + // update the view matrix to account for this image's rotation + const localMatrix = t1.multiply(imageRotationMatrix).multiply(t2); + overallMatrix = viewMatrix.multiply(localMatrix); + } + + // Check MAX_TEXTURE_IMAGE_UNITS - throw error if invalid (will be caught by outer try-catch) + const maxTextures = this._glContext.getMaxTextures(); + if(maxTextures <= 0 || maxTextures === null || maxTextures === undefined){ + // This can apparently happen on some systems if too many WebGL contexts have been created + // in which case maxTextures can be null, leading to out of bounds errors with the array. + // For example, when viewers were created and not destroyed in the test suite, this error + // occurred in the TravisCI tests, though it did not happen when testing locally either in + // a browser or on the command line via grunt test. + + throw new Error(`WebGL error: bad value for gl parameter MAX_TEXTURE_IMAGE_UNITS (${maxTextures}). This could happen + if too many contexts have been created and not released, or there is another problem with the graphics card.`); + } + + const texturePositionArray = new Float32Array(maxTextures * 12); // 6 vertices (2 triangles) x 2 coordinates per vertex + const textureDataArray = new Array(maxTextures); + const matrixArray = new Array(maxTextures); + const opacityArray = new Array(maxTextures); + + // iterate over tiles and add data for each one to the buffers + for(let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){ + const tile = tilesToDraw[tileIndex].tile; + const indexInDrawArray = tileIndex % maxTextures; + const numTilesToDraw = indexInDrawArray + 1; + const textureInfo = this.getDataToDraw(tile); + + if (textureInfo && textureInfo.texture) { + this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray); + } + // else { + // If the texture info is not available, we cannot draw this tile. This is either because + // the tile data is still being processed, or the data was not correct - in that case, + // internalCacheCreate(..) already logged an error. + // } + + if( (numTilesToDraw === maxTextures) || (tileIndex === tilesToDraw.length - 1)){ + // We've filled up the buffers: time to draw this set of tiles + + // bind each tile's texture to the appropriate gl.TEXTURE# + for(let i = 0; i < numTilesToDraw; i++){ + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, textureDataArray[i]); + } + + // set the buffer data for the texture coordinates to use for each tile + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferTexturePosition); + gl.bufferData(gl.ARRAY_BUFFER, texturePositionArray, gl.DYNAMIC_DRAW); + + // set the transform matrix uniform for each tile + matrixArray.forEach( (matrix, index) => { + gl.uniformMatrix3fv(firstPass.uTransformMatrices[index], false, matrix); + }); + // set the opacity uniform for each tile + gl.uniform1fv(firstPass.uOpacities, new Float32Array(opacityArray)); + + // bind vertex buffers and (re)set attributes before calling gl.drawArrays() + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferOutputPosition); + gl.vertexAttribPointer(firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferTexturePosition); + gl.vertexAttribPointer(firstPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, firstPass.bufferIndex); + gl.vertexAttribPointer(firstPass.aIndex, 1, gl.FLOAT, false, 0, 0); + + // Draw! 6 vertices per tile (2 triangles per rectangle) + gl.drawArrays(gl.TRIANGLES, 0, 6 * numTilesToDraw ); + } + } + + if(useTwoPassRendering){ + // Second rendering pass: Render the tiled image from the framebuffer into the back buffer + gl.useProgram(secondPass.shaderProgram); + + // set the rendering target to the back buffer (null) + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + // bind the rendered texture from the first pass to use during this second pass + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, renderToTexture); + + // set opacity to the value for the current tiledImage + gl.uniform1f(secondPass.uOpacityMultiplier, tiledImage.opacity); + + // bind buffers and set attributes before calling gl.drawArrays + gl.bindBuffer(gl.ARRAY_BUFFER, secondPass.bufferTexturePosition); + gl.vertexAttribPointer(secondPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, secondPass.bufferOutputPosition); + gl.vertexAttribPointer(secondPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0); + + // Draw the quad (two triangles) + gl.drawArrays(gl.TRIANGLES, 0, 6); + + } + + renderingBufferHasImageData = true; + + if(useContext2dPipeline){ + // draw from the rendering canvas onto the output canvas, clipping/cropping if needed. + this._applyContext2dPipeline(tiledImage, tilesToDraw, tiledImageIndex); + renderingBufferHasImageData = false; + // clear the buffer + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer + } + + // after drawing the first TiledImage, fire the tiled-image-drawn event (for testing) + if(tiledImageIndex === 0){ + this._raiseTiledImageDrawnEvent(tiledImage, tilesToDraw.map(info=>info.tile)); + } + } + + }); + + if(renderingBufferHasImageData){ + this._outputContext.drawImage(this._renderingCanvas, 0, 0); + } + } + /** + * + * @param {Array} tiledImages Array of TiledImage objects to draw + * @param {Boolean} [isRetry=false] Internal flag to prevent infinite retry loops + */ + draw(tiledImages, isRetry = false){ + try { + this._draw(tiledImages, isRetry); + } catch (error) { + // Handle WebGL context errors that occur at any point during the draw operation + if (this._isWebGLContextError(error)) { + // Try recovery if enabled and not a retry + if (this._enableContextRecovery && !isRetry) { + $.console.warn('WebGL context error detected during draw operation, attempting to recreate context...', error); + const recreatedDrawer = this._recreateContext(); + if (recreatedDrawer) { + $.console.info('WebGL context recreated successfully, retrying draw operation'); + // Raise event for successful recovery + if (this.viewer) { + /** + * Raised when the WebGL drawer successfully recovers from a context loss. + * + * @event webgl-context-recovered + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.WebGLDrawer} drawer - The drawer instance (same instance, context recreated). + * @property {Error} error - The original error that triggered the recovery. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('webgl-context-recovered', { + drawer: this, + error: error + }); + } + // Retry draw on same instance + this.draw(tiledImages, true); + } else { + // Recovery attempted but failed - fall back to canvas drawer (if allowed) + this._fallbackToCanvasDrawer(error, tiledImages); + } + } else { + // Recovery disabled or retry - fall back only when recovery was enabled (retry case) + if (this._enableContextRecovery) { + this._fallbackToCanvasDrawer(error, tiledImages); // will only happen if canvas fallback is allowed + } else { + throw error; + } + } + } else { + // Not a WebGL context error - re-throw + throw error; + } + } + } + + // Public API required by all Drawer implementations + /** + * Sets whether image smoothing is enabled or disabled + * @param {Boolean} enabled If true, uses gl.LINEAR as the TEXTURE_MIN_FILTER and TEXTURE_MAX_FILTER, otherwise gl.NEAREST. + */ + setImageSmoothingEnabled(enabled){ + if( this._imageSmoothingEnabled !== enabled ){ + this._imageSmoothingEnabled = enabled; + if (this._glContext) { + this._glContext.setImageSmoothingEnabled(enabled); + } + this.setInternalCacheNeedsRefresh(); + this.viewer.forceRedraw(); + } + } + + /** + * Sets whether textures are unpacked with premultiplied alpha + * @param {Boolean} enabled If true, sets gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL to true. + */ + setUnpackWithPremultipliedAlpha(enabled){ + if (this._unpackWithPremultipliedAlpha !== enabled){ + this._unpackWithPremultipliedAlpha = enabled; + if (this._glContext) { + this._glContext.setUnpackWithPremultipliedAlpha(enabled); + } + this.setInternalCacheNeedsRefresh(); + this.viewer.forceRedraw(); + } + } + + /** + * Draw a rect onto the output canvas for debugging purposes + * @param {OpenSeadragon.Rect} rect + */ + drawDebuggingRect(rect){ + const context = this._outputContext; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.strokeStyle = this.debugGridColor[0]; + context.fillStyle = this.debugGridColor[0]; + + context.strokeRect( + rect.x * $.pixelDensityRatio, + rect.y * $.pixelDensityRatio, + rect.width * $.pixelDensityRatio, + rect.height * $.pixelDensityRatio + ); + + context.restore(); + } + + /** + * Draw data from the rendering canvas onto the output canvas, with clipping, + * cropping and/or debug info as requested. + * @private + * @param {OpenSeadragon.TiledImage} tiledImage - the tiledImage to draw + * @param {Array} tilesToDraw - array of objects containing tiles that were drawn + */ + _applyContext2dPipeline(tiledImage, tilesToDraw, tiledImageIndex){ + // composite onto the output canvas, clipping if necessary + this._outputContext.save(); + + // set composite operation; ignore for first image drawn + this._outputContext.globalCompositeOperation = tiledImageIndex === 0 ? null : tiledImage.compositeOperation || this.viewer.compositeOperation; + if(tiledImage._croppingPolygons || tiledImage._clip){ + this._renderToClippingCanvas(tiledImage); + this._outputContext.drawImage(this._clippingCanvas, 0, 0); + + } else { + this._outputContext.drawImage(this._renderingCanvas, 0, 0); + } + this._outputContext.restore(); + if(tiledImage.debugMode){ + const flipped = this.viewer.viewport.getFlip(); + if(flipped){ + this._flip(); + } + this._drawDebugInfo(tilesToDraw, tiledImage, flipped); + if(flipped){ + this._flip(); + } + } + + + } + + // private + _getTileData(tile, tiledImage, textureInfo, viewMatrix, index, texturePositionArray, textureDataArray, matrixArray, opacityArray){ + + const texture = textureInfo.texture; + const textureQuad = textureInfo.position; + const overlapFraction = textureInfo.overlapFraction; + + // set the position of this texture + texturePositionArray.set(textureQuad, index * 12); + + // compute offsets that account for tile overlap; needed for calculating the transform matrix appropriately + const xOffset = tile.positionedBounds.width * overlapFraction.x; + const yOffset = tile.positionedBounds.height * overlapFraction.y; + const x = tile.positionedBounds.x + (tile.x === 0 ? 0 : xOffset); + const y = tile.positionedBounds.y + (tile.y === 0 ? 0 : yOffset); + const right = tile.positionedBounds.x + tile.positionedBounds.width - (tile.isRightMost ? 0 : xOffset); + const bottom = tile.positionedBounds.y + tile.positionedBounds.height - (tile.isBottomMost ? 0 : yOffset); + + const model = new $.Mat3([ + right - x, 0, 0, // right - x = width + 0, bottom - y, 0, // bottom - y = height + x, y, 1 + ]); + + if (tile.flipped) { + // For documentation: + // // flip the tile around the center of the unit quad + // let t1 = $.Mat3.makeTranslation(0.5, 0); + // let t2 = $.Mat3.makeTranslation(-0.5, 0); + // + // // update the view matrix to account for this image's rotation + // let localMatrix = t1.multiply($.Mat3.makeScaling(-1, 1)).multiply(t2); + // matrix = matrix.multiply(localMatrix); + + //Optimized: this works since matrix only contains main diagonal values & translation + model.scaleAndTranslateSelf(-1, 1, 1, 0); + } + + model.scaleAndTranslateOtherSetSelf(viewMatrix); + opacityArray[index] = tile.opacity; + textureDataArray[index] = texture; + matrixArray[index] = model.values; + } + + + // private + _setupRenderer(){ + if(!this._glContext || !this._glContext.getContext()){ + $.console.error('_setupCanvases must be called before _setupRenderer'); + return; + } + this._glContext.setupRenderer(this._renderingCanvas.width, this._renderingCanvas.height); + } + + + // private + _resizeRenderer(){ + if(!this._glContext){ + return; + } + this._glContext.resizeRenderer(this._renderingCanvas.width, this._renderingCanvas.height); + } + + // private + _setupCanvases(){ + const _this = this; + + this._outputCanvas = this.canvas; //output canvas + this._outputContext = this._outputCanvas.getContext('2d'); + + this._renderingCanvas = document.createElement('canvas'); + + this._clippingCanvas = document.createElement('canvas'); + this._clippingContext = this._clippingCanvas.getContext('2d'); + this._renderingCanvas.width = this._clippingCanvas.width = this._outputCanvas.width; + this._renderingCanvas.height = this._clippingCanvas.height = this._outputCanvas.height; + + // Create WebGL context manager + this._glContext = new WebglContextManager({ + renderingCanvas: this._renderingCanvas, + unpackWithPremultipliedAlpha: this._unpackWithPremultipliedAlpha, + imageSmoothingEnabled: this._imageSmoothingEnabled, + initShaderProgram: this.constructor.initShaderProgram + }); + + this._resizeHandler = function(){ + + if(_this._outputCanvas !== _this.viewer.drawer.canvas){ + _this._outputCanvas.style.width = _this.viewer.drawer.canvas.clientWidth + 'px'; + _this._outputCanvas.style.height = _this.viewer.drawer.canvas.clientHeight + 'px'; + } + + const viewportSize = _this._calculateCanvasSize(); + if( _this._outputCanvas.width !== viewportSize.x || + _this._outputCanvas.height !== viewportSize.y ) { + _this._outputCanvas.width = viewportSize.x; + _this._outputCanvas.height = viewportSize.y; + } + + _this._renderingCanvas.style.width = _this._outputCanvas.clientWidth + 'px'; + _this._renderingCanvas.style.height = _this._outputCanvas.clientHeight + 'px'; + _this._renderingCanvas.width = _this._clippingCanvas.width = _this._outputCanvas.width; + _this._renderingCanvas.height = _this._clippingCanvas.height = _this._outputCanvas.height; + + // important - update the size of the rendering viewport! + _this._resizeRenderer(); + }; + + //make the additional canvas elements mirror size changes to the output canvas + this.viewer.addHandler("resize", this._resizeHandler); + } + + + /** + * Check if an error is related to WebGL context issues. + * @param {Error} error - The error to check + * @returns {Boolean} true if the error is a WebGL context error, false otherwise + * @private + */ + _isWebGLContextError(error) { + if (!error || !error.message) { + return false; + } + const message = error.message.toLowerCase(); + return message.includes('max_texture_image_units') || + (message.includes('webgl') && ((message.includes('context') || message.includes('lost') || message.includes('invalid')))); + } + + /** + * Recreate the WebGL context when it has been lost or exhausted. + * This method recreates only the WebglContextManager, preserving the drawer instance + * and all drawer state (canvases, options, cache, etc.). + * @returns {OpenSeadragon.WebGLDrawer|null} The same drawer instance if successful, null otherwise + * @private + */ + _recreateContext() { + if (this._destroyed) { + return null; + } + + try { + // Store old canvas properties + const oldCanvas = this._renderingCanvas; + const oldWidth = oldCanvas.width; + const oldHeight = oldCanvas.height; + const oldStyleWidth = oldCanvas.style.width; + const oldStyleHeight = oldCanvas.style.height; + + // Destroy internal cache FIRST (while old context still exists) + // This ensures textures are freed using the old context before it's destroyed + this.destroyInternalCache(); + + // Destroy old context manager + if (this._glContext) { + this._glContext.destroy(); + this._glContext = null; + } + + // Note: destroyInternalCache() above already properly cleaned up all texture + // and glContext references via internalCacheFree() callbacks + + // Create new rendering canvas element + this._renderingCanvas = document.createElement('canvas'); + this._renderingCanvas.width = oldWidth; + this._renderingCanvas.height = oldHeight; + if (oldStyleWidth) { + this._renderingCanvas.style.width = oldStyleWidth; + } + if (oldStyleHeight) { + this._renderingCanvas.style.height = oldStyleHeight; + } + + // Create new context manager with new canvas + this._glContext = new WebglContextManager({ + renderingCanvas: this._renderingCanvas, + unpackWithPremultipliedAlpha: this._unpackWithPremultipliedAlpha, + imageSmoothingEnabled: this._imageSmoothingEnabled, + initShaderProgram: this.constructor.initShaderProgram + }); + + // Verify context is valid + if (!this._glContext.getContext()) { + $.console.error('Failed to recreate WebGL context: no GL context'); + return null; + } + + // Check if the new context has valid MAX_TEXTURE_IMAGE_UNITS + try { + const maxTextures = this._glContext.getMaxTextures(); + if (!maxTextures || maxTextures <= 0) { + $.console.error('Failed to recreate WebGL context: invalid MAX_TEXTURE_IMAGE_UNITS'); + return null; + } + } catch (e) { + $.console.error('Failed to verify new WebGL context:', e); + return null; + } + + // Reinitialize renderer (shaders, framebuffers) + this._setupRenderer(); + + // Mark cache as needing refresh for future entries + // (Old entries were already freed above) + this.setInternalCacheNeedsRefresh(); + + return this; // Return same drawer instance + } catch (e) { + $.console.error('Failed to recreate WebGL context:', e); + return null; + } + } + + /** + * Fall back to canvas drawer when WebGL fails (requires viewer.drawerCandidates to include 'canvas'). + * If allowed, switches the viewer to use the canvas drawer, raises the webgl-context-recovery-failed event + * with the canvas drawer, and draws the current frame. + * Otherwise, raise the event with canvasDrawer: null and rethrow the error. + * + * @param {Error} error - The error that triggered the fallback + * @param {Array} tiledImages - Array of TiledImage objects to draw with the new drawer + * @throws {Error} Re-throws the error if canvas is not an allowed fallback or if canvas drawer creation fails + * @private + */ + _fallbackToCanvasDrawer(error, tiledImages) { + const oldWebGLDrawer = this; + if (!this._canvasFallbackAllowed) { + oldWebGLDrawer._raiseContextRecoveryFailedEvent(error, null); + throw error; + } + const canvasDrawer = this.viewer.requestDrawer('canvas', { + mainDrawer: true, + redrawImmediately: false + }); + + if (canvasDrawer) { + $.console.error('Failed to recreate WebGL context, switching to canvas drawer'); + oldWebGLDrawer._raiseContextRecoveryFailedEvent(error, canvasDrawer); + this.viewer.world.requestInvalidate(true); + } else { + $.console.error('Failed to create canvas drawer as fallback'); + oldWebGLDrawer._raiseContextRecoveryFailedEvent(error, null); + throw error; + } + } + + /** + * Raise the webgl-context-recovery-failed event. + * @param {Error} error - The error that triggered the recovery failure + * @param {OpenSeadragon.CanvasDrawer} [canvasDrawer=null] - The canvas drawer that was created as a fallback, or null if canvas was not an allowed fallback or creation failed + * @private + */ + _raiseContextRecoveryFailedEvent(error, canvasDrawer = null) { + if (!this.viewer) { + return; + } + /** + * Raised when the WebGL drawer fails to recover from a context loss. The drawer may fall back to + * canvas drawer only when canvas is in the viewer's drawer list; otherwise canvasDrawer is null and no switch occurs. + * + * @event webgl-context-recovery-failed + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.WebGLDrawer} drawer - The WebGL drawer instance that failed to recover (may be destroyed). + * @property {OpenSeadragon.CanvasDrawer} canvasDrawer - The canvas drawer that was created as a fallback, or null if canvas was not an allowed fallback or creation failed. + * @property {Error} error - The original error that triggered the recovery attempt. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('webgl-context-recovery-failed', { + drawer: this, + canvasDrawer: canvasDrawer, + error: error + }); + } + + internalCacheCreate(cache, tile) { + const tiledImage = tile.tiledImage; + const gl = this._glContext ? this._glContext.getContext() : null; + if (!gl) { + $.console.error('WebGL context not available in internalCacheCreate'); + return {}; + } + let texture; + let position; + + let data = cache.data; + let isCanvas = false; + if (data instanceof CanvasRenderingContext2D) { + data = data.canvas; + isCanvas = true; + } + + if (!tiledImage.getIssue('webgl')) { + if (isCanvas && $.isCanvasTainted(data)){ + tiledImage.setIssue('webgl', 'WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?'); + this._raiseDrawerErrorEvent(tiledImage, this._canvasFallbackAllowed ? + 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.' : + 'Tainted data cannot be used by the WebGLDrawer, and canvas fallback is not enabled.'); + this.setInternalCacheNeedsRefresh(); + } else { + let sourceWidthFraction, sourceHeightFraction; + if (tile.sourceBounds) { + sourceWidthFraction = Math.min(tile.sourceBounds.width, data.width) / data.width; + sourceHeightFraction = Math.min(tile.sourceBounds.height, data.height) / data.height; + } else { + sourceWidthFraction = 1; + sourceHeightFraction = 1; + } + + const overlap = tiledImage.source.tileOverlap; + const overlapFraction = this._calculateOverlapFraction(tile, tiledImage); + if( overlap > 0){ + // calculate the normalized position of the rect to actually draw + // discarding overlap. + const left = (tile.x === 0 ? 0 : overlapFraction.x) * sourceWidthFraction; + const top = (tile.y === 0 ? 0 : overlapFraction.y) * sourceHeightFraction; + const right = (tile.isRightMost ? 1 : 1 - overlapFraction.x) * sourceWidthFraction; + const bottom = (tile.isBottomMost ? 1 : 1 - overlapFraction.y) * sourceHeightFraction; + position = this._glContext.makeQuadVertexBuffer(left, right, top, bottom); + } else if (sourceWidthFraction === 1 && sourceHeightFraction === 1) { + // no overlap and no padding: this texture can use the unit quad as its position data + position = this._glContext.getUnitQuad(); + } else { + position = this._glContext.makeQuadVertexBuffer(0, sourceWidthFraction, 0, sourceHeightFraction); + } + + // create a gl Texture for this tile using the manager + texture = this._glContext.createTexture(data, { + unpackWithPremultipliedAlpha: this._unpackWithPremultipliedAlpha + }); + + if (!texture) { + tiledImage.setIssue('webgl', 'Error creating texture in WebGL.'); + const canvasAllowed = this._canvasFallbackAllowed; + this._raiseDrawerErrorEvent(tiledImage, canvasAllowed ? + 'Unknown error when creating texture. Falling back to CanvasDrawer for this TiledImage.' : + 'Cannot use WebGL for this TiledImage; canvas fallback is not enabled.'); + this.setInternalCacheNeedsRefresh(); + } else { + // TextureInfo stored in the cache + // Store reference to the context that created this texture + return { + texture: texture, + position: position, + overlapFraction: overlapFraction, + glContext: this._glContext // Store context reference for safe deletion + }; + } + } + } + if (data instanceof Image) { + const canvas = document.createElement( 'canvas' ); + canvas.width = data.width; + canvas.height = data.height; + const context = canvas.getContext('2d', { willReadFrequently: true }); + context.drawImage( data, 0, 0 ); + data = context; + } + if (data instanceof CanvasRenderingContext2D) { + return data; + } + $.console.error("Unsupported data used for WebGL Drawer - probably a bug!"); + return {}; + } + + internalCacheFree(data) { + if (data && data.texture) { + // Use the stored context reference if available, otherwise fall back to current context + const glContext = data.glContext || this._glContext; + + if (glContext && !glContext.isDestroyed()) { + try { + glContext.deleteTexture(data.texture); + } catch (e) { + // Context may have been destroyed between check and deletion - safe to ignore + } + } + + // Always nullify references + data.texture = null; + data.glContext = null; + } + } + + + // private + _calculateOverlapFraction(tile, tiledImage){ + const overlap = tiledImage.source.tileOverlap; + const nativeWidth = tile.sourceBounds.width; // in pixels + const nativeHeight = tile.sourceBounds.height; // in pixels + const overlapWidth = (tile.x === 0 ? 0 : overlap) + (tile.isRightMost ? 0 : overlap); // in pixels + const overlapHeight = (tile.y === 0 ? 0 : overlap) + (tile.isBottomMost ? 0 : overlap); // in pixels + const widthOverlapFraction = overlap / (nativeWidth + overlapWidth); // as a fraction of image including overlap + const heightOverlapFraction = overlap / (nativeHeight + overlapHeight); // as a fraction of image including overlap + return { + x: widthOverlapFraction, + y: heightOverlapFraction + }; + } + + _setClip(){ + // no-op: called by _renderToClippingCanvas when tiledImage._clip is truthy + // so that tests will pass. + } + + // private + _renderToClippingCanvas(item){ + + this._clippingContext.clearRect(0, 0, this._clippingCanvas.width, this._clippingCanvas.height); + this._clippingContext.save(); + if(this.viewer.viewport.getFlip()){ + const point = new $.Point(this.canvas.width / 2, this.canvas.height / 2); + this._clippingContext.translate(point.x, 0); + this._clippingContext.scale(-1, 1); + this._clippingContext.translate(-point.x, 0); + } + + if(item._clip){ + const polygon = [ + {x: item._clip.x, y: item._clip.y}, + {x: item._clip.x + item._clip.width, y: item._clip.y}, + {x: item._clip.x + item._clip.width, y: item._clip.y + item._clip.height}, + {x: item._clip.x, y: item._clip.y + item._clip.height}, + ]; + const clipPoints = polygon.map(coord => { + const point = item.imageToViewportCoordinates(coord.x, coord.y, true) + .rotate(this.viewer.viewport.getRotation(true), this.viewer.viewport.getCenter(true)); + const clipPoint = this.viewportCoordToDrawerCoord(point); + return clipPoint; + }); + this._clippingContext.beginPath(); + clipPoints.forEach( (coord, i) => { + this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y); + }); + this._clippingContext.clip(); + this._setClip(); + } + if(item._croppingPolygons){ + const polygons = item._croppingPolygons.map(polygon => { + return polygon.map(coord => { + const point = item.imageToViewportCoordinates(coord.x, coord.y, true) + .rotate(this.viewer.viewport.getRotation(true), this.viewer.viewport.getCenter(true)); + const clipPoint = this.viewportCoordToDrawerCoord(point); + return clipPoint; + }); + }); + this._clippingContext.beginPath(); + polygons.forEach((polygon) => { + polygon.forEach( (coord, i) => { + this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y); + }); + }); + this._clippingContext.clip(); + } + + if(this.viewer.viewport.getFlip()){ + const point = new $.Point(this.canvas.width / 2, this.canvas.height / 2); + this._clippingContext.translate(point.x, 0); + this._clippingContext.scale(-1, 1); + this._clippingContext.translate(-point.x, 0); + } + + this._clippingContext.drawImage(this._renderingCanvas, 0, 0); + + this._clippingContext.restore(); + } + + /** + * Set rotations for viewport & tiledImage + * @private + * @param {OpenSeadragon.TiledImage} tiledImage + */ + _setRotations(tiledImage) { + let saveContext = false; + if (this.viewport.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: this.viewport.getRotation(true), + saveContext: saveContext + }); + saveContext = false; + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: tiledImage.getRotation(true), + point: this.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + saveContext: saveContext + }); + } + } + + // private + _offsetForRotation(options) { + const point = options.point ? + options.point.times($.pixelDensityRatio) : + this._getCanvasCenter(); + + const context = this._outputContext; + context.save(); + + context.translate(point.x, point.y); + context.rotate(Math.PI / 180 * options.degrees); + context.translate(-point.x, -point.y); + } + + // private + _flip(options) { + options = options || {}; + const point = options.point ? + options.point.times($.pixelDensityRatio) : + this._getCanvasCenter(); + const context = this._outputContext; + + context.translate(point.x, 0); + context.scale(-1, 1); + context.translate(-point.x, 0); + } + + // private + _drawDebugInfo( tilesToDraw, tiledImage, flipped ) { + + for ( let i = tilesToDraw.length - 1; i >= 0; i-- ) { + const tile = tilesToDraw[ i ].tile; + try { + this._drawDebugInfoOnTile(tile, tilesToDraw.length, i, tiledImage, flipped); + } catch(e) { + $.console.error(e); + } + } + } + + // private + _drawDebugInfoOnTile(tile, count, i, tiledImage, flipped) { + + const colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length; + const context = this.context; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial'; + context.strokeStyle = this.debugGridColor[colorIndex]; + context.fillStyle = this.debugGridColor[colorIndex]; + + this._setRotations(tiledImage); + + if(flipped){ + this._flip({point: tile.position.plus(tile.size.divide(2))}); + } + + context.strokeRect( + tile.position.x * $.pixelDensityRatio, + tile.position.y * $.pixelDensityRatio, + tile.size.x * $.pixelDensityRatio, + tile.size.y * $.pixelDensityRatio + ); + + const tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio; + const tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio; + + // Rotate the text the right way around. + context.translate( tileCenterX, tileCenterY ); + const angleInDegrees = this.viewport.getRotation(true); + context.rotate( Math.PI / 180 * -angleInDegrees ); + context.translate( -tileCenterX, -tileCenterY ); + + if( tile.x === 0 && tile.y === 0 ){ + context.fillText( + "Zoom: " + this.viewport.getZoom(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 30) * $.pixelDensityRatio + ); + context.fillText( + "Pan: " + this.viewport.getBounds().toString(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 20) * $.pixelDensityRatio + ); + } + context.fillText( + "Level: " + tile.level, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 20) * $.pixelDensityRatio + ); + context.fillText( + "Column: " + tile.x, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 30) * $.pixelDensityRatio + ); + context.fillText( + "Row: " + tile.y, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 40) * $.pixelDensityRatio + ); + context.fillText( + "Order: " + i + " of " + count, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 50) * $.pixelDensityRatio + ); + context.fillText( + "Size: " + tile.size.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 60) * $.pixelDensityRatio + ); + context.fillText( + "Position: " + tile.position.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 70) * $.pixelDensityRatio + ); + + if (this.viewport.getRotation(true) % 360 !== 0 ) { + this._restoreRotationChanges(); + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._restoreRotationChanges(); + } + + context.restore(); + } + + _drawPlaceholder(tiledImage){ + + const bounds = tiledImage.getBounds(true); + const rect = this.viewportToDrawerRectangle(tiledImage.getBounds(true)); + const context = this._outputContext; + + let fillStyle; + if ( typeof tiledImage.placeholderFillStyle === "function" ) { + fillStyle = tiledImage.placeholderFillStyle(tiledImage, context); + } + else { + fillStyle = tiledImage.placeholderFillStyle; + } + + this._offsetForRotation({degrees: this.viewer.viewport.getRotation(true)}); + context.fillStyle = fillStyle; + context.translate(rect.x, rect.y); + context.rotate(Math.PI / 180 * bounds.degrees); + context.translate(-rect.x, -rect.y); + context.fillRect(rect.x, rect.y, rect.width, rect.height); + this._restoreRotationChanges(); + + } + + /** + * Get the canvas center + * @private + * @returns {OpenSeadragon.Point} The center point of the canvas + */ + _getCanvasCenter() { + return new $.Point(this.canvas.width / 2, this.canvas.height / 2); + } + + // private + _restoreRotationChanges() { + const context = this._outputContext; + context.restore(); + } + + // modified from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Adding_2D_content_to_a_WebGL_context + static initShaderProgram(gl, vsSource, fsSource) { + + function loadShader(gl, type, source) { + const shader = gl.createShader(type); + + // Send the source to the shader object + + gl.shaderSource(shader, source); + + // Compile the shader program + + gl.compileShader(shader); + + // See if it compiled successfully + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + $.console.error( + `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}` + ); + gl.deleteShader(shader); + return null; + } + + return shader; + } + + const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); + const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); + + // Create the shader program + + const shaderProgram = gl.createProgram(); + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + + // If creating the shader program failed, alert + + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { + $.console.error( + `Unable to initialize the shader program: ${gl.getProgramInfoLog( + shaderProgram + )}` + ); + return null; + } + + return shaderProgram; + } + }; + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Viewport + * + * 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 Viewport + * @memberof OpenSeadragon + * @classdesc Handles coordinate-related functionality (zoom, pan, rotation, etc.) + * for an {@link OpenSeadragon.Viewer}. + * @param {Object} options - Options for this Viewport. + * @param {Object} [options.margins] - See viewportMargins in {@link OpenSeadragon.Options}. + * @param {Number} [options.springStiffness] - See springStiffness in {@link OpenSeadragon.Options}. + * @param {Number} [options.animationTime] - See animationTime in {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomImageRatio] - See minZoomImageRatio in {@link OpenSeadragon.Options}. + * @param {Number} [options.maxZoomPixelRatio] - See maxZoomPixelRatio in {@link OpenSeadragon.Options}. + * @param {Number} [options.visibilityRatio] - See visibilityRatio in {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapHorizontal] - See wrapHorizontal in {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapVertical] - See wrapVertical in {@link OpenSeadragon.Options}. + * @param {Number} [options.defaultZoomLevel] - See defaultZoomLevel in {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomLevel] - See minZoomLevel in {@link OpenSeadragon.Options}. + * @param {Number} [options.maxZoomLevel] - See maxZoomLevel in {@link OpenSeadragon.Options}. + * @param {Number} [options.degrees] - See degrees in {@link OpenSeadragon.Options}. + * @param {Boolean} [options.homeFillsViewer] - See homeFillsViewer in {@link OpenSeadragon.Options}. + * @param {Boolean} [options.silenceMultiImageWarnings] - See silenceMultiImageWarnings in {@link OpenSeadragon.Options}. + */ +$.Viewport = function( options ) { + + //backward compatibility for positional args while preferring more + //idiomatic javascript options object as the only argument + const args = arguments; + if (args.length && args[0] instanceof $.Point) { + options = { + containerSize: args[0], + contentSize: args[1], + config: args[2] + }; + } + + //options.config and the general config argument are deprecated + //in favor of the more direct specification of optional settings + //being passed directly on the options object + if ( options.config ){ + $.extend( true, options, options.config ); + delete options.config; + } + + this._margins = $.extend({ + left: 0, + top: 0, + right: 0, + bottom: 0 + }, options.margins || {}); + + delete options.margins; + + options.initialDegrees = options.degrees; + delete options.degrees; + + $.extend( true, this, { + + //required settings + containerSize: null, + contentSize: null, + + //internal state properties + zoomPoint: null, + rotationPivot: null, + viewer: null, + + //configurable options + springStiffness: $.DEFAULT_SETTINGS.springStiffness, + animationTime: $.DEFAULT_SETTINGS.animationTime, + minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, + maxZoomPixelRatio: $.DEFAULT_SETTINGS.maxZoomPixelRatio, + visibilityRatio: $.DEFAULT_SETTINGS.visibilityRatio, + wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, + wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, + defaultZoomLevel: $.DEFAULT_SETTINGS.defaultZoomLevel, + minZoomLevel: $.DEFAULT_SETTINGS.minZoomLevel, + maxZoomLevel: $.DEFAULT_SETTINGS.maxZoomLevel, + initialDegrees: $.DEFAULT_SETTINGS.degrees, + flipped: $.DEFAULT_SETTINGS.flipped, + homeFillsViewer: $.DEFAULT_SETTINGS.homeFillsViewer, + silenceMultiImageWarnings: $.DEFAULT_SETTINGS.silenceMultiImageWarnings + + }, options ); + + this._updateContainerInnerSize(); + + this.centerSpringX = new $.Spring({ + initial: 0, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + this.centerSpringY = new $.Spring({ + initial: 0, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + this.zoomSpring = new $.Spring({ + exponential: true, + initial: 1, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this.degreesSpring = new $.Spring({ + initial: options.initialDegrees, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._oldCenterX = this.centerSpringX.current.value; + this._oldCenterY = this.centerSpringY.current.value; + this._oldZoom = this.zoomSpring.current.value; + this._oldDegrees = this.degreesSpring.current.value; + + this._sizeChanged = false; + + this._setContentBounds(new $.Rect(0, 0, 1, 1), 1); + + this.goHome(true); + this.update(); +}; + +/** @lends OpenSeadragon.Viewport.prototype */ +$.Viewport.prototype = { + + // deprecated + get degrees () { + $.console.warn('Accessing [Viewport.degrees] is deprecated. Use viewport.getRotation instead.'); + return this.getRotation(); + }, + + // deprecated + set degrees (degrees) { + $.console.warn('Setting [Viewport.degrees] is deprecated. Use viewport.rotateTo, viewport.rotateBy, or viewport.setRotation instead.'); + this.rotateTo(degrees); + }, + + /** + * Updates the viewport's home bounds and constraints for the given content size. + * @function + * @param {OpenSeadragon.Point} contentSize - size of the content in content units + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:reset-size + */ + resetContentSize: function(contentSize) { + $.console.assert(contentSize, "[Viewport.resetContentSize] contentSize is required"); + $.console.assert(contentSize instanceof $.Point, "[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point"); + $.console.assert(contentSize.x > 0, "[Viewport.resetContentSize] contentSize.x must be greater than 0"); + $.console.assert(contentSize.y > 0, "[Viewport.resetContentSize] contentSize.y must be greater than 0"); + + this._setContentBounds(new $.Rect(0, 0, 1, contentSize.y / contentSize.x), contentSize.x); + return this; + }, + + // deprecated + setHomeBounds: function(bounds, contentFactor) { + $.console.error("[Viewport.setHomeBounds] this function is deprecated; The content bounds should not be set manually."); + this._setContentBounds(bounds, contentFactor); + }, + + // Set the viewport's content bounds + // @param {OpenSeadragon.Rect} bounds - the new bounds in viewport coordinates + // without rotation + // @param {Number} contentFactor - how many content units per viewport unit + // @fires OpenSeadragon.Viewer.event:reset-size + // @private + _setContentBounds: function(bounds, contentFactor) { + $.console.assert(bounds, "[Viewport._setContentBounds] bounds is required"); + $.console.assert(bounds instanceof $.Rect, "[Viewport._setContentBounds] bounds must be an OpenSeadragon.Rect"); + $.console.assert(bounds.width > 0, "[Viewport._setContentBounds] bounds.width must be greater than 0"); + $.console.assert(bounds.height > 0, "[Viewport._setContentBounds] bounds.height must be greater than 0"); + + this._contentBoundsNoRotate = bounds.clone(); + this._contentSizeNoRotate = this._contentBoundsNoRotate.getSize().times( + contentFactor); + + this._contentBounds = bounds.rotate(this.getRotation()).getBoundingBox(); + this._contentSize = this._contentBounds.getSize().times(contentFactor); + this._contentAspectRatio = this._contentSize.x / this._contentSize.y; + + if (this.viewer) { + /** + * Raised when the viewer's content size or home bounds are reset + * (see {@link OpenSeadragon.Viewport#resetContentSize}). + * + * @event reset-size + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.Point} contentSize + * @property {OpenSeadragon.Rect} contentBounds - Content bounds. + * @property {OpenSeadragon.Rect} homeBounds - Content bounds. + * Deprecated use contentBounds instead. + * @property {Number} contentFactor + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('reset-size', { + contentSize: this._contentSizeNoRotate.clone(), + contentFactor: contentFactor, + homeBounds: this._contentBoundsNoRotate.clone(), + contentBounds: this._contentBounds.clone() + }); + } + }, + + /** + * Returns the home zoom in "viewport zoom" value. + * @function + * @returns {Number} The home zoom in "viewport zoom". + */ + getHomeZoom: function() { + if (this.defaultZoomLevel) { + return this.defaultZoomLevel; + } + + const aspectFactor = this._contentAspectRatio / this.getAspectRatio(); + let output; + if (this.homeFillsViewer) { // fill the viewer and clip the image + output = aspectFactor >= 1 ? aspectFactor : 1; + } else { + output = aspectFactor >= 1 ? 1 : aspectFactor; + } + + return output / this._contentBounds.width; + }, + + /** + * Returns the home bounds in viewport coordinates. + * @function + * @returns {OpenSeadragon.Rect} The home bounds in vewport coordinates. + */ + getHomeBounds: function() { + return this.getHomeBoundsNoRotate().rotate(-this.getRotation()); + }, + + /** + * Returns the home bounds in viewport coordinates. + * This method ignores the viewport rotation. Use + * {@link OpenSeadragon.Viewport#getHomeBounds} to take it into account. + * @function + * @returns {OpenSeadragon.Rect} The home bounds in vewport coordinates. + */ + getHomeBoundsNoRotate: function() { + const center = this._contentBounds.getCenter(); + const width = 1.0 / this.getHomeZoom(); + const height = width / this.getAspectRatio(); + + return new $.Rect( + center.x - (width / 2.0), + center.y - (height / 2.0), + width, + height + ); + }, + + /** + * @function + * @param {Boolean} immediately + * @fires OpenSeadragon.Viewer.event:home + */ + goHome: function(immediately) { + if (this.viewer) { + /** + * Raised when the "home" operation occurs (see {@link OpenSeadragon.Viewport#goHome}). + * + * @event home + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {Boolean} immediately + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('home', { + immediately: immediately + }); + } + return this.fitBounds(this.getHomeBounds(), immediately); + }, + + /** + * @function + */ + getMinZoom: function() { + const homeZoom = this.getHomeZoom(); + const zoom = this.minZoomLevel ? + this.minZoomLevel : + this.minZoomImageRatio * homeZoom; + + return zoom; + }, + + /** + * @function + */ + getMaxZoom: function() { + let zoom = this.maxZoomLevel; + if (!zoom) { + zoom = this._contentSize.x * this.maxZoomPixelRatio / this._containerInnerSize.x; + zoom /= this._contentBounds.width; + } + + return Math.max( zoom, this.getHomeZoom() ); + }, + + /** + * @function + */ + getAspectRatio: function() { + return this._containerInnerSize.x / this._containerInnerSize.y; + }, + + /** + * @function + * @returns {OpenSeadragon.Point} The size of the container, in screen coordinates. + */ + getContainerSize: function() { + return new $.Point( + this.containerSize.x, + this.containerSize.y + ); + }, + + /** + * The margins push the "home" region in from the sides by the specified amounts. + * @function + * @returns {Object} Properties (Numbers, in screen coordinates): left, top, right, bottom. + */ + getMargins: function() { + return $.extend({}, this._margins); // Make a copy so we are not returning our original + }, + + /** + * The margins push the "home" region in from the sides by the specified amounts. + * @function + * @param {Object} margins - Properties (Numbers, in screen coordinates): left, top, right, bottom. + */ + setMargins: function(margins) { + $.console.assert($.type(margins) === 'object', '[Viewport.setMargins] margins must be an object'); + + this._margins = $.extend({ + left: 0, + top: 0, + right: 0, + bottom: 0 + }, margins); + + this._updateContainerInnerSize(); + if (this.viewer) { + this.viewer.forceRedraw(); + } + }, + + /** + * Returns the bounds of the visible area in viewport coordinates. + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, in viewport coordinates. + */ + getBounds: function(current) { + return this.getBoundsNoRotate(current).rotate(-this.getRotation(current)); + }, + + /** + * Returns the bounds of the visible area in viewport coordinates. + * This method ignores the viewport rotation. Use + * {@link OpenSeadragon.Viewport#getBounds} to take it into account. + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, in viewport coordinates. + */ + getBoundsNoRotate: function(current) { + const center = this.getCenter(current); + const width = 1.0 / this.getZoom(current); + const height = width / this.getAspectRatio(); + + return new $.Rect( + center.x - (width / 2.0), + center.y - (height / 2.0), + width, + height + ); + }, + + /** + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, + * including the space taken by margins, in viewport coordinates. + */ + getBoundsWithMargins: function(current) { + return this.getBoundsNoRotateWithMargins(current).rotate( + -this.getRotation(current), this.getCenter(current)); + }, + + /** + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, + * including the space taken by margins, in viewport coordinates. + */ + getBoundsNoRotateWithMargins: function(current) { + const bounds = this.getBoundsNoRotate(current); + const factor = this._containerInnerSize.x * this.getZoom(current); + bounds.x -= this._margins.left / factor; + bounds.y -= this._margins.top / factor; + bounds.width += (this._margins.left + this._margins.right) / factor; + bounds.height += (this._margins.top + this._margins.bottom) / factor; + return bounds; + }, + + /** + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + */ + getCenter: function( current ) { + const centerCurrent = new $.Point( + this.centerSpringX.current.value, + this.centerSpringY.current.value + ); + const centerTarget = new $.Point( + this.centerSpringX.target.value, + this.centerSpringY.target.value + ); + + if ( current ) { + return centerCurrent; + } else if ( !this.zoomPoint ) { + return centerTarget; + } + + const oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true); + + const zoom = this.getZoom(); + const width = 1.0 / zoom; + const height = width / this.getAspectRatio(); + const bounds = new $.Rect( + centerCurrent.x - width / 2.0, + centerCurrent.y - height / 2.0, + width, + height + ); + + const newZoomPixel = this._pixelFromPoint(this.zoomPoint, bounds); + const deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ).rotate(-this.getRotation(true)); + const deltaZoomPoints = deltaZoomPixels.divide( this._containerInnerSize.x * zoom ); + + return centerTarget.plus( deltaZoomPoints ); + }, + + /** + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + */ + getZoom: function( current ) { + if ( current ) { + return this.zoomSpring.current.value; + } else { + return this.zoomSpring.target.value; + } + }, + + // private + _applyZoomConstraints: function(zoom) { + return Math.max( + Math.min(zoom, this.getMaxZoom()), + this.getMinZoom()); + }, + + /** + * @function + * @private + * @param {OpenSeadragon.Rect} bounds + * @returns {OpenSeadragon.Rect} constrained bounds. + */ + _applyBoundaryConstraints: function(bounds) { + const newBounds = this.viewportToViewerElementRectangle(bounds).getBoundingBox(); + const cb = this.viewportToViewerElementRectangle(this._contentBoundsNoRotate).getBoundingBox(); + + let xConstrained = false; + let yConstrained = false; + + if (this.wrapHorizontal) { + //do nothing + } else { + const boundsRight = newBounds.x + newBounds.width; + const contentRight = cb.x + cb.width; + + let horizontalThreshold, leftDx, rightDx; + if (newBounds.width > cb.width) { + horizontalThreshold = this.visibilityRatio * cb.width; + } else { + horizontalThreshold = this.visibilityRatio * newBounds.width; + } + + leftDx = cb.x - boundsRight + horizontalThreshold; + rightDx = contentRight - newBounds.x - horizontalThreshold; + if (horizontalThreshold > cb.width) { + newBounds.x += (leftDx + rightDx) / 2; + xConstrained = true; + } else if (rightDx < 0) { + newBounds.x += rightDx; + xConstrained = true; + } else if (leftDx > 0) { + newBounds.x += leftDx; + xConstrained = true; + } + + } + + if (this.wrapVertical) { + //do nothing + } else { + const boundsBottom = newBounds.y + newBounds.height; + const contentBottom = cb.y + cb.height; + + let verticalThreshold, topDy, bottomDy; + if (newBounds.height > cb.height) { + verticalThreshold = this.visibilityRatio * cb.height; + } else{ + verticalThreshold = this.visibilityRatio * newBounds.height; + } + + topDy = cb.y - boundsBottom + verticalThreshold; + bottomDy = contentBottom - newBounds.y - verticalThreshold; + if (verticalThreshold > cb.height) { + newBounds.y += (topDy + bottomDy) / 2; + yConstrained = true; + } else if (bottomDy < 0) { + newBounds.y += bottomDy; + yConstrained = true; + } else if (topDy > 0) { + newBounds.y += topDy; + yConstrained = true; + } + + } + + const constraintApplied = xConstrained || yConstrained; + const newViewportBounds = constraintApplied ? this.viewerElementToViewportRectangle(newBounds) : bounds.clone(); + newViewportBounds.xConstrained = xConstrained; + newViewportBounds.yConstrained = yConstrained; + newViewportBounds.constraintApplied = constraintApplied; + + return newViewportBounds; + }, + + /** + * @function + * @private + * @param {Boolean} [immediately=false] - whether the function that triggered this event was + * called with the "immediately" flag + */ + _raiseConstraintsEvent: function(immediately) { + if (this.viewer) { + /** + * Raised when the viewport constraints are applied (see {@link OpenSeadragon.Viewport#applyConstraints}). + * + * @event constrain + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {Boolean} immediately - whether the function that triggered this event was + * called with the "immediately" flag + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'constrain', { + immediately: immediately + }); + } + }, + + /** + * Enforces the minZoom, maxZoom and visibilityRatio constraints by + * zooming and panning to the closest acceptable zoom and location. + * @function + * @param {Boolean} [immediately=false] + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:constrain if constraints were applied + */ + applyConstraints: function(immediately) { + const actualZoom = this.getZoom(); + const constrainedZoom = this._applyZoomConstraints(actualZoom); + + if (actualZoom !== constrainedZoom) { + this.zoomTo(constrainedZoom, this.zoomPoint, immediately); + } + + const constrainedBounds = this.getConstrainedBounds(false); + + if(constrainedBounds.constraintApplied){ + this.fitBounds(constrainedBounds, immediately); + this._raiseConstraintsEvent(immediately); + } + + return this; + }, + + /** + * Equivalent to {@link OpenSeadragon.Viewport#applyConstraints} + * @function + * @param {Boolean} [immediately=false] + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:constrain + */ + ensureVisible: function(immediately) { + return this.applyConstraints(immediately); + }, + + /** + * @function + * @private + * @param {OpenSeadragon.Rect} bounds + * @param {Object} options (immediately=false, constraints=false) + * @returns {OpenSeadragon.Viewport} Chainable. + */ + _fitBounds: function(bounds, options) { + options = options || {}; + const immediately = options.immediately || false; + const constraints = options.constraints || false; + + const aspect = this.getAspectRatio(); + const center = bounds.getCenter(); + + // Compute width and height of bounding box. + const newBounds = new $.Rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + bounds.degrees + this.getRotation()) + .getBoundingBox(); + + if (newBounds.getAspectRatio() >= aspect) { + newBounds.height = newBounds.width / aspect; + } else { + newBounds.width = newBounds.height * aspect; + } + + // Compute x and y from width, height and center position + newBounds.x = center.x - newBounds.width / 2; + newBounds.y = center.y - newBounds.height / 2; + let newZoom = 1.0 / newBounds.width; + + console.log('New center:', center, 'zoom:', newZoom, 'imm:', immediately) + if (immediately) { + this.panTo(center, true); + this.zoomTo(newZoom, null, true); + if(constraints){ + this.applyConstraints(true); + } + return this; + } + + const currentCenter = this.getCenter(true); + const currentZoom = this.getZoom(true); + this.panTo(currentCenter, true); + this.zoomTo(currentZoom, null, true); + + const oldBounds = this.getBounds(); + const oldZoom = this.getZoom(); + + if (oldZoom === 0 || Math.abs(newZoom / oldZoom - 1) < 0.00000001) { + this.zoomTo(newZoom, null, true); + this.panTo(center, immediately); + if(constraints){ + this.applyConstraints(false); + } + return this; + } + + if(constraints){ + this.panTo(center, false); + + newZoom = this._applyZoomConstraints(newZoom); + this.zoomTo(newZoom, null, false); + + const constrainedBounds = this.getConstrainedBounds(); + + this.panTo(currentCenter, true); + this.zoomTo(currentZoom, null, true); + + this.fitBounds(constrainedBounds); + } else { + const rotatedNewBounds = newBounds.rotate(-this.getRotation()); + const referencePoint = rotatedNewBounds.getTopLeft().times(newZoom) + .minus(oldBounds.getTopLeft().times(oldZoom)) + .divide(newZoom - oldZoom); + + this.zoomTo(newZoom, referencePoint, immediately); + } + return this; + }, + + /** + * Makes the viewport zoom and pan so that the specified bounds take + * as much space as possible in the viewport. + * Note: this method ignores the constraints (minZoom, maxZoom and + * visibilityRatio). + * Use {@link OpenSeadragon.Viewport#fitBoundsWithConstraints} to enforce + * them. + * @function + * @param {OpenSeadragon.Rect} bounds + * @param {Boolean} [immediately=false] + * @returns {OpenSeadragon.Viewport} Chainable. + */ + fitBounds: function(bounds, immediately) { + return this._fitBounds(bounds, { + immediately: immediately, + constraints: false + }); + }, + + /** + * Makes the viewport zoom and pan so that the specified bounds take + * as much space as possible in the viewport while enforcing the constraints + * (minZoom, maxZoom and visibilityRatio). + * Note: because this method enforces the constraints, part of the + * provided bounds may end up outside of the viewport. + * Use {@link OpenSeadragon.Viewport#fitBounds} to ignore them. + * @function + * @param {OpenSeadragon.Rect} bounds + * @param {Boolean} [immediately=false] + * @returns {OpenSeadragon.Viewport} Chainable. + */ + fitBoundsWithConstraints: function(bounds, immediately) { + return this._fitBounds(bounds, { + immediately: immediately, + constraints: true + }); + }, + + /** + * Zooms so the image just fills the viewer vertically. + * @param {Boolean} immediately + * @returns {OpenSeadragon.Viewport} Chainable. + */ + fitVertically: function(immediately) { + const box = new $.Rect( + this._contentBounds.x + (this._contentBounds.width / 2), + this._contentBounds.y, + 0, + this._contentBounds.height); + return this.fitBounds(box, immediately); + }, + + /** + * Zooms so the image just fills the viewer horizontally. + * @param {Boolean} immediately + * @returns {OpenSeadragon.Viewport} Chainable. + */ + fitHorizontally: function(immediately) { + const box = new $.Rect( + this._contentBounds.x, + this._contentBounds.y + (this._contentBounds.height / 2), + this._contentBounds.width, + 0); + return this.fitBounds(box, immediately); + }, + + + /** + * Returns bounds taking constraints into account + * Added to improve constrained panning + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @returns {OpenSeadragon.Rect} The bounds in viewport coordinates after applying constraints. The returned $.Rect + * contains additional properties constraintsApplied, xConstrained and yConstrained. + * These flags indicate whether the viewport bounds were modified by the constraints + * of the viewer rectangle, and in which dimension(s). + */ + getConstrainedBounds: function(current) { + const bounds = this.getBounds(current); + const constrainedBounds = this._applyBoundaryConstraints(bounds); + + return constrainedBounds; + }, + + /** + * @function + * @param {OpenSeadragon.Point} delta + * @param {Boolean} immediately + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:pan + */ + panBy: function( delta, immediately ) { + const center = new $.Point(); + if (immediately) { + center.x = this.centerSpringX.current.value; + center.y = this.centerSpringY.current.value; + } else { + center.x = this.centerSpringX.target.value; + center.y = this.centerSpringY.target.value; + } + return this.panTo( center.plus( delta ), immediately ); + }, + + /** + * @function + * @param {OpenSeadragon.Point} center + * @param {Boolean} immediately + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:pan + */ + panTo: function( center, immediately ) { + if ( immediately ) { + this.centerSpringX.resetTo( center.x ); + this.centerSpringY.resetTo( center.y ); + } else { + this.centerSpringX.springTo( center.x ); + this.centerSpringY.springTo( center.y ); + } + + if( this.viewer ){ + /** + * Raised when the viewport is panned (see {@link OpenSeadragon.Viewport#panBy} and {@link OpenSeadragon.Viewport#panTo}). + * + * @event pan + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.Point} center + * @property {Boolean} immediately + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'pan', { + center: center, + immediately: immediately + }); + } + + return this; + }, + + /** + * @function + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:zoom + */ + zoomBy: function(factor, refPoint, immediately) { + return this.zoomTo( + this.zoomSpring.target.value * factor, refPoint, immediately); + }, + + /** + * Zooms to the specified zoom level + * @function + * @param {Number} zoom The zoom level to zoom to. + * @param {OpenSeadragon.Point} [refPoint] The point which will stay at + * the same screen location. Defaults to the viewport center. + * @param {Boolean} [immediately=false] + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:zoom + */ + zoomTo: function(zoom, refPoint, immediately) { + const _this = this; + + this.zoomPoint = refPoint instanceof $.Point && + !isNaN(refPoint.x) && + !isNaN(refPoint.y) ? + refPoint : + null; + + if (immediately) { + this._adjustCenterSpringsForZoomPoint(function() { + _this.zoomSpring.resetTo(zoom); + }); + } else { + this.zoomSpring.springTo(zoom); + } + + if (this.viewer) { + /** + * Raised when the viewport zoom level changes (see {@link OpenSeadragon.Viewport#zoomBy} and {@link OpenSeadragon.Viewport#zoomTo}). + * + * @event zoom + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {Number} zoom + * @property {OpenSeadragon.Point} refPoint + * @property {Boolean} immediately + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('zoom', { + zoom: zoom, + refPoint: refPoint, + immediately: immediately + }); + } + + return this; + }, + + /** + * Rotates this viewport to the angle specified. + * @function + * @param {Number} degrees The degrees to set the rotation to. + * @param {Boolean} [immediately=false] Whether to animate to the new angle + * or rotate immediately. + * * @returns {OpenSeadragon.Viewport} Chainable. + */ + setRotation: function(degrees, immediately) { + return this.rotateTo(degrees, null, immediately); + }, + + /** + * Gets the current rotation in degrees. + * @function + * @param {Boolean} [current=false] True for current rotation, false for target. + * @returns {Number} The current rotation in degrees. + */ + getRotation: function(current) { + return current ? + this.degreesSpring.current.value : + this.degreesSpring.target.value; + }, + + /** + * Rotates this viewport to the angle specified around a pivot point. Alias for rotateTo. + * @function + * @param {Number} degrees The degrees to set the rotation to. + * @param {OpenSeadragon.Point} [pivot] (Optional) point in viewport coordinates + * around which the rotation should be performed. Defaults to the center of the viewport. + * @param {Boolean} [immediately=false] Whether to animate to the new angle + * or rotate immediately. + * * @returns {OpenSeadragon.Viewport} Chainable. + */ + setRotationWithPivot: function(degrees, pivot, immediately) { + return this.rotateTo(degrees, pivot, immediately); + }, + + /** + * Rotates this viewport to the angle specified. + * @function + * @param {Number} degrees The degrees to set the rotation to. + * @param {OpenSeadragon.Point} [pivot] (Optional) point in viewport coordinates + * around which the rotation should be performed. Defaults to the center of the viewport. + * @param {Boolean} [immediately=false] Whether to animate to the new angle + * or rotate immediately. + * @returns {OpenSeadragon.Viewport} Chainable. + */ + rotateTo: function(degrees, pivot, immediately){ + if (!this.viewer || !this.viewer.drawer.canRotate()) { + return this; + } + + if (this.degreesSpring.target.value === degrees && + this.degreesSpring.isAtTargetValue()) { + return this; + } + this.rotationPivot = pivot instanceof $.Point && + !isNaN(pivot.x) && + !isNaN(pivot.y) ? + pivot : + null; + if (immediately) { + if(this.rotationPivot){ + const changeInDegrees = degrees - this._oldDegrees; + if(!changeInDegrees){ + this.rotationPivot = null; + return this; + } + this._rotateAboutPivot(degrees); + } else{ + this.degreesSpring.resetTo(degrees); + } + } else { + const normalizedFrom = $.positiveModulo(this.degreesSpring.current.value, 360); + let normalizedTo = $.positiveModulo(degrees, 360); + const diff = normalizedTo - normalizedFrom; + if (diff > 180) { + normalizedTo -= 360; + } else if (diff < -180) { + normalizedTo += 360; + } + + const reverseDiff = normalizedFrom - normalizedTo; + this.degreesSpring.resetTo(degrees + reverseDiff); + this.degreesSpring.springTo(degrees); + } + + this._setContentBounds( + this.viewer.world.getHomeBounds(), + this.viewer.world.getContentFactor()); + this.viewer.forceRedraw(); + + /** + * Raised when rotation has been changed. + * + * @event rotate + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {Number} degrees - The number of degrees the rotation was set to. + * @property {Boolean} immediately - Whether the rotation happened immediately or was animated + * @property {OpenSeadragon.Point} pivot - The point in viewport coordinates around which the rotation (if any) happened + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('rotate', {degrees: degrees, immediately: !!immediately, pivot: this.rotationPivot || this.getCenter()}); + return this; + }, + + /** + * Rotates this viewport by the angle specified. + * @function + * @param {Number} degrees The degrees by which to rotate the viewport. + * @param {OpenSeadragon.Point} [pivot] (Optional) point in viewport coordinates + * around which the rotation should be performed. Defaults to the center of the viewport. + * * @param {Boolean} [immediately=false] Whether to animate to the new angle + * or rotate immediately. + * @returns {OpenSeadragon.Viewport} Chainable. + */ + rotateBy: function(degrees, pivot, immediately){ + return this.rotateTo(this.degreesSpring.target.value + degrees, pivot, immediately); + }, + + /** + * @function + * @returns {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:resize + */ + resize: function( newContainerSize, maintain ) { + const oldBounds = this.getBoundsNoRotate(); + const newBounds = oldBounds; + let widthDeltaFactor; + this._sizeChanged = !this.containerSize.equals(newContainerSize); + + this.containerSize.x = newContainerSize.x; + this.containerSize.y = newContainerSize.y; + + this._updateContainerInnerSize(); + + if ( maintain ) { + // TODO: widthDeltaFactor will always be 1; probably not what's intended + widthDeltaFactor = newContainerSize.x / this.containerSize.x; + newBounds.width = oldBounds.width * widthDeltaFactor; + newBounds.height = newBounds.width / this.getAspectRatio(); + } + + if( this.viewer ){ + /** + * Raised when a viewer resize operation is initiated (see {@link OpenSeadragon.Viewport#resize}). + * This event happens before the viewport bounds have been updated. + * See also {@link OpenSeadragon.Viewer#after-resize} which reflects + * the new viewport bounds following the resize action. + * + * @event resize + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.Point} newContainerSize + * @property {Boolean} maintain + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'resize', { + newContainerSize: newContainerSize, + maintain: maintain + }); + } + + const output = this.fitBounds( newBounds, true ); + + if( this.viewer ){ + /** + * Raised after the viewer is resized (see {@link OpenSeadragon.Viewport#resize}). + * See also {@link OpenSeadragon.Viewer#resize} event which happens + * before the new bounds have been calculated and applied. + * + * @event after-resize + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.Point} newContainerSize + * @property {Boolean} maintain + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'after-resize', { + newContainerSize: newContainerSize, + maintain: maintain + }); + } + + return output; + }, + + // private + _updateContainerInnerSize: function() { + this._containerInnerSize = new $.Point( + Math.max(1, this.containerSize.x - (this._margins.left + this._margins.right)), + Math.max(1, this.containerSize.y - (this._margins.top + this._margins.bottom)) + ); + }, + + /** + * Update the zoom, degrees, and center (X and Y) springs. + * @function + * @returns {Boolean} True if the viewport is still animating, false otherwise. + */ + update: function() { + const _this = this; + this._adjustCenterSpringsForZoomPoint(function() { + _this.zoomSpring.update(); + }); + if(this.degreesSpring.isAtTargetValue()){ + this.rotationPivot = null; + } + this.centerSpringX.update(); + this.centerSpringY.update(); + + if(this.rotationPivot){ + this._rotateAboutPivot(true); + } + else{ + this.degreesSpring.update(); + } + + + const changed = this.centerSpringX.current.value !== this._oldCenterX || + this.centerSpringY.current.value !== this._oldCenterY || + this.zoomSpring.current.value !== this._oldZoom || + this.degreesSpring.current.value !== this._oldDegrees || + this._sizeChanged; + + this._sizeChanged = false; + + + this._oldCenterX = this.centerSpringX.current.value; + this._oldCenterY = this.centerSpringY.current.value; + this._oldZoom = this.zoomSpring.current.value; + this._oldDegrees = this.degreesSpring.current.value; + + const isAnimating = changed || + !this.zoomSpring.isAtTargetValue() || + !this.centerSpringX.isAtTargetValue() || + !this.centerSpringY.isAtTargetValue() || + !this.degreesSpring.isAtTargetValue(); + + return isAnimating; + }, + + // private - pass true to use spring, or a number for degrees for immediate rotation + _rotateAboutPivot: function(degreesOrUseSpring){ + const useSpring = degreesOrUseSpring === true; + + const delta = this.rotationPivot.minus(this.getCenter()); + this.centerSpringX.shiftBy(delta.x); + this.centerSpringY.shiftBy(delta.y); + + if(useSpring){ + this.degreesSpring.update(); + } else { + this.degreesSpring.resetTo(degreesOrUseSpring); + } + + const changeInDegrees = this.degreesSpring.current.value - this._oldDegrees; + const rdelta = delta.rotate(changeInDegrees * -1).times(-1); + this.centerSpringX.shiftBy(rdelta.x); + this.centerSpringY.shiftBy(rdelta.y); + }, + + // private + _adjustCenterSpringsForZoomPoint: function(zoomSpringHandler) { + if (this.zoomPoint) { + const oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true); + zoomSpringHandler(); + const newZoomPixel = this.pixelFromPoint(this.zoomPoint, true); + + const deltaZoomPixels = newZoomPixel.minus(oldZoomPixel); + const deltaZoomPoints = this.deltaPointsFromPixels( + deltaZoomPixels, true); + + this.centerSpringX.shiftBy(deltaZoomPoints.x); + this.centerSpringY.shiftBy(deltaZoomPoints.y); + + if (this.zoomSpring.isAtTargetValue()) { + this.zoomPoint = null; + } + } else { + zoomSpringHandler(); + } + }, + + /** + * Convert a delta (translation vector) from viewport coordinates to pixels + * coordinates. This method does not take rotation into account. + * Consider using deltaPixelsFromPoints if you need to account for rotation. + * @param {OpenSeadragon.Point} deltaPoints - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + deltaPixelsFromPointsNoRotate: function(deltaPoints, current) { + return deltaPoints.times( + this._containerInnerSize.x * this.getZoom(current) + ); + }, + + /** + * Convert a delta (translation vector) from viewport coordinates to pixels + * coordinates. + * @param {OpenSeadragon.Point} deltaPoints - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + deltaPixelsFromPoints: function(deltaPoints, current) { + return this.deltaPixelsFromPointsNoRotate( + deltaPoints.rotate(this.getRotation(current)), + current); + }, + + /** + * Convert a delta (translation vector) from pixels coordinates to viewport + * coordinates. This method does not take rotation into account. + * Consider using deltaPointsFromPixels if you need to account for rotation. + * @param {OpenSeadragon.Point} deltaPixels - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + deltaPointsFromPixelsNoRotate: function(deltaPixels, current) { + return deltaPixels.divide( + this._containerInnerSize.x * this.getZoom(current) + ); + }, + + /** + * Convert a delta (translation vector) from pixels coordinates to viewport + * coordinates. + * @param {OpenSeadragon.Point} deltaPixels - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + deltaPointsFromPixels: function(deltaPixels, current) { + return this.deltaPointsFromPixelsNoRotate(deltaPixels, current) + .rotate(-this.getRotation(current)); + }, + + /** + * Convert viewport coordinates to pixels coordinates. + * This method does not take rotation into account. + * Consider using pixelFromPoint if you need to account for rotation. + * @param {OpenSeadragon.Point} point the viewport coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + pixelFromPointNoRotate: function(point, current) { + return this._pixelFromPointNoRotate( + point, this.getBoundsNoRotate(current)); + }, + + /** + * Convert viewport coordinates to pixel coordinates. + * @param {OpenSeadragon.Point} point the viewport coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + pixelFromPoint: function(point, current) { + return this._pixelFromPoint(point, this.getBoundsNoRotate(current)); + }, + + // private + _pixelFromPointNoRotate: function(point, bounds) { + return point.minus( + bounds.getTopLeft() + ).times( + this._containerInnerSize.x / bounds.width + ).plus( + new $.Point(this._margins.left, this._margins.top) + ); + }, + + // private + _pixelFromPoint: function(point, bounds) { + return this._pixelFromPointNoRotate( + point.rotate(this.getRotation(true), this.getCenter(true)), + bounds); + }, + + /** + * Convert pixel coordinates to viewport coordinates. + * This method does not take rotation into account. + * Consider using pointFromPixel if you need to account for rotation. + * @param {OpenSeadragon.Point} pixel Pixel coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + pointFromPixelNoRotate: function(pixel, current) { + const bounds = this.getBoundsNoRotate(current); + return pixel.minus( + new $.Point(this._margins.left, this._margins.top) + ).divide( + this._containerInnerSize.x / bounds.width + ).plus( + bounds.getTopLeft() + ); + }, + + /** + * Convert pixel coordinates to viewport coordinates. + * @param {OpenSeadragon.Point} pixel Pixel coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + pointFromPixel: function(pixel, current) { + return this.pointFromPixelNoRotate(pixel, current).rotate( + -this.getRotation(current), + this.getCenter(current) + ); + }, + + // private + _viewportToImageDelta: function( viewerX, viewerY ) { + const scale = this._contentBoundsNoRotate.width; + return new $.Point( + viewerX * this._contentSizeNoRotate.x / scale, + viewerY * this._contentSizeNoRotate.x / scale); + }, + + /** + * Translates from OpenSeadragon viewer coordinate system to image coordinate system. + * This method can be called either by passing X,Y coordinates or an + * OpenSeadragon.Point + * Note: not accurate with multi-image; use TiledImage.viewportToImageCoordinates instead. + * @function + * @param {(OpenSeadragon.Point|Number)} viewerX either a point or the X + * coordinate in viewport coordinate system. + * @param {Number} [viewerY] Y coordinate in viewport coordinate system. + * @returns {OpenSeadragon.Point} a point representing the coordinates in the image. + */ + viewportToImageCoordinates: function(viewerX, viewerY) { + if (viewerX instanceof $.Point) { + //they passed a point instead of individual components + return this.viewportToImageCoordinates(viewerX.x, viewerX.y); + } + + if (this.viewer) { + const count = this.viewer.world.getItemCount(); + if (count > 1) { + if (!this.silenceMultiImageWarnings) { + $.console.error('[Viewport.viewportToImageCoordinates] is not accurate ' + + 'with multi-image; use TiledImage.viewportToImageCoordinates instead.'); + } + } else if (count === 1) { + // It is better to use TiledImage.viewportToImageCoordinates + // because this._contentBoundsNoRotate can not be relied on + // with clipping. + const item = this.viewer.world.getItemAt(0); + return item.viewportToImageCoordinates(viewerX, viewerY, true); + } + } + + return this._viewportToImageDelta( + viewerX - this._contentBoundsNoRotate.x, + viewerY - this._contentBoundsNoRotate.y); + }, + + // private + _imageToViewportDelta: function( imageX, imageY ) { + const scale = this._contentBoundsNoRotate.width; + return new $.Point( + imageX / this._contentSizeNoRotate.x * scale, + imageY / this._contentSizeNoRotate.x * scale); + }, + + /** + * Translates from image coordinate system to OpenSeadragon viewer coordinate system + * This method can be called either by passing X,Y coordinates or an + * OpenSeadragon.Point + * Note: not accurate with multi-image; use TiledImage.imageToViewportCoordinates instead. + * @function + * @param {(OpenSeadragon.Point | Number)} imageX the point or the + * X coordinate in image coordinate system. + * @param {Number} [imageY] Y coordinate in image coordinate system. + * @returns {OpenSeadragon.Point} a point representing the coordinates in the viewport. + */ + imageToViewportCoordinates: function(imageX, imageY) { + if (imageX instanceof $.Point) { + //they passed a point instead of individual components + return this.imageToViewportCoordinates(imageX.x, imageX.y); + } + + if (this.viewer) { + const count = this.viewer.world.getItemCount(); + if (count > 1) { + if (!this.silenceMultiImageWarnings) { + $.console.error('[Viewport.imageToViewportCoordinates] is not accurate ' + + 'with multi-image; use TiledImage.imageToViewportCoordinates instead.'); + } + } else if (count === 1) { + // It is better to use TiledImage.viewportToImageCoordinates + // because this._contentBoundsNoRotate can not be relied on + // with clipping. + const item = this.viewer.world.getItemAt(0); + return item.imageToViewportCoordinates(imageX, imageY, true); + } + } + + const point = this._imageToViewportDelta(imageX, imageY); + point.x += this._contentBoundsNoRotate.x; + point.y += this._contentBoundsNoRotate.y; + return point; + }, + + /** + * Translates from a rectangle which describes a portion of the image in + * pixel coordinates to OpenSeadragon viewport rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an + * OpenSeadragon.Rect + * Note: not accurate with multi-image; use TiledImage.imageToViewportRectangle instead. + * @function + * @param {(OpenSeadragon.Rect | Number)} imageX the rectangle or the X + * coordinate of the top left corner of the rectangle in image coordinate system. + * @param {Number} [imageY] the Y coordinate of the top left corner of the rectangle + * in image coordinate system. + * @param {Number} [pixelWidth] the width in pixel of the rectangle. + * @param {Number} [pixelHeight] the height in pixel of the rectangle. + * @returns {OpenSeadragon.Rect} This image's bounds in viewport coordinates + */ + imageToViewportRectangle: function(imageX, imageY, pixelWidth, pixelHeight) { + let rect = imageX; + if (!(rect instanceof $.Rect)) { + //they passed individual components instead of a rectangle + rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); + } + + if (this.viewer) { + const count = this.viewer.world.getItemCount(); + if (count > 1) { + if (!this.silenceMultiImageWarnings) { + $.console.error('[Viewport.imageToViewportRectangle] is not accurate ' + + 'with multi-image; use TiledImage.imageToViewportRectangle instead.'); + } + } else if (count === 1) { + // It is better to use TiledImage.imageToViewportRectangle + // because this._contentBoundsNoRotate can not be relied on + // with clipping. + const item = this.viewer.world.getItemAt(0); + return item.imageToViewportRectangle( + imageX, imageY, pixelWidth, pixelHeight, true); + } + } + + const coordA = this.imageToViewportCoordinates(rect.x, rect.y); + const coordB = this._imageToViewportDelta(rect.width, rect.height); + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y, + rect.degrees + ); + }, + + /** + * Translates from a rectangle which describes a portion of + * the viewport in point coordinates to image rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an + * OpenSeadragon.Rect + * Note: not accurate with multi-image; use TiledImage.viewportToImageRectangle instead. + * @function + * @param {(OpenSeadragon.Rect | Number)} viewerX either a rectangle or + * the X coordinate of the top left corner of the rectangle in viewport + * coordinate system. + * @param {Number} [viewerY] the Y coordinate of the top left corner of the rectangle + * in viewport coordinate system. + * @param {Number} [pointWidth] the width of the rectangle in viewport coordinate system. + * @param {Number} [pointHeight] the height of the rectangle in viewport coordinate system. + */ + viewportToImageRectangle: function(viewerX, viewerY, pointWidth, pointHeight) { + let rect = viewerX; + if (!(rect instanceof $.Rect)) { + //they passed individual components instead of a rectangle + rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); + } + + if (this.viewer) { + const count = this.viewer.world.getItemCount(); + if (count > 1) { + if (!this.silenceMultiImageWarnings) { + $.console.error('[Viewport.viewportToImageRectangle] is not accurate ' + + 'with multi-image; use TiledImage.viewportToImageRectangle instead.'); + } + } else if (count === 1) { + // It is better to use TiledImage.viewportToImageCoordinates + // because this._contentBoundsNoRotate can not be relied on + // with clipping. + const item = this.viewer.world.getItemAt(0); + return item.viewportToImageRectangle( + viewerX, viewerY, pointWidth, pointHeight, true); + } + } + + const coordA = this.viewportToImageCoordinates(rect.x, rect.y); + const coordB = this._viewportToImageDelta(rect.width, rect.height); + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y, + rect.degrees + ); + }, + + /** + * Convert pixel coordinates relative to the viewer element to image + * coordinates. + * Note: not accurate with multi-image. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + viewerElementToImageCoordinates: function( pixel ) { + const point = this.pointFromPixel( pixel, true ); + return this.viewportToImageCoordinates( point ); + }, + + /** + * Convert pixel coordinates relative to the image to + * viewer element coordinates. + * Note: not accurate with multi-image. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToViewerElementCoordinates: function( pixel ) { + const point = this.imageToViewportCoordinates( pixel ); + return this.pixelFromPoint( point, true ); + }, + + /** + * Convert pixel coordinates relative to the window to image coordinates. + * Note: not accurate with multi-image. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + windowToImageCoordinates: function(pixel) { + $.console.assert(this.viewer, + "[Viewport.windowToImageCoordinates] the viewport must have a viewer."); + const viewerCoordinates = pixel.minus( + $.getElementPosition(this.viewer.container)); + return this.viewerElementToImageCoordinates(viewerCoordinates); + }, + + /** + * Convert image coordinates to pixel coordinates relative to the window. + * Note: not accurate with multi-image. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToWindowCoordinates: function(pixel) { + $.console.assert(this.viewer, + "[Viewport.imageToWindowCoordinates] the viewport must have a viewer."); + const viewerCoordinates = this.imageToViewerElementCoordinates(pixel); + return viewerCoordinates.plus( + $.getElementPosition(this.viewer.container)); + }, + + /** + * Convert pixel coordinates relative to the viewer element to viewport + * coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + viewerElementToViewportCoordinates: function( pixel ) { + return this.pointFromPixel( pixel, true ); + }, + + /** + * Convert viewport coordinates to pixel coordinates relative to the + * viewer element. + * @param {OpenSeadragon.Point} point + * @returns {OpenSeadragon.Point} + */ + viewportToViewerElementCoordinates: function( point ) { + return this.pixelFromPoint( point, true ); + }, + + /** + * Convert a rectangle in pixel coordinates relative to the viewer element + * to viewport coordinates. + * @param {OpenSeadragon.Rect} rectangle the rectangle to convert + * @returns {OpenSeadragon.Rect} the converted rectangle + */ + viewerElementToViewportRectangle: function(rectangle) { + return $.Rect.fromSummits( + this.pointFromPixel(rectangle.getTopLeft(), true), + this.pointFromPixel(rectangle.getTopRight(), true), + this.pointFromPixel(rectangle.getBottomLeft(), true) + ); + }, + + /** + * Convert a rectangle in viewport coordinates to pixel coordinates relative + * to the viewer element. + * @param {OpenSeadragon.Rect} rectangle the rectangle to convert + * @returns {OpenSeadragon.Rect} the converted rectangle + */ + viewportToViewerElementRectangle: function(rectangle) { + return $.Rect.fromSummits( + this.pixelFromPoint(rectangle.getTopLeft(), true), + this.pixelFromPoint(rectangle.getTopRight(), true), + this.pixelFromPoint(rectangle.getBottomLeft(), true) + ); + }, + + /** + * Convert pixel coordinates relative to the window to viewport coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + windowToViewportCoordinates: function(pixel) { + $.console.assert(this.viewer, + "[Viewport.windowToViewportCoordinates] the viewport must have a viewer."); + const viewerCoordinates = pixel.minus( + $.getElementPosition(this.viewer.container)); + return this.viewerElementToViewportCoordinates(viewerCoordinates); + }, + + /** + * Convert viewport coordinates to pixel coordinates relative to the window. + * @param {OpenSeadragon.Point} point + * @returns {OpenSeadragon.Point} + */ + viewportToWindowCoordinates: function(point) { + $.console.assert(this.viewer, + "[Viewport.viewportToWindowCoordinates] the viewport must have a viewer."); + const viewerCoordinates = this.viewportToViewerElementCoordinates(point); + return viewerCoordinates.plus( + $.getElementPosition(this.viewer.container)); + }, + + /** + * Convert a viewport zoom to an image zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * Note: not accurate with multi-image. + * @function + * @param {Number} viewportZoom The viewport zoom + * target zoom. + * @returns {Number} imageZoom The image zoom + */ + viewportToImageZoom: function(viewportZoom) { + if (this.viewer) { + const count = this.viewer.world.getItemCount(); + if (count > 1) { + if (!this.silenceMultiImageWarnings) { + $.console.error('[Viewport.viewportToImageZoom] is not ' + + 'accurate with multi-image.'); + } + } else if (count === 1) { + // It is better to use TiledImage.viewportToImageZoom + // because this._contentBoundsNoRotate can not be relied on + // with clipping. + const item = this.viewer.world.getItemAt(0); + return item.viewportToImageZoom(viewportZoom); + } + } + + const imageWidth = this._contentSizeNoRotate.x; + const containerWidth = this._containerInnerSize.x; + const scale = this._contentBoundsNoRotate.width; + const viewportToImageZoomRatio = (containerWidth / imageWidth) * scale; + return viewportZoom * viewportToImageZoomRatio; + }, + + /** + * Convert an image zoom to a viewport zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * Note: not accurate with multi-image; use [TiledImage.imageToViewportZoom] for the specific image of interest. + * @function + * @param {Number} imageZoom The image zoom + * target zoom. + * @returns {Number} viewportZoom The viewport zoom + */ + imageToViewportZoom: function(imageZoom) { + if (this.viewer) { + const count = this.viewer.world.getItemCount(); + if (count > 1) { + if (!this.silenceMultiImageWarnings) { + $.console.error('[Viewport.imageToViewportZoom] is not accurate ' + + 'with multi-image. Instead, use [TiledImage.imageToViewportZoom] for the specific image of interest'); + } + } else if (count === 1) { + // It is better to use TiledImage.imageToViewportZoom + // because this._contentBoundsNoRotate can not be relied on + // with clipping. + const item = this.viewer.world.getItemAt(0); + return item.imageToViewportZoom(imageZoom); + } + } + + const imageWidth = this._contentSizeNoRotate.x; + const containerWidth = this._containerInnerSize.x; + const scale = this._contentBoundsNoRotate.width; + const viewportToImageZoomRatio = (imageWidth / containerWidth) / scale; + return imageZoom * viewportToImageZoomRatio; + }, + + /** + * Toggles flip state and demands a new drawing on navigator and viewer objects. + * @function + * @returns {OpenSeadragon.Viewport} Chainable. + */ + toggleFlip: function() { + this.setFlip(!this.getFlip()); + return this; + }, + + /** + * Get flip state stored on viewport. + * @function + * @returns {Boolean} Flip state. + */ + getFlip: function() { + return this.flipped; + }, + + /** + * Sets flip state according to the state input argument. + * @function + * @param {Boolean} state - Flip state to set. + * @returns {OpenSeadragon.Viewport} Chainable. + */ + setFlip: function( state ) { + if ( this.flipped === state ) { + return this; + } + + this.flipped = state; + if(this.viewer.navigator){ + this.viewer.navigator.setFlip(this.getFlip()); + } + this.viewer.forceRedraw(); + + /** + * Raised when flip state has been changed. + * + * @event flip + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {Number} flipped - The flip state after this change. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('flip', {flipped: state}); + return this; + }, + + /** + * Gets current max zoom pixel ratio + * @function + * @returns {Number} Max zoom pixel ratio + */ + getMaxZoomPixelRatio: function() { + return this.maxZoomPixelRatio; + }, + + /** + * Sets max zoom pixel ratio + * @function + * @param {Number} ratio - Max zoom pixel ratio + * @param {Boolean} [applyConstraints=true] - Apply constraints after setting ratio; + * Takes effect only if current zoom is greater than set max zoom pixel ratio + * @param {Boolean} [immediately=false] - Whether to animate to new zoom + */ + setMaxZoomPixelRatio: function(ratio, applyConstraints = true, immediately = false) { + + $.console.assert(!isNaN(ratio), "[Viewport.setMaxZoomPixelRatio] ratio must be a number"); + + if (isNaN(ratio)) { + return; + } + + this.maxZoomPixelRatio = ratio; + + if (applyConstraints) { + if (this.getZoom() > this.getMaxZoom()) { + this.applyConstraints(immediately); + } + } + }, + +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - TiledImage + * + * 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( $ ){ + +/** + * Object that keeps ready-to-draw tile state information. These properties might differ in time + * dynamically, e.g. when blending/animating. + * TODO: info.level is probably info.tile.level - remove? + * @typedef {Object} OpenSeadragon.TiledImage.DrawTileInfo + * @property {Number} level + * @property {Number} levelOpacity + * @property {Number} currentTime + * @property {OpenSeadragon.Tile} tile + */ + +/** + * Issues enum that records issues for the target image + * @typedef {('webgl')} OpenSeadragon.TiledImage.Issue + */ + +/** + * You shouldn't have to create a TiledImage instance directly; get it asynchronously by + * using {@link OpenSeadragon.Viewer#open} or {@link OpenSeadragon.Viewer#addTiledImage} instead. + * @class TiledImage + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. + * A new instance is created for each TileSource opened. + * @param {Object} options - Configuration for this TiledImage. + * @param {OpenSeadragon.TileSource} options.source - The TileSource that defines this TiledImage. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this TiledImage. + * @param {OpenSeadragon.TileCache} options.tileCache - The TileCache for this TiledImage to use. + * @param {OpenSeadragon.Drawer} options.drawer - The Drawer for this TiledImage to draw onto. + * @param {OpenSeadragon.ImageLoader} options.imageLoader - The ImageLoader for this TiledImage to use. + * @param {Number} [options.x=0] - Left position, in viewport coordinates. + * @param {Number} [options.y=0] - Top position, in viewport coordinates. + * @param {Number} [options.width=1] - Width, in viewport coordinates. + * @param {Number} [options.height] - Height, in viewport coordinates. + * @param {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates + * to fit the image into. If specified, x, y, width and height get ignored. + * @param {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER] + * How to anchor the image in the bounds if options.fitBounds is set. + * @param {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. + * @param {Number} [options.springStiffness] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.animationTime] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomImageRatio] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapHorizontal] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapVertical] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.immediateRender] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.blendTime] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.alwaysBlend] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.smoothTileEdgesMinZoom] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.opacity=1] - Set to draw at proportional opacity. If zero, images will not draw. + * @param {Boolean} [options.preload=false] - Set true to load even when the image is hidden by zero opacity. + * @param {String} [options.compositeOperation] - How the image is composited onto other images; + * see compositeOperation in {@link OpenSeadragon.Options} for possible values. + * @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. + * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. + * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.ajaxWithCredentials] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.loadTilesWithAjax] + * Whether to load tile data using AJAX requests. + * Defaults to the setting in {@link OpenSeadragon.Options}. + * @param {Object} [options.ajaxHeaders={}] + * A set of headers to include when making tile AJAX requests. + * @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, for example, in case a plugin always requires a certain + * format to convert to. + */ +$.TiledImage = function( options ) { + this._initialized = false; + /** + * The {@link OpenSeadragon.TileSource} that defines this TiledImage. + * @member {OpenSeadragon.TileSource} source + * @memberof OpenSeadragon.TiledImage# + */ + $.console.assert( options.tileCache, "[TiledImage] options.tileCache is required" ); + $.console.assert( options.drawer, "[TiledImage] options.drawer is required" ); + $.console.assert( options.viewer, "[TiledImage] options.viewer is required" ); + $.console.assert( options.imageLoader, "[TiledImage] options.imageLoader is required" ); + $.console.assert( options.source, "[TiledImage] options.source is required" ); + $.console.assert(!options.clip || options.clip instanceof $.Rect, + "[TiledImage] options.clip must be an OpenSeadragon.Rect if present"); + + $.EventSource.call( this ); + // Asynchronously loaded items remember where users wanted them + this._optimalWorldIndex = undefined; + + this._tileCache = options.tileCache; + delete options.tileCache; + + this._drawer = options.drawer; + delete options.drawer; + + this._imageLoader = options.imageLoader; + delete options.imageLoader; + + if (options.clip instanceof $.Rect) { + this._clip = options.clip.clone(); + } + + delete options.clip; + + const x = options.x || 0; + delete options.x; + const y = options.y || 0; + delete options.y; + + // Ratio of zoomable image height to width. + this.normHeight = options.source.dimensions.y / options.source.dimensions.x; + this.contentAspectX = options.source.dimensions.x / options.source.dimensions.y; + + let scale = 1; + if ( options.width ) { + scale = options.width; + delete options.width; + + if ( options.height ) { + $.console.error( "specifying both width and height to a tiledImage is not supported" ); + delete options.height; + } + } else if ( options.height ) { + scale = options.height / this.normHeight; + delete options.height; + } + + const fitBounds = options.fitBounds; + delete options.fitBounds; + const fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER; + delete options.fitBoundsPlacement; + + const degrees = options.degrees || 0; + delete options.degrees; + + const ajaxHeaders = options.ajaxHeaders; + delete options.ajaxHeaders; + + // Setter ensures lowercase + this.crossOriginPolicy = options.crossOriginPolicy; + delete options.crossOriginPolicy; + + $.extend( true, this, { + + //internal state properties + viewer: null, + tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. + coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas have been drawn. + loadingCoverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas are loaded or are being loaded/blended. + lastResetTime: 0, // Last time for which the tiledImage was reset. + _needsDraw: true, // Does the tiledImage need to be drawn again? + _needsUpdate: true, // Does the tiledImage need to update the viewport again? + _hasOpaqueTile: false, // Do we have even one fully opaque tile? + _tilesLoading: 0, // The number of pending tile requests. + _zombieCache: false, // Allow cache to stay in memory upon deletion. + _tilesToDraw: [], // info about the tiles currently in the viewport, two deep: array[level][tile] + _lastDrawn: [], // array of tiles that were last fetched by the drawer + _arrayCacheMap: [], // array cache to avoid constant re-creation and GC overload + _isBlending: false, // Are any tiles still being blended? + _wasBlending: false, // Were any tiles blending before the last draw? + _issues: {}, // An issue flag map - image was marked as problematic by some entity (usually a drawer)? + //configurable settings + springStiffness: $.DEFAULT_SETTINGS.springStiffness, + animationTime: $.DEFAULT_SETTINGS.animationTime, + minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, + wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, + wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, + immediateRender: $.DEFAULT_SETTINGS.immediateRender, + loadDestinationTilesOnAnimation: $.DEFAULT_SETTINGS.loadDestinationTilesOnAnimation, + blendTime: $.DEFAULT_SETTINGS.blendTime, + alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, + minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, + smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, + iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, + debugMode: $.DEFAULT_SETTINGS.debugMode, + ajaxWithCredentials: $.DEFAULT_SETTINGS.ajaxWithCredentials, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity, + preload: $.DEFAULT_SETTINGS.preload, + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation, + subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency, + maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame, + originalDataType: undefined, + _currentMaxTilesPerFrame: (options.maxTilesPerFrame || $.DEFAULT_SETTINGS.maxTilesPerFrame) * 10 + }, options ); + + this._preload = this.preload; + delete this.preload; + + this._fullyLoaded = false; + + this._xSpring = new $.Spring({ + initial: x, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._ySpring = new $.Spring({ + initial: y, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._scaleSpring = new $.Spring({ + initial: scale, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._degreesSpring = new $.Spring({ + initial: degrees, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._updateForScale(); + + if (fitBounds) { + this.fitBounds(fitBounds, fitBoundsPlacement, true); + } + + this._ownAjaxHeaders = {}; + this.setAjaxHeaders(ajaxHeaders, false); + this._initialized = true; + // this.invalidatedAt = 0; +}; + +$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ + /** + * @returns {Boolean} Whether the TiledImage needs to be drawn. + */ + needsDraw: function() { + return this._needsDraw; + }, + + /** + * Mark the tiled image as needing to be (re)drawn + */ + redraw: function() { + this._needsDraw = true; + }, + + /** + * @returns {Boolean} Whether all tiles necessary for this TiledImage to draw at the current view have been loaded. + */ + 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 + */ + whenFullyLoaded: function(callback) { + if (this.getFullyLoaded()) { + setTimeout(callback, 1); // Asynchronous execution + } else { + this.addOnceHandler('fully-loaded-change', function() { + callback(); // Maintain context + }); + } + }, + + // private + _setFullyLoaded: function(flag) { + if (flag === this._fullyLoaded) { + return; + } + + this._fullyLoaded = flag; + + /** + * Fired when the TiledImage's "fully loaded" flag (whether all tiles necessary for this TiledImage + * to draw at the current view have been loaded) changes. + * + * @event fully-loaded-change + * @memberof OpenSeadragon.TiledImage + * @type {object} + * @property {Boolean} fullyLoaded - The new "fully loaded" value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('fully-loaded-change', { + fullyLoaded: this._fullyLoaded + }); + }, + + /** + * Forces the system consider all tiles in this tiled image + * as outdated, and fire tile update event on relevant tiles + * Detailed description is available within the 'tile-invalidated' + * event. + * @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data + * @param {boolean} [viewportOnly=false] optionally invalidate only viewport-visible tiles if true + * @param {number} [tStamp=OpenSeadragon.now()] optionally provide tStamp of the update event + * @return {OpenSeadragon.Promise} + */ + requestInvalidate: function (restoreTiles = true, viewportOnly = false, tStamp = $.now()) { + const tiles = viewportOnly ? this._lastDrawn.map(x => x.tile) : this._tileCache.getLoadedTilesFor(this); + return this.viewer.world.requestTileInvalidateEvent(tiles, tStamp, restoreTiles); + }, + + /** + * Clears all tiles and triggers an update on the next call to + * {@link OpenSeadragon.TiledImage#update}. + */ + reset: function() { + this._tileCache.clearTilesFor(this); + this._currentMaxTilesPerFrame = this.maxTilesPerFrame * 10; + this.lastResetTime = $.now(); + this._needsDraw = true; + this._fullyLoaded = false; + }, + + /** + * Updates the TiledImage's bounds, animating if needed. Based on the new + * bounds, updates the levels and tiles to be drawn into the viewport. + * @param viewportChanged Whether the viewport changed meaning tiles need to be updated. + * @returns {Boolean} Whether the TiledImage needs to be drawn. + */ + update: function(viewportChanged) { + const xUpdated = this._xSpring.update(); + const yUpdated = this._ySpring.update(); + const scaleUpdated = this._scaleSpring.update(); + const degreesUpdated = this._degreesSpring.update(); + + const updated = (xUpdated || yUpdated || scaleUpdated || degreesUpdated || this._needsUpdate); + + if (updated || viewportChanged || !this._fullyLoaded){ + const fullyLoadedFlag = this._updateLevelsForViewport(); + this._setFullyLoaded(fullyLoadedFlag); + } + + this._needsUpdate = false; + + if (updated) { + this._updateForScale(); + this._raiseBoundsChange(); + this._needsDraw = true; + return true; + } + + return false; + }, + + /** + * Mark this TiledImage as having been drawn, so that it will only be drawn + * again if something changes about the image. If the image is still blending, + * this will have no effect. + * @returns {Boolean} whether the item still needs to be drawn due to blending + */ + setDrawn: function(){ + this._needsDraw = this._isBlending || this._wasBlending || + (this.opacity > 0 && this._lastDrawn.length < 1); + return this._needsDraw; + }, + + get crossOriginPolicy(){ + return this._crossOriginPolicy; + }, + + set crossOriginPolicy(crossOriginPolicy) { + if (typeof crossOriginPolicy === 'string') { + this._crossOriginPolicy = crossOriginPolicy.toLowerCase(); + } else { + this._crossOriginPolicy = $.DEFAULT_SETTINGS.crossOriginPolicy; + } + }, + + /** + * Set the internal issue flag for this TiledImage. Lazy loaded - not + * checked each time a Tile is loaded, but can be set if a consumer of the + * tiles (e.g. a Drawer) discovers a Tile to have certain data issue so that further + * checks are not needed and alternative rendering strategies can be used. + * @param {OpenSeadragon.TiledImage.Issue} issueType + * @param {string} description + * @param {Error|any} error + * @private + */ + setIssue(issueType, description = undefined, error = undefined){ + const errorText = error ? (error.message || error) : ''; + this._issues[issueType] = (description || `TiledImage is ${issueType}}`) + errorText; + $.console.warn(this._issues[issueType], error); + }, + + /** + * @param {OpenSeadragon.TiledImage.Issue} issueType + * @returns {string} issue details or undefined if the issue does not apply + */ + getIssue(issueType) { + return this._issues[issueType]; + }, + + /** + * @param {OpenSeadragon.TiledImage.Issue} issueType + * @returns {Boolean} whether the TiledImage has been marked with a given issue + */ + hasIssue(issueType){ + return !!this.getIssue(issueType); + }, + + /** + * Destroy the TiledImage (unload current loaded tiles). + */ + destroy: function() { + this.reset(); + this.source.destroy(this.viewer); + }, + + /** + * Get this TiledImage's bounds in viewport coordinates. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + */ + getBounds: function(current) { + return this.getBoundsNoRotate(current) + .rotate(this.getRotation(current), this._getRotationPoint(current)); + }, + + /** + * Get this TiledImage's bounds in viewport coordinates without taking + * rotation into account. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + */ + getBoundsNoRotate: function(current) { + return current ? + new $.Rect( + this._xSpring.current.value, + this._ySpring.current.value, + this._worldWidthCurrent, + this._worldHeightCurrent) : + new $.Rect( + this._xSpring.target.value, + this._ySpring.target.value, + this._worldWidthTarget, + this._worldHeightTarget); + }, + + // deprecated + getWorldBounds: function() { + $.console.error('[TiledImage.getWorldBounds] is deprecated; use TiledImage.getBounds instead'); + return this.getBounds(); + }, + + /** + * Get the bounds of the displayed part of the tiled image. + * @param {Boolean} [current=false] Pass true for the current location, + * false for the target location. + * @returns {$.Rect} The clipped bounds in viewport coordinates. + */ + getClippedBounds: function(current) { + let bounds = this.getBoundsNoRotate(current); + if (this._clip) { + const worldWidth = current ? + this._worldWidthCurrent : this._worldWidthTarget; + const ratio = worldWidth / this.source.dimensions.x; + const clip = this._clip.times(ratio); + bounds = new $.Rect( + bounds.x + clip.x, + bounds.y + clip.y, + clip.width, + clip.height); + } + return bounds.rotate(this.getRotation(current), this._getRotationPoint(current)); + }, + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @returns {OpenSeadragon.Rect} Where this tile fits (in normalized coordinates). + */ + getTileBounds: function( level, x, y ) { + const numTiles = this.source.getNumTiles(level); + const xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + const yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + const bounds = this.source.getTileBounds(level, xMod, yMod); + if (this.getFlip()) { + bounds.x = Math.max(0, 1 - bounds.x - bounds.width); + } + bounds.x += (x - xMod) / numTiles.x; + bounds.y += (this._worldHeightCurrent / this._worldWidthCurrent) * ((y - yMod) / numTiles.y); + return bounds; + }, + + /** + * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. + */ + getContentSize: function() { + return new $.Point(this.source.dimensions.x, this.source.dimensions.y); + }, + + /** + * @returns {OpenSeadragon.Point} The TiledImage's content size, in window coordinates. + */ + getSizeInWindowCoordinates: function() { + const topLeft = this.imageToWindowCoordinates(new $.Point(0, 0)); + const bottomRight = this.imageToWindowCoordinates(this.getContentSize()); + return new $.Point(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); + }, + + /** + * Get tile list that was used to draw the viewport current or last frame. + * @return {OpenSeadragon.OpenSeadragon.TiledImage.DrawTileInfo[]} + */ + get lastDrawn() { + return this._lastDrawn; + }, + + /** + * Get drawer instance used to draw this tiled image. Normally, + * it is the drawer of the base viewer that owns the tiled image. + * However, if the image is instantiated manually and used for example + * in offscreen rendering, you might need to change the reference drawer. + * @returns {OpenSeadragon.DrawerBase} The drawer instance used to draw this tiled image. + */ + getDrawer: function () { + return this.viewer.drawer; + }, + + // private + _viewportToImageDelta: function( viewerX, viewerY, current ) { + const scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point(viewerX * (this.source.dimensions.x / scale), + viewerY * ((this.source.dimensions.y * this.contentAspectX) / scale)); + }, + + /** + * Translates from OpenSeadragon viewer coordinate system to image coordinate system. + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} viewerX - The X coordinate or point in viewport coordinate system. + * @param {Number} [viewerY] - The Y coordinate in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @returns {OpenSeadragon.Point} A point representing the coordinates in the image. + */ + viewportToImageCoordinates: function(viewerX, viewerY, current) { + let point; + if (viewerX instanceof $.Point) { + //they passed a point instead of individual components + current = viewerY; + point = viewerX; + } else { + point = new $.Point(viewerX, viewerY); + } + + point = point.rotate(-this.getRotation(current), this._getRotationPoint(current)); + return current ? + this._viewportToImageDelta( + point.x - this._xSpring.current.value, + point.y - this._ySpring.current.value) : + this._viewportToImageDelta( + point.x - this._xSpring.target.value, + point.y - this._ySpring.target.value); + }, + + // private + _imageToViewportDelta: function( imageX, imageY, current ) { + const scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point((imageX / this.source.dimensions.x) * scale, + (imageY / this.source.dimensions.y / this.contentAspectX) * scale); + }, + + /** + * Translates from image coordinate system to OpenSeadragon viewer coordinate system + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} imageX - The X coordinate or point in image coordinate system. + * @param {Number} [imageY] - The Y coordinate in image coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @returns {OpenSeadragon.Point} A point representing the coordinates in the viewport. + */ + imageToViewportCoordinates: function(imageX, imageY, current) { + if (imageX instanceof $.Point) { + //they passed a point instead of individual components + current = imageY; + imageY = imageX.y; + imageX = imageX.x; + } + + const point = this._imageToViewportDelta(imageX, imageY, current); + if (current) { + point.x += this._xSpring.current.value; + point.y += this._ySpring.current.value; + } else { + point.x += this._xSpring.target.value; + point.y += this._ySpring.target.value; + } + + return point.rotate(this.getRotation(current), this._getRotationPoint(current)); + }, + + /** + * Translates from a rectangle which describes a portion of the image in + * pixel coordinates to OpenSeadragon viewport rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} imageX - The left coordinate or rectangle in image coordinate system. + * @param {Number} [imageY] - The top coordinate in image coordinate system. + * @param {Number} [pixelWidth] - The width in pixel of the rectangle. + * @param {Number} [pixelHeight] - The height in pixel of the rectangle. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @returns {OpenSeadragon.Rect} A rect representing the coordinates in the viewport. + */ + imageToViewportRectangle: function(imageX, imageY, pixelWidth, pixelHeight, current) { + let rect = imageX; + if (rect instanceof $.Rect) { + //they passed a rect instead of individual components + current = imageY; + } else { + rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); + } + + const coordA = this.imageToViewportCoordinates(rect.getTopLeft(), current); + const coordB = this._imageToViewportDelta(rect.width, rect.height, current); + + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y, + rect.degrees + this.getRotation(current) + ); + }, + + /** + * Translates from a rectangle which describes a portion of + * the viewport in point coordinates to image rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} viewerX - The left coordinate or rectangle in viewport coordinate system. + * @param {Number} [viewerY] - The top coordinate in viewport coordinate system. + * @param {Number} [pointWidth] - The width in viewport coordinate system. + * @param {Number} [pointHeight] - The height in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @returns {OpenSeadragon.Rect} A rect representing the coordinates in the image. + */ + viewportToImageRectangle: function( viewerX, viewerY, pointWidth, pointHeight, current ) { + let rect = viewerX; + if (viewerX instanceof $.Rect) { + //they passed a rect instead of individual components + current = viewerY; + } else { + rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); + } + + const coordA = this.viewportToImageCoordinates(rect.getTopLeft(), current); + const coordB = this._viewportToImageDelta(rect.width, rect.height, current); + + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y, + rect.degrees - this.getRotation(current) + ); + }, + + /** + * Convert pixel coordinates relative to the viewer element to image + * coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + viewerElementToImageCoordinates: function( pixel ) { + const point = this.viewport.pointFromPixel( pixel, true ); + return this.viewportToImageCoordinates( point ); + }, + + /** + * Convert pixel coordinates relative to the image to + * viewer element coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToViewerElementCoordinates: function( pixel ) { + const point = this.imageToViewportCoordinates( pixel ); + return this.viewport.pixelFromPoint( point, true ); + }, + + /** + * Convert pixel coordinates relative to the window to image coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + windowToImageCoordinates: function( pixel ) { + const viewerCoordinates = pixel.minus( + OpenSeadragon.getElementPosition( this.viewer.element )); + return this.viewerElementToImageCoordinates( viewerCoordinates ); + }, + + /** + * Convert image coordinates to pixel coordinates relative to the window. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToWindowCoordinates: function( pixel ) { + const viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); + return viewerCoordinates.plus( + OpenSeadragon.getElementPosition( this.viewer.element )); + }, + + // private + // Convert rectangle in viewport coordinates to this tiled image point + // coordinates (x in [0, 1] and y in [0, aspectRatio]) + _viewportToTiledImageRectangle: function(rect) { + const scale = this._scaleSpring.current.value; + rect = rect.rotate(-this.getRotation(true), this._getRotationPoint(true)); + return new $.Rect( + (rect.x - this._xSpring.current.value) / scale, + (rect.y - this._ySpring.current.value) / scale, + rect.width / scale, + rect.height / scale, + rect.degrees); + }, + + /** + * Convert a viewport zoom to an image zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * @function + * @param {Number} viewportZoom The viewport zoom + * @returns {Number} imageZoom The image zoom + */ + viewportToImageZoom: function( viewportZoom ) { + const ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return ratio * viewportZoom; + }, + + /** + * Convert an image zoom to a viewport zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * @function + * @param {Number} imageZoom The image zoom + * @returns {Number} viewportZoom The viewport zoom + */ + imageToViewportZoom: function( imageZoom ) { + const ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return imageZoom / ratio; + }, + + /** + * Sets the TiledImage's position in the world. + * @param {OpenSeadragon.Point} position - The new position, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new position or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setPosition: function(position, immediately) { + const sameTarget = (this._xSpring.target.value === position.x && + this._ySpring.target.value === position.y); + + if (immediately) { + if (sameTarget && this._xSpring.current.value === position.x && + this._ySpring.current.value === position.y) { + return; + } + + this._xSpring.resetTo(position.x); + this._ySpring.resetTo(position.y); + this._needsDraw = true; + this._needsUpdate = true; + } else { + if (sameTarget) { + return; + } + + this._xSpring.springTo(position.x); + this._ySpring.springTo(position.y); + this._needsDraw = true; + this._needsUpdate = true; + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + /** + * Sets the TiledImage's width in the world, adjusting the height to match based on aspect ratio. + * @param {Number} width - The new width, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setWidth: function(width, immediately) { + this._setScale(width, immediately); + }, + + /** + * Sets the TiledImage's height in the world, adjusting the width to match based on aspect ratio. + * @param {Number} height - The new height, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setHeight: function(height, immediately) { + this._setScale(height / this.normHeight, immediately); + }, + + /** + * Sets an array of polygons to crop the TiledImage during draw tiles. + * The render function will use the default non-zero winding rule. + * @param {OpenSeadragon.Point[][]} polygons - represented in an array of point object in image coordinates. + * Example format: [ + * [{x: 197, y:172}, {x: 226, y:172}, {x: 226, y:198}, {x: 197, y:198}], // First polygon + * [{x: 328, y:200}, {x: 330, y:199}, {x: 332, y:201}, {x: 329, y:202}] // Second polygon + * [{x: 321, y:201}, {x: 356, y:205}, {x: 341, y:250}] // Third polygon + * ] + */ + setCroppingPolygons: function( polygons ) { + const isXYObject = function(obj) { + return obj instanceof $.Point || (typeof obj.x === 'number' && typeof obj.y === 'number'); + }; + + const objectToSimpleXYObject = function(objs) { + return objs.map(function(obj) { + try { + if (isXYObject(obj)) { + return { x: obj.x, y: obj.y }; + } else { + throw new Error(); + } + } catch(e) { + throw new Error('A Provided cropping polygon point is not supported'); + } + }); + }; + + try { + if (!$.isArray(polygons)) { + throw new Error('Provided cropping polygon is not an array'); + } + this._croppingPolygons = polygons.map(function(polygon){ + return objectToSimpleXYObject(polygon); + }); + this._needsDraw = true; + } catch (e) { + $.console.error('[TiledImage.setCroppingPolygons] Cropping polygon format not supported'); + $.console.error(e); + this.resetCroppingPolygons(); + } + }, + + /** + * Resets the cropping polygons, thus next render will remove all cropping + * polygon effects. + */ + resetCroppingPolygons: function() { + this._croppingPolygons = null; + this._needsDraw = true; + }, + + /** + * Positions and scales the TiledImage to fit in the specified bounds. + * Note: this method fires OpenSeadragon.TiledImage.event:bounds-change + * twice + * @param {OpenSeadragon.Rect} bounds The bounds to fit the image into. + * @param {OpenSeadragon.Placement} [anchor=OpenSeadragon.Placement.CENTER] + * How to anchor the image in the bounds. + * @param {Boolean} [immediately=false] Whether to animate to the new size + * or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + fitBounds: function(bounds, anchor, immediately) { + anchor = anchor || $.Placement.CENTER; + const anchorProperties = $.Placement.properties[anchor]; + let aspectRatio = this.contentAspectX; + let xOffset = 0; + let yOffset = 0; + let displayedWidthRatio = 1; + let displayedHeightRatio = 1; + + if (this._clip) { + aspectRatio = this._clip.getAspectRatio(); + displayedWidthRatio = this._clip.width / this.source.dimensions.x; + displayedHeightRatio = this._clip.height / this.source.dimensions.y; + if (bounds.getAspectRatio() > aspectRatio) { + xOffset = this._clip.x / this._clip.height * bounds.height; + yOffset = this._clip.y / this._clip.height * bounds.height; + } else { + xOffset = this._clip.x / this._clip.width * bounds.width; + yOffset = this._clip.y / this._clip.width * bounds.width; + } + } + + if (bounds.getAspectRatio() > aspectRatio) { + // We will have margins on the X axis + const height = bounds.height / displayedHeightRatio; + let marginLeft = 0; + if (anchorProperties.isHorizontallyCentered) { + marginLeft = (bounds.width - bounds.height * aspectRatio) / 2; + } else if (anchorProperties.isRight) { + marginLeft = bounds.width - bounds.height * aspectRatio; + } + this.setPosition( + new $.Point(bounds.x - xOffset + marginLeft, bounds.y - yOffset), + immediately); + this.setHeight(height, immediately); + } else { + // We will have margins on the Y axis + const width = bounds.width / displayedWidthRatio; + let marginTop = 0; + if (anchorProperties.isVerticallyCentered) { + marginTop = (bounds.height - bounds.width / aspectRatio) / 2; + } else if (anchorProperties.isBottom) { + marginTop = bounds.height - bounds.width / aspectRatio; + } + this.setPosition( + new $.Point(bounds.x - xOffset, bounds.y - yOffset + marginTop), + immediately); + this.setWidth(width, immediately); + } + }, + + /** + * @returns {OpenSeadragon.Rect|null} The TiledImage's current clip rectangle, + * in image pixels, or null if none. + */ + getClip: function() { + if (this._clip) { + return this._clip.clone(); + } + + return null; + }, + + /** + * @param {OpenSeadragon.Rect|null} newClip - 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. + * @fires OpenSeadragon.TiledImage.event:clip-change + */ + setClip: function(newClip) { + $.console.assert(!newClip || newClip instanceof $.Rect, + "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); + + if (newClip instanceof $.Rect) { + this._clip = newClip.clone(); + } else { + this._clip = null; + } + + this._needsUpdate = true; + this._needsDraw = true; + /** + * Raised when the TiledImage's clip is changed. + * @event clip-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('clip-change'); + }, + + /** + * @returns {Boolean} Whether the TiledImage should be flipped before rendering. + */ + getFlip: function() { + return this.flipped; + }, + + /** + * @param {Boolean} flip Whether the TiledImage should be flipped before rendering. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setFlip: function(flip) { + this.flipped = flip; + }, + + get flipped() { + return this._flipped; + }, + set flipped(flipped) { + const changed = this._flipped !== !!flipped; + this._flipped = !!flipped; + if (changed && this._initialized) { + this.update(true); + this._needsDraw = true; + this._raiseBoundsChange(); + } + }, + + get wrapHorizontal(){ + return this._wrapHorizontal; + }, + set wrapHorizontal(wrap){ + const changed = this._wrapHorizontal !== !!wrap; + this._wrapHorizontal = !!wrap; + if(this._initialized && changed){ + this.update(true); + this._needsDraw = true; + // this._raiseBoundsChange(); + } + }, + + get wrapVertical(){ + return this._wrapVertical; + }, + set wrapVertical(wrap){ + const changed = this._wrapVertical !== !!wrap; + this._wrapVertical = !!wrap; + if(this._initialized && changed){ + this.update(true); + this._needsDraw = true; + // this._raiseBoundsChange(); + } + }, + + get debugMode(){ + return this._debugMode; + }, + set debugMode(debug){ + this._debugMode = !!debug; + this._needsDraw = true; + }, + + /** + * @returns {Number} The TiledImage's current opacity. + */ + getOpacity: function() { + return this.opacity; + }, + + /** + * @param {Number} opacity Opacity the tiled image should be drawn at. + * @fires OpenSeadragon.TiledImage.event:opacity-change + */ + setOpacity: function(opacity) { + this.opacity = opacity; + }, + + get opacity() { + return this._opacity; + }, + + set opacity(opacity) { + if (opacity === this.opacity) { + return; + } + + this._opacity = opacity; + this._needsDraw = true; + this._needsUpdate = true; + /** + * Raised when the TiledImage's opacity is changed. + * @event opacity-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {Number} opacity - The new opacity value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('opacity-change', { + opacity: this.opacity + }); + }, + + /** + * @returns {Boolean} whether the tiledImage can load its tiles even when it has zero opacity. + */ + getPreload: function() { + return this._preload; + }, + + /** + * Set true to load even when hidden. Set false to block loading when hidden. + */ + setPreload: function(preload) { + this._preload = !!preload; + this._needsDraw = true; + }, + + /** + * Get the rotation of this tiled image in degrees. + * @param {Boolean} [current=false] True for current rotation, false for target. + * @returns {Number} the rotation of this tiled image in degrees. + */ + getRotation: function(current) { + return current ? + this._degreesSpring.current.value : + this._degreesSpring.target.value; + }, + + /** + * Set the current rotation of this tiled image in degrees. + * @param {Number} degrees the rotation in degrees. + * @param {Boolean} [immediately=false] Whether to animate to the new angle + * or rotate immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setRotation: function(degrees, immediately) { + if (this._degreesSpring.target.value === degrees && + this._degreesSpring.isAtTargetValue()) { + return; + } + if (immediately) { + this._degreesSpring.resetTo(degrees); + } else { + this._degreesSpring.springTo(degrees); + } + this._needsDraw = true; + this._needsUpdate = true; + this._raiseBoundsChange(); + }, + + /** + * Get the region of this tiled image that falls within the viewport. + * @returns {OpenSeadragon.Rect} the region of this tiled image that falls within the viewport. + * Returns false for images with opacity==0 unless preload==true + */ + getDrawArea: function(){ + + if( this._opacity === 0 && !this._preload){ + return false; + } + + let drawArea = this._viewportToTiledImageRectangle( + this.viewport.getBoundsWithMargins(true)); + + if (!this.wrapHorizontal && !this.wrapVertical) { + const tiledImageBounds = this._viewportToTiledImageRectangle( + this.getClippedBounds(true)); + drawArea = drawArea.intersection(tiledImageBounds); + } + + return drawArea; + }, + + getLoadArea: function() { + let loadArea = this._viewportToTiledImageRectangle( + this.viewport.getBoundsWithMargins(false)); + + if (!this.wrapHorizontal && !this.wrapVertical) { + const tiledImageBounds = this._viewportToTiledImageRectangle( + this.getClippedBounds(false)); + loadArea = loadArea.intersection(tiledImageBounds); + } + + return loadArea; + }, + + /** + * Get tiles that should be drawn at the current position of tiled image. + * Note: this method should be called only once per frame. + * @returns {OpenSeadragon.TiledImage.DrawTileInfo[]} Array of Tiles that make up the current view + */ + getTilesToDraw: function(){ + // start with all the tiles added to this._tilesToDraw during the most recent + // call to this.update. Then update them so the blending and coverage properties + // are updated based on the current time + + // reuse last-drawn array to avoid allocations + const lastDrawn = this._lastDrawn; + + let insertionIndex = 0; + for (const maybeNested of this._tilesToDraw) { + if (Array.isArray(maybeNested)) { + for (const item of maybeNested) { + lastDrawn[insertionIndex++] = item; + } + } else if (maybeNested) { + lastDrawn[insertionIndex++] = maybeNested; + } + } + lastDrawn.length = insertionIndex; + // update all tiles, which can change the coverage provided + this._updateTilesInViewport(lastDrawn); + + // _tilesToDraw might have been updated by the update; refresh it + // mark the tiles as being drawn, so that they won't be discarded from + // the tileCache + insertionIndex = 0; + for (const maybeNested of this._tilesToDraw) { + if (Array.isArray(maybeNested)) { + for (const item of maybeNested) { + if (item.tile.loaded) { + item.tile.beingDrawn = true; + lastDrawn[insertionIndex++] = item; + } + } + } else if (maybeNested) { + if (maybeNested.tile.loaded) { + maybeNested.tile.beingDrawn = true; + lastDrawn[insertionIndex++] = maybeNested; + } + } + } + lastDrawn.length = insertionIndex; + return lastDrawn; + }, + + /** + * Get the point around which this tiled image is rotated + * @private + * @param {Boolean} current True for current rotation point, false for target. + * @returns {OpenSeadragon.Point} + */ + _getRotationPoint: function(current) { + return this.getBoundsNoRotate(current).getCenter(); + }, + + get compositeOperation(){ + return this._compositeOperation; + }, + + set compositeOperation(compositeOperation){ + + if (compositeOperation === this._compositeOperation) { + return; + } + this._compositeOperation = compositeOperation; + this._needsDraw = true; + /** + * Raised when the TiledImage's opacity is changed. + * @event composite-operation-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {String} compositeOperation - The new compositeOperation value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('composite-operation-change', { + compositeOperation: this._compositeOperation + }); + + }, + + /** + * @returns {String} The TiledImage's current compositeOperation. + */ + getCompositeOperation: function() { + return this._compositeOperation; + }, + + /** + * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. + * @fires OpenSeadragon.TiledImage.event:composite-operation-change + */ + setCompositeOperation: function(compositeOperation) { + this.compositeOperation = compositeOperation; //invokes setter + }, + + /** + * 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 tiles and queued image loader jobs. + * + * Note that the rules for merging headers still apply, i.e. headers returned by + * {@link OpenSeadragon.TileSource#getTileAjaxHeaders} take precedence over + * the headers here in the tiled image (`TiledImage.ajaxHeaders`). + * + * @function + * @param {Object} ajaxHeaders Updated AJAX headers, which will be merged over any headers specified in {@link OpenSeadragon.Options}. + * @param {Boolean} [propagate=true] Whether to propagate updated headers to existing tiles and queued image loader jobs. + */ + setAjaxHeaders: function(ajaxHeaders, propagate) { + if (ajaxHeaders === null) { + ajaxHeaders = {}; + } + if (!$.isPlainObject(ajaxHeaders)) { + $.console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); + return; + } + + this._ownAjaxHeaders = ajaxHeaders; + this._updateAjaxHeaders(propagate); + }, + + /** + * Update headers to include when making AJAX requests. + * + * This function has the same effect as calling {@link OpenSeadragon.TiledImage#setAjaxHeaders}, + * except that the headers for this tiled image do not change. This is especially useful + * for propagating updated headers from {@link OpenSeadragon.TileSource#getTileAjaxHeaders} + * to existing tiles. + * + * @private + * @function + * @param {Boolean} [propagate=true] Whether to propagate updated headers to existing tiles and queued image loader jobs. + */ + _updateAjaxHeaders: function(propagate) { + if (propagate === undefined) { + propagate = true; + } + + // merge with viewer's headers + if ($.isPlainObject(this.viewer.ajaxHeaders)) { + this.ajaxHeaders = $.extend({}, this.viewer.ajaxHeaders, this._ownAjaxHeaders); + } else { + this.ajaxHeaders = this._ownAjaxHeaders; + } + + // propagate header updates to all tiles and queued image loader jobs + if (propagate) { + let numTiles, xMod, yMod, tile; + + for (const level in this.tilesMatrix) { + numTiles = this.source.getNumTiles(level); + const matrixLevel = this.tilesMatrix[level]; + + for (const x in matrixLevel) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + + for (const y in matrixLevel[x]) { + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + tile = matrixLevel[x][y]; + + tile.loadWithAjax = this.loadTilesWithAjax; + if (tile.loadWithAjax) { + const tileAjaxHeaders = this.source.getTileAjaxHeaders( level, xMod, yMod ); + tile.ajaxHeaders = $.extend({}, this.ajaxHeaders, tileAjaxHeaders); + } else { + tile.ajaxHeaders = null; + } + } + } + } + + for (let i = 0; i < this._imageLoader.jobQueue.length; i++) { + const job = this._imageLoader.jobQueue[i]; + job.loadWithAjax = job.tile.loadWithAjax; + job.ajaxHeaders = job.tile.loadWithAjax ? job.tile.ajaxHeaders : null; + } + } + }, + + /** + * Enable cache preservation even without this tile image, + * by default disabled. It means that upon removing, + * the tile cache does not get immediately erased but + * stays in the memory to be potentially re-used by other + * TiledImages. + * @param {boolean} allow + */ + allowZombieCache: function(allow) { + this._zombieCache = allow; + }, + + // private + _setScale: function(scale, immediately) { + const sameTarget = (this._scaleSpring.target.value === scale); + if (immediately) { + if (sameTarget && this._scaleSpring.current.value === scale) { + return; + } + + this._scaleSpring.resetTo(scale); + this._updateForScale(); + this._needsDraw = true; + this._needsUpdate = true; + } else { + if (sameTarget) { + return; + } + + this._scaleSpring.springTo(scale); + this._updateForScale(); + this._needsDraw = true; + this._needsUpdate = true; + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + // private + _updateForScale: function() { + this._worldWidthTarget = this._scaleSpring.target.value; + this._worldHeightTarget = this.normHeight * this._scaleSpring.target.value; + this._worldWidthCurrent = this._scaleSpring.current.value; + this._worldHeightCurrent = this.normHeight * this._scaleSpring.current.value; + }, + + // private + _raiseBoundsChange: function() { + /** + * Raised when the TiledImage's bounds are changed. + * Note that this event is triggered only when the animation target is changed; + * not for every frame of animation. + * @event bounds-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('bounds-change'); + }, + + // private + _isBottomItem: function() { + return this.viewer.world.getItemAt(0) === this; + }, + + // private + _getLevelsInterval: function() { + let lowestLevel = Math.max( + this.source.minLevel, + Math.floor(Math.log(this.minZoomImageRatio) / Math.log(2)) + ); + const currentZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(0), true).x * + this._scaleSpring.current.value; + let highestLevel = Math.min( + Math.abs(this.source.maxLevel), + Math.abs(Math.floor( + Math.log(currentZeroRatio / this.minPixelRatio) / Math.log(2) + )) + ); + + // Calculations for the interval of levels to draw + // can return invalid intervals; fix that here if necessary + highestLevel = Math.max(highestLevel, this.source.minLevel || 0); + lowestLevel = Math.min(lowestLevel, highestLevel); + return { + lowestLevel: lowestLevel, + highestLevel: highestLevel + }; + }, + + // returns boolean flag of whether the image should be marked as fully loaded + _updateLevelsForViewport: function(){ + const levelsInterval = this._getLevelsInterval(); + const lowestLevel = levelsInterval.lowestLevel; // the lowest level we should draw at our current zoom + const highestLevel = levelsInterval.highestLevel; // the highest level we should draw at our current zoom + const drawArea = this.getDrawArea(); + + let loadArea = drawArea; + let bestLoadTileCandidates = this._getCachedArray('bestLoadTileCandidates', 0); + + if (this.loadDestinationTilesOnAnimation) { + loadArea = this.getLoadArea(); + } + const currentTime = $.now(); + + // reset each tile's beingDrawn flag + for (const tileInfo of this._lastDrawn) { + tileInfo.tile.beingDrawn = false; + } + // clear the list of tiles to draw + this._tilesToDraw.length = 0; + this._tilesLoading = 0; + this.loadingCoverage = {}; + + if (!drawArea){ + this._needsDraw = false; + return this._fullyLoaded; + } + + // make a list of levels to use for the current zoom level + const levelList = this._getCachedArray('levelList', highestLevel - lowestLevel + 1); + // go from highest to lowest resolution + for (let i = 0, level = highestLevel; level >= lowestLevel; level--, i++) { + levelList[i] = level; + } + + // if a single-tile level is loaded, add that to the end of the list + // as a fallback to use during zooming out, until a lower-res tile is + // loaded + for (let level = highestLevel + 1; level <= this.source.maxLevel; level++) { + const tile = ( + this.tilesMatrix[level] && + this.tilesMatrix[level][0] && + this.tilesMatrix[level][0][0] + ); + if (tile && tile.isBottomMost && tile.isRightMost && tile.loaded) { + levelList.push(level); + break; + } + } + + + // Update any level that will be drawn. + // We are iterating from highest resolution to lowest resolution + // Once a level fully covers the viewport the loop is halted and + // lower-resolution levels are skipped + let useLevel = false; + for (let i = 0; i < levelList.length; i++) { + const level = levelList[i]; + + const currentRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + true + ).x * this._scaleSpring.current.value; + + // make sure we skip levels until currentRenderPixelRatio becomes >= minPixelRatio + // but always use the last level in the list so we draw something + if (i === levelList.length - 1 || currentRenderPixelRatio >= this.minPixelRatio ) { + useLevel = true; + } else if (!useLevel) { + continue; + } + + const targetRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + false + ).x * this._scaleSpring.current.value; + + const targetZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio( + Math.max( + this.source.getClosestLevel(), + 0 + ) + ), + false + ).x * this._scaleSpring.current.value; + + const optimalRatio = this.immediateRender ? 1 : targetZeroRatio; + const levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5); + const levelVisibility = optimalRatio / Math.abs( + optimalRatio - targetRenderPixelRatio + ); + + // Update the level and keep track of 'best' tiles to load + const result = this._updateLevel( + level, + levelOpacity, + levelVisibility, + drawArea, + loadArea, + currentTime, + bestLoadTileCandidates + ); + + this.viewer.world.ensureTilesUpToDate(result.tilesToDraw); + + bestLoadTileCandidates = result.bestLoadTileCandidates; + this._tilesToDraw[level] = result.tilesToDraw; + + // Stop the loop if lower-res tiles would all be covered by + // already drawn tiles + if (this._providesCoverage(this.coverage, level)) { + break; + } + } + + + // Load the new 'best' n tiles + if (bestLoadTileCandidates && bestLoadTileCandidates.length > 0) { + // We need to set loading state immediatelly, if we need setTimeout() here, + // we should immediatelly set loading=true to all tiles + for (const tile of bestLoadTileCandidates) { + if (tile) { + this._loadTile(tile, currentTime); + } + } + this._needsDraw = true; + return false; + } else { + return this._tilesLoading === 0; + } + }, + + /** + * Update all tiles that contribute to the current view + * @private + * + */ + _updateTilesInViewport: function(tiles) { + const currentTime = $.now(); + const _this = this; + this._tilesLoading = 0; + this._wasBlending = this._isBlending; + this._isBlending = false; + this.loadingCoverage = {}; + const lowestLevel = tiles.length ? tiles[0].level : 0; + + const drawArea = this.getDrawArea(); + if(!drawArea){ + return; + } + + // Update each tile in the list of tiles. As the tiles are updated, + // the coverage provided is also updated. If a level provides coverage + // as part of this process, discard tiles from lower levels + let level = 0; + for (const info of tiles) { + const tile = info.tile; + if (tile && tile.loaded) { + const tileIsBlending = _this._blendTile( + tile, + tile.x, + tile.y, + info.level, + info.levelOpacity, + currentTime, + lowestLevel + ); + _this._isBlending = _this._isBlending || tileIsBlending; + _this._needsDraw = _this._needsDraw || tileIsBlending || _this._wasBlending; + } + + if (this._providesCoverage(this.coverage, info.level)) { + level = Math.max(level, info.level); + } + } + if (level > 0) { + for (const levelKey in this._tilesToDraw) { + if (levelKey < level) { + this._tilesToDraw[levelKey] = undefined; + } + } + } + }, + + /** + * Updates the opacity of a tile according to the time it has been on screen + * to perform a fade-in. + * Updates coverage once a tile is fully opaque. + * Returns whether the fade-in has completed. + * @private + * + * @param {OpenSeadragon.Tile} tile + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} levelOpacity + * @param {Number} currentTime + * @param {Boolean} lowestLevel + * @returns {Boolean} true if blending did not yet finish + */ + _blendTile: function(tile, x, y, level, levelOpacity, currentTime, lowestLevel ){ + let blendTimeMillis = 1000 * this.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + // if this tile is at the lowest level being drawn, render at opacity=1 + if(level === lowestLevel){ + opacity = 1; + deltaTime = blendTimeMillis; + } + + if ( this.alwaysBlend ) { + opacity *= levelOpacity; + } + tile.opacity = opacity; + + if ( opacity === 1 ) { + this._setCoverage( this.coverage, level, x, y, true ); + this._hasOpaqueTile = true; + } + // return true if the tile is still blending + return deltaTime < blendTimeMillis; + }, + + /** + * Updates all tiles at a given resolution level. + * @private + * @param {Number} level + * @param {Number} levelOpacity + * @param {Number} levelVisibility + * @param {OpenSeadragon.Rect} drawArea + * @param {OpenSeadragon.Rect} loadArea + * @param {Number} currentTime + * @param {OpenSeadragon.Tile[]} bestLoadTileCandidates Array of the current best tiles + * @returns {Object} Dictionary { + * bestLoadTileCandidates: OpenSeadragon.Tile - the current "best" tiles to draw, + * tilesToDraw: OpenSeadragon.Tile) - the updated tiles + * } + */ + _updateLevel: function(level, levelOpacity, + levelVisibility, drawArea, loadArea, currentTime, bestLoadTileCandidates) { + + const drawTopLeftBound = drawArea.getBoundingBox().getTopLeft(); + const drawBottomRightBound = drawArea.getBoundingBox().getBottomRight(); + if (this.viewer) { + /** + * - Needs documentation - + * + * @event update-level + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {Object} havedrawn - deprecated, always true (kept for backwards compatibility) + * @property {Object} level + * @property {Object} opacity + * @property {Object} visibility + * @property {OpenSeadragon.Rect} drawArea + * @property {Object} topleft deprecated, use drawArea instead + * @property {Object} bottomright deprecated, use drawArea instead + * @property {Object} currenttime + * @property {Object[]} best + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('update-level', { + tiledImage: this, + havedrawn: true, // deprecated, kept for backwards compatibility + level: level, + opacity: levelOpacity, + visibility: levelVisibility, + drawArea: drawArea, + topleft: drawTopLeftBound, + bottomright: drawBottomRightBound, + currenttime: currentTime, + // todo misleading name, consider changing + best: bestLoadTileCandidates + }); + } + + const numberOfTiles = this.source.getNumTiles(level); + const viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter()); + this._resetCoverage(this.coverage, level); + if (loadArea) { + this._resetCoverage(this.loadingCoverage, level); + } + + let tilesToDraw = null; + let tileIndex = 0; + + // Iterate over tiles and decide, which will be loaded and drawn + this._visitTiles(level, drawArea, (x, y, total) => { + const tile = this._getTile( + x, y, + level, + currentTime, + numberOfTiles + ); + + if (!tilesToDraw) { + tilesToDraw = this._getCachedArray(level, total); + } + + ///////////////////////////////////////////////////// + // First Part: Decide if tile will be used to draw // + ///////////////////////////////////////////////////// + + if( this.viewer ){ + /** + * This event is called before tile is being updated: its position, coverage and other properties. + * Note that this does not mean the tile will be loaded, it might be just updated greedily to avoid + * visible load animation (happens if position is updated lazily when tile.loaded / loading is true). + * + * @event update-tile + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'update-tile', { + tiledImage: this, + tile: tile + }); + } + + this._setCoverage( this.coverage, level, x, y, false ); + + if (tile.exists) { + if (tile.loaded) { + if (tile.opacity === 1) { + this._setCoverage( this.coverage, level, x, y, true ); + } + + // Tiles are carried in info objects + tilesToDraw[tileIndex++] = { + tile: tile, + level: level, + levelOpacity: levelOpacity, + currentTime: currentTime + }; + this._setCoverage(this.loadingCoverage, level, x, y, true); + } + + this._positionTile( + tile, + this.source.tileOverlap, + this.viewport, + viewportCenter, + levelVisibility + ); + } + + ///////////////////////////////////////////////////// + // Second Part: Decide if tile will be loaded // + ///////////////////////////////////////////////////// + + if (loadArea && !tile.loaded) { + let loadingCoverage = tile.loading || this._isCovered(this.loadingCoverage, level, x, y); + this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage); + + if ( !tile.exists ) { + return; + } + + // Try-find will populate tile with data if equal tile exists in system + if (!tile.loading && this._tryFindTileCacheRecord(tile)) { + loadingCoverage = true; + } + + if (tile.loading) { + // the tile is already in the download queue or being processed + this._tilesLoading++; + } else if (!loadingCoverage) { + // add tile to best tiles to load only when not loaded already + bestLoadTileCandidates = this._compareTiles( bestLoadTileCandidates, tile, this._currentMaxTilesPerFrame); + } + } + }); + + // _currentMaxTilesPerFrame can be temporarily boosted, bring it down after each usage if necessary + if (this._currentMaxTilesPerFrame > this.maxTilesPerFrame) { + this._currentMaxTilesPerFrame = Math.max(Math.ceil(this._currentMaxTilesPerFrame / 2), this.maxTilesPerFrame); + } + + if (tilesToDraw) { + tilesToDraw.length = tileIndex; + } + + return { + bestLoadTileCandidates: bestLoadTileCandidates, + tilesToDraw: tilesToDraw || [] + }; + }, + + /** + * Visit all tiles in an a given area on a given level. + * @private + * @param {Number} level + * @param {OpenSeadragon.Rect} area + * @param {Function} callback - x, y, total - tile x, y position and total number of tiles + */ + _visitTiles: function(level, area, callback) { + const bbox = area.getBoundingBox(); + const drawCornerTiles = this._getCornerTiles(level, bbox.getTopLeft(), bbox.getBottomRight()); + const drawTopLeftTile = drawCornerTiles.topLeft; + const drawBottomRightTile = drawCornerTiles.bottomRight; + + const numberOfTiles = this.source.getNumTiles(level); + + if (this.getFlip()) { + // The right-most tile can be narrower than the others. When flipped, + // this tile is now on the left. Because it is narrower than the normal + // left-most tile, the subsequent tiles may not be wide enough to completely + // fill the viewport. Fix this by rendering an extra column of tiles. If we + // are not wrapping, make sure we never render more than the number of tiles + // in the image. + drawBottomRightTile.x += 1; + if (!this.wrapHorizontal) { + drawBottomRightTile.x = Math.min(drawBottomRightTile.x, numberOfTiles.x - 1); + } + } + const numTiles = Math.max(0, (drawBottomRightTile.x - drawTopLeftTile.x) * (drawBottomRightTile.y - drawTopLeftTile.y)); + + for (let x = drawTopLeftTile.x; x <= drawBottomRightTile.x; x++) { + for (let y = drawTopLeftTile.y; y <= drawBottomRightTile.y; y++) { + + let flippedX; + if (this.getFlip()) { + const xMod = ( numberOfTiles.x + ( x % numberOfTiles.x ) ) % numberOfTiles.x; + flippedX = x + numberOfTiles.x - xMod - xMod - 1; + } else { + flippedX = x; + } + + if (area.intersection(this.getTileBounds(level, flippedX, y)) === null) { + // This tile is not in the draw area + continue; + } + + callback(flippedX, y, numTiles); + } + } + }, + + /** + * @private + * @param {OpenSeadragon.Tile} tile + * @param {Boolean} overlap + * @param {OpenSeadragon.Viewport} viewport + * @param {OpenSeadragon.Point} viewportCenter + * @param {Number} levelVisibility + */ + _positionTile: function( tile, overlap, viewport, viewportCenter, levelVisibility ){ + const boundsTL = tile.bounds.getTopLeft(); + + boundsTL.x *= this._scaleSpring.current.value; + boundsTL.y *= this._scaleSpring.current.value; + boundsTL.x += this._xSpring.current.value; + boundsTL.y += this._ySpring.current.value; + + const boundsSize = tile.bounds.getSize(); + + boundsSize.x *= this._scaleSpring.current.value; + boundsSize.y *= this._scaleSpring.current.value; + + tile.positionedBounds.x = boundsTL.x; + tile.positionedBounds.y = boundsTL.y; + tile.positionedBounds.width = boundsSize.x; + tile.positionedBounds.height = boundsSize.y; + + const positionC = viewport.pixelFromPointNoRotate(boundsTL, true); + const positionT = viewport.pixelFromPointNoRotate(boundsTL, false); + let sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true); + const sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false); + const tileCenter = positionT.plus( sizeT.divide( 2 ) ); + const tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter ); + + if(this.getDrawer().minimumOverlapRequired(this)){ + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point(1, 1)); + } + + if (tile.isRightMost && this.wrapHorizontal) { + sizeC.x += 0.75; // Otherwise Firefox and Safari show seams + } + + if (tile.isBottomMost && this.wrapVertical) { + sizeC.y += 0.75; // Otherwise Firefox and Safari show seams + } + } + + tile.position = positionC; + tile.size = sizeC; + tile.squaredDistance = tileSquaredDistance; + tile.visibility = levelVisibility; + }, + + // private + _getCornerTiles: function(level, topLeftBound, bottomRightBound) { + let leftX; + let rightX; + if (this.wrapHorizontal) { + leftX = $.positiveModulo(topLeftBound.x, 1); + rightX = $.positiveModulo(bottomRightBound.x, 1); + } else { + leftX = Math.max(0, topLeftBound.x); + rightX = Math.min(1, bottomRightBound.x); + } + let topY; + let bottomY; + const aspectRatio = 1 / this.source.aspectRatio; + if (this.wrapVertical) { + topY = $.positiveModulo(topLeftBound.y, aspectRatio); + bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio); + } else { + topY = Math.max(0, topLeftBound.y); + bottomY = Math.min(aspectRatio, bottomRightBound.y); + } + + const topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY)); + const bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY)); + const numTiles = this.source.getNumTiles(level); + + if (this.wrapHorizontal) { + topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x); + bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x); + } + if (this.wrapVertical) { + topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio); + bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio); + } + + return { + topLeft: topLeftTile, + bottomRight: bottomRightTile, + }; + }, + + /** + * @private + * @inner + * Try to find existing cache of the tile + * @param {OpenSeadragon.Tile} tile + */ + _tryFindTileCacheRecord: function(tile) { + const record = this._tileCache.getCacheRecord(tile.originalCacheKey); + + if (!record) { + return false; + } + tile.loading = true; + this._setTileLoaded(tile, record.data, null, null, record.type); + return true; + }, + + /** + * @private + * @inner + * Obtains a tile at the given location. + * @private + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} time + * @param {Number} numTiles + * @returns {OpenSeadragon.Tile} + */ + _getTile: function( + x, y, + level, + time, + numTiles + ) { + let xMod, + yMod, + bounds, + sourceBounds, + exists, + urlOrGetter, + post, + ajaxHeaders, + tile, + tilesMatrix = this.tilesMatrix, + tileSource = this.source; + + let matrixLevel = tilesMatrix[ level ]; + if ( !matrixLevel ) { + tilesMatrix[ level ] = matrixLevel = {}; + } + if ( !matrixLevel[ x ] ) { + matrixLevel[ x ] = {}; + } + + if ( !matrixLevel[ x ][ y ] || !matrixLevel[ x ][ y ].flipped !== !this.flipped ) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + bounds = this.getTileBounds( level, x, y ); + sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true ); + exists = tileSource.tileExists( level, xMod, yMod ); + urlOrGetter = tileSource.getTileUrl( level, xMod, yMod ); + post = tileSource.getTilePostData( level, xMod, yMod ); + + // Headers are only applicable if loadTilesWithAjax is set + if (this.loadTilesWithAjax) { + ajaxHeaders = tileSource.getTileAjaxHeaders( level, xMod, yMod ); + // Combine tile AJAX headers with tiled image AJAX headers (if applicable) + if ($.isPlainObject(this.ajaxHeaders)) { + ajaxHeaders = $.extend({}, this.ajaxHeaders, ajaxHeaders); + } + } else { + ajaxHeaders = null; + } + + tile = new $.Tile( + level, + x, + y, + bounds, + exists, + urlOrGetter, + undefined, + this.loadTilesWithAjax, + ajaxHeaders, + sourceBounds, + post, + tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, post) + ); + + if (this.getFlip()) { + if (xMod === 0) { + tile.isRightMost = true; + } + } else { + if (xMod === numTiles.x - 1) { + tile.isRightMost = true; + } + } + + if (yMod === numTiles.y - 1) { + tile.isBottomMost = true; + } + + tile.flipped = this.flipped; + + matrixLevel[ x ][ y ] = tile; + } else { + tile = matrixLevel[ x ][ y ]; + } + tile.lastTouchTime = time; + + return tile; + }, + + /** + * Dispatch a job to the ImageLoader to load the Image for a Tile. + * @private + * @param {OpenSeadragon.Tile} tile + * @param {Number} time + */ + _loadTile: function(tile, time ) { + const _this = this; + tile.loading = true; + tile.tiledImage = this; + if (!this._imageLoader.addJob({ + src: tile.getUrl(), + tile: tile, + source: this.source, + postData: tile.postData, + loadWithAjax: tile.loadWithAjax, + ajaxHeaders: tile.ajaxHeaders, + crossOriginPolicy: this.crossOriginPolicy, + ajaxWithCredentials: this.ajaxWithCredentials, + callback: function( data, errorMsg, tileRequest, dataType, tries ){ + _this._onTileLoad( tile, time, data, errorMsg, tileRequest, dataType, tries ); + }, + abort: function() { + tile.loading = false; + } + })) { + /** + * Triggered if tile load job was added to a full queue. + * This allows to react upon e.g. network not being able to serve the tiles fast enough. + * @event job-queue-full + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Tile} tile - The tile that failed to load. + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to. + * @property {number} time - The time in milliseconds when the tile load began. + */ + this.viewer.raiseEvent("job-queue-full", { + tile: tile, + tiledImage: this, + time: time, + }); + } + }, + + /** + * Callback fired when a Tile's Image finished downloading. + * @private + * @param {OpenSeadragon.Tile} tile + * @param {Number} time + * @param {*} data image data + * @param {String} errorMsg + * @param {XMLHttpRequest} tileRequest + * @param {String} [dataType=undefined] data type, derived automatically if not set + * @param {number} tries - The number of times the tile has been retried. + */ + _onTileLoad: function( tile, time, data, errorMsg, tileRequest, dataType, tries ) { + //data is set to null on error by image loader, allow custom falsey values (e.g. 0) + if ( data === null || data === undefined ) { + $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), errorMsg ); + /** + * Triggered when a tile fails to load. + * + * @event tile-load-failed + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Tile} tile - The tile that failed to load. + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to. + * @property {number} time - The time in milliseconds when the tile load began. + * @property {string} message - The error message. + * @property {number} tries - The number of times the tile has been retried. + * @property {boolean} maxReached - Whether the maximum number of retries has been reached. + * @property {XMLHttpRequest} tileRequest - The XMLHttpRequest used to load the tile if available. + */ + this.viewer.raiseEvent("tile-load-failed", { + tile: tile, + tiledImage: this, + time: time, + message: errorMsg, + tileRequest: tileRequest, + tries: tries, + maxReached: this.viewer.tileRetryMax === 0 ? true : tries >= this.viewer.tileRetryMax + }); + tile.loading = false; + tile.exists = false; + return; + } else { + tile.exists = true; + } + + if ( time < this.lastResetTime ) { + $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.getUrl() ); + tile.loading = false; + return; + } + + if (this.originalDataType) { + // First fetch conversion path to ensure safe conversion and to deduce target type it chooses as optimal one + const conversion = $.converter.getConversionPath(dataType, this.originalDataType); + if (conversion) { + const desiredType = $.converter.getConversionPathFinalType(conversion); + $.converter.convert(tile, data, dataType, desiredType).then(newData => { + this._setTileLoaded(tile, newData, null, tileRequest, desiredType); + }).catch(e => { + $.console.warn("Failed to satisfy original type [%s] %s from %s: %s", desiredType, tile, dataType, e); + this._setTileLoaded(tile, data, null, tileRequest, dataType); + }); + } else { + $.console.warn( "Ignoring default base tile data type %s: no conversion possible from %s", this.originalDataType, dataType); + this._setTileLoaded(tile, data, null, tileRequest, dataType); + } + } else { + this._setTileLoaded(tile, data, null, tileRequest, dataType); + } + }, + + /** + * @private + * @param {OpenSeadragon.Tile} tile + * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object, + * can be null: in that case, cache is assigned to a tile without further processing + * @param {?Number} cutoff ignored, @deprecated + * @param {?XMLHttpRequest} tileRequest + * @param {?String} [dataType=undefined] data type, derived automatically if not set + */ + _setTileLoaded: function(tile, data, cutoff, tileRequest, dataType) { + tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back + // does nothing if tile.cacheKey already present + + $.console.assert(dataType !== undefined, "TileSource::downloadTileStart must return a dataType."); + + let tileCacheCreated = false; + tile.addCache(tile.cacheKey, () => { + tileCacheCreated = true; + return data; + }, dataType, false, false); + + let resolver = null, + increment = 0, + eventFinished = false; + const _this = this; + + function completionCallback() { + increment--; + if (increment > 0) { + return; + } + eventFinished = true; + + //do not override true if set (false is default) + tile.hasTransparency = tile.hasTransparency || _this.source.hasTransparency( + undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData + ); + tile.loading = false; + tile.loaded = true; + _this.redraw(); + resolver(tile); + } + + function getCompletionCallback() { + if (eventFinished) { + $.console.error("Event 'tile-loaded' argument getCompletionCallback must be called synchronously. " + + "Its return value should be called asynchronously."); + } + increment++; + return completionCallback; + } + + function markTileAsReady() { + const fallbackCompletion = getCompletionCallback(); + + /** + * Triggered when a tile has just been loaded in memory. That means that the + * image has been downloaded and can be modified before being drawn to the canvas. + * This event is _awaiting_, it supports asynchronous functions or functions that return a promise. + * + * @event tile-loaded + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {Image|*} image - The image (data) of the tile. Deprecated. + * @property {*} data image data, the data sent to ImageJob.prototype.finish(), + * by default an Image object. Deprecated + * @property {String} dataType type of the data + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. + * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. + * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable). + * @property {OpenSeadragon.Promise} - Promise resolved when the tile gets fully loaded. + * NOTE: DO NOT await the promise in the handler: you will create a deadlock! + * @property {function} getCompletionCallback - deprecated + */ + _this.viewer.raiseEventAwaiting("tile-loaded", { + tile: tile, + tiledImage: _this, + tileRequest: tileRequest, + promise: new $.Promise(resolve => { + resolver = resolve; + }), + get image() { + $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile-invalidated' event to modify data instead."); + return data; + }, + get data() { + $.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile-invalidated' event to modify data instead."); + return data; + }, + getCompletionCallback: function () { + $.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: " + + "use async event handlers instead, execution order is deducted by addHandler(...) priority argument."); + return getCompletionCallback(); + }, + }).catch(() => { + $.console.error("[tile-loaded] event finished with failure: there might be a problem with a plugin you are using."); + }).then(fallbackCompletion); + } + + + if (tileCacheCreated) { + // setting invalidation tstamp to 1 makes sure any update gets applied later on + this.viewer.world.requestTileInvalidateEvent([tile], undefined, false, true, true).then(markTileAsReady).catch(markTileAsReady); + } else { + const origCache = tile.getCache(tile.originalCacheKey); + // First, ensure we really are ready to draw the tile + const ensureValidDrawerType = (cache) => { + if (this.viewer.isDestroyed()) { + return $.Promise.resolve(); + } + const drawer = this.getDrawer(); + if (!cache.isUsableForDrawer(drawer)) { + return cache.prepareForRendering(drawer); + } + return $.Promise.resolve(); + }; + + // Tile-invalidated not called on each tile, but only on tiles with new data! Verify we share the main cache + for (const t of origCache._tiles) { + + // To keep consistent, if we find main cache tile that differs from original cache key, we inherit also main cache + if (t.cacheKey !== tile.cacheKey) { + + // add reference also to the main cache, no matter what the other tile state has + // completion of the invaldate event should take care of all such tiles + const targetMainCache = t.getCache(); + ensureValidDrawerType(targetMainCache).then( + () => tile.setCache(t.cacheKey, targetMainCache, true, false) + ).then(markTileAsReady); + return; + } + + if (t.processing) { + // Or, if there is a processing promise, we wait for it to complete and then update the tile state. + t.processingPromise.then(t => { + const targetMainCache = t.getCache(); + ensureValidDrawerType(targetMainCache).then(() => { + tile.setCache(t.cacheKey, targetMainCache, true, false); + if (!targetMainCache.loaded) { + return targetMainCache.await(); + } + return null; + }).then(markTileAsReady); + }); + return; + } + } + ensureValidDrawerType(origCache).then(markTileAsReady); + } + }, + + + /** + * Determines the 'best tiles' from the given 'last best' tiles and the + * tile in question. Keeps the best tiles sorted according to visibility and distance. + * @private + * + * @param {OpenSeadragon.Tile[]} previousBest The best tiles so far. Must be sorted. If not, + * call previousBest.sort(this._sortTilesComparator) first. + * @param {OpenSeadragon.Tile} tile The new tile to consider. + * @param {Number} maxNTiles The max number of best tiles. + * @returns {OpenSeadragon.Tile[]} The new best tiles. + */ + _compareTiles: function( previousBest, tile, maxNTiles ) { + if ( !previousBest ) { + return [tile]; + } + + let inserted = false; + for (let tileIndex = 0; tileIndex < previousBest.length; tileIndex++) { + const nextTile = previousBest[tileIndex]; + if (this._sortTilesComparator(nextTile, tile) > 0) { + previousBest.splice(tileIndex, 0, tile); + inserted = true; + break; + } + } + + if (!inserted) { + previousBest.push(tile); + } + + if (previousBest.length > maxNTiles) { + previousBest.pop(); + } + return previousBest; + }, + + /** + * Comparator for tile sorting according to visibility and distance. + * @private + * + * @param {OpenSeadragon.Tile} a The tile a. + * @param {OpenSeadragon.Tile} b The tile b. + */ + _sortTilesComparator: function(a, b) { + if (a === null) { + return 1; + } + if (b === null) { + return -1; + } + if (a.visibility === b.visibility) { + // sort by smallest squared distance + return a.squaredDistance - b.squaredDistance; + } + // sort by largest visibility value + return b.visibility - a.visibility; + }, + + /** + * To avoid repeatedly creating arrays for each frame, we cache them. + * @param {any} key + * @param {Number} [length=undefined] + * @private + */ + _getCachedArray: function(key, length = undefined) { + let arr = this._arrayCacheMap[key]; + if (!arr) { + if (length !== undefined) { + arr = this._arrayCacheMap[key] = new Array(length); + } else { + arr = this._arrayCacheMap[key] = []; + } + } else if (length !== undefined) { + arr.length = length; + } + return arr; + }, + + /** + * Returns true if the given tile provides coverage to lower-level tiles of + * lower resolution representing the same content. If neither x nor y is + * given, returns true if the entire visible level provides coverage. + * + * Note that out-of-bounds tiles provide coverage in this sense, since + * there's no content that they would need to cover. Tiles at non-existent + * levels that are within the image bounds, however, do not. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of the tile. + * @param {Number} x - The X position of the tile. + * @param {Number} y - The Y position of the tile. + * @returns {Boolean} + */ + _providesCoverage: function( coverage, level, x, y ) { + let rows; + let cols; + let i, j; + + if ( !coverage[ level ] ) { + return false; + } + + if ( x === undefined || y === undefined ) { + rows = coverage[ level ]; + for ( i in rows ) { + if ( Object.prototype.hasOwnProperty.call( rows, i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( Object.prototype.hasOwnProperty.call( cols, j ) && !cols[ j ] ) { + return false; + } + } + } + } + + return true; + } + + return ( + coverage[ level ][ x] === undefined || + coverage[ level ][ x ][ y ] === undefined || + coverage[ level ][ x ][ y ] === true + ); + }, + + /** + * Returns true if the given tile is completely covered by higher-level + * tiles of higher resolution representing the same content. If neither x + * nor y is given, returns true if the entire visible level is covered. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of the tile. + * @param {Number} x - The X position of the tile. + * @param {Number} y - The Y position of the tile. + * @returns {Boolean} + */ + _isCovered: function( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return this._providesCoverage( coverage, level + 1 ); + } else { + return ( + this._providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + this._providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + this._providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + this._providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) + ); + } + }, + + /** + * Sets whether the given tile provides coverage or not. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of the tile. + * @param {Number} x - The X position of the tile. + * @param {Number} y - The Y position of the tile. + * @param {Boolean} covers - Whether the tile provides coverage. + */ + _setCoverage: function( coverage, level, x, y, covers ) { + if ( !coverage[ level ] ) { + $.console.warn( + "Setting coverage for a tile before its level's coverage has been reset: %s", + level + ); + return; + } + + if ( !coverage[ level ][ x ] ) { + coverage[ level ][ x ] = {}; + } + + coverage[ level ][ x ][ y ] = covers; + }, + + /** + * Resets coverage information for the given level. This should be called + * after every draw routine. Note that at the beginning of the next draw + * routine, coverage for every visible tile should be explicitly set. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of tiles to completely reset. + */ + _resetCoverage: function( coverage, level ) { + coverage[ level ] = {}; + } +}); + + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - TileCache + * + * 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( $ ){ + + const OpenSeadragon = $; // alias for JSDoc + + const DRAWER_INTERNAL_CACHE = Symbol("DRAWER_INTERNAL_CACHE"); + + /** + * @class OpenSeadragon.CacheRecord + * @memberof OpenSeadragon + * @classdesc Cached Data Record, the cache object. Keeps only latest object type required. + * + * This class acts like the Maybe type: + * - it has 'loaded' flag indicating whether the tile data is ready + * - it has 'data' property that has value if loaded=true + * + * Furthermore, it has a 'getData' function that returns a promise resolving + * with the value on the desired type passed to the function. + */ + OpenSeadragon.CacheRecord = class CacheRecord { + constructor() { + this.revive(); + } + + /** + * Access the cache record data directly. Preferred way of data access. + * Might be undefined if this.loaded = false. + * You can access the data in synchronous way, but the data might not be available. + * If you want to access the data indirectly (await), use this.transformTo or this.getDataAs + * @returns {any} + */ + get data() { + return this._data; + } + + /** + * Read the cache type. The type can dynamically change, but should be consistent at + * one point in the time. For available types see the OpenSeadragon.Converter, or the tutorials. + * @returns {string} + */ + get type() { + return this._type; + } + + /** + * Await ongoing process so that we get cache ready on callback. + * @returns {OpenSeadragon.Promise} + */ + await() { + if (!this._promise) { //if not cache loaded, do not fail + return $.Promise.resolve(this._data); + } + return this._promise; + } + + getImage() { + $.console.error("[CacheRecord.getImage] options.image is deprecated. Moreover, it might not work" + + " correctly as the cache system performs conversion asynchronously in case the type needs to be converted."); + this.transformTo("image"); + return this.data; + } + + getRenderedContext() { + $.console.error("[CacheRecord.getRenderedContext] options.getRenderedContext is deprecated. Moreover, it might not work" + + " correctly as the cache system performs conversion asynchronously in case the type needs to be converted."); + this.transformTo("context2d"); + return this.data; + } + + /** + * Set the cache data. Asynchronous. + * @param {any} data + * @param {string} type + * @returns {OpenSeadragon.Promise} the old cache data that has been overwritten + */ + setDataAs(data, type) { + //allow set data with destroyed state, destroys the data if necessary + $.console.assert(data !== undefined && data !== null, "[CacheRecord.setDataAs] needs valid data to set!"); + if (this._conversionJobQueue) { + //delay saving if ongiong conversion, these were registered first + let resolver = null; + const promise = new $.Promise((resolve, reject) => { + resolver = resolve; + }); + this._conversionJobQueue.push(() => resolver(this._overwriteData(data, type))); + return promise; + } + return this._overwriteData(data, type); + } + + /** + * Access the cache record data indirectly. Preferred way of data access. Asynchronous. + * @param {string} [type=undefined] + * @param {boolean} [copy=true] if false and same type is retrieved as the cache type, + * copy is not performed: note that this is potentially dangerous as it might + * introduce race conditions (you get a cache data direct reference you modify). + * @returns {OpenSeadragon.Promise} desired data type in promise, undefined if the cache was destroyed + */ + getDataAs(type = undefined, copy = true) { + if (this.loaded) { + if (type === this._type) { + return copy ? $.converter.copy(this._tRef, this._data, type || this._type) : this._promise; + } + return this._transformDataIfNeeded(this._tRef, this._data, type || this._type, copy) || this._promise; + } + return this._promise.then(data => this._transformDataIfNeeded(this._tRef, data, type || this._type, copy) || data); + } + + _transformDataIfNeeded(referenceTile, data, type, copy) { + //might get destroyed in meanwhile + if (this._destroyed) { + return $.Promise.resolve(); + } + + let result; + if (type !== this._type) { + result = $.converter.convert(referenceTile, data, this._type, type); + } else if (copy) { //convert does not copy data if same type, do explicitly + result = $.converter.copy(referenceTile, data, type); + } + if (result) { + return result.then(finalData => { + if (this._destroyed) { + $.converter.destroy(finalData, type); + return undefined; + } + return finalData; + }).catch(e => { + this._handleConversionError(e); + return undefined; + }); + } + return false; // no conversion needed, parent function returns item as-is + } + + /** + * Access of the data by drawers, synchronous function. Should always access a valid main cache. + * This is ensured by invalidation routine that executes data modification on a copy record, and + * then synchronously swaps records (main caches) to the new data between render calls. + * + * If a drawer decides to have internal cache with synchronous behavior, it is (if necessary) + * performed during this phase. + * + * @param {OpenSeadragon.DrawerBase} drawer drawer reference which requests the data: the drawer + * defines the supported formats this cache should return **synchronously** + * @param {OpenSeadragon.Tile} tileToDraw reference to the tile that is in the process of drawing and + * for which we request the data; if we attempt to draw such tile while main cache target is destroyed, + * attempt to reset the tile state to force system to re-download it again + * @returns {OpenSeadragon.CacheRecord|OpenSeadragon.InternalCacheRecord|undefined} desired data if available, + * wrapped in the cache container. This data is guaranteed to be loaded & in the type supported by the drawer. + * Returns undefined if the data is not ready for rendering. + * @private + */ + getDataForRendering(drawer, tileToDraw) { + // Test cache state + if (this._destroyed) { + $.console.error(`Attempt to draw tile with destroyed main cache ${this}!`); + tileToDraw._unload(); + return undefined; + } + if (!this.loaded) { + // If a conversion/load is in progress, it is normal that the cache is temporarily not loaded. + // Avoid spamming errors; just skip drawing this tile this frame. + if (this._promise) { + return undefined; + } + $.console.error(`Attempt to draw cache ${this} when not loaded!`); + return undefined; + } + if (this._destroyed) { + $.console.error(`Attempt to draw tile with destroyed main cache ${this}!`); + tileToDraw._unload(); // try to restore the state so that the tile is later on fetched again + return undefined; + } + + // Ensure cache in a format suitable for the current drawer. If not it is an error, prepareForRendering + // should be called at the end of invalidation routine instead. Since the processing is async, we are + // unable to provide the rendering data immediatelly - return. + const supportedTypes = drawer.getSupportedDataFormats(); + if (!supportedTypes.includes(this.type)) { + $.console.error(`Attempt to draw tile cache ${this} with unsupported type '${this.type}' for the target drawer!`); + this.prepareForRendering(drawer); + return undefined; + } + + // If we support internal cache + if (drawer.options.usePrivateCache) { + // let sync preparation handle data if no preloading desired + if (!drawer.options.preloadCache) { + return this.prepareInternalCacheSync(drawer); + } + // or check internal cache state before returning + const internalCache = this._getInternalCacheRef(drawer); + if (!internalCache || !internalCache.loaded) { + $.console.error(`Attempt to draw tile cache ${this} with internal cache non-ready state!`); + return undefined; + } + return internalCache; + } + + // else just return self reference + return this; + } + + /** + * Check whether the cache is usable for the given drawer. The cache is considered + * usable if it is in a format supported by the drawer and, if the drawer uses internal cache, + * the internal cache was created (it might not be loaded yet though). + * @param {OpenSeadragon.DrawerBase} drawer + * @return {boolean} + */ + isUsableForDrawer(drawer) { + const supportedTypes = drawer.getSupportedDataFormats(); + if (!supportedTypes.includes(this.type)) { + return false; + } + if (drawer.options.usePrivateCache) { + const internalCache = this._getInternalCacheRef(drawer); + if (!internalCache) { + return false; + } + } + return true; + } + + /** + * Preparation for rendering ensures the CacheRecord is in a format supported by the current + * drawer. Furthermore, if internal cache is to be used by a drawer with preloading enabled, + * it happens in this step. + * + * Note: Should not be called if cache type is already among supported types. + * @private + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.Promise<*>} reference to the data, + * or null if not data yet loaded/ready (usually due to error) + */ + prepareForRendering(drawer) { + const supportedTypes = drawer.getRequiredDataFormats(); + + // If not loaded, await until ready and try again + if (!this.loaded) { + return this.await().then(_ => this.prepareForRendering(drawer)); + } + + let selfPromise; + // If not in one of required types, transform + if (!supportedTypes.includes(this.type)) { + selfPromise = this.transformTo(supportedTypes); + } else { + selfPromise = this.await(); + } + + const swallow = (p) => p.catch(e => { + this._handleConversionError(e); + return null; + }); + + // If internal cache wanted and preloading enabled, convert now + if (drawer.options.usePrivateCache && drawer.options.preloadCache) { + return swallow(selfPromise.then(_ => this.prepareInternalCacheAsync(drawer))); + } + return swallow(selfPromise); + } + + /** + * Internal cache is defined by a Drawer. Async preparation happens as the last step in the + * invalidation routine. + * Must not be called if drawer.options.usePrivateCache == false. Called inside prepareForRenderine + * by cache itself if preloadCache == true (supports async behavior). + * + * @private + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.Promise<*>} reference to the data wrapped in a promise, + * or null if not data yet loaded/ready (usually due to error) + */ + prepareInternalCacheAsync(drawer) { + let internalCache = this._getInternalCacheRef(drawer); + if (this._checkInternalCacheUpToDate(internalCache, drawer)) { + return internalCache.await(); + } + + // Force reset + if (internalCache && !internalCache.loaded) { + internalCache.await().then(() => internalCache.destroy()); + } + + $.console.assert(this._tRef, "Data Create called from invalidation routine needs tile reference!"); + const transformedData = drawer.internalCacheCreate(this, this._tRef); + $.console.assert(transformedData !== undefined, "[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!"); + const drawerID = drawer.getId(); + internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, + drawerID, (data) => drawer.internalCacheFree(data)); + return internalCache.await(); + } + + /** + * Internal cache is defined by a Drawer. Sync preparation happens directly before rendering. + * Must not be called if drawer.options.usePrivateCache == false. Called inside getDataForRendering + * by cache itself if preloadCache == false (without support for async behavior). + * @private + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.InternalCacheRecord} reference to the cache + */ + prepareInternalCacheSync(drawer) { + let internalCache = this._getInternalCacheRef(drawer); + if (this._checkInternalCacheUpToDate(internalCache, drawer)) { + return internalCache; + } + + // Force reset + if (internalCache) { + internalCache.destroy(); + } + + $.console.assert(this._tRef, "Data Create called from drawing loop needs tile reference!"); + const transformedData = drawer.internalCacheCreate(this, this._tRef); + $.console.assert(transformedData !== undefined, "[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!"); + + const drawerID = drawer.getId(); + internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, + drawerID, (data) => drawer.internalCacheFree(data)); + return internalCache; + } + + /** + * Get an internal cache reference for given drawer + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.InternalCacheRecord|undefined} + * @private + */ + _getInternalCacheRef(drawer) { + const options = drawer.options; + if (!options.usePrivateCache) { + $.console.error("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false."); + return undefined; + } + + // we can get here only if we want to render incompatible type + let internalCache = this[DRAWER_INTERNAL_CACHE]; + if (!internalCache) { + internalCache = this[DRAWER_INTERNAL_CACHE] = {}; + } + return internalCache[drawer.getId()]; + } + + /** + * Check if internal cache is up to date. Might be in loading state. + * @param {OpenSeadragon.InternalCacheRecord} internalCache + * @param {OpenSeadragon.DrawerBase} drawer + * @return {boolean} false if the internal cache is outdated + * @private + */ + _checkInternalCacheUpToDate(internalCache, drawer) { + // We respect existing records, unless they are outdated. Invalidation routine by its nature + // destroys internal cache, therefore we do not need to check if internal cache is consistent with its parent. + return internalCache && internalCache.tstamp >= drawer._dataNeedsRefresh; + } + + /** + * Transform cache to desired type and get the data after conversion. + * Does nothing if the type equals to the current type. Asynchronous. + * Transformation is LAZY, meaning conversions are performed only to + * match the last conversion request target type. + * @param {string|string[]} type if array provided, the system will + * try to optimize for the best type to convert to. + * @return {OpenSeadragon.Promise} + */ + transformTo(type = this._type) { + if (!this.loaded) { + this._conversionJobQueue = this._conversionJobQueue || []; + let resolver = null; + const promise = new $.Promise((resolve, reject) => { + resolver = resolve; + }); + + // Todo consider submitting only single tranform job to queue: any other transform calls will have + // no effect, the last one decides the target format + this._conversionJobQueue.push(() => { + if (this._destroyed) { + return; + } + //must re-check types since we perform in a queue of conversion requests + if ((typeof type === "string" && type !== this._type) || (Array.isArray(type) && !type.includes(this._type))) { + //ensures queue gets executed after finish + this._convert(this._type, type); + this._promise.then(data => resolver(data)); + } else { + //must ensure manually, but after current promise finished, we won't wait for the following job + this._promise.then(data => { + this._checkAwaitsConvert(); + return resolver(data); + }); + } + }); + return promise; + } + + if ((typeof type === "string" && type !== this._type) || (Array.isArray(type) && !type.includes(this._type))) { + this._convert(this._type, type); + } + return this._promise; + } + + /** + * If cache ceases to be the primary one, free data + * @param {string} drawerId if undefined, all caches are freed, else only target one + * @private + */ + destroyInternalCache(drawerId = undefined) { + const internal = this[DRAWER_INTERNAL_CACHE]; + if (internal) { + if (drawerId) { + const cache = internal[drawerId]; + if (cache) { + cache.destroy(); + delete internal[drawerId]; + } + } else { + for (const iCache in internal) { + internal[iCache].destroy(); + } + delete this[DRAWER_INTERNAL_CACHE]; + } + } + } + + /** + * Conversion requires tile references: + * keep the most 'up to date' ref here. It is called and managed automatically. + * @param {OpenSeadragon.Tile} ref + * @return {OpenSeadragon.CacheRecord} self reference for builder pattern + * @private + */ + withTileReference(ref) { + this._tRef = ref; + return this; + } + + /** + * Get cache description. Used for system messages and errors. + * @return {string} + */ + toString() { + const tile = this._tRef || (this._tiles.length && this._tiles[0]); + return tile ? `Cache ${this.type} [used e.g. by ${tile.toString()}]` : `Orphan cache!`; + } + + /** + * Set initial state, prepare for usage. + * Must not be called on active cache, e.g. first call destroy(). + */ + revive() { + $.console.assert(!this.loaded && !this._type, "[CacheRecord::revive] must not be called when loaded!"); + this._tiles = []; + this._data = null; + this._type = null; + this.loaded = false; + this._promise = null; + this._destroyed = false; + + // Optional ownership metadata (set by TileCache for managed records). + // Working caches created during invalidation are intentionally left without an owner. + this._ownerTileCache = null; + this.cacheKey = null; + } + + /** + * Free all the data and call data destructors if defined. + */ + destroy() { + if (!this._destroyed) { + delete this._conversionJobQueue; + this._destroyed = true; + + // make sure this gets destroyed even if loaded=false + if (this.loaded) { + this._destroySelfUnsafe(this._data, this._type); + } else if (this._promise) { + const oldType = this._type; + this._promise.then(x => this._destroySelfUnsafe(x, oldType)).catch($.console.error); + } + } + + } + + _destroySelfUnsafe(data, type) { + // ensure old data destroyed + $.converter.destroy(data, type); + this.destroyInternalCache(); + // might've got revived in meanwhile if async ... + if (!this._destroyed) { + return; + } + this.loaded = false; + this._tiles = null; + this._data = null; + this._type = null; + this._tRef = null; + this._promise = null; + } + + /** + * Add tile dependency on this record + * @param tile + * @param data can be null|undefined => optimization, will skip data initialization and just adds tile reference + * @param type + */ + addTile(tile, data, type) { + if (this._destroyed) { + return; + } + $.console.assert(tile, '[CacheRecord.addTile] tile is required'); + + // first come first served, data for existing tiles is NOT overridden + if (data !== undefined && data !== null && this._tiles.length < 1) { + // Since we IGNORE new data if already initialized, we support 'data getter' + if (typeof data === 'function') { + data = data(); + } + + // in case we attempt to write to existing data object + if (this.type && this._promise) { + if (data instanceof $.Promise) { + this._promise = data.then(d => { + this._overwriteData(d, type); + }); + } else { + this._overwriteData(data, type); + } + } else { + // If we receive async callback, we consume the async state + if (data instanceof $.Promise) { + this._promise = data.then(data => { + if (this._destroyed) { + try { + $.converter.destroy(data, this._type); + } catch (e) { + // no-op + } + return undefined; + } + this.loaded = true; + this._data = data; + return data; + }).catch(e => { + this._handleConversionError(e); + return undefined; + }); + this._data = null; + } else { + this._promise = $.Promise.resolve(data); + this._data = data; + this.loaded = true; + } + + this._type = type; + } + this._tiles.push(tile); + } else { + const tileExists = this._tiles.includes(tile); + if (!tileExists && this.type && this._promise) { + // here really check we are loaded, since if optimization allows sending no data and we add tile without + // proper initialization it is a bug + this._tiles.push(tile); + } else if (!tileExists) { + $.console.warn("Tile %s caching attempt without data argument on uninitialized cache entry!", tile); + } + } + } + + /** + * Remove tile dependency on this record. + * @param tile + * @returns {Boolean} true if record removed + */ + removeTile(tile) { + if (this._destroyed) { + return false; + } + for (let i = 0; i < this._tiles.length; i++) { + if (this._tiles[i] === tile) { + this._tiles.splice(i, 1); + if (this._tRef === tile) { + // keep fresh ref + this._tRef = this._tiles[i - 1]; + } + return true; + } + } + $.console.warn('[CacheRecord.removeTile] trying to remove unknown tile', tile); + return false; + } + + /** + * Get the amount of tiles sharing this record. + * @return {number} + */ + getTileCount() { + return this._tiles ? this._tiles.length : 0; + } + + /** + * Private conversion that makes sure collided requests are + * processed eventually + * @private + */ + _checkAwaitsConvert() { + if (!this._conversionJobQueue || this._destroyed) { + return; + } + //let other code finish first + setTimeout(() => { + //check again, meanwhile things might've changed + if (!this._conversionJobQueue || this._destroyed) { + return; + } + const job = this._conversionJobQueue[0]; + this._conversionJobQueue.splice(0, 1); + if (this._conversionJobQueue.length === 0) { + delete this._conversionJobQueue; + } + job(); + }); + } + + _triggerNeedsDraw() { + if (this._tiles.length > 0) { + this._tiles[0].tiledImage.viewer.forceRedraw(); + } + } + + /** + * Safely overwrite the cache data and return the old data + * @private + */ + _overwriteData(data, type) { + if (this._destroyed) { + //we have received the ownership of the data, destroy it too since we are destroyed + $.converter.destroy(data, type); + return $.Promise.resolve(); + } + if (this.loaded) { + // No-op if attempt to replace with the same object + if (this._data === data && this._type === type) { + return this._promise; + } + $.converter.destroy(this._data, this._type); + this._type = type; + this._data = data; + this._promise = $.Promise.resolve(data); + const internal = this[DRAWER_INTERNAL_CACHE]; + if (internal) { + for (const iCache in internal) { + internal[iCache].setDataAs(data, type); + } + } + this._triggerNeedsDraw(); + return this._promise; + } + return this._promise.then(() => { + // No-op if attempt to replace with the same object + if (this._data === data && this._type === type) { + return this._data; + } + $.converter.destroy(this._data, this._type); + this._type = type; + this._data = data; + this._promise = $.Promise.resolve(data); + const internal = this[DRAWER_INTERNAL_CACHE]; + if (internal) { + for (const iCache in internal) { + internal[iCache].setDataAs(data, type); + } + } + this._triggerNeedsDraw(); + return this._data; + }); + } + + /** + * Private conversion that makes sure the cache knows its data is ready + * @param to array or a string - allowed types + * @param from string - type origin + * @private + */ + _convert(from, to) { + const converter = $.converter, + conversionPath = converter.getConversionPath(from, to); + if (!conversionPath) { + $.console.error(`[CacheRecord._convert] Conversion ${from} ---> ${to} cannot be done!`); + return; //no-op + } + + const originalData = this._data; + const stepCount = conversionPath.length; + const _this = this; + const convert = (x, i) => { + if (i >= stepCount) { + _this._data = x; + _this.loaded = true; + _this._checkAwaitsConvert(); + return $.Promise.resolve(x); + } + const edge = conversionPath[i]; + let y; + try { + y = edge.transform(_this._tRef, x); + } catch (err) { + converter.destroy(x, edge.origin.value); // prevent leak + return $.Promise.reject(`[CacheRecord._convert] sync failure (while converting using ${edge.target.value}, ${edge.origin.value})`); + } + if (y === undefined) { + _this.loaded = false; + converter.destroy(x, edge.origin.value); // prevent leak + return $.Promise.reject(`[CacheRecord._convert] data mid result undefined value (while converting using ${edge.target.value}, ${edge.origin.value})`); + } + converter.destroy(x, edge.origin.value); + const result = $.type(y) === "promise" ? y : $.Promise.resolve(y); + return result.then(res => convert(res, i + 1)); + }; + + this.loaded = false; + this._data = undefined; + // Read target type from the conversion path: [edge.target] = Vertex, its value=type + this._type = conversionPath[stepCount - 1].target.value; + + // IMPORTANT: conversion failures must not poison the cache record with a permanently + // rejected promise (methods rely on being able to await() without throwing). + this._promise = convert(originalData, 0).catch(e => { + this._handleConversionError(e); + return undefined; + }); + } + + /** + * Handle conversion error by cleaning up and unloading affected tiles + * @param {Error} e + * @private + */ + _handleConversionError(e) { + $.console.error("[CacheRecord] Conversion/preparation error:", e); + + this._destroyed = true; + this.loaded = false; + this._data = null; + + // WORKING CACHE: do not escalate to TileCache, do not unload tiles. + // A working cache is not registered (no cacheKey and/or no owner). + if (!this.cacheKey || !this._ownerTileCache) { + this._promise = $.Promise.resolve(undefined); + this._tiles = []; + this._tRef = null; + return; + } + + // MANAGED CACHE: notify TileCache to remove record and possibly mark tile missing. + this._ownerTileCache._handleBrokenCacheRecord(this); + } + }; + + /** + * @class OpenSeadragon.InternalCacheRecord + * @memberof OpenSeadragon + * @classdesc Simple cache record without robust support for async access. Meant for internal use only. + * + * This class acts like the Maybe type: + * - it has 'loaded' flag indicating whether the tile data is ready + * - it has 'data' property that has value if loaded=true + * + * This class supposes synchronous access, no collision of transform calls. + * It also does not record tiles nor allows cache/tile sharing. + * @private + */ + OpenSeadragon.InternalCacheRecord = class InternalCacheRecord { + constructor(data, type, onDestroy) { + this.tstamp = $.now(); + this._ondestroy = onDestroy; + this._type = type; + + if (data instanceof $.Promise) { + this._promise = data; + data.then(data => { + this.loaded = true; + this._data = data; + }); + } else { + this._promise = null; + this.loaded = true; + this._data = data; + } + } + + /** + * Sync access to the data + * @returns {any} + */ + get data() { + return this._data; + } + + /** + * Sync access to the current type + * @returns {string} + */ + get type() { + return this._type; + } + + /** + * Await ongoing process so that we get cache ready on callback. + * @returns {OpenSeadragon.Promise} + */ + await() { + if (!this._promise) { //if not cache loaded, do not fail + return $.Promise.resolve(this._data); + } + return this._promise; + } + + /** + * Must be called before transformTo or setDataAs. To keep + * compatible api with CacheRecord where tile refs are known. + * @param {OpenSeadragon.Tile} referenceTile reference tile for conversion + * @return {OpenSeadragon.InternalCacheRecord} self reference for builder pattern + */ + withTileReference(referenceTile) { + this._temporaryTileRef = referenceTile; + return this; + } + + /** + * Free all the data and call data destructors if defined. + */ + destroy() { + if (this.loaded) { + if (this._ondestroy) { + this._ondestroy(this._data); + } + this._data = null; + this.loaded = false; + } + } + }; + + + /** + * @class OpenSeadragon.TileCache + * @memberof OpenSeadragon + * @classdesc Stores all the tiles displayed in a {@link OpenSeadragon.Viewer}. + * You generally won't have to interact with the TileCache directly. + * @param {Object} options - Configuration for this TileCache. + * @param {Number} [options.maxImageCacheCount] - See maxImageCacheCount in + * {@link OpenSeadragon.Options} for details. + */ + OpenSeadragon.TileCache = class TileCache { + constructor( options ) { + + options = options || {}; + + this._maxCacheItemCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount; + // requestInvalidate() touches this private property due to performance reasons + this._tilesLoaded = []; + this._zombiesLoaded = []; + this._zombiesLoadedCount = 0; + this._cachesLoaded = []; + this._cachesLoadedCount = 0; + } + + /** + * @returns {Number} The total number of tiles that have been loaded by + * this TileCache. Note that the tile might be recorded here mutliple times, + * once for each cache it uses. + */ + numTilesLoaded() { + return this._tilesLoaded.length; + } + + /** + * @returns {Number} The total number of cached objects (+ zombies) + */ + numCachesLoaded() { + return this._zombiesLoadedCount + this._cachesLoadedCount; + } + + /** + * Caches the specified tile, removing an old tile if necessary to stay under the + * maxImageCacheCount specified on construction. Note that if multiple tiles reference + * the same image, there may be more tiles than maxImageCacheCount; the goal is to keep + * the number of images below that number. Note, as well, that even the number of images + * may temporarily surpass that number, but should eventually come back down to the max specified. + * @private + * @param {Object} options - Cache creation parameters. + * @param {OpenSeadragon.Tile} options.tile - The tile to cache. + * @param {?String} [options.cacheKey=undefined] - Cache Key to use. Defaults to options.tile.cacheKey + * @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache. + * Used if options.cacheKey not set. + * @param {Image} options.image - The image of the tile to cache. Deprecated. + * @param {*} options.data - The data of the tile to cache. If `typeof data === 'function'` holds, + * the data is called to obtain the data item: this is an optimization to load data only when necessary. + * @param {string} [options.dataType] - The data type of the tile to cache. Required. + * @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this + * function will release an old tile. The cutoff option specifies a tile level at or below which + * tiles will not be released. + * @returns {OpenSeadragon.CacheRecord} - The cache record the tile was attached to. + */ + cacheTile(options) { + $.console.assert(options, "[TileCache.cacheTile] options is required"); + const theTile = options.tile; + $.console.assert(theTile, "[TileCache.cacheTile] options.tile is required"); + $.console.assert(theTile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required"); + + if (options.image instanceof Image) { + $.console.warn("[TileCache.cacheTile] options.image is deprecated!"); + options.data = options.image; + options.dataType = "image"; + } + + const cacheKey = options.cacheKey || theTile.cacheKey; + + let cacheRecord = this._cachesLoaded[cacheKey]; + if (!cacheRecord) { + if (options.data === undefined) { + $.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " + + "has been deprecated and will be removed in the future."); + options.data = options.image; + } + + cacheRecord = this._zombiesLoaded[cacheKey]; + if (cacheRecord) { + // zombies should not be (yet) destroyed, but if we encounter one... + if (cacheRecord._destroyed) { + // if destroyed, invalidation routine will get triggered for us automatically + cacheRecord.revive(); + } else { + // if zombie ready, do not overwrite its data, in that case try to call + // we need to trigger invalidation routine, data was not part of the system! + if (typeof options.data === 'function') { + options.data(); + } + delete options.data; + } + delete this._zombiesLoaded[cacheKey]; + this._zombiesLoadedCount--; + this._cachesLoaded[cacheKey] = cacheRecord; + this._cachesLoadedCount++; + } else { + //allow anything but undefined, null, false (other values mean the data was set, for example '0') + const validData = options.data !== undefined && options.data !== null && options.data !== false; + $.console.assert(validData, "[TileCache.cacheTile] options.data is required to create an CacheRecord"); + cacheRecord = this._cachesLoaded[cacheKey] = new $.CacheRecord(); + this._cachesLoadedCount++; + } + } + + if (!options.dataType) { + $.console.error("[TileCache.cacheTile] options.dataType is newly required. " + + "For easier use of the cache system, use the tile instance API."); + + // We need to force data acquisition now to guess the type + if (typeof options.data === 'function') { + $.console.error("[TileCache.cacheTile] options.dataType is mandatory " + + " when data item is a callback!"); + } + options.dataType = $.converter.guessType(options.data); + } + + cacheRecord._ownerTileCache = this; + cacheRecord.cacheKey = cacheKey; + cacheRecord.addTile(theTile, options.data, options.dataType); + this._freeOldRecordRoutine(theTile, options.cutoff || 0); + return cacheRecord; + } + + /** + * Changes cache key + * @private + * @param {Object} options - Cache creation parameters. + * @param {String} options.oldCacheKey - Current key + * @param {String} options.newCacheKey - New key to set + * @return {OpenSeadragon.CacheRecord | null} + */ + renameCache(options) { + const newKey = options.newCacheKey, + oldKey = options.oldCacheKey; + let originalCache = this._cachesLoaded[oldKey]; + + if (!originalCache) { + originalCache = this._zombiesLoaded[oldKey]; + $.console.assert(originalCache, "[TileCache.renameCache] oldCacheKey must reference existing cache!"); + if (this._zombiesLoaded[newKey]) { + $.console.error("Cannot rename zombie cache %s to %s: the target cache is occupied!", + oldKey, newKey); + return null; + } + this._zombiesLoaded[newKey] = originalCache; + delete this._zombiesLoaded[oldKey]; + } else if (this._cachesLoaded[newKey]) { + $.console.error("Cannot rename cache %s to %s: the target cache is occupied!", + oldKey, newKey); + return null; // do not remove, we perform additional fixes on caches later on when swap occurred + } else { + this._cachesLoaded[newKey] = originalCache; + delete this._cachesLoaded[oldKey]; + } + + originalCache._ownerTileCache = this; + originalCache.cacheKey = newKey; + for (const tile of originalCache._tiles) { + tile.reflectCacheRenamed(oldKey, newKey); + } + + // do not call free old record routine, we did not increase cache size + return originalCache; + } + + /** + * Reads a cache if it exists and creates a new copy of a target, different cache if it does not + * @param {Object} options + * @param {OpenSeadragon.Tile} options.tile - The tile to own ot add record for the cache. + * @param {String} options.copyTargetKey - The unique key used to identify this tile in the cache. + * @param {String} options.newCacheKey - The unique key the copy will be created for. + * @param {String} [options.desiredType=undefined] - For optimization purposes, the desired type. Can + * be ignored. + * @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this + * function will release an old tile. The cutoff option specifies a tile level at or below which + * tiles will not be released. + * @returns {OpenSeadragon.Promise} - New record. + * @private + */ + cloneCache(options) { + const theTile = options.tile; + const cacheKey = options.copyTargetKey; + const cacheRecord = this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey]; + $.console.assert(cacheRecord, "[TileCache.cloneCache] attempt to clone non-existent cache %s!", cacheKey); + $.console.assert(!this._cachesLoaded[options.newCacheKey], + "[TileCache.cloneCache] attempt to copy clone to existing cache %s!", options.newCacheKey); + + const desiredType = options.desiredType || undefined; + return cacheRecord.getDataAs(desiredType, true).then(data => { + const newRecord = this._cachesLoaded[options.newCacheKey] = new $.CacheRecord(); + newRecord.addTile(theTile, data, cacheRecord.type); + this._cachesLoadedCount++; + this._freeOldRecordRoutine(theTile, options.cutoff || 0); + return newRecord; + }); + } + + /** + * Inject new cache to the system + * @param {Object} options + * @param {OpenSeadragon.Tile} options.tile - Reference tile. All tiles sharing original data will be affected. + * @param {OpenSeadragon.CacheRecord} options.cache - Cache that will be injected. + * @param {String} options.targetKey - The target cache key to inhabit. Can replace existing cache. + * @param {Boolean} options.setAsMainCache - If true, tiles main cache gets updated to consumerKey. + * Otherwise, if consumerKey==tile.cacheKey the cache is set as main too. + * @param {Boolean} options.tileAllowNotLoaded - if true, tile that is not loaded is also processed, + * this is internal parameter used in tile-loaded completion routine, as we need to prepare tile but + * it is not yet loaded and cannot be marked as so (otherwise the system would think it is ready) + * @private + */ + injectCache(options) { + const targetKey = options.targetKey, + tile = options.tile; + if (!options.tileAllowNotLoaded && !tile.loaded && !tile.loading) { + $.console.warn("Attempt to inject cache on tile in invalid state: this is probably a bug!"); + return; + } + const consumer = this._cachesLoaded[targetKey]; + if (consumer) { + // We need to avoid async execution here: replace consumer instead of overwriting the data. + const iterateTiles = [...consumer._tiles]; // unloadCacheForTile() will modify the array, use a copy + for (const tile of iterateTiles) { + this.unloadCacheForTile(tile, targetKey, true, false); + } + } + if (this._cachesLoaded[targetKey]) { + $.console.error("The inject routine should've freed cache!"); + } + + const cache = options.cache; + this._cachesLoaded[targetKey] = cache; + cache._ownerTileCache = this; + cache.cacheKey = targetKey; + + // Update cache: add the new cache, we must add since we removed above with unloadCacheForTile() + for (const t of tile.getCache(tile.originalCacheKey)._tiles) { // grab all cache-equal tiles + t.setCache(targetKey, cache, options.setAsMainCache, false); + } + } + + /** + * Replace cache (and update tile references) by another cache + * @param {Object} options + * @param {OpenSeadragon.Tile} options.tile - The tile to own ot add record for the cache. + * @param {String} options.victimKey - Cache that will be erased. In fact, the victim _replaces_ consumer, + * inheriting its tiles and key. + * @param {String} options.consumerKey - The cache that consumes the victim. In fact, it gets destroyed and + * replaced by victim, which inherits all its metadata. + * @param {Boolean} options.setAsMainCache - If true, tiles main cache gets updated to consumerKey. + * Otherwise, if consumerKey==tile.cacheKey the cache is set as main too. + * @param {Boolean} options.tileAllowNotLoaded - if true, tile that is not loaded is also processed, + * this is internal parameter used in tile-loaded completion routine, as we need to prepare tile but + * it is not yet loaded and cannot be marked as so (otherwise the system would think it is ready) + * @private + */ + replaceCache(options) { + const victimKey = options.victimKey, + consumerKey = options.consumerKey, + victim = this._cachesLoaded[victimKey], + tile = options.tile; + if (!victim || (!options.tileAllowNotLoaded && !tile.loaded && !tile.loading)) { + $.console.warn("Attempt to consume cache on tile in invalid state: this is probably a bug!"); + return; + } + const consumer = this._cachesLoaded[consumerKey]; + if (consumer) { + // We need to avoid async execution here: replace consumer instead of overwriting the data. + const iterateTiles = [...consumer._tiles]; // unloadCacheForTile() will modify the array, use a copy + for (const tile of iterateTiles) { + this.unloadCacheForTile(tile, consumerKey, true, false); + } + } + if (this._cachesLoaded[consumerKey]) { + $.console.error("The consume routine should've freed cache!"); + } + // Just swap victim to become new consumer + const resultCache = this.renameCache({ + oldCacheKey: victimKey, + newCacheKey: consumerKey + }); + + if (resultCache) { + // Only one cache got working item, other caches were idle: update cache: add the new cache + // we must add since we removed above with unloadCacheForTile() + for (const t of tile.getCache(tile.originalCacheKey)._tiles) { // grab all cache-equal tiles + t.setCache(consumerKey, resultCache, options.setAsMainCache, false); + } + } + } + + /** + * This method ensures other tiles are restored if one of the tiles + * was requested restore(). + * @param tile + * @param originalCache + * @param freeIfUnused if true, zombie is not created + * @private + */ + restoreTilesThatShareOriginalCache(tile, originalCache, freeIfUnused) { + for (const t of originalCache._tiles) { + if (t.cacheKey !== t.originalCacheKey) { + this.unloadCacheForTile(t, t.cacheKey, freeIfUnused, true); + delete t._caches[t.cacheKey]; + t.cacheKey = t.originalCacheKey; + } + } + } + + _freeOldRecordRoutine(theTile, cutoff) { + let insertionIndex = this._tilesLoaded.length, + worstTileIndex = -1; + + // Note that just because we're unloading a tile doesn't necessarily mean + // we're unloading its cache records. With repeated calls it should sort itself out, though. + if (this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount) { + //prefer zombie deletion, faster, better + if (this._zombiesLoadedCount > 0) { + for (const zombie in this._zombiesLoaded) { + this._zombiesLoaded[zombie].destroy(); + delete this._zombiesLoaded[zombie]; + this._zombiesLoadedCount--; + break; + } + } else { + let worstTile = null; + let prevTile, worstTime, worstLevel, prevTime, prevLevel; + + for (let i = this._tilesLoaded.length - 1; i >= 0; i--) { + prevTile = this._tilesLoaded[i]; + + if (prevTile.level <= cutoff || + prevTile.beingDrawn || + prevTile.loading || + prevTile.processing) { + continue; + } + if (!worstTile) { + worstTile = prevTile; + worstTileIndex = i; + continue; + } + + prevTime = prevTile.lastTouchTime; + worstTime = worstTile.lastTouchTime; + prevLevel = prevTile.level; + worstLevel = worstTile.level; + + if (prevTime < worstTime || + (prevTime === worstTime && prevLevel > worstLevel)) { + worstTile = prevTile; + worstTileIndex = i; + } + } + + if (worstTile && worstTileIndex >= 0) { + this._unloadTile(worstTile, true); + insertionIndex = worstTileIndex; + } + } + } + + if (theTile.getCacheSize() === 0) { + this._tilesLoaded[insertionIndex] = theTile; + } else if (worstTileIndex >= 0) { + //tile is already recorded, do not add tile, but remove the tile at insertion index + this._tilesLoaded.splice(insertionIndex, 1); + } + } + + _handleBrokenCacheRecord(cache) { + if (!cache) { + return; + } + + const key = cache.cacheKey; + + if (key && this._cachesLoaded[key] === cache) { + delete this._cachesLoaded[key]; + this._cachesLoadedCount--; + } + if (key && this._zombiesLoaded[key] === cache) { + delete this._zombiesLoaded[key]; + this._zombiesLoadedCount--; + } + + const tiles = cache._tiles ? [...cache._tiles] : []; + for (const tile of tiles) { + const isMainCache = tile.getCache && tile.getCache() === cache; + const isOriginalCache = key && tile.originalCacheKey === key; + + if (isMainCache || isOriginalCache) { + tile.exists = false; // prevents the tile from loading (TODO: consider ability to revive!) + tile.unload(true); + } else { + if (tile.removeCache && key) { + tile.removeCache(key); + } + cache.removeTile(tile); + } + } + + cache._promise = $.Promise.resolve(undefined); + cache._tiles = []; + cache._tRef = null; + cache._ownerTileCache = null; + } + + /** + * Clears all tiles associated with the specified tiledImage. + * @param {OpenSeadragon.TiledImage} tiledImage + */ + clearTilesFor(tiledImage) { + $.console.assert(tiledImage, '[TileCache.clearTilesFor] tiledImage is required'); + let tile; + + let cacheOverflows = this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount; + if (tiledImage._zombieCache && cacheOverflows && this._zombiesLoadedCount > 0) { + //prefer newer (fresh ;) zombies + for (const zombie in this._zombiesLoaded) { + this._zombiesLoaded[zombie].destroy(); + delete this._zombiesLoaded[zombie]; + } + this._zombiesLoadedCount = 0; + cacheOverflows = this._cachesLoadedCount > this._maxCacheItemCount; + } + for (let i = this._tilesLoaded.length - 1; i >= 0; i--) { + tile = this._tilesLoaded[i]; + + if (tile.tiledImage === tiledImage) { + if (!tile.loaded) { + //iterates from the array end, safe to remove + this._tilesLoaded.splice(i, 1); + } else if (tile.tiledImage === tiledImage) { + this._unloadTile(tile, !tiledImage._zombieCache || cacheOverflows, i); + } + } + } + } + + /** + * Delete all data in the cache + * @param {boolean} withZombies + */ + clear(withZombies = true) { + for (const zombie in this._zombiesLoaded) { + this._zombiesLoaded[zombie].destroy(); + } + for (const tile in this._tilesLoaded) { + this._unloadTile(tile, true); + } + this._tilesLoaded = []; + this._zombiesLoaded = []; + this._zombiesLoadedCount = 0; + this._cachesLoaded = []; + this._cachesLoadedCount = 0; + } + + /** + * Clean up internal drawer data for a given drawer + * @param {OpenSeadragon.DrawerBase} drawer + */ + clearDrawerInternalCache(drawer) { + const drawerId = drawer.getId(); + for (const zombie of this._zombiesLoaded) { + if (zombie) { + zombie.destroyInternalCache(drawerId); + } + } + for (const cache of this._cachesLoaded) { + if (cache) { + cache.destroyInternalCache(drawerId); + } + } + } + + /** + * Returns reference to all tiles loaded by a particular + * tiled image item + * @param {OpenSeadragon.TiledImage|null} tiledImage if null, gets all tiles, else filters out tiles + * that belong to a specific image + */ + getLoadedTilesFor(tiledImage) { + if (!tiledImage) { + return [...this._tilesLoaded]; + } + return this._tilesLoaded.filter(tile => tile.tiledImage === tiledImage); + } + + /** + * Get cache record (might be a unattached record, i.e. a zombie) + * @param cacheKey + * @returns {OpenSeadragon.CacheRecord|undefined} + */ + getCacheRecord(cacheKey) { + $.console.assert(cacheKey, '[TileCache.getCacheRecord] cacheKey is required'); + return this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey]; + } + + /** + * Delete cache safely from the system if it is not needed + * @param {OpenSeadragon.CacheRecord} cache + */ + safeUnloadCache(cache) { + if (cache && !cache._destroyed && cache.getTileCount() < 1) { + for (const i in this._zombiesLoaded) { + const c = this._zombiesLoaded[i]; + if (c === cache) { + delete this._zombiesLoaded[i]; + c.destroy(); + return; + } + } + $.console.error("Attempt to delete an orphan cache that is not in zombie list: this could be a bug!", cache); + cache.destroy(); + } + } + + /** + * Delete cache record for a given til + * @param {OpenSeadragon.Tile} tile + * @param {string} key cache key + * @param {boolean} destroy if true, empty cache is destroyed, else left as a zombie + * @param {boolean} okIfNotExists sometimes we call destruction just to make sure, if true do not report as error + * @private + */ + unloadCacheForTile(tile, key, destroy, okIfNotExists) { + const cacheRecord = this._cachesLoaded[key]; + //unload record only if relevant - the tile exists in the record + if (cacheRecord) { + if (cacheRecord.removeTile(tile)) { + if (!cacheRecord.getTileCount()) { + if (destroy) { + // #1 tile marked as destroyed (e.g. too much cached tiles or not a zombie) + cacheRecord.destroy(); + } else { + // #2 Tile is a zombie. Do not delete record, reuse. + this._zombiesLoaded[key] = cacheRecord; + this._zombiesLoadedCount++; + } + // Either way clear cache + delete this._cachesLoaded[key]; + this._cachesLoadedCount--; + } + return true; + } + $.console.error("[TileCache.unloadCacheForTile] System tried to delete tile from cache it " + + "does not belong to! This could mean a bug in the cache system."); + return false; + } + if (!okIfNotExists) { + $.console.warn("[TileCache.unloadCacheForTile] Attempting to delete missing cache!"); + } + return false; + } + + /** + * Unload tile: this will free the tile data and mark the tile as unloaded. + * @param {OpenSeadragon.Tile} tile + * @param {boolean} destroy if set to true, tile data is not preserved as zombies but deleted immediatelly + */ + unloadTile(tile, destroy = false) { + if (!tile.loaded) { + $.console.warn("Attempt to unload already unloaded tile."); + return; + } + const index = this._tilesLoaded.findIndex(x => x === tile); + this._unloadTile(tile, destroy, index); + } + + /** + * @param {OpenSeadragon.Tile} tile tile to unload + * @param {boolean} destroy destroy tile cache if the cache tile counts falls to zero + * @param {Number} [deleteAtIndex=undefined] index to remove the tile record at, will not remove from _tilesLoaded if not set + * @private + */ + _unloadTile(tile, destroy, deleteAtIndex = undefined) { + $.console.assert(tile, '[TileCache._unloadTile] tile is required'); + + for (const key in tile._caches) { + //we are 'ok' to remove tile caches here since we later call destroy on tile, otherwise + //tile has count of its cache size --> would be inconsistent + this.unloadCacheForTile(tile, key, destroy, false); + } + //delete also the tile record + if (deleteAtIndex !== undefined) { + this._tilesLoaded.splice(deleteAtIndex, 1); + } + + // Possible error: it can happen that unloaded tile gets to this stage. Should it even be allowed to happen? + if (!tile.loaded) { + return; + } + + const tiledImage = tile.tiledImage; + tile._unload(); + + /** + * Triggered when a tile has just been unloaded from memory. + @@ -255,12 +668,15 @@ $.TileCache.prototype = { + * @type {object} + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the unloaded tile. + * @property {OpenSeadragon.Tile} tile - The tile which has been unloaded. + * @property {boolean} destroyed - False if the tile data was kept in the system. + */ + tiledImage.viewer.raiseEvent("tile-unloaded", { + tile: tile, + tiledImage: tiledImage, + destroyed: destroy + }); + } + }; + +}(OpenSeadragon)); + +/* + * OpenSeadragon - World + * + * 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 World + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @classdesc Keeps track of all of the tiled images in the scene. + * @param {Object} options - World options. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this World. + **/ +$.World = function( options ) { + const _this = this; + + $.console.assert( options.viewer, "[World] options.viewer is required" ); + + $.EventSource.call( this ); + + this.viewer = options.viewer; + this._items = []; + this._needsDraw = false; + this.__invalidatedAt = 1; + this._autoRefigureSizes = true; + this._needsSizesFigured = false; + this._delegatedFigureSizes = function(event) { + if (_this._autoRefigureSizes) { + _this._figureSizes(); + } else { + _this._needsSizesFigured = true; + } + }; + + this._figureSizes(); +}; + +$.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.World.prototype */{ + /** + * Add the specified item. + * @param {OpenSeadragon.TiledImage} item - The item to add. + * @param {Object} options - Options affecting insertion. + * @param {Number} [options.index] - Index for the item. If not specified, goes at the top. + * @fires OpenSeadragon.World.event:add-item + * @fires OpenSeadragon.World.event:metrics-change + */ + addItem: function( item, options ) { + $.console.assert(item, "[World.addItem] item is required"); + $.console.assert(item instanceof $.TiledImage, "[World.addItem] only TiledImages supported at this time"); + + options = options || {}; + if (options.index !== undefined) { + const index = Math.max(0, Math.min(this._items.length, options.index)); + this._items.splice(index, 0, item); + } else { + this._items.push( item ); + } + + if (this._autoRefigureSizes) { + this._figureSizes(); + } else { + this._needsSizesFigured = true; + } + + this._needsDraw = true; + + item.addHandler('bounds-change', this._delegatedFigureSizes); + item.addHandler('clip-change', this._delegatedFigureSizes); + + /** + * Raised when an item is added to the World. + * @event add-item + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the World which raised the event. + * @property {OpenSeadragon.TiledImage} item - The item that has been added. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'add-item', { + item: item + } ); + }, + + /** + * Get the item at the specified index. + * @param {Number} index - The item's index. + * @returns {OpenSeadragon.TiledImage} The item at the specified index. + */ + getItemAt: function( index ) { + $.console.assert(index !== undefined, "[World.getItemAt] index is required"); + return this._items[ index ]; + }, + + /** + * Get the index of the given item or -1 if not present. + * @param {OpenSeadragon.TiledImage} item - The item. + * @returns {Number} The index of the item or -1 if not present. + */ + getIndexOfItem: function( item ) { + $.console.assert(item, "[World.getIndexOfItem] item is required"); + return $.indexOf( this._items, item ); + }, + + /** + * @returns {Number} The number of items used. + */ + getItemCount: function() { + return this._items.length; + }, + + /** + * Change the index of a item so that it appears over or under others. + * @param {OpenSeadragon.TiledImage} item - The item to move. + * @param {Number} index - The new index. + * @fires OpenSeadragon.World.event:item-index-change + */ + setItemIndex: function( item, index ) { + $.console.assert(item, "[World.setItemIndex] item is required"); + $.console.assert(index !== undefined, "[World.setItemIndex] index is required"); + + const oldIndex = this.getIndexOfItem( item ); + + if ( index >= this._items.length ) { + throw new Error( "Index bigger than number of layers." ); + } + + this._items.splice( oldIndex, 1 ); + this._items.splice( index, 0, item ); + this._needsDraw = true; + + /** + * Raised when the order of the indexes has been changed. + * @event item-index-change + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the World which raised the event. + * @property {OpenSeadragon.TiledImage} item - The item whose index has + * been changed + * @property {Number} previousIndex - The previous index of the item + * @property {Number} newIndex - The new index of the item + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'item-index-change', { + item: item, + previousIndex: oldIndex, + newIndex: index + } ); + }, + + /** + * Remove an item. + * @param {OpenSeadragon.TiledImage} item - The item to remove. + * @fires OpenSeadragon.World.event:remove-item + * @fires OpenSeadragon.World.event:metrics-change + */ + removeItem: function( item ) { + $.console.assert(item, "[World.removeItem] item is required"); + + const index = $.indexOf(this._items, item ); + if ( index === -1 ) { + return; + } + + item.removeHandler('bounds-change', this._delegatedFigureSizes); + item.removeHandler('clip-change', this._delegatedFigureSizes); + item.destroy(); + this._items.splice( index, 1 ); + this._figureSizes(); + this._needsDraw = true; + this._raiseRemoveItem(item); + }, + + /** + * Remove all items. + * @fires OpenSeadragon.World.event:remove-item + * @fires OpenSeadragon.World.event:metrics-change + */ + removeAll: function() { + // We need to make sure any pending images are canceled so the world items don't get messed up + this.viewer._cancelPendingImages(); + let item; + for (let i = 0; i < this._items.length; i++) { + item = this._items[i]; + item.removeHandler('bounds-change', this._delegatedFigureSizes); + item.removeHandler('clip-change', this._delegatedFigureSizes); + item.destroy(); + } + + const removedItems = this._items; + this._items = []; + this._figureSizes(); + this._needsDraw = true; + + for (let i = 0; i < removedItems.length; i++) { + item = removedItems[i]; + this._raiseRemoveItem(item); + } + }, + + /** + * Forces the system consider all tiles across all tiled images + * as outdated, and fire tile update event on relevant tiles + * Detailed description is available within the 'tile-invalidated' + * event. + * @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data + * @param {number} [tStamp=OpenSeadragon.now()] optionally provide tStamp of the update event + * @function + * @fires OpenSeadragon.Viewer.event:tile-invalidated + * @return {OpenSeadragon.Promise} + */ + requestInvalidate: function (restoreTiles = true, tStamp = $.now()) { + + // Note: Getting the async cache + invalidation flow right is VERY tricky. + // + // When debugging invalidation flow, instead of this optimized version, + // uncomment the following snipplet and test in easier setting: + // + // this.__invalidatedAt = tStamp; + // const batch = this.viewer.tileCache.getLoadedTilesFor(null); + // OpenSeadragon.trace(`Invalidate request ${tStamp} - ${batch.length} tiles`); + // return this.requestTileInvalidateEvent(batch, tStamp, restoreTiles); + // + // This makes the code easier to reason about. Also, recommended is to put logging + // messages into a buffered logger using OpenSeadragon.trace(..) + // to avoid change of flow in the async execution with detailed logs. + + + this.__invalidatedAt = tStamp; + let drawnTstamp = Infinity; + for (const item of this._items) { + if (item._lastDrawn.length) { + drawnTstamp = Math.min(drawnTstamp, item._lastDrawn[0].tile.lastTouchTime); + } + // Might be nested + for (const tileSet of item._tilesToDraw) { + if (Array.isArray(tileSet)) { + if (tileSet.length) { + drawnTstamp = Math.min(drawnTstamp, tileSet[0].tile.lastTouchTime); + } + } else if (tileSet) { + drawnTstamp = Math.min(drawnTstamp, tileSet.tile.lastTouchTime); + } + } + } + + const allTiles = this.viewer.tileCache.getLoadedTilesFor(null); + const tilesToRestore = new Array(allTiles.length); + + let restoreIndex = 0; + let deletedTiles = 0; + + const cache = this.viewer.tileCache; + for (let i = 0; i < allTiles.length; i++) { + const tile = allTiles[i]; + const isRecentlyTouched = tile.lastTouchTime >= drawnTstamp; + const isAboveCutoff = tile.level <= (tile.tiledImage.source.getClosestLevel() || 0); + + if (isRecentlyTouched || isAboveCutoff) { + tilesToRestore[restoreIndex++] = tile; + } else { + cache._unloadTile(tile, false, i - deletedTiles); + deletedTiles++; + } + } + tilesToRestore.length = restoreIndex; + return this.requestTileInvalidateEvent(tilesToRestore, tStamp, restoreTiles); + }, + + /** + * Requests tile data update. + * @function OpenSeadragon.Viewer.prototype._updateSequenceButtons + * @private + * @param {OpenSeadragon.Tile[]} tilesToProcess tiles to update + * @param {Number} tStamp timestamp in milliseconds, if active timestamp of the same value is executing, + * changes are added to the cycle, else they await next iteration + * @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data + * @param {Boolean} [_allowTileUnloaded=false] internal flag for calling on tiles that come new to the system + * @param {Boolean} [_isFromTileLoad=false] internal flag that must not be used manually + * @fires OpenSeadragon.Viewer.event:tile-invalidated + * @return {OpenSeadragon.Promise} + */ + requestTileInvalidateEvent: function(tilesToProcess, tStamp, restoreTiles = true, + _allowTileUnloaded = false, _isFromTileLoad = false) { + // Calling the event is not considered invalidation, as tile load events finishes with this too. + if (!this.viewer.isOpen()) { + return $.Promise.resolve(); + } + + if (tStamp === undefined) { + tStamp = this.__invalidatedAt; + } + + const tilesThatNeedReprocessing = []; + + const jobList = tilesToProcess.map(tile => { + // We allow re-execution on tiles that are in process but have too low processing timestamp, + // which must be solved by ensuring subsequent data calls in the suddenly outdated processing + // pipeline take no effect. + // Note that in the same list we can have tiles that have shared cache and such + // cache needs to be processed just once. + if (!tile || (!_allowTileUnloaded && !tile.loaded && !tile.processing)) { + // OpenSeadragon.trace(`Ignoring tile ${tile ? tile.toString() : 'null'} tstamp ${tStamp}`); + return Promise.resolve(); + } + + const tiledImage = tile.tiledImage; + const drawer = tiledImage.getDrawer(); + // We call the event on the parent viewer window no matter what, nested viewers have parent viewer ref. + // we use the knowledge that drawerBase keeps track of parent viewer to register into, we use this ref. + // We could turn this into API... + const eventTarget = drawer._parentViewer || this.viewer; + const originalCache = tile.getCache(tile.originalCacheKey); + const tileCache = tile.getCache(tile.originalCacheKey); + if (tileCache.__invStamp && tileCache.__invStamp >= tStamp) { + // OpenSeadragon.trace(`Ignoring tile - old, ${tile ? tile.toString() : 'null'} tstamp ${tStamp}`); + return Promise.resolve(); + } + + + let wasOutdatedRun = false; + if (originalCache.__finishProcessing) { + // OpenSeadragon.trace(` Tile Pre-Finisher, ${tile ? tile.toString() : 'null'} as Invalid from future ${tStamp}`); + originalCache.__finishProcessing(true); + } + + // Keep the original promise alive until the processing finished normally. If the + // processing was interrupted, the old promise gets reused in the new run - awaited logics + // will wait for proper invalidation finish. + let promise; + if (!originalCache.__resolve) { + promise = new $.Promise((resolve) => { + originalCache.__resolve = resolve; + }); + } + + originalCache.__finishProcessing = (asInvalidRun) => { + wasOutdatedRun = wasOutdatedRun || asInvalidRun; + // OpenSeadragon.trace(` Tile Finisher, ${tile ? tile.toString() : 'null'} as Invalid run ${asInvalidRun} with ${tStamp}`); + tile.processing = false; + originalCache.__finishProcessing = null; + // resolve only when finished without interruption + if (!asInvalidRun) { + originalCache.__resolve(tile); + originalCache.__resolve = null; + } + }; + + for (const t of originalCache._tiles) { + // Mark all related tiles as processing and register callback to unmark later on + t.processing = tStamp; + if (promise) { + t.processingPromise = promise; + } + } + originalCache.__invStamp = tStamp; + originalCache.__wasRestored = restoreTiles; + + + let workingCache = null; + const getWorkingCacheData = (type) => { + if (workingCache) { + return workingCache.getDataAs(type, false); + } + + const targetCopyKey = restoreTiles ? tile.originalCacheKey : tile.cacheKey; + const origCache = tile.getCache(targetCopyKey); + if (!origCache) { + $.console.error("[Tile::getData] There is no cache available for tile with key %s", targetCopyKey); + return $.Promise.reject(); + } + // Here ensure type is defined, rquired by data callbacks + type = type || origCache.type; + workingCache = new $.CacheRecord().withTileReference(tile); + return origCache.getDataAs(type, true).then(data => { + if (data === undefined || data === null) { + // Conversion/loading failed upstream; abort invalidation for this tile. + return $.Promise.reject(new Error('[World.getData] Working cache source data unavailable')); + } + workingCache.addTile(tile, data, type); + return workingCache.data; + }); + }; + const setWorkingCacheData = (value, type) => { + // // OpenSeadragon.trace(` WORKER tile, ${tile ? tile.toString() : 'null'} tstamp ${tStamp}`); + if (!workingCache) { + workingCache = new $.CacheRecord().withTileReference(tile); + workingCache.addTile(tile, value, type); + return $.Promise.resolve(); + } + return workingCache.setDataAs(value, type); + }; + const atomicCacheSwap = () => { + if (workingCache) { + const newCacheKey = tile.buildDistinctMainCacheKey(); + tiledImage._tileCache.injectCache({ + tile: tile, + cache: workingCache, + targetKey: newCacheKey, + setAsMainCache: true, + tileAllowNotLoaded: tile.loading + }); + } else if (restoreTiles) { + // If we requested restore, perform now + tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, tile.getCache(tile.originalCacheKey), true); + } + }; + + const outdatedTest = () => wasOutdatedRun || + (typeof originalCache.__invStamp === "number" && originalCache.__invStamp < this.__invalidatedAt) || + (!tile.loaded && !tile.loading); + + // OpenSeadragon.trace(` Procesing tile, ${tile ? tile.toString() : 'null'} tstamp ${tStamp}`); + /** + * @event tile-invalidated + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {AsyncNullaryFunction} outdated - predicate that evaluates to true if the event + * is outdated and should not be longer processed (has no effect) + * @property {AsyncUnaryFunction} getData - get data of desired type (string argument) + * @property {AsyncBinaryFunction} setData - set data (any) + * and the type of the data (string) + * @property {function} resetData - function that deletes any previous data modification in the current + * execution pipeline + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + return eventTarget.raiseEventAwaiting('tile-invalidated', { + tile: tile, + tiledImage: tiledImage, + outdated: outdatedTest, + getData: getWorkingCacheData, + setData: setWorkingCacheData, + resetData: () => { + if (workingCache) { + workingCache.destroy(); + workingCache = null; + } + }, + stopPropagation: () => { + const result = outdatedTest(); + // if (result) { + // OpenSeadragon.trace( ` Stop propagation ${tile.toString()}: out: ${wasOutdatedRun} | ${originalCache.__invStamp} ${tile.loaded} ${tile.loading}`); + // } + return result; + }, + }).catch(err => { + // Plugin/invalidation error: keep existing main cache, discard working cache, and finish processing as invalid. + $.console.error('Update routine error:', err); + if (workingCache) { + try { + workingCache.destroy(); + } catch (e) { + //no-op + } + workingCache = null; + } + wasOutdatedRun = true; + if (originalCache.__finishProcessing) { + originalCache.__finishProcessing(true); + } + return null; + }).then(_ => { + if (this.viewer.isDestroyed()) { + if (originalCache.__finishProcessing) { + originalCache.__finishProcessing(true); + } + return null; + } + + if (wasOutdatedRun) { + return null; + } + + // OpenSeadragon.trace(` FF Tile, ${tile ? tile.toString() : 'null'} FINISH ${tStamp}`); + + // If we do not have the handler, we were already discarded + if (originalCache.__finishProcessing) { + // If we are not in outdated run, we can finish the data processing if the state is valid + if (!wasOutdatedRun && (tile.loaded || tile.loading)) { + // If we find out that processing was outdated but the system did not find about this yet, request re-processing + if (originalCache.__invStamp < this.__invalidatedAt) { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} tstamp ${tStamp} needs reprocessing`); + // todo consider some recursion loop prevention + tilesThatNeedReprocessing.push(tile); + // we will let it fall through to handle later + } else if (originalCache.__invStamp === tStamp) { + // If we matched the invalidation state, ensure the new working cache (if created) is used + if (workingCache) { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} tstamp ${tStamp} finishing normally, working cache exists.`); + return workingCache.prepareForRendering(drawer).then(c => { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} swapping working cache ${tStamp}`); + + // Inside async then, we need to again check validity of the state + if (!wasOutdatedRun) { + if (!outdatedTest() && c) { + atomicCacheSwap(); + } else { + workingCache.destroy(); + workingCache = null; + } + originalCache.__finishProcessing(); + } else { + workingCache.destroy(); + workingCache = null; + } + }); + } + + // If we requested restore, restore to originalCacheKey + if (restoreTiles) { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} tstamp ${tStamp} finishing normally, original data restored.`); + + const mainCacheRef = tile.getCache(); + const freshOriginalCacheRef = tile.getCache(tile.originalCacheKey); + if (mainCacheRef !== freshOriginalCacheRef) { + return freshOriginalCacheRef.prepareForRendering(drawer).then((c) => { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} SWAP2 ${tStamp}`); + // Inside async then, we need to again check validity of the state + if (!wasOutdatedRun) { + if (!outdatedTest() && c) { + atomicCacheSwap(); + } + originalCache.__finishProcessing(); + } + }); + } else { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} tstamp ${tStamp} finished - no need to swap cache.`); + return null; + } + } + // else we will let it fall through to handle later + } else { + $.console.error( + `Invalidation flow error: tile processing state is invalid. ` + + `Tile: ${tile ? tile.toString() : 'null'}, ` + + `loaded: ${tile ? tile.loaded : 'n/a'}, loading: ${tile ? tile.loading : 'n/a'}, ` + + `originalCache.__invStamp: ${originalCache.__invStamp}, ` + + `this.__invalidatedAt: ${this.__invalidatedAt}, ` + + `tStamp: ${tStamp}, wasOutdatedRun: ${wasOutdatedRun}` + ); + } + + // If we did not handle the data, finish here - still a valid run. + + // If this is also the first run on the tile, ensure the main cache, whatever it is, is ready for render + if (_isFromTileLoad) { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} needs render prep as a first run ${tStamp}`); + const freshMainCacheRef = tile.getCache(); + return freshMainCacheRef.prepareForRendering(drawer).then(() => { + // Inside async then, we need to again check validity of the state + if (!wasOutdatedRun && originalCache.__finishProcessing) { + originalCache.__finishProcessing(); + } + // else: do not destroy, we are the initial base cache, the system will remove + // any rendering internal cache on events such as atomic cache swap + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} SWAP FIRST LOAD FINISH ${tStamp}`); + }); + } + originalCache.__finishProcessing(); + return null; + } + // else invalid state, let this fall through + // OpenSeadragon.trace(`Tile, ${tile ? tile.toString() : 'null'} tstamp ${tStamp} discarded.`); + if (!wasOutdatedRun) { + originalCache.__finishProcessing(true); + } + } + + // If this is also the first run on the tile, ensure the main cache, whatever it is, is ready for render + if (_isFromTileLoad) { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} needs render prep as a first run ${tStamp} - from invalid event!`); + const freshMainCacheRef = tile.getCache(); + return freshMainCacheRef.prepareForRendering(drawer).then(() => { + // OpenSeadragon.trace(` Tile, ${tile ? tile.toString() : 'null'} SWAP FIRST LOAD FINISH ${tStamp}`); + if (!wasOutdatedRun && originalCache.__finishProcessing) { + originalCache.__finishProcessing(); + } + // else: do not destroy, we are the initial base cache, the system will remove + // any rendering internal cache on events such as atomic cache swap + }); + } + + if (workingCache) { + workingCache.destroy(); + workingCache = null; + } + return null; + }).catch(e => { + $.console.error("Update routine error:", e); + if (workingCache) { + workingCache.destroy(); + workingCache = null; + } + originalCache.__finishProcessing(); + }); + }); + + return $.Promise.all(jobList).then(() => { + if (tilesThatNeedReprocessing.length) { + this.requestTileInvalidateEvent(tilesThatNeedReprocessing, undefined, restoreTiles, true); + } + if (!_allowTileUnloaded && !this.viewer.isDestroyed()) { + this.draw(); + } + }); + }, + + /** + * Check if a tile needs update, update such tiles in the given list + * @param {OpenSeadragon.Tile[]} tileList + */ + ensureTilesUpToDate: function(tileList) { + let updateList; + // we cannot track this on per-tile level, but at least we try to find last used value + let wasRestored; + for (let tile of tileList) { + tile = tile.tile || tile; // osd uses objects of draw-spec with nested tile ref + if (!tile.loaded || tile.processing) { + continue; + } + + const originalCache = tile.getCache(tile.originalCacheKey); + wasRestored = originalCache.__wasRestored; + if (originalCache.__invStamp < this.__invalidatedAt) { + if (!updateList) { + updateList = [tile]; + } else { + updateList.push(tile); + } + } + } + if (updateList && updateList.length) { + // OpenSeadragon.trace(`Ensure tiles up to date ${this.__invalidatedAt} - ${updateList.length} tiles`); + this.requestTileInvalidateEvent(updateList, $.now(), wasRestored, false); + } + }, + + /** + * Clears all tiles and triggers updates for all items. + */ + resetItems: function() { + for ( let i = 0; i < this._items.length; i++ ) { + this._items[i].reset(); + } + }, + + /** + * Updates (i.e. animates bounds of) all items. + * @function + * @param viewportChanged Whether the viewport changed, which indicates that + * all TiledImages need to be updated. + */ + update: function(viewportChanged) { + let animated = false; + for ( let i = 0; i < this._items.length; i++ ) { + animated = this._items[i].update(viewportChanged) || animated; + } + + return animated; + }, + + /** + * Draws all items. + */ + draw: function() { + this.viewer.drawer.draw(this._items); + this._needsDraw = false; + for (const item of this._items) { + this._needsDraw = item.setDrawn() || this._needsDraw; + } + }, + + /** + * @returns {Boolean} true if any items need updating. + */ + needsDraw: function() { + for ( let i = 0; i < this._items.length; i++ ) { + if ( this._items[i].needsDraw() ) { + return true; + } + } + return this._needsDraw; + }, + + /** + * @returns {OpenSeadragon.Rect} The smallest rectangle that encloses all items, in viewport coordinates. + */ + getHomeBounds: function() { + return this._homeBounds.clone(); + }, + + /** + * To facilitate zoom constraints, we keep track of the pixel density of the + * densest item in the World (i.e. the item whose content size to viewport size + * ratio is the highest) and save it as this "content factor". + * @returns {Number} the number of content units per viewport unit. + */ + getContentFactor: function() { + return this._contentFactor; + }, + + /** + * As a performance optimization, setting this flag to false allows the bounds-change event handler + * on tiledImages to skip calculations on the world bounds. If a lot of images are going to be positioned in + * rapid succession, this is a good idea. When finished, setAutoRefigureSizes should be called with true + * or the system may behave oddly. + * @param {Boolean} [value] The value to which to set the flag. + */ + setAutoRefigureSizes: function(value) { + this._autoRefigureSizes = value; + if (value & this._needsSizesFigured) { + this._figureSizes(); + this._needsSizesFigured = false; + } + }, + + /** + * Arranges all of the TiledImages with the specified settings. + * @param {Object} options - Specifies how to arrange. + * @param {Boolean} [options.immediately=false] - Whether to animate to the new arrangement. + * @param {String} [options.layout] - See collectionLayout in {@link OpenSeadragon.Options}. + * @param {Number} [options.rows] - See collectionRows in {@link OpenSeadragon.Options}. + * @param {Number} [options.columns] - See collectionColumns in {@link OpenSeadragon.Options}. + * @param {Number} [options.tileSize] - See collectionTileSize in {@link OpenSeadragon.Options}. + * @param {Number} [options.tileMargin] - See collectionTileMargin in {@link OpenSeadragon.Options}. + * @fires OpenSeadragon.World.event:metrics-change + */ + arrange: function(options) { + options = options || {}; + const immediately = options.immediately || false; + const layout = options.layout || $.DEFAULT_SETTINGS.collectionLayout; + const rows = options.rows || $.DEFAULT_SETTINGS.collectionRows; + const columns = options.columns || $.DEFAULT_SETTINGS.collectionColumns; + const tileSize = options.tileSize || $.DEFAULT_SETTINGS.collectionTileSize; + const tileMargin = options.tileMargin || $.DEFAULT_SETTINGS.collectionTileMargin; + const increment = tileSize + tileMargin; + let wrap; + if (!options.rows && columns) { + wrap = columns; + } else { + wrap = Math.ceil(this._items.length / rows); + } + let x = 0; + let y = 0; + let item, box, width, height, position; + + this.setAutoRefigureSizes(false); + for (let i = 0; i < this._items.length; i++) { + if (i && (i % wrap) === 0) { + if (layout === 'horizontal') { + y += increment; + x = 0; + } else { + x += increment; + y = 0; + } + } + + item = this._items[i]; + box = item.getBoundsNoRotate(); + if (box.width > box.height) { + width = tileSize; + } else { + width = tileSize * (box.width / box.height); + } + + height = width * (box.height / box.width); + position = new $.Point(x + ((tileSize - width) / 2), + y + ((tileSize - height) / 2)); + + item.setPosition(position, immediately); + item.setWidth(width, immediately); + + if (layout === 'horizontal') { + x += increment; + } else { + y += increment; + } + } + this.setAutoRefigureSizes(true); + }, + + // private + _figureSizes: function() { + const oldHomeBounds = this._homeBounds ? this._homeBounds.clone() : null; + const oldContentSize = this._contentSize ? this._contentSize.clone() : null; + const oldContentFactor = this._contentFactor || 0; + + if (!this._items.length) { + this._homeBounds = new $.Rect(0, 0, 1, 1); + this._contentSize = new $.Point(1, 1); + this._contentFactor = 1; + } else { + let item = this._items[0]; + let bounds = item.getBounds(); + this._contentFactor = item.getContentSize().x / bounds.width; + let clippedBounds = item.getClippedBounds().getBoundingBox(); + let left = clippedBounds.x; + let top = clippedBounds.y; + let right = clippedBounds.x + clippedBounds.width; + let bottom = clippedBounds.y + clippedBounds.height; + for (let i = 1; i < this._items.length; i++) { + item = this._items[i]; + bounds = item.getBounds(); + this._contentFactor = Math.max(this._contentFactor, + item.getContentSize().x / bounds.width); + clippedBounds = item.getClippedBounds().getBoundingBox(); + left = Math.min(left, clippedBounds.x); + top = Math.min(top, clippedBounds.y); + right = Math.max(right, clippedBounds.x + clippedBounds.width); + bottom = Math.max(bottom, clippedBounds.y + clippedBounds.height); + } + + this._homeBounds = new $.Rect(left, top, right - left, bottom - top); + this._contentSize = new $.Point( + this._homeBounds.width * this._contentFactor, + this._homeBounds.height * this._contentFactor); + } + + if (this._contentFactor !== oldContentFactor || + !this._homeBounds.equals(oldHomeBounds) || + !this._contentSize.equals(oldContentSize)) { + /** + * Raised when the home bounds or content factor change. + * @event metrics-change + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the World which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('metrics-change', {}); + } + }, + + // private + _raiseRemoveItem: function(item) { + /** + * Raised when an item is removed. + * @event remove-item + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the World which raised the event. + * @property {OpenSeadragon.TiledImage} item - The item's underlying item. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'remove-item', { item: item } ); + } +}); + +}( OpenSeadragon )); + +//# sourceMappingURL=openseadragon.js.map \ No newline at end of file diff --git a/exact/exact/annotations/static/annotations/js/openseadragon.min.js b/exact/exact/annotations/static/annotations/js/openseadragon.min.js index b53236bd..713fbc64 100644 --- a/exact/exact/annotations/static/annotations/js/openseadragon.min.js +++ b/exact/exact/annotations/static/annotations/js/openseadragon.min.js @@ -3,108 +3,4 @@ //! Git commit: v6.0.2-0-7842cd92 //! http://openseadragon.github.io //! License: http://openseadragon.github.io/license/ - - -function OpenSeadragon(e){return new OpenSeadragon.Viewer(e)}!function(n){n.version={versionStr:"6.0.2",major:parseInt("6",10),minor:parseInt("0",10),revision:parseInt("2",10)};const t={"[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"};const i=Object.prototype.toString;const r=Object.prototype.hasOwnProperty;n.isFunction=function(e){return"function"===n.type(e)};n.isArray=Array.isArray||function(e){return"array"===n.type(e)};n.isWindow=function(e){return e&&"object"==typeof e&&"setInterval"in e};n.type=function(e){return null==e?String(e):t[i.call(e)]||"object"};n.isPlainObject=function(e){if(!e||"object"!==OpenSeadragon.type(e)||e.nodeType||n.isWindow(e))return!1;if(e.constructor&&!r.call(e,"constructor")&&!r.call(e.constructor.prototype,"isPrototypeOf"))return!1;let t;for(const i in e)t=i;return void 0===t||r.call(e,t)};n.isEmptyObject=function(e){for(const t in e)return!1;return!0};n.freezeObject=function(e){Object.freeze?n.freezeObject=Object.freeze:n.freezeObject=function(e){return e};return n.freezeObject(e)};n.supportsCanvas=function(){const e=document.createElement("canvas");return!(!n.isFunction(e.getContext)||!e.getContext("2d"))}();n.isCanvasTainted=function(e){let t=!1;try{e.getContext("2d").getImageData(0,0,1,1)}catch(e){t=!0}return t};n.supportsAddEventListener=!(!document.documentElement.addEventListener||!document.addEventListener);n.supportsRemoveEventListener=!(!document.documentElement.removeEventListener||!document.removeEventListener);n.supportsEventListenerOptions=function(){let t=0;if(n.supportsAddEventListener)try{var e={get capture(){t++;return!1},get once(){t++;return!1},get passive(){t++;return!1}};window.addEventListener("test",null,e);window.removeEventListener("test",null,e)}catch(e){t=0}return 3<=t}();n.supportsAsync=!0;n.getCurrentPixelDensityRatio=function(){if(n.supportsCanvas){var e=document.createElement("canvas").getContext("2d");var t=window.devicePixelRatio||1;e=e.webkitBackingStorePixelRatio||e.mozBackingStorePixelRatio||e.msBackingStorePixelRatio||e.oBackingStorePixelRatio||e.backingStorePixelRatio||1;return Math.max(t,1)/e}return 1};n.pixelDensityRatio=n.getCurrentPixelDensityRatio()}(OpenSeadragon);!function(u){u.extend=function(){var e;let t;let i;let n;let r;let o=arguments[0]||{};var s=arguments.length;let a=!1;let l=1;if("boolean"==typeof o){a=o;o=arguments[1]||{};l=2}"object"==typeof o||OpenSeadragon.isFunction(o)||(o={});if(s===l){o=this;--l}for(;l=i.x&&t.x=i.y},getMousePosition:function(e){if("number"==typeof e.pageX)u.getMousePosition=function(e){const t=new u.Point;t.x=e.pageX;t.y=e.pageY;return t};else{if("number"!=typeof e.clientX)throw new Error("Unknown event mouse position, no known technique.");u.getMousePosition=function(e){const t=new u.Point;t.x=e.clientX+document.body.scrollLeft+document.documentElement.scrollLeft;t.y=e.clientY+document.body.scrollTop+document.documentElement.scrollTop;return t}}return u.getMousePosition(e)},getPageScroll:function(){var e=document.documentElement||{};var t=document.body||{};if("number"==typeof window.pageXOffset)u.getPageScroll=function(){return new u.Point(window.pageXOffset,window.pageYOffset)};else if(t.scrollLeft||t.scrollTop)u.getPageScroll=function(){return new u.Point(document.body.scrollLeft,document.body.scrollTop)};else{if(!e.scrollLeft&&!e.scrollTop)return new u.Point(0,0);u.getPageScroll=function(){return new u.Point(document.documentElement.scrollLeft,document.documentElement.scrollTop)}}return u.getPageScroll()},setPageScroll:function(t){if(void 0!==window.scrollTo)u.setPageScroll=function(e){window.scrollTo(e.x,e.y)};else{var i=u.getPageScroll();if(i.x===t.x&&i.y===t.y)return;document.body.scrollLeft=t.x;document.body.scrollTop=t.y;let e=u.getPageScroll();if(e.x!==i.x&&e.y!==i.y){u.setPageScroll=function(e){document.body.scrollLeft=e.x;document.body.scrollTop=e.y};return}document.documentElement.scrollLeft=t.x;document.documentElement.scrollTop=t.y;e=u.getPageScroll();if(e.x!==i.x&&e.y!==i.y){u.setPageScroll=function(e){document.documentElement.scrollLeft=e.x;document.documentElement.scrollTop=e.y};return}u.setPageScroll=function(e){}}u.setPageScroll(t)},getWindowSize:function(){var e=document.documentElement||{};var t=document.body||{};if("number"==typeof window.innerWidth)u.getWindowSize=function(){return new u.Point(window.innerWidth,window.innerHeight)};else if(e.clientWidth||e.clientHeight)u.getWindowSize=function(){return new u.Point(document.documentElement.clientWidth,document.documentElement.clientHeight)};else{if(!t.clientWidth&&!t.clientHeight)throw new Error("Unknown window size, no known technique.");u.getWindowSize=function(){return new u.Point(document.body.clientWidth,document.body.clientHeight)}}return u.getWindowSize()},makeCenteredNode:function(e){e=u.getElement(e);const t=[u.makeNeutralElement("div"),u.makeNeutralElement("div"),u.makeNeutralElement("div")];u.extend(t[0].style,{display:"table",height:"100%",width:"100%"});u.extend(t[1].style,{display:"table-row"});u.extend(t[2].style,{display:"table-cell",verticalAlign:"middle",textAlign:"center"});t[0].appendChild(t[1]);t[1].appendChild(t[2]);t[2].appendChild(e);return t[0]},trace:function(e,t=!1){this.__traceLogs=[];setInterval(()=>{if(this.__traceLogs.length){console.log(this.__traceLogs.join("\n"));this.__traceLogs=[]}},2e3);this.trace=function(e,t=!1){if("string"!=typeof e){const i=(e=e instanceof OpenSeadragon.Tile?e.getCache(e.originalCacheKey):e)._tiles[0];this.__traceLogs.push(`Cache ${i.toString()} loaded ${i.loaded} loading ${i.loading} cacheCount ${Object.keys(i._caches).length} - CACHE `+e.__invStamp);t&&this.__traceLogs.push(...(new Error).stack.split("\n").slice(1))}else{this.__traceLogs.push(e);t&&this.__traceLogs.push(...(new Error).stack.split("\n").slice(1))}};this.trace(e,t)},makeNeutralElement:function(e){e=document.createElement(e);const t=e.style;t.background="transparent none";t.border="none";t.margin="0px";t.padding="0px";t.position="static";return e},now:function(){Date.now?u.now=Date.now:u.now=function(){return(new Date).getTime()};return u.now()},makeTransparentImage:function(e){const t=u.makeNeutralElement("img");t.src=e;return t},setElementOpacity:function(e,t,i){e=u.getElement(e);i&&!u.Browser.alpha&&(t=Math.round(t));if(u.Browser.opacity)e.style.opacity=t<1?t:"";else if(t<1){t=Math.round(100*t);e.style.filter="alpha(opacity="+t+")"}else e.style.filter=""},setElementTouchActionNone:function(e){void 0!==(e=u.getElement(e)).style.touchAction?e.style.touchAction="none":void 0!==e.style.msTouchAction&&(e.style.msTouchAction="none")},setElementPointerEvents:function(e,t){void 0!==(e=u.getElement(e)).style&&void 0!==e.style.pointerEvents&&(e.style.pointerEvents=t)},setElementPointerEventsNone:function(e){u.setElementPointerEvents(e,"none")},addClass:function(e,t){(e=u.getElement(e)).className?-1===(" "+e.className+" ").indexOf(" "+t+" ")&&(e.className+=" "+t):e.className=t},indexOf:function(e,t,i){Array.prototype.indexOf?this.indexOf=function(e,t,i){return e.indexOf(t,i)}:this.indexOf=function(t,i,e){let n=e||0;if(!t)throw new TypeError;var r=t.length;if(0===r||n>=r)return-1;n<0&&(n=r-Math.abs(n));for(let e=n;e{for(;e instanceof u.Promise;)e=e._value;this._value=e},e=>{for(;e instanceof u.Promise;)e=e._value;this._value=e;this._error=!0})}catch(e){this._value=e;this._error=!0}}then(e){if(!this._error)try{this._value=e(this._value)}catch(e){this._value=e;this._error=!0}return this}catch(e){if(this._error)try{this._value=e(this._value);this._error=!1}catch(e){this._value=e;this._error=!0}return this}get _value(){return this.__value}set _value(e){e&&e.constructor===this.constructor&&(e=e._value);this.__value=e}static resolve(t){return new this(e=>e(t))}static reject(i){return new this((e,t)=>t(i))}static all(t){return new this(e=>e(t.map(e=>e())))}static race(t){return t.length<1?this.resolve():new this(e=>e(t[0]()))}}}(OpenSeadragon);!function(e,t){if("function"==typeof define&&define.amd)define([],function(){return t});else if("object"==typeof module&&module.exports)module.exports=t;else{(e=e||"object"==typeof window&&window)||t.console.error("OpenSeadragon must run in browser environment!");e.OpenSeadragon=t}}(this,OpenSeadragon);!function(e){e.Mat3=class y{constructor(e){this.values=e=e||[0,0,0,0,0,0,0,0,0]}static makeIdentity(){return new y([1,0,0,0,1,0,0,0,1])}static makeTranslation(e,t){return new y([1,0,0,0,1,0,e,t,1])}static makeRotation(e){var t=Math.cos(e);e=Math.sin(e);return new y([t,-e,0,e,t,0,0,0,1])}static makeScaling(e,t){return new y([e,0,0,0,t,0,0,0,1])}multiply(e){var t=this.values;var i=e.values;var n=t[0],r=t[1],o=t[2];var s=t[3],a=t[4],l=t[5];var h=t[6],c=t[7],u=t[8];var d=i[0],p=i[1],g=i[2];var m=i[3],f=i[4],v=i[5];e=i[6],t=i[7],i=i[8];return new y([d*n+p*s+g*h,d*r+p*a+g*c,d*o+p*l+g*u,m*n+f*s+v*h,m*r+f*a+v*c,m*o+f*l+v*u,e*n+t*s+i*h,e*r+t*a+i*c,e*o+t*l+i*u])}setValues(e,t,i,n,r,o,s,a,l){this.values[0]=e;this.values[1]=t;this.values[2]=i;this.values[3]=n;this.values[4]=r;this.values[5]=o;this.values[6]=s;this.values[7]=a;this.values[8]=l}scaleAndTranslate(e,t,i,n){var r=this.values;var o=r[0];var s=r[1];var a=r[2];var l=r[3];var h=r[4];r=r[5];return new y([e*o,e*s,e*a,t*l,t*h,t*r,i*o+n*l,i*s+n*h,i*a+n*r])}scaleAndTranslateSelf(e,t,i,n){const r=this.values;var o=r[0],s=r[1],a=r[2];var l=r[3],h=r[4],c=r[5];r[0]=e*o;r[1]=e*s;r[2]=e*a;r[3]=t*l;r[4]=t*h;r[5]=t*c;r[6]=i*o+n*l+r[6];r[7]=i*s+n*h+r[7];r[8]=i*a+n*c+r[8]}scaleAndTranslateOtherSetSelf(e){var t=e.values;const i=this.values;var n=i[0];var r=i[4];var o=i[6];e=i[7];i[0]=n*t[0];i[1]=n*t[1];i[2]=n*t[2];i[3]=r*t[3];i[4]=r*t[4];i[5]=r*t[5];i[6]=o*t[0]+e*t[3]+t[6];i[7]=o*t[1]+e*t[4]+t[7];i[8]=o*t[2]+e*t[5]+t[8]}}}(OpenSeadragon);!function(t){const e={supportsFullScreen:!1,isFullScreen:function(){return!1},getFullScreenElement:function(){return null},requestFullScreen:function(){},exitFullScreen:function(){},cancelFullScreen:function(){},fullScreenEventName:"",fullScreenErrorEventName:""};if(document.exitFullscreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.fullscreenElement};e.requestFullScreen=function(e){return e.requestFullscreen().catch(function(e){t.console.error("Fullscreen request failed: ",e)})};e.exitFullScreen=function(){document.exitFullscreen().catch(function(e){t.console.error("Error while exiting fullscreen: ",e)})};e.fullScreenEventName="fullscreenchange";e.fullScreenErrorEventName="fullscreenerror"}else if(document.msExitFullscreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.msFullscreenElement};e.requestFullScreen=function(e){return e.msRequestFullscreen()};e.exitFullScreen=function(){document.msExitFullscreen()};e.fullScreenEventName="MSFullscreenChange";e.fullScreenErrorEventName="MSFullscreenError"}else if(document.webkitExitFullscreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.webkitFullscreenElement};e.requestFullScreen=function(e){return e.webkitRequestFullscreen()};e.exitFullScreen=function(){document.webkitExitFullscreen()};e.fullScreenEventName="webkitfullscreenchange";e.fullScreenErrorEventName="webkitfullscreenerror"}else if(document.webkitCancelFullScreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.webkitCurrentFullScreenElement};e.requestFullScreen=function(e){return e.webkitRequestFullScreen()};e.exitFullScreen=function(){document.webkitCancelFullScreen()};e.fullScreenEventName="webkitfullscreenchange";e.fullScreenErrorEventName="webkitfullscreenerror"}else if(document.mozCancelFullScreen){e.supportsFullScreen=!0;e.getFullScreenElement=function(){return document.mozFullScreenElement};e.requestFullScreen=function(e){return e.mozRequestFullScreen()};e.exitFullScreen=function(){document.mozCancelFullScreen()};e.fullScreenEventName="mozfullscreenchange";e.fullScreenErrorEventName="mozfullscreenerror"}e.isFullScreen=function(){return null!==e.getFullScreenElement()};e.cancelFullScreen=function(){t.console.error("cancelFullScreen is deprecated. Use exitFullScreen instead.");e.exitFullScreen()};t.extend(t,e)}(OpenSeadragon);!function(c){c.EventSource=function(){this.events={};this._rejectedEventList={}};c.EventSource.prototype={addOnceHandler:function(t,i,e,n,r){const o=this;n=n||1;let s=0;function a(e){s++;s===n&&o.removeHandler(t,a);return i(e)}return this.addHandler(t,a,e,r)},addHandler:function(e,i,n,r){if(Object.prototype.hasOwnProperty.call(this._rejectedEventList,e)){c.console.error(`Error adding handler for ${e}. `+this._rejectedEventList[e]);return!1}let o=this.events[e];o||(this.events[e]=o=[]);if(i&&c.isFunction(i)){let e=o.length,t={handler:i,userData:n||null,priority:r||0};o[e]=t;for(;0{const o=h.length;!function e(t){if(t>=o||!h[t]){n(l);return null}a.eventSource=s;a.userData=h[t].userData;let i;try{i=h[t].handler(a)}catch(e){return r(e)}i=i&&"promise"===c.type(i)?i:c.Promise.resolve();return i.then(()=>!a.stopPropagation||"function"==typeof a.stopPropagation&&!1===a.stopPropagation()?e(t+1):e(o))}(0).catch(r)})}},raiseEvent:function(e,t){if(Object.prototype.hasOwnProperty.call(this._rejectedEventList,e)){c.console.error(`Error adding handler for ${e}. `+this._rejectedEventList[e]);return!1}const i=this.getHandler(e);i&&i(this,t||{});return!0},raiseEventAwaiting:function(e,t,i=null){const n=this.getAwaitingHandler(e,i);return n?n(this,t||{}):c.Promise.resolve(i)},rejectEventHandler(e,t=""){this._rejectedEventList[e]=t},allowEventHandler(e){delete this._rejectedEventList[e]}}}(OpenSeadragon);!function(c){const n=[];const u={};c.MouseTracker=function(e){n.push(this);var t=arguments;c.isPlainObject(e)||(e={element:t[0],clickTimeThreshold:t[1],clickDistThreshold:t[2]});this.hash=function(){let e=Date.now().toString(36)+Math.random().toString(36).substring(2);for(;e in u;)e=Date.now().toString(36)+Math.random().toString(36).substring(2);return e}();this.element=c.getElement(e.element);this.clickTimeThreshold=e.clickTimeThreshold||c.DEFAULT_SETTINGS.clickTimeThreshold;this.clickDistThreshold=e.clickDistThreshold||c.DEFAULT_SETTINGS.clickDistThreshold;this.dblClickTimeThreshold=e.dblClickTimeThreshold||c.DEFAULT_SETTINGS.dblClickTimeThreshold;this.dblClickDistThreshold=e.dblClickDistThreshold||c.DEFAULT_SETTINGS.dblClickDistThreshold;this.userData=e.userData||null;this.stopDelay=e.stopDelay||50;this.preProcessEventHandler=e.preProcessEventHandler||null;this.contextMenuHandler=e.contextMenuHandler||null;this.enterHandler=e.enterHandler||null;this.leaveHandler=e.leaveHandler||null;this.exitHandler=e.exitHandler||null;this.overHandler=e.overHandler||null;this.outHandler=e.outHandler||null;this.pressHandler=e.pressHandler||null;this.nonPrimaryPressHandler=e.nonPrimaryPressHandler||null;this.releaseHandler=e.releaseHandler||null;this.nonPrimaryReleaseHandler=e.nonPrimaryReleaseHandler||null;this.moveHandler=e.moveHandler||null;this.scrollHandler=e.scrollHandler||null;this.clickHandler=e.clickHandler||null;this.dblClickHandler=e.dblClickHandler||null;this.dragHandler=e.dragHandler||null;this.dragEndHandler=e.dragEndHandler||null;this.pinchHandler=e.pinchHandler||null;this.stopHandler=e.stopHandler||null;this.keyDownHandler=e.keyDownHandler||null;this.keyUpHandler=e.keyUpHandler||null;this.keyHandler=e.keyHandler||null;this.focusHandler=e.focusHandler||null;this.blurHandler=e.blurHandler||null;const i=this;u[this.hash]={click:function(e){!function(e,t){var i={originalEvent:t,eventType:"click",pointerType:"mouse",isEmulated:!1};A(e,i);i.preventDefault&&!i.defaultPrevented&&c.cancelEvent(t);i.stopPropagation&&c.stopEvent(t)}(i,e)},dblclick:function(e){!function(e,t){var i={originalEvent:t,eventType:"dblclick",pointerType:"mouse",isEmulated:!1};A(e,i);i.preventDefault&&!i.defaultPrevented&&c.cancelEvent(t);i.stopPropagation&&c.stopEvent(t)}(i,e)},keydown:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"keydown",pointerType:"",isEmulated:!1};A(e,n);if(e.keyDownHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,keyCode:t.keyCode||t.charCode,ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,originalEvent:t,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.keyDownHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},keyup:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"keyup",pointerType:"",isEmulated:!1};A(e,n);if(e.keyUpHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,keyCode:t.keyCode||t.charCode,ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,originalEvent:t,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.keyUpHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},keypress:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"keypress",pointerType:"",isEmulated:!1};A(e,n);if(e.keyHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,keyCode:t.keyCode||t.charCode,ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,originalEvent:t,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.keyHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},focus:function(e){!function(e,t){var i={originalEvent:t,eventType:"focus",pointerType:"",isEmulated:!1};A(e,i);e.focusHandler&&!i.preventGesture&&e.focusHandler({eventSource:e,originalEvent:t,userData:e.userData})}(i,e)},blur:function(e){!function(e,t){var i={originalEvent:t,eventType:"blur",pointerType:"",isEmulated:!1};A(e,i);e.blurHandler&&!i.preventGesture&&e.blurHandler({eventSource:e,originalEvent:t,userData:e.userData})}(i,e)},contextmenu:function(e){!function(e,t){let i=null;var n={originalEvent:t,eventType:"contextmenu",pointerType:"mouse",isEmulated:!1};A(e,n);if(e.contextMenuHandler&&!n.preventGesture&&!n.defaultPrevented){i={eventSource:e,position:f(g(t),e.element),originalEvent:n.originalEvent,preventDefault:n.preventDefault||n.defaultPrevented,userData:e.userData};e.contextMenuHandler(i)}(i&&i.preventDefault||n.preventDefault&&!n.defaultPrevented)&&c.cancelEvent(t);n.stopPropagation&&c.stopEvent(t)}(i,e)},wheel:function(e){w(i,e,e)},mousewheel:function(e){y(i,e)},DOMMouseScroll:function(e){y(i,e)},MozMousePixelScroll:function(e){y(i,e)},losecapture:function(e){!function(e,t){var i={id:c.MouseTracker.mousePointerId,type:"mouse"};var n={originalEvent:t,eventType:"lostpointercapture",pointerType:"mouse",isEmulated:!1};A(e,n);t.target===e.element&&F(e,i,!1);n.stopPropagation&&c.stopEvent(t)}(i,e)},mouseenter:function(e){_(i,e)},mouseleave:function(e){T(i,e)},mouseover:function(e){x(i,e)},mouseout:function(e){S(i,e)},mousedown:function(e){E(i,e)},mouseup:function(e){C(i,e)},mousemove:function(e){P(i,e)},touchstart:function(e){!function(t,i){var n=i.changedTouches.length;const r=t.getActivePointersListByType("touch");var o=c.now();r.getLength()>i.touches.length-n&&c.console.warn("Tracked touch contact count doesn't match event.touches.length");var s={originalEvent:i,eventType:"pointerdown",pointerType:"touch",isEmulated:!1};A(t,s);for(let e=0;e{e[t]=i[t];delete i[t];return e},{}),i.drawerOptions);m.extend(!0,this,{id:i.id,hash:i.hash||a++,viewer:null,initialPage:0,element:null,container:null,canvas:null,overlays:[],overlaysContainer:null,previousDisplayValuesOfBodyChildren:[],customControls:[],source:null,drawer:null,drawerCandidates:null,world:null,viewport:null,navigator:null,collectionViewport:null,collectionDrawer:null,navImages:null,buttonGroup:null,profiler:null},m.DEFAULT_SETTINGS,i);if(void 0===this.hash)throw new Error("A hash must be defined, either by specifying options.id or options.hash.");void 0!==u[this.hash]&&m.console.warn("Hash "+this.hash+" has already been used.");u[this.hash]={fsBoundsDelta:new m.Point(1,1),prevContainerSize:null,animating:!1,forceRedraw:!1,needsResize:!1,forceResize:!1,mouseInside:!1,group:null,zooming:!1,zoomFactor:null,lastZoomTime:null,fullPage:!1,onfullscreenchange:null,lastClickTime:null,draggingToZoom:!1};this._sequenceIndex=0;this._firstOpen=!0;this._updateRequestId=null;this._loadQueue=[];this.currentOverlays=[];this._updatePixelDensityRatioBind=null;this._lastScrollTime=m.now();this._fullyLoaded=!1;this._navActionFrames={};this._navActionVirtuallyHeld={};this._minNavActionFrames=10;this._activeActions={panUp:!1,panDown:!1,panLeft:!1,panRight:!1,zoomIn:!1,zoomOut:!1};m.EventSource.call(this);this.addHandler("open-failed",function(e){e=m.getString("Errors.OpenFailed",e.eventSource,e.message);n._showMessage(e)});m.ControlDock.call(this,i);this.xmlPath&&(this.tileSources=[this.xmlPath]);this.element=this.element||document.getElementById(this.id);this.canvas=m.makeNeutralElement("div");this.canvas.className="openseadragon-canvas";if(!document.querySelector("style[data-openseadragon-mobile-css]")){const o=document.createElement("style");o.setAttribute("data-openseadragon-mobile-css","true");o.textContent="@media (hover: none) { .openseadragon-canvas:focus { outline: none !important; }}";document.head.appendChild(o)}!function(e){e.width="100%";e.height="100%";e.overflow="hidden";e.position="absolute";e.top="0px";e.left="0px"}(this.canvas.style);m.setElementTouchActionNone(this.canvas);""!==i.tabIndex&&(this.canvas.tabIndex=void 0===i.tabIndex?0:i.tabIndex);this.container.className="openseadragon-container";!function(e){e.width="100%";e.height="100%";e.position="relative";e.overflow="hidden";e.left="0px";e.top="0px";e.textAlign="left"}(this.container.style);m.setElementTouchActionNone(this.container);this.container.insertBefore(this.canvas,this.container.firstChild);this.element.appendChild(this.container);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 m.MouseTracker({userData:"Viewer.innerTracker",element:this.canvas,startDisabled:!this.mouseNavEnabled,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,dblClickTimeThreshold:this.dblClickTimeThreshold,dblClickDistThreshold:this.dblClickDistThreshold,contextMenuHandler:m.delegate(this,p),keyDownHandler:m.delegate(this,y),keyUpHandler:m.delegate(this,g),keyHandler:m.delegate(this,w),clickHandler:m.delegate(this,_),dblClickHandler:m.delegate(this,T),dragHandler:m.delegate(this,x),dragEndHandler:m.delegate(this,S),enterHandler:m.delegate(this,E),leaveHandler:m.delegate(this,C),pressHandler:m.delegate(this,b),releaseHandler:m.delegate(this,P),nonPrimaryPressHandler:m.delegate(this,R),nonPrimaryReleaseHandler:m.delegate(this,D),scrollHandler:m.delegate(this,L),pinchHandler:m.delegate(this,I),focusHandler:m.delegate(this,A),blurHandler:m.delegate(this,F)});this.outerTracker=new m.MouseTracker({userData:"Viewer.outerTracker",element:this.container,startDisabled:!this.mouseNavEnabled,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,dblClickTimeThreshold:this.dblClickTimeThreshold,dblClickDistThreshold:this.dblClickDistThreshold,enterHandler:m.delegate(this,O),leaveHandler:m.delegate(this,k)});this.toolbar&&(this.toolbar=new m.ControlDock({element:this.toolbar}));this.bindStandardControls();u[this.hash].prevContainerSize=l(this.container);if(window.ResizeObserver){this._autoResizePolling=!1;this._resizeObserver=new ResizeObserver(function(){u[n.hash].needsResize=!0});this._resizeObserver.observe(this.container,{})}else this._autoResizePolling=!0;this.world=new m.World({viewer:this});this.world.addHandler("add-item",function(e){n.source=n.world.getItemAt(0).source;u[n.hash].forceRedraw=!0;n._updateRequestId||(n._updateRequestId=c(n,B));const t=e.item;function i(){var e=n._areAllFullyLoaded();if(e!==n._fullyLoaded){n._fullyLoaded=e;n.raiseEvent("fully-loaded-change",{fullyLoaded:e})}}t._fullyLoadedHandlerForViewer=i;t.addHandler("fully-loaded-change",i)});this.world.addHandler("remove-item",function(e){const t=e.item;if(t._fullyLoadedHandlerForViewer){t.removeHandler("fully-loaded-change",t._fullyLoadedHandlerForViewer);delete t._fullyLoadedHandlerForViewer}n.world.getItemCount()?n.source=n.world.getItemAt(0).source:n.source=null;u[n.hash].forceRedraw=!0});this.world.addHandler("metrics-change",function(e){n.viewport&&n.viewport._setContentBounds(n.world.getHomeBounds(),n.world.getContentFactor())});this.world.addHandler("item-index-change",function(e){n.source=n.world.getItemAt(0).source});this.viewport=new m.Viewport({containerSize:u[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());this.imageLoader=new m.ImageLoader({jobLimit:this.imageLoaderLimit,timeout:i.timeout,tileRetryMax:this.tileRetryMax,tileRetryDelay:this.tileRetryDelay});this.tileCache=new m.TileCache({viewer:this,maxImageCacheCount:this.maxImageCacheCount});if(Object.prototype.hasOwnProperty.call(this.drawerOptions,"useCanvas")){m.console.error('useCanvas is deprecated, use the "drawer" option to indicate preferred drawer(s)');this.drawerOptions.useCanvas||(this.drawer=m.HTMLDrawer);delete this.drawerOptions.useCanvas}let r=Array.isArray(this.drawer)?this.drawer:[this.drawer];if(0===r.length){r=[m.DEFAULT_SETTINGS.drawer].flat();m.console.warn("No valid drawers were selected. Using the default value.")}r=r.flatMap(function(e){return"auto"===e?V():[e]});r=r.filter(function(e,t,i){return i.indexOf(e)===t});this.drawerCandidates=r.map(G).filter(Boolean);this.drawer=null;for(const s of r)if(this.requestDrawer(s,{mainDrawer:!0,redrawImmediately:!1}))break;if(!this.drawer){m.console.error("No drawer could be created!");throw"Error with creating the selected drawer(s)"}this.drawer.setImageSmoothingEnabled(this.imageSmoothingEnabled);this.overlaysContainer=m.makeNeutralElement("div");this.canvas.appendChild(this.overlaysContainer);if(!this.drawer.canRotate()){if(this.rotateLeft){t=this.buttonGroup.buttons.indexOf(this.rotateLeft);this.buttonGroup.buttons.splice(t,1);this.buttonGroup.element.removeChild(this.rotateLeft.element)}if(this.rotateRight){t=this.buttonGroup.buttons.indexOf(this.rotateRight);this.buttonGroup.buttons.splice(t,1);this.buttonGroup.element.removeChild(this.rotateRight.element)}}this._addUpdatePixelDensityRatioEvent();"navigatorAutoResize"in this&&m.console.warn("navigatorAutoResize is deprecated, this value will be ignored.");this.showNavigator&&(this.navigator=new m.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}));this.sequenceMode&&this.bindSequenceControls();this.tileSources&&this.open(this.tileSources);for(t=0;te.viewer.world.requestInvalidate(t,i)))},close:function(){if(!u[this.hash])return this;this._opening=!1;this.navigator&&this.navigator.close();if(!this.preserveOverlays){this.clearOverlays();this.overlaysContainer.innerHTML=""}u[this.hash].animating=!1;this.world.removeAll();this.tileCache.clear();this.imageLoader.clear();this.raiseEvent("close");return this},destroy:function(){if(u[this.hash]){this.raiseEvent("before-destroy");this._removeUpdatePixelDensityRatioEvent();this.close();this.clearOverlays();this.overlaysContainer.innerHTML="";this._resizeObserver&&this._resizeObserver.disconnect();if(this.referenceStrip){this.referenceStrip.destroy();this.referenceStrip=null}if(null!==this._updateRequestId){m.cancelAnimationFrame(this._updateRequestId);this._updateRequestId=null}this.drawer&&this.drawer.destroy();if(this.navigator){this.navigator.destroy();u[this.navigator.hash]=null;delete u[this.navigator.hash];this.navigator=null}if(this.buttonGroup)this.buttonGroup.destroy();else if(this.customButtons)for(;this.customButtons.length;)this.customButtons.pop().destroy();this.paging&&this.paging.destroy();this.container&&this.container.parentNode===this.element&&this.element.removeChild(this.container);this.container.onsubmit=null;this.clearControls();this.innerTracker&&this.innerTracker.destroy();this.outerTracker&&this.outerTracker.destroy();u[this.hash]=null;delete u[this.hash];this.canvas=null;this.container=null;m._viewers.delete(this.element);this.element=null;this.raiseEvent("destroy");this.removeAllHandlers()}},isDestroyed(){return!u[this.hash]},requestDrawer(t,e){var i=(e=m.extend(!0,{mainDrawer:!0,redrawImmediately:!0,drawerOptions:null},e)).mainDrawer;var n=e.redrawImmediately;e=e.drawerOptions;const r=this.drawer;let o=null;if(t&&t.prototype instanceof m.DrawerBase){o=t;t="custom"}else"string"==typeof t&&(o=m.determineDrawer(t));o||m.console.warn("Unsupported drawer %s! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.",t);let s=!1;if(o)try{s=o.isSupported()}catch(e){m.console.warn("Error in %s isSupported(); treating this drawer as unsupported:",t,e&&e.message?e.message:e)}if(s){r&&i&&r.destroy();t=new o({viewer:this,viewport:this.viewport,element:this.canvas,debugGridColor:this.debugGridColor,options:e||this.drawerOptions[t]});if(i){this.drawer=t;n&&this.forceRedraw()}return t}return!1},isMouseNavEnabled:function(){return this.innerTracker.tracking},setMouseNavEnabled:function(e){this.innerTracker.setTracking(e);this.outerTracker.setTracking(e);this.raiseEvent("mouse-enabled",{enabled:e});return this},isKeyboardNavEnabled:function(){return this.keyboardNavEnabled},setKeyboardNavEnabled:function(e){this.keyboardNavEnabled=e;this.raiseEvent("keyboard-enabled",{enabled:e});return this},areControlsEnabled:function(){let t=this.controls.length;for(let e=0;e{if(this.collectionMode){this.world.arrange({immediately:e.options.collectionImmediately,rows:this.collectionRows,columns:this.collectionColumns,layout:this.collectionLayout,tileSize:this.collectionTileSize,tileMargin:this.collectionTileMargin});this.world.setAutoRefigureSizes(!0)}};const i=e=>{for(let e=0;e{l.tiledImage=e.item;l.originalSuccess=a;let t,i;for(;this._loadQueue.length;){t=this._loadQueue[0];var n=t.tiledImage;if(!n)break;this._loadQueue.splice(0,1);var r=n.source;if(t.options.replace){const s=t.options.replaceItem;var o=this.world.getIndexOfItem(s);-1!==o&&(t.options.index=o);!s._zombieCache&&s.source.equals(r)&&s.allowZombieCache(!0);this.world.removeItem(s)}this.collectionMode&&this.world.setAutoRefigureSizes(!1);if(this.navigator){i=m.extend({},t.options,{replace:!1,originalTiledImage:n,tileSource:r});this.navigator.addTiledImage(i)}this.world.addItem(n,{index:t.options.index});0===this._loadQueue.length&&h(t);1!==this.world.getItemCount()||this.preserveViewport||this.viewport.goHome(!0);t.originalSuccess&&t.originalSuccess({item:n});this.drawer&&this.drawer.tiledImageCreated(n)}};e.error=i;this.instantiateTiledImageClass(e)}},instantiateTiledImageClass:function(t){return this.instantiateTileSourceClass(t).then(e=>{e=new m.TiledImage({viewer:this,source:e.source,viewport:this.viewport,drawer:this.drawer,tileCache:this.tileCache,imageLoader:this.imageLoader,x:t.x,y:t.y,width:t.width,height:t.height,fitBounds:t.fitBounds,fitBoundsPlacement:t.fitBoundsPlacement,clip:t.clip,placeholderFillStyle:t.placeholderFillStyle,opacity:t.opacity,preload:t.preload,degrees:t.degrees,flipped:t.flipped,compositeOperation:t.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:t.crossOriginPolicy,ajaxWithCredentials:t.ajaxWithCredentials,loadTilesWithAjax:t.loadTilesWithAjax,ajaxHeaders:t.ajaxHeaders,debugMode:this.debugMode,subPixelRoundingForTransparency:this.subPixelRoundingForTransparency,callTileLoadedWithCachedData:this.callTileLoadedWithCachedData,originalDataType:t.originalDataType});t.success({item:e});return e}).catch(e=>{if(t.error){t.error(e);return e}throw e})},instantiateTileSourceClass(s){return new m.Promise((i,n)=>{void 0===s.placeholderFillStyle&&(s.placeholderFillStyle=this.placeholderFillStyle);void 0===s.opacity&&(s.opacity=this.opacity);void 0===s.preload&&(s.preload=this.preload);void 0===s.compositeOperation&&(s.compositeOperation=this.compositeOperation);void 0===s.crossOriginPolicy&&(s.crossOriginPolicy=(void 0!==s.tileSource.crossOriginPolicy?s.tileSource:this).crossOriginPolicy);void 0===s.ajaxWithCredentials&&(s.ajaxWithCredentials=this.ajaxWithCredentials);void 0===s.loadTilesWithAjax&&(s.loadTilesWithAjax=this.loadTilesWithAjax);m.isPlainObject(s.ajaxHeaders)||(s.ajaxHeaders={});let r=s.tileSource;if("string"===m.type(r))if(r.match(/^\s*<.*>\s*$/))r=m.parseXml(r);else if(r.match(/^\s*[{[].*[}\]]\s*$/))try{r=m.parseJSON(r)}catch(e){}function o(e,t){if(e.ready)i({source:e});else{e.addHandler("ready",function(e){i({source:e.tileSource})});e.addHandler("open-failed",function(e){n({message:e.message,source:t})})}}setTimeout(()=>{if("string"===m.type(r)){r=new m.TileSource({url:r,crossOriginPolicy:(void 0!==s.crossOriginPolicy?s:this).crossOriginPolicy,ajaxWithCredentials:this.ajaxWithCredentials,ajaxHeaders:m.extend({},this.ajaxHeaders,s.ajaxHeaders),splitHashDataForPost:this.splitHashDataForPost});o(r,r)}else if(m.isPlainObject(r)||r.nodeType){void 0!==r.crossOriginPolicy||void 0===s.crossOriginPolicy&&void 0===this.crossOriginPolicy||(r.crossOriginPolicy=(void 0!==s.crossOriginPolicy?s:this).crossOriginPolicy);void 0===r.ajaxWithCredentials&&(r.ajaxWithCredentials=this.ajaxWithCredentials);if(m.isFunction(r.getTileUrl)){const e=new m.TileSource(r);e.getTileUrl=r.getTileUrl;r.ready=!1;o(e,r)}else{const t=m.TileSource.determineType(this,r,null);if(t){const i=t.prototype.configure.apply(this,[r]);i.ready=!1;o(new t(i),r)}else n({message:"Unable to load TileSource",source:r,error:!0})}}else o(r,r)})})},addSimpleImage:function(e){m.console.assert(e,"[Viewer.addSimpleImage] options is required");m.console.assert(e.url,"[Viewer.addSimpleImage] options.url is required");const t=m.extend({},e,{tileSource:{type:"image",url:e.url}});delete t.url;this.addTiledImage(t)},addLayer:function(t){const i=this;m.console.error("[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead.");var e=m.extend({},t,{success:function(e){i.raiseEvent("add-layer",{options:t,drawer:e.item})},error:function(e){i.raiseEvent("add-layer-failed",e)}});this.addTiledImage(e);return this},getLayerAtLevel:function(e){m.console.error("[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead.");return this.world.getItemAt(e)},getLevelOfLayer:function(e){m.console.error("[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead.");return this.world.getIndexOfItem(e)},getLayersCount:function(){m.console.error("[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead.");return this.world.getItemCount()},setLayerLevel:function(e,t){m.console.error("[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead.");return this.world.setItemIndex(e,t)},removeLayer:function(e){m.console.error("[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead.");return this.world.removeItem(e)},forceRedraw:function(){u[this.hash].forceRedraw=!0;return this},forceResize:function(){u[this.hash].needsResize=!0;u[this.hash].forceResize=!0},bindSequenceControls:function(){var e=m.delegate(this,f);var t=m.delegate(this,v);var i=m.delegate(this,this.goToNextPage);var n=m.delegate(this,this.goToPreviousPage);var r=this.navImages;let o=!0;if(this.showSequenceControl){(this.previousButton||this.nextButton)&&(o=!1);this.previousButton=new m.Button({element:this.previousButton?m.getElement(this.previousButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.PreviousPage"),srcRest:M(this.prefixUrl,r.previous.REST),srcGroup:M(this.prefixUrl,r.previous.GROUP),srcHover:M(this.prefixUrl,r.previous.HOVER),srcDown:M(this.prefixUrl,r.previous.DOWN),onRelease:n,onFocus:e,onBlur:t});this.nextButton=new m.Button({element:this.nextButton?m.getElement(this.nextButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.NextPage"),srcRest:M(this.prefixUrl,r.next.REST),srcGroup:M(this.prefixUrl,r.next.GROUP),srcHover:M(this.prefixUrl,r.next.HOVER),srcDown:M(this.prefixUrl,r.next.DOWN),onRelease:i,onFocus:e,onBlur:t});this.navPrevNextWrap||this.previousButton.disable();this.tileSources&&this.tileSources.length||this.nextButton.disable();if(o){this.paging=new m.ButtonGroup({buttons:[this.previousButton,this.nextButton],clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.pagingControl=this.paging.element;this.toolbar?this.toolbar.addControl(this.pagingControl,{anchor:m.ControlAnchor.BOTTOM_RIGHT}):this.addControl(this.pagingControl,{anchor:this.sequenceControlAnchor||m.ControlAnchor.TOP_LEFT})}}return this},bindStandardControls:function(){var e=m.delegate(this,this.startZoomInAction);var t=m.delegate(this,this.endZoomAction);var i=m.delegate(this,this.singleZoomInAction);var n=m.delegate(this,this.startZoomOutAction);var r=m.delegate(this,this.singleZoomOutAction);var o=m.delegate(this,H);var s=m.delegate(this,N);var a=m.delegate(this,U);var l=m.delegate(this,W);var h=m.delegate(this,j);var c=m.delegate(this,f);var u=m.delegate(this,v);var d=this.navImages;const p=[];let g=!0;if(this.showNavigationControl){(this.zoomInButton||this.zoomOutButton||this.homeButton||this.fullPageButton||this.rotateLeftButton||this.rotateRightButton||this.flipButton)&&(g=!1);if(this.showZoomControl){p.push(this.zoomInButton=new m.Button({element:this.zoomInButton?m.getElement(this.zoomInButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomIn"),srcRest:M(this.prefixUrl,d.zoomIn.REST),srcGroup:M(this.prefixUrl,d.zoomIn.GROUP),srcHover:M(this.prefixUrl,d.zoomIn.HOVER),srcDown:M(this.prefixUrl,d.zoomIn.DOWN),onPress:e,onRelease:t,onClick:i,onEnter:e,onExit:t,onFocus:c,onBlur:u}));p.push(this.zoomOutButton=new m.Button({element:this.zoomOutButton?m.getElement(this.zoomOutButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomOut"),srcRest:M(this.prefixUrl,d.zoomOut.REST),srcGroup:M(this.prefixUrl,d.zoomOut.GROUP),srcHover:M(this.prefixUrl,d.zoomOut.HOVER),srcDown:M(this.prefixUrl,d.zoomOut.DOWN),onPress:n,onRelease:t,onClick:r,onEnter:n,onExit:t,onFocus:c,onBlur:u}))}this.showHomeControl&&p.push(this.homeButton=new m.Button({element:this.homeButton?m.getElement(this.homeButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Home"),srcRest:M(this.prefixUrl,d.home.REST),srcGroup:M(this.prefixUrl,d.home.GROUP),srcHover:M(this.prefixUrl,d.home.HOVER),srcDown:M(this.prefixUrl,d.home.DOWN),onRelease:o,onFocus:c,onBlur:u}));this.showFullPageControl&&p.push(this.fullPageButton=new m.Button({element:this.fullPageButton?m.getElement(this.fullPageButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.FullPage"),srcRest:M(this.prefixUrl,d.fullpage.REST),srcGroup:M(this.prefixUrl,d.fullpage.GROUP),srcHover:M(this.prefixUrl,d.fullpage.HOVER),srcDown:M(this.prefixUrl,d.fullpage.DOWN),onRelease:s,onFocus:c,onBlur:u}));if(this.showRotationControl){p.push(this.rotateLeftButton=new m.Button({element:this.rotateLeftButton?m.getElement(this.rotateLeftButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateLeft"),srcRest:M(this.prefixUrl,d.rotateleft.REST),srcGroup:M(this.prefixUrl,d.rotateleft.GROUP),srcHover:M(this.prefixUrl,d.rotateleft.HOVER),srcDown:M(this.prefixUrl,d.rotateleft.DOWN),onRelease:a,onFocus:c,onBlur:u}));p.push(this.rotateRightButton=new m.Button({element:this.rotateRightButton?m.getElement(this.rotateRightButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateRight"),srcRest:M(this.prefixUrl,d.rotateright.REST),srcGroup:M(this.prefixUrl,d.rotateright.GROUP),srcHover:M(this.prefixUrl,d.rotateright.HOVER),srcDown:M(this.prefixUrl,d.rotateright.DOWN),onRelease:l,onFocus:c,onBlur:u}))}this.showFlipControl&&p.push(this.flipButton=new m.Button({element:this.flipButton?m.getElement(this.flipButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Flip"),srcRest:M(this.prefixUrl,d.flip.REST),srcGroup:M(this.prefixUrl,d.flip.GROUP),srcHover:M(this.prefixUrl,d.flip.HOVER),srcDown:M(this.prefixUrl,d.flip.DOWN),onRelease:h,onFocus:c,onBlur:u}));if(g){this.buttonGroup=new m.ButtonGroup({buttons:p,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.navControl=this.buttonGroup.element;this.addHandler("open",m.delegate(this,z));(this.toolbar||this).addControl(this.navControl,{anchor:this.navigationControlAnchor||m.ControlAnchor.TOP_LEFT})}else this.customButtons=p}return this},currentPage:function(){return this._sequenceIndex},goToPage:function(e){if(this.tileSources&&0<=e&&e=this.tileSources.length&&(e=0);this.goToPage(e)},isAnimating:function(){return u[this.hash].animating},startZoomInAction:function(){u[this.hash].lastZoomTime=m.now();u[this.hash].zoomFactor=this.zoomPerSecond;u[this.hash].zooming=!0;o(this)},startZoomOutAction:function(){u[this.hash].lastZoomTime=m.now();u[this.hash].zoomFactor=1/this.zoomPerSecond;u[this.hash].zooming=!0;o(this)},endZoomAction:function(){u[this.hash].zooming=!1},singleZoomInAction:function(){if(this.viewport){u[this.hash].zooming=!1;this.viewport.zoomBy(+this.zoomPerClick);this.viewport.applyConstraints()}},singleZoomOutAction:function(){if(this.viewport){u[this.hash].zooming=!1;this.viewport.zoomBy(1/this.zoomPerClick);this.viewport.applyConstraints()}}});function l(e){e=m.getElement(e);return new m.Point(0===e.clientWidth?1:e.clientWidth,0===e.clientHeight?1:e.clientHeight)}function h(i,n){if(n instanceof m.Overlay)return n;let e=null;if(n.element)e=m.getElement(n.element);else{var t=n.id||"openseadragon-overlay-"+Math.floor(1e7*Math.random());e=m.getElement(n.id);if(!e){e=document.createElement("a");e.href="#/overlay/"+t}e.id=t;m.addClass(e,n.className||"openseadragon-overlay")}let r=n.location;let o=n.width;let s=n.height;if(!r){let e=n.x;let t=n.y;if(void 0!==n.px){i=i.viewport.imageToViewportRectangle(new m.Rect(n.px,n.py,o||0,s||0));e=i.x;t=i.y;o=void 0!==o?i.width:void 0;s=void 0!==s?i.height:void 0}r=new m.Point(e,t)}let a=n.placement;a&&"string"===m.type(a)&&(a=m.Placement[n.placement.toUpperCase()]);return new m.Overlay({element:e,location:r,placement:a,onDraw:n.onDraw,checkResize:n.checkResize,width:o,height:s,rotationMode:n.rotationMode})}function s(t,i){for(let e=t.length-1;0<=e;e--)if(t[e].element===i)return e;return-1}function c(e,t){return m.requestAnimationFrame(function(){t(e)})}function n(e){m.requestAnimationFrame(function(){!function(t){if(t.controlsShouldFade){var i=1-(m.now()-t.controlsFadeBeginTime)/t.controlsFadeLength;i=Math.min(1,i);i=Math.max(0,i);for(let e=t.controls.length-1;0<=e;e--)t.controls[e].autoFade&&t.controls[e].setOpacity(i);0{t=i(e,t);if(t&&this._activeActions[t]){this._activeActions[t]=!1;this._navActionFrames[t]=n.flickMinSpeed){let e=0;this.panHorizontal&&(e=n.flickMomentum*i.speed*Math.cos(i.direction));let t=0;this.panVertical&&(t=n.flickMomentum*i.speed*Math.sin(i.direction));i=this.viewport.pixelFromPoint(this.viewport.getCenter(!0));i=this.viewport.pointFromPixel(new m.Point(i.x-e,i.y-t));this.viewport.panTo(i,!1)}this.viewport.applyConstraints()}n.dblClickDragToZoom&&!0===u[this.hash].draggingToZoom&&(u[this.hash].draggingToZoom=!1)}function E(e){this.raiseEvent("canvas-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function C(e){this.raiseEvent("canvas-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function b(e){this.raiseEvent("canvas-press",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,insideElementPressed:e.insideElementPressed,insideElementReleased:e.insideElementReleased,originalEvent:e.originalEvent});if(this.gestureSettingsByDeviceType(e.pointerType).dblClickDragToZoom){var t=u[this.hash].lastClickTime;e=m.now();if(null!==t){e-tthis.minScrollDeltaTime){this._lastScrollTime=n;t={tracker:e.eventSource,position:e.position,scroll:e.scroll,shift:e.shift,originalEvent:e.originalEvent,preventDefaultAction:!1,preventDefault:!0};this.raiseEvent("canvas-scroll",t);if(!t.preventDefaultAction&&this.viewport){this.viewport.flipped&&(e.position.x=this.viewport.getContainerSize().x-e.position.x);if((i=this.gestureSettingsByDeviceType(e.pointerType)).scrollToZoom){n=Math.pow(this.zoomPerScroll,e.scroll);this.viewport.zoomBy(n,i.zoomToRefPoint?this.viewport.pointFromPixel(e.position,!0):null);this.viewport.applyConstraints()}}e.preventDefault=t.preventDefault}else e.preventDefault=!0}function O(e){u[this.hash].mouseInside=!0;r(this);this.raiseEvent("container-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function k(e){if(e.pointers<1){u[this.hash].mouseInside=!1;u[this.hash].animating||d(this)}this.raiseEvent("container-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function B(e){!function(i){!function(i){for(const e in i._activeActions)if(i._activeActions[e]||i._navActionVirtuallyHeld[e]){i._navActionFrames[e]++;i._navActionFrames[e]>=i._minNavActionFrames&&(i._navActionVirtuallyHeld[e]=!1)}function n(e){return i._activeActions[e]||i._navActionVirtuallyHeld[e]}var r=i.pixelsPerArrowPress/10;r=i.viewport.deltaPointsFromPixels(new OpenSeadragon.Point(r,r));if(n("zoomIn")){i.viewport.zoomBy(1.01,null,!0);i.viewport.applyConstraints()}else if(n("zoomOut")){i.viewport.zoomBy(.99,null,!0);i.viewport.applyConstraints()}else{let e=0;let t=0;if(!i.preventVerticalPan){n("panUp")&&(t-=r.y);n("panDown")&&(t+=r.y)}if(!i.preventHorizontalPan){n("panLeft")&&(e-=r.x);n("panRight")&&(e+=r.x)}if(0!==e||0!==t){i.viewport.panBy(new OpenSeadragon.Point(e,t),!0);i.viewport.applyConstraints()}}}(i);if(!i._opening&&u[i.hash]){let t=!1;if(i.autoResize||u[i.hash].forceResize){let e;if(i._autoResizePolling){e=l(i.container);var n=u[i.hash].prevContainerSize;e.equals(n)||(u[i.hash].needsResize=!0)}if(u[i.hash].needsResize){!function(e,t){const i=e.viewport;var n=i.getZoom();var r=i.getCenter();i.resize(t,e.preserveImageSizeOnResize);i.panTo(r,!0);let o;if(e.preserveImageSizeOnResize)o=u[e.hash].prevContainerSize.x/t.x;else{var s=new m.Point(0,0);r=new m.Point(u[e.hash].prevContainerSize.x,u[e.hash].prevContainerSize.y).distanceTo(s);s=new m.Point(t.x,t.y).distanceTo(s);o=s/r*u[e.hash].prevContainerSize.x/t.x}i.zoomTo(n*o,null,!0);u[e.hash].prevContainerSize=t;u[e.hash].forceRedraw=!0;u[e.hash].needsResize=!1;u[e.hash].forceResize=!1}(i,e||l(i.container));t=!0}}n=i.viewport.update()||t;let e=i.world.update(n)||n;n&&i.raiseEvent("viewport-change");i.referenceStrip&&(e=i.referenceStrip.update(i.viewport)||e);n=u[i.hash].animating;if(!n&&e){i.raiseEvent("animation-start");r(i)}n=n&&!e;n&&(u[i.hash].animating=!1);if(e||n||u[i.hash].forceRedraw||i.world.needsDraw()){!function(e){e.imageLoader.clear();e.world.draw();e.raiseEvent("update-viewport",{})}(i);i._drawOverlays();i.navigator&&i.navigator.update(i.viewport);u[i.hash].forceRedraw=!1;e&&i.raiseEvent("animation")}if(n){i.raiseEvent("animation-finish");u[i.hash].mouseInside||d(i)}u[i.hash].animating=e}}(e);e.isOpen()?e._updateRequestId=c(e,B):e._updateRequestId=!1}function M(e,t){return e?e+t:t}function o(e){m.requestAnimationFrame(m.delegate(e,t))}function t(){if(u[this.hash].zooming&&this.viewport){var e=m.now();var t=e-u[this.hash].lastZoomTime;t=Math.pow(u[this.hash].zoomFactor,t/1e3);this.viewport.zoomBy(t);this.viewport.applyConstraints();u[this.hash].lastZoomTime=e;o(this)}}function z(){if(this.buttonGroup){this.buttonGroup.emulateEnter();this.buttonGroup.emulateLeave()}}function H(){this.viewport&&this.viewport.goHome()}function N(){this.isFullPage()&&!m.isFullScreen()?this.setFullPage(!1):this.setFullScreen(!this.isFullPage());this.buttonGroup&&this.buttonGroup.emulateLeave();this.fullPageButton.element.focus();this.viewport&&this.viewport.applyConstraints()}function U(){if(this.viewport){let e=this.viewport.getRotation();this.viewport.flipped?e+=this.rotationIncrement:e-=this.rotationIncrement;this.viewport.setRotation(e)}}function W(){if(this.viewport){let e=this.viewport.getRotation();this.viewport.flipped?e-=this.rotationIncrement:e+=this.rotationIncrement;this.viewport.setRotation(e)}}function j(){this.viewport.toggleFlip()}function G(e){if("string"==typeof e)return e;const t=e&&e.prototype;return t&&t instanceof OpenSeadragon.DrawerBase&&m.isFunction(t.getType)?t.getType.call(e):void 0}function V(){var e=window.matchMedia("(pointer: coarse)").matches;return/iPad|iPhone|iPod|Mac/.test(navigator.userAgent)&&e?["canvas"]:["webgl","canvas"]}m.determineDrawer=function(e){"auto"===e&&(e=V()[0]);for(const i in OpenSeadragon){var t=OpenSeadragon[i];const n=t.prototype;if(n&&n instanceof OpenSeadragon.DrawerBase&&m.isFunction(n.getType)&&n.getType.call(t)===e)return t}return null}}(OpenSeadragon);!function(h){h.Navigator=function(i){const e=i.viewer;const n=this;var t;if(i.element||i.id){if(i.element){i.id&&h.console.warn("Given option.id for Navigator was ignored since option.element was provided and is being used instead.");i.element.id?i.id=i.element.id:i.id="navigator-"+h.now();this.element=i.element}else this.element=document.getElementById(i.id);i.controlOptions={anchor:h.ControlAnchor.NONE,attachToViewer:!1,autoFade:!1}}else{i.id="navigator-"+h.now();this.element=h.makeNeutralElement("div");i.controlOptions={anchor:h.ControlAnchor.TOP_RIGHT,attachToViewer:!0,autoFade:i.autoFade};if(i.position)if("BOTTOM_RIGHT"===i.position)i.controlOptions.anchor=h.ControlAnchor.BOTTOM_RIGHT;else if("BOTTOM_LEFT"===i.position)i.controlOptions.anchor=h.ControlAnchor.BOTTOM_LEFT;else if("TOP_RIGHT"===i.position)i.controlOptions.anchor=h.ControlAnchor.TOP_RIGHT;else if("TOP_LEFT"===i.position)i.controlOptions.anchor=h.ControlAnchor.TOP_LEFT;else if("ABSOLUTE"===i.position){i.controlOptions.anchor=h.ControlAnchor.ABSOLUTE;i.controlOptions.top=i.top;i.controlOptions.left=i.left;i.controlOptions.height=i.height;i.controlOptions.width=i.width}}this.element.id=i.id;this.element.className+=" navigator";(i=h.extend(!0,{sizeRatio:h.DEFAULT_SETTINGS.navigatorSizeRatio},i,{element:this.element,tabIndex:-1,showNavigator:!1,mouseNavEnabled:!1,showNavigationControl:!1,showSequenceControl:!1,immediateRender:!0,blendTime:0,animationTime:i.animationTime,autoResize:!1,minZoomImageRatio:1,background:i.background,opacity:i.opacity,borderColor:i.borderColor,displayRegionColor:i.displayRegionColor})).minPixelRatio=this.minPixelRatio=e.minPixelRatio;h.setElementTouchActionNone(this.element);this.borderWidth=2;this.fudge=new h.Point(1,1);this.totalBorderWidths=new h.Point(2*this.borderWidth,2*this.borderWidth).minus(this.fudge);i.controlOptions.anchor!==h.ControlAnchor.NONE&&function(e,t){e.margin="0px";e.border=t+"px solid "+i.borderColor;e.padding="0px";e.background=i.background;e.opacity=i.opacity;e.overflow="hidden"}(this.element.style,this.borderWidth);this.displayRegion=h.makeNeutralElement("div");this.displayRegion.id=this.element.id+"-displayregion";this.displayRegion.className="displayregion";!function(e,t){e.position="relative";e.top="0px";e.left="0px";e.fontSize="0px";e.overflow="hidden";e.border=t+"px solid "+i.displayRegionColor;e.margin="0px";e.padding="0px";e.background="transparent";e.float="left";e.cssFloat="left";e.zIndex=999999999;e.cursor="default";e.boxSizing="content-box"}(this.displayRegion.style,this.borderWidth);h.setElementPointerEventsNone(this.displayRegion);h.setElementTouchActionNone(this.displayRegion);this.displayRegionContainer=h.makeNeutralElement("div");this.displayRegionContainer.id=this.element.id+"-displayregioncontainer";this.displayRegionContainer.className="displayregioncontainer";this.displayRegionContainer.style.width="100%";this.displayRegionContainer.style.height="100%";h.setElementPointerEventsNone(this.displayRegionContainer);h.setElementTouchActionNone(this.displayRegionContainer);e.addControl(this.element,i.controlOptions);this._resizeWithViewer=i.controlOptions.anchor!==h.ControlAnchor.ABSOLUTE&&i.controlOptions.anchor!==h.ControlAnchor.NONE;if(i.width&&i.height){this.setWidth(i.width);this.setHeight(i.height)}else if(this._resizeWithViewer){t=h.getElementSize(e.element);this.element.style.height=Math.round(t.y*i.sizeRatio)+"px";this.element.style.width=Math.round(t.x*i.sizeRatio)+"px";this.oldViewerSize=t;t=h.getElementSize(this.element);this.elementArea=t.x*t.y}this.oldContainerSize=new h.Point(0,0);h.Viewer.apply(this,[i]);this.displayRegionContainer.appendChild(this.displayRegion);this.element.getElementsByTagName("div")[0].appendChild(this.displayRegionContainer);function r(e,t){c(n.displayRegionContainer,e);c(n.displayRegion,-e);n.viewport.setRotation(e,t)}if(i.navigatorRotate){r(i.viewer.viewport?i.viewer.viewport.getRotation():i.viewer.degrees||0,!0);i.viewer.addHandler("rotate",function(e){r(e.degrees,e.immediately)})}this.innerTracker.destroy();this.innerTracker=new h.MouseTracker({userData:"Navigator.innerTracker",element:this.element,dragHandler:h.delegate(this,s),clickHandler:h.delegate(this,o),releaseHandler:h.delegate(this,a),scrollHandler:h.delegate(this,l),preProcessEventHandler:function(e){"wheel"===e.eventType&&(e.preventDefault=!0)}});this.outerTracker.userData="Navigator.outerTracker";h.setElementPointerEventsNone(this.canvas);h.setElementPointerEventsNone(this.container);this.addHandler("reset-size",function(){n.viewport&&n.viewport.goHome(!0)});e.world.addHandler("item-index-change",function(t){window.setTimeout(function(){var e=n.world.getItemAt(t.previousIndex);n.world.setItemIndex(e,t.newIndex)},1)});e.world.addHandler("remove-item",function(e){e=e.item;e=n._getMatchingItem(e);e&&n.world.removeItem(e)});this.update(e.viewport)};h.extend(h.Navigator.prototype,h.EventSource.prototype,h.Viewer.prototype,{updateSize:function(){if(this.viewport){const e=new h.Point(0===this.container.clientWidth?1:this.container.clientWidth,0===this.container.clientHeight?1:this.container.clientHeight);if(!e.equals(this.oldContainerSize)){this.viewport.resize(e,!0);this.viewport.goHome(!0);this.oldContainerSize=e;this.world.update();this.world.draw();this.update(this.viewer.viewport)}}},setWidth:function(e){this.width=e;this.element.style.width="number"==typeof e?e+"px":e;this._resizeWithViewer=!1;this.updateSize()},setHeight:function(e){this.height=e;this.element.style.height="number"==typeof e?e+"px":e;this._resizeWithViewer=!1;this.updateSize()},setFlip:function(e){this.viewport.setFlip(e);this.setDisplayTransform(this.viewer.viewport.getFlip()?"scale(-1,1)":"scale(1,1)");return this},setDisplayTransform:function(e){i(this.canvas,e);i(this.element,e)},update:function(e){let t;let i;let n;let r;let o;e=e||this.viewer.viewport;t=h.getElementSize(this.viewer.element);if(this._resizeWithViewer&&t.x&&t.y&&!t.equals(this.oldViewerSize)){this.oldViewerSize=t;if(this.maintainSizeRatio||!this.elementArea){i=t.x*this.sizeRatio;n=t.y*this.sizeRatio}else{i=Math.sqrt(this.elementArea*(t.x/t.y));n=this.elementArea/i}this.element.style.width=Math.round(i)+"px";this.element.style.height=Math.round(n)+"px";this.elementArea||(this.elementArea=i*n);this.updateSize()}if(e&&this.viewport){r=e.getBoundsNoRotate(!0);o=this.viewport.pixelFromPointNoRotate(r.getTopLeft(),!1);a=this.viewport.pixelFromPointNoRotate(r.getBottomRight(),!1).minus(this.totalBorderWidths);if(!this.navigatorRotate){var s=e.getRotation(!0);c(this.displayRegion,-s)}const l=this.displayRegion.style;l.display=this.world.getItemCount()?"block":"none";l.top=o.y.toFixed(2)+"px";l.left=o.x.toFixed(2)+"px";s=a.x-o.x;var a=a.y-o.y;l.width=Math.round(Math.max(s,0))+"px";l.height=Math.round(Math.max(a,0))+"px"}},addTiledImage:function(e){const n=this;const r=e.originalTiledImage;delete e.original;e=h.extend({},e,{success:function(e){const t=e.item;t._originalForNavigator=r;n._matchBounds(t,r,!0);n._matchOpacity(t,r);n._matchCompositeOperation(t,r);function i(){n._matchBounds(t,r)}r.addHandler("bounds-change",i);r.addHandler("clip-change",i);r.addHandler("opacity-change",function(){n._matchOpacity(t,r)});r.addHandler("composite-operation-change",function(){n._matchCompositeOperation(t,r)})}});return h.Viewer.prototype.addTiledImage.apply(this,[e])},destroy:function(){return h.Viewer.prototype.destroy.apply(this)},_getMatchingItem:function(t){var i=this.world.getItemCount();for(let e=0;e{const t=e.tileSource;this.ready=!0;this.aspectRatio=t.width&&t.height?t.width/t.height:1;this.dimensions=new c.Point(t.width,t.height);if(t.tileSize){this._tileWidth=this._tileHeight=t.tileSize;delete this.tileSize}else{if(t.tileWidth){this._tileWidth=t.tileWidth;delete this.tileWidth}else this._tileWidth=0;if(t.tileHeight){this._tileHeight=t.tileHeight;delete this.tileHeight}else this._tileHeight=0}this.tileOverlap=t.tileOverlap||0;this.minLevel=t.minLevel||0;this.maxLevel=void 0!==t.maxLevel&&null!==t.maxLevel?t.maxLevel:t.width&&t.height?Math.ceil(Math.log(Math.max(t.width,t.height))/Math.log(2)):0;t.success&&c.isFunction(t.success)&&t.success(this)},null,1/0);if("string"===c.type(e)){this.url=e;e=void 0}else c.extend(!0,this,e);if(this.url&&!this.ready){this.aspectRatio=1;this.dimensions=new c.Point(10,10);this._tileWidth=0;this._tileHeight=0;this.tileOverlap=0;this.minLevel=0;this.maxLevel=0;this.ready=!1;this._uniqueIdentifier=this.url;setTimeout(()=>this.getImageInfo(this.url))}else{this._uniqueIdentifier=Math.floor(1e10*Math.random()).toString(36);this.ready||void 0===this.ready?this.raiseEvent("ready",{tileSource:this}):setTimeout(()=>this.raiseEvent("ready",{tileSource:this}))}return this};c.TileSource.prototype={getTileSize:function(e){c.console.error("[TileSource.getTileSize] is deprecated. Use TileSource.getTileWidth() and TileSource.getTileHeight() instead");return this._tileWidth},getTileWidth:function(e){return this._tileWidth||this.getTileSize(e)},getTileHeight:function(e){return this._tileHeight||this.getTileSize(e)},setMaxLevel:function(e){this.maxLevel=e;this._memoizeLevelScale()},getLevelScale:function(e){this._memoizeLevelScale();return this.getLevelScale(e)},_memoizeLevelScale:function(){const t={};let e;for(e=0;e<=this.maxLevel;e++)t[e]=1/Math.pow(2,this.maxLevel-e);this.getLevelScale=function(e){return t[e]}},getNumTiles:function(e){var t=this.getLevelScale(e);var i=Math.ceil(t*this.dimensions.x/this.getTileWidth(e));e=Math.ceil(t*this.dimensions.y/this.getTileHeight(e));return new c.Point(i,e)},getPixelRatio:function(e){var t=this.dimensions.times(this.getLevelScale(e));e=1/t.x*c.pixelDensityRatio;t=1/t.y*c.pixelDensityRatio;return new c.Point(e,t)},getClosestLevel:function(){let e;var t;for(e=this.minLevel+1;e<=this.maxLevel&&!(1<(t=this.getNumTiles(e)).x||1=1/this.aspectRatio-1e-15&&(o=this.getNumTiles(e).y-1);return new c.Point(r,o)},getTileBounds:function(e,t,i,n){var r=this.dimensions.times(this.getLevelScale(e));var o=this.getTileWidth(e);var s=this.getTileHeight(e);var a=0===t?0:o*t-this.tileOverlap;e=0===i?0:s*i-this.tileOverlap;t=o+(0===t?1:2)*this.tileOverlap;s+=(0===i?1:2)*this.tileOverlap;i=1/r.x;t=Math.min(t,r.x-a);s=Math.min(s,r.y-e);return n?new c.Rect(0,0,t,s):new c.Rect(a*i,e*i,t*i,s*i)},getImageInfo:function(r){const o=this;let t;let i;let n;let e;let s;var a;if(r){e=r.split("/");s=e[e.length-1];-1<(a=s.lastIndexOf("."))&&(e[e.length-1]=s.slice(0,a))}let l=null;if(this.splitHashDataForPost){var h=r.indexOf("#");if(-1!==h){l=r.substring(h+1);r=r.substr(0,h)}}t=function(e){"string"==typeof e&&(e=c.parseXml(e));const t=c.TileSource.determineType(o,e,r);if(t){n=t.prototype.configure.apply(o,[e,r,l]);void 0===n.ajaxWithCredentials&&(n.ajaxWithCredentials=o.ajaxWithCredentials);n.ready=!0;i=new t(n);o.ready=!0;o.raiseEvent("ready",{tileSource:i})}else o.raiseEvent("open-failed",{message:"Unable to load TileSource",source:r})};if(r.match(/\.js$/)){h=r.split("/").pop().replace(".js","");c.jsonp({url:r,async:!1,callbackName:h,callback:t})}else c.makeAjaxRequest({url:r,postData:l,withCredentials:this.ajaxWithCredentials,headers:this.ajaxHeaders,success:function(e){e=function(t){const i=t.responseText;var e=t.status;var n;let r;{if(!t)throw new Error(c.getString("Errors.Security"));if(200!==t.status&&0!==t.status){e=t.status;n=404===e?"Not Found":t.statusText;throw new Error(c.getString("Errors.Status",e,n))}}if(i.match(/^\s*<.*/))try{r=t.responseXML&&t.responseXML.documentElement?t.responseXML:c.parseXml(i)}catch(e){r=t.responseText}else if(i.match(/\s*[{[].*/))try{r=c.parseJSON(i)}catch(e){r=i}else r=i;return r}(e);t(e)},error:function(e,i){let n;try{n="HTTP "+e.status+" attempting to load TileSource: "+r}catch(e){let t;t=void 0!==i&&i.toString?i.toString():"Unknown error";n=t+" attempting to load TileSource: "+r}c.console.error(n);o.raiseEvent("open-failed",{message:n,source:r,postData:l})}})},supports:function(e,t){return!1},equals:function(e){return this===e},batchEnabled(){return!1},batchCompatible(e){return!1},batchMaxJobs(){return-1},batchTimeout(){return 5},configure:function(e,t,i){throw new Error("Method not implemented.")},destroy:function(e){},getTileUrl:function(e,t,i){throw new Error("Method not implemented.")},getTilePostData:function(e,t,i){return null},getTileAjaxHeaders:function(e,t,i){return{}},getTileHashKey:function(e,t,i,n,r,o){function s(e){return r?e+"+"+JSON.stringify(r):e}return s("string"!=typeof n?this._uniqueIdentifier+":"+e+"/"+t+"_"+i:n)},tileExists:function(e,t,i){var n=this.getNumTiles(e);return e>=this.minLevel&&e<=this.maxLevel&&0<=t&&0<=i&&tthis.maxLevel)return!1;if(!r||!r.length)return!0;for(let e=r.length-1;0<=e;e--){var h=r[e];if(!(th.maxLevel)){l=this.getLevelScale(t);o=h.x*l;s=h.y*l;a=o+h.width*l;l=s+h.height*l;o=Math.floor(o/this._tileWidth);s=Math.floor(s/this._tileWidth);a=Math.ceil(a/this._tileWidth);l=Math.ceil(l/this._tileWidth);if(o<=i&&ie.width-t.width);if(a[i-1].width=this.minLevel&&t<=this.maxLevel&&(e=this.levels[t].width/this.levels[this.maxLevel].width);return e}return l.TileSource.prototype.getLevelScale.call(this,t)},getNumTiles:function(e){if(this.emulateLegacyImagePyramid)return this.getLevelScale(e)?new l.Point(1,1):new l.Point(0,0);if(this.levelSizes){var t=this.levelSizes[e];var i=Math.ceil(t.width/this.getTileWidth(e));t=Math.ceil(t.height/this.getTileHeight(e));return new l.Point(i,t)}return l.TileSource.prototype.getNumTiles.call(this,e)},getTileAtPoint:function(i,n){if(this.emulateLegacyImagePyramid)return new l.Point(0,0);if(this.levelSizes){var r=0<=n.x&&n.x<=1&&0<=n.y&&n.y<=1/this.aspectRatio;l.console.assert(r,"[TileSource.getTileAtPoint] must be called with a valid point.");var o=this.levelSizes[i].width;r=n.x*o;o=n.y*o;let e=Math.floor(r/this.getTileWidth(i));let t=Math.floor(o/this.getTileHeight(i));1<=n.x&&(e=this.getNumTiles(i).x-1);n.y>=1/this.aspectRatio-1e-15&&(t=this.getNumTiles(i).y-1);return new l.Point(e,t)}return l.TileSource.prototype.getTileAtPoint.call(this,i,n)},getTileUrl:function(t,e,i){if(this.emulateLegacyImagePyramid){let e=null;0=this.minLevel&&t<=this.maxLevel&&(e=this.levels[t].url);return e}var n=Math.pow(.5,this.maxLevel-t);let r;let o;let s;let a;let l;let h;let c;let u;var d;var p;let g;if(this.levelSizes){r=this.levelSizes[t].width;o=this.levelSizes[t].height}else{r=Math.ceil(this.width*n);o=Math.ceil(this.height*n)}d=this.getTileWidth(t);p=this.getTileHeight(t);t=Math.round(d/n);n=Math.round(p/n);g=1===this.version?"native."+this.tileFormat:"default."+this.tileFormat;if(r({width:Math.ceil(e.x_tiles*this._tileWidth),height:Math.ceil(e.y_tiles*this._tileHeight),xTiles:Math.ceil(e.x_tiles),yTiles:Math.ceil(e.y_tiles)}));this.levelScales=t.map(e=>e.scale/n);this.minLevel=0;this.maxLevel=Math.ceil(this.levelSizes.length-1)},getImageInfo:function(n){const r=this;o.makeAjaxRequest({url:n,type:"GET",async:!0,success:function(e){try{var t=JSON.parse(e.responseText);r.parseMetadata(t);r.ready=!0;r.raiseEvent("ready",{tileSource:r})}catch(e){t="IrisTileSource: Error parsing metadata: "+e.message;o.console.error(t);r.raiseEvent("open-failed",{message:t,source:n})}},error:function(e,t){var i="IrisTileSource: Unable to get metadata from "+n;o.console.error(i);r.raiseEvent("open-failed",{message:i,source:n})}})},getNumTiles:function(e){return ethis.maxLevel||!this.levelSizes[e]?new o.Point(0,0):new o.Point(Math.ceil(this.levelSizes[e].xTiles),Math.ceil(this.levelSizes[e].yTiles))},getTileUrl:function(e,t,i){t=i*this.levelSizes[e].xTiles+t;return`${this.serverUrl}/slides/${this.slideId}/layers/${e}/tiles/`+t},getLevelScale:function(e){return this.levelScales[e]},configure:function(e){return e}});o.extend(!0,o.IrisTileSource.prototype,o.EventSource.prototype)}(OpenSeadragon);!function(s){s.OsmTileSource=function(e,t,i,n,r){let o;o=s.isPlainObject(e)?e:{width:e,height:t,tileSize:i,tileOverlap:n,tilesUrl:r};if(!o.width||!o.height){o.width=67108864;o.height=67108864}if(!o.tileSize){o.tileSize=256;o.tileOverlap=0}o.tilesUrl||(o.tilesUrl="http://tile.openstreetmap.org/");o.minLevel=8;s.TileSource.apply(this,[o])};s.extend(s.OsmTileSource.prototype,s.TileSource.prototype,{supports:function(e,t){return e.type&&"openstreetmaps"===e.type},configure:function(e,t,i){return e},getTileUrl:function(e,t,i){return this.tilesUrl+(e-8)+"/"+t+"/"+i+".png"},equals:function(e){return e&&this.tilesUrl===e.tilesUrl}})}(OpenSeadragon);!function(h){h.TmsTileSource=function(e,t,i,n,r){let o;o=h.isPlainObject(e)?e:{width:e,height:t,tileSize:i,tileOverlap:n,tilesUrl:r};var s=256*Math.ceil(o.width/256);var a=256*Math.ceil(o.height/256);let l;l=ae.tileSize||parseInt(t.y,10)>e.tileSize;){t.x=Math.floor(t.x/2);t.y=Math.floor(t.y/2);e.imageSizes.push({x:t.x,y:t.y});e.gridSize.push(this._getGridSize(t.x,t.y,e.tileSize))}e.imageSizes.reverse();e.gridSize.reverse();e.minLevel=0;e.maxLevel=e.gridSize.length-1;i.TileSource.apply(this,[e])};i.extend(i.ZoomifyTileSource.prototype,i.TileSource.prototype,{_getGridSize:function(e,t,i){return{x:Math.ceil(e/i),y:Math.ceil(t/i)}},_calculateAbsoluteTileNumber:function(t,e,i){let n=0;let r={};for(let e=0;e");return i.sort(function(e,t){return e.height-t.height})}(t.levels);if(0=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t},getNumTiles:function(e){return this.getLevelScale(e)?new s.Point(1,1):new s.Point(0,0)},getTileUrl:function(e,t,i){let n=null;0=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n},equals:function(t){if(!t||!t.levels||t.levels.length!==this.levels.length)return!1;for(let e=this.minLevel;e<=this.maxLevel;e++)if(this.levels[e].url!==t.levels[e].url)return!1;return!0}})}(OpenSeadragon);!function(r){r.ImageTileSource=class extends r.TileSource{constructor(e){super(r.extend({buildPyramid:!0,crossOriginPolicy:!1,ajaxWithCredentials:!1},e))}supports(e,t){return e.type&&"image"===e.type}configure(e,t,i){return e}getImageInfo(e){const t=new Image,i=this;this.crossOriginPolicy&&(t.crossOrigin=this.crossOriginPolicy);r.addEvent(t,"load",function(){i.width=t.naturalWidth;i.height=t.naturalHeight;i.tileWidth=i.width;i.tileHeight=i.height;i.tileOverlap=0;i.minLevel=0;i.image=t;i.levels=i._buildLevels(t);i.maxLevel=i.levels.length-1;i.raiseEvent("ready",{tileSource:i})});r.addEvent(t,"error",function(){i.image=null;i.raiseEvent("open-failed",{message:"Error loading image at "+e,source:e})});t.src=e}getLevelScale(e){let t=NaN;e>=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t}getNumTiles(e){return this.getLevelScale(e)?new r.Point(1,1):new r.Point(0,0)}getTileUrl(e,t,i){return e===this.maxLevel?this.url:this.url+`?l=${e}&x=${t}&y=`+i}equals(e){return this.url===e.url}getTilePostData(e,t,i){return{level:e,x:t,y:i}}getContext2D(e,t,i){r.console.error("Using [TiledImage.getContext2D] (for plain images only) is deprecated. Use overridden downloadTileStart (https://openseadragon.github.io/examples/advanced-data-model/) instead.");return this._createContext2D()}downloadTileStart(e){var t=e.postData;if(t.level!==this.maxLevel)if(t.level>=this.minLevel&&t.level<=this.maxLevel){var i=this.levels[t.level];i=this._createContext2D(this.image,i.width,i.height);e.finish(i,null,"context2d")}else e.fail(`Invalid level ${t.level} for plain image source. Did you forget to set buildPyramid=true?`);else e.finish(this.image,null,"image")}downloadTileAbort(e){}_buildLevels(e){const t=[{url:e.src,width:e.naturalWidth,height:e.naturalHeight}];if(!this.buildPyramid||!r.supportsCanvas||!this.useCanvas)return t;let i=e.naturalWidth,n=e.naturalHeight;for(;2<=i&&2<=n;){i=Math.floor(i/2);n=Math.floor(n/2);t.push({width:i,height:n})}return t.reverse()}_createContext2D(e,t,i){const n=document.createElement("canvas"),r=n.getContext("2d");n.width=t;n.height=i;r.drawImage(e,0,0,t,i);return r}}}(OpenSeadragon);!function(r){r.TileSourceCollection=function(e,t,i,n){r.console.error("TileSourceCollection is deprecated; use World instead")}}(OpenSeadragon);!function(o){const e=o;e.PriorityQueue=class{constructor(e=void 0){this.nodes_=[];e&&this.insertAll(e)}insert(e,t){this.insertNode(new Node(e,t))}insertNode(e){const t=this.nodes_;e.index=t.length;t.push(e);this.moveUp_(e.index)}insertAll(e){let t,i;if(!(e instanceof o.PriorityQueue))throw"insertAll supports only OpenSeadragon.PriorityQueue object!";t=e.getKeys();i=e.getValues();if(this.getCount()<=0){const n=this.nodes_;for(let e=0;e>1;){var r=this.getLeftChildIndex_(e);var o=this.getRightChildIndex_(e);r=on.key)break;t[e]=t[r];t[e].index=e;e=r}t[e]=n;n&&(n.index=e)}moveUp_(e){const t=this.nodes_;const i=t[e];for(;0i.key))break;t[e]=t[n];t[e].index=e;e=n}t[e]=i;i&&(i.index=e)}getLeftChildIndex_(e){return 2*e+1}getRightChildIndex_(e){return 2*e+2}getParentIndex_(e){return e-1>>1}getValues(){return this.nodes_.map(e=>e.value)}getKeys(){return this.nodes_.map(e=>e.key)}containsValue(t){return this.nodes_.some(e=>e.value==t)}containsKey(t){return this.nodes_.some(e=>e.value==t)}clone(){return new o.PriorityQueue(this)}getCount(){return this.nodes_.length}isEmpty(){return 0===this.nodes_.length}clear(){this.nodes_.length=0}};e.PriorityQueue.Node=class Node{constructor(e,t){this.key=e;this.value=t;this.index=0}clone(){return new Node(this.key,this.value)}}}(OpenSeadragon);!function(u){const m=u;class e{constructor(){this.adjacencyList={};this.vertices={}}addVertex(e){if(this.vertices[e])return!1;this.vertices[e]=new u.PriorityQueue.Node(0,e);this.adjacencyList[e]=[];return!0}addEdge(e,t,i,n){i<0&&u.console.error("WeightedGraph: negative weights will make for invalid shortest path computation!");const r=this.adjacencyList[e],o=r.findIndex(e=>e.target===this.vertices[t]),s={target:this.vertices[t],origin:this.vertices[e],weight:i,transform:n};if(o<0){this.adjacencyList[e].push(s);return!0}this.adjacencyList[e][o]=s;return!1}dijkstra(e,t){const i=[];if(e===t)return{path:i,cost:0};const n=new m.PriorityQueue;let r;for(var o in this.vertices){o=this.vertices[o];if(o.value===e){o.key=0;n.insertNode(o)}else{o.key=1/0;delete o.index}o._previous=null}for(;0e.target.value===d));r=p}return{path:i.reverse(),cost:h}}}}let c;let t=0;const d=new Map;let p=!1;const g="undefined"!=typeof SharedArrayBuffer&&!0===self.crossOriginIsolated;function s(o,s,{timeoutMs:a=15e3}={}){const l=function(){if(c)return c;var e=URL.createObjectURL(new Blob([` -self.onmessage = async (e) => { - const { id, op, } = e.data; - let error; - try { - if (op === 'decodeFromBlob') { - const bmp = await createImageBitmap(e.data.blob, { colorSpaceConversion: 'none' }); - postMessage({ id, ok: true, bmp }, [bmp]); - return; - } - if (op === 'decodeFromBytes') { - const u8 = new Uint8Array(e.data.bytes); - const b = new Blob([u8], { type: e.data.mime || '' }); - const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' }); - postMessage({ id, ok: true, bmp }, [bmp]); - return; - } - if (op === 'fetchDecode') { - const res = await fetch(e.data.url, e.data.setup); - if (!res.ok) throw new Error('HTTP ' + res.status); - const b = await res.blob(); - const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' }); - postMessage({ id, ok: true, bmp }, [bmp]); - return; - } - error = 'Unknown op: ' + op; - } catch (err) { - error = String(err && err.message || err); - } - postMessage({ id, ok: false, err: error }); -}; -`],{type:"text/javascript"}));c=new Worker(e);c.onmessage=e=>{var{id:t,ok:i,bmp:n,err:e}=e.data||{};const r=d.get(t);if(r){d.delete(t);if(r.timer){clearTimeout(r.timer);r.timer=null}i?r.resolve(n):r.reject(new Error(e))}};c.onerror=e=>{for(var[,t]of d){if(t.timer){clearTimeout(t.timer);t.timer=null}t.reject(new Error("Worker error"))}d.clear()};return c}();const h=++t;return new u.Promise((e,t)=>{s.id=h;s.op=o;const i={resolve:e,reject:t,timer:null};0{i.timer=null;d.delete(h);t(new Error(`Worker timeout (${o})`))},a));d.set(h,i);if("decodeFromBytes"!==o)l.postMessage(s);else if(g){e=s.u8;var n=new SharedArrayBuffer(e.byteLength);new Uint8Array(n).set(e);l.postMessage({id:h,op:o,bytes:n,mime:s.mime})}else{if(!p){p=!0;console.warn("[Converter] SharedArrayBuffer unavailable; falling back to ArrayBuffer.")}const r=s.u8;n=0===r.byteOffset&&r.byteLength===r.buffer.byteLength?r:r.slice();l.postMessage({id:h,op:o,bytes:n.buffer,mime:s.mime},[n.buffer])}})}m.DataTypeConverter=class{constructor(){this.graph=new e;this.destructors={};this.copyings={};const i=(e,t)=>{const i=document.createElement("canvas");i.width=t.width;i.height=t.height;const n=i.getContext("2d",{willReadFrequently:!0});n.drawImage(t,0,0);return n};this.learn("rasterBlob","image",(e,r)=>new u.Promise((e,t)=>{var i=(window.URL||window.webkitURL).createObjectURL(r);if(!u.supportsAsync)return t("Not supported in sync mode!");const n=new Image;n.onerror=n.onabort=e=>{(window.URL||window.webkitURL).revokeObjectURL(r);t(e)};n.onload=()=>{(window.URL||window.webkitURL).revokeObjectURL(r);e(n)};n.decoding="async";n.src=i}),1,2);this.learn("context2d","rasterBlob",(e,i)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");i.canvas.toBlob(e)}),1,2);this.learn("rasterBlob","imageBitmap",(e,i)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");(c?s("decodeFromBlob",{blob:i}):createImageBitmap(i,{colorSpaceConversion:"none"})).then(e).catch(t)}),1,1);this.learn("imageBitmap","context2d",(e,t)=>{const i=document.createElement("canvas");i.width=t.width;i.height=t.height;const n=i.getContext("2d",{willReadFrequently:!0});n.drawImage(t,0,0);return n},1,2);this.learn("image","imageBitmap",(e,t)=>createImageBitmap(t,{colorSpaceConversion:"none"}),1,2);this.learn("image","context2d",i,1,2);this.learn("image","image",(e,t)=>((n,r)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");const i=new Image;i.onerror=i.onabort=e=>t("Failed to load image: "+r);i.onload=()=>e(i);n.tiledImage&&n.tiledImage.crossOriginPolicy&&(i.crossOrigin=n.tiledImage.crossOriginPolicy);i.src=r}))(e,t.src),1,1);this.learn("context2d","context2d",(e,t)=>i(0,t.canvas));this.learn("rasterBlob","rasterBlob",(e,t)=>t,0,1);this.learn("imageBitmap","imageBitmap",(e,r)=>new u.Promise((e,t)=>{try{if(!u.supportsAsync)return t("Not supported in sync mode!");if(!r)return t(new Error("No ImageBitmap to copy"));if("undefined"!=typeof OffscreenCanvas&&r.width&&r.height){const i=new OffscreenCanvas(r.width,r.height);const n=i.getContext("2d",{willReadFrequently:!1});n.drawImage(r,0,0);return"function"!=typeof i.transferToImageBitmap?createImageBitmap(i,{colorSpaceConversion:"none"}).then(e):e(i.transferToImageBitmap())}return createImageBitmap(r,{colorSpaceConversion:"none"}).then(e)}catch(e){return t(e)}}),1,1);this.learnDestroy("context2d",e=>{e.canvas.width=0;e.canvas.height=0})}guessType(e){if(Array.isArray(e)){const n=[];for(const r of e)if(void 0!==r&&null!==r){var t=this.guessType(r);n.includes(t)||n.push(t)}n.sort();return`Array [${n.join(",")}]`}const i=u.type(e);return"dom-node"===i?i.nodeName.toLowerCase():"object"===i&&u.isFunction(e.getType)?e.getType():i}learn(e,t,i,n=0,r=1){u.console.assert(0<=n&&n<=7,"[DataTypeConverter] Conversion costPower must be between <0, 7>.");u.console.assert(u.isFunction(i),"[DataTypeConverter:learn] Callback must be a valid function!");if(e===t)this.copyings[t]=i;else{n++;r=Math.min(Math.max(r,1),15);this.graph.addVertex(e);this.graph.addVertex(t);this.graph.addEdge(e,t,10*n^5+r,i);this._known={}}}learnDestroy(e,t){this.destructors[e]=t}convert(s,e,t,...i){const a=this.getConversionPath(t,i);if(!a){u.console.error(`[OpenSeadragon.converter.convert] Conversion ${t} ---> ${i} cannot be done!`);return u.Promise.resolve()}const l=a.length;const h=this;const c=(t,i,n=!0)=>{if(i>=l)return u.Promise.resolve(t);const r=a[i];let e;try{e=r.transform(s,t)}catch(e){n&&h.destroy(t,r.origin.value);return u.Promise.reject(`[OpenSeadragon.converter.convert] sync failure (while converting using ${r.origin.value} -> ${r.target.value})`)}if(void 0===e){n&&h.destroy(t,r.origin.value);return u.Promise.reject(`[OpenSeadragon.converter.convert] data mid result undefined value (while converting using ${r.origin.value} -> ${r.target.value})`)}n&&h.destroy(t,r.origin.value);const o="promise"===u.type(e)?e:u.Promise.resolve(e);return o.then(e=>c(e,i+1))};return c(e,0,!1)}copy(e,t,i){const n=this.copyings[i];if(n){t=n(e,t);return"promise"===u.type(t)?t:u.Promise.resolve(t)}u.console.warn("[OpenSeadragon.converter.copy] is not supported with type %s",i);return u.Promise.resolve(void 0)}destroy(e,t){const i=this.destructors[t];if(i){e=i(e);return"promise"===u.type(e)?e:u.Promise.resolve(e)}}getConversionPath(i,e){let n;let r=this._known[i];r||(this._known[i]=r={});if(Array.isArray(e)){u.console.assert(0e.cost){n=e;t=e.cost}}}else{u.console.assert("string"==typeof e,"[getConversionPath] conversion 'to' type must be defined.");n=r[e];if(void 0===n){n=this.graph.dijkstra(i,e);this._known[i][e]=n}}return n?n.path:void 0}getConversionPathFinalType(e){if(e&&e.length)return e[e.length-1].target.value}getKnownTypes(){return Object.keys(this.graph.vertices)}existsType(e){return!!this.graph.vertices[e]}};u.converter=new u.DataTypeConverter;u.converter.learn("__private__imageUrl","imageBitmap",(r,o)=>new u.Promise((e,t)=>{if(!u.supportsAsync)return t("Not supported in sync mode!");let i;if(r.tiledImage&&r.tiledImage.crossOriginPolicy){var n=r.tiledImage.crossOriginPolicy;"anonymous"===n?i={mode:"cors",credentials:"omit"}:"use-credentials"===n?i={mode:"cors",credentials:"include"}:n&&u.console.error(`Unsupported crossOriginPolicy ${n}. Ignoring the property.`)}return(c?s("fetchDecode",{url:o,setup:i}):fetch(o,i).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status} loading `+o);return e.blob()}).then(e=>createImageBitmap(e,{colorSpaceConversion:"none"}))).then(e).catch(t)}),1,1);u.converter.learn("__private__imageUrl","__private__imageUrl",(e,t)=>t,0,1)}(OpenSeadragon);!function(i){i.ButtonState={REST:0,GROUP:1,HOVER:2,DOWN:3};i.Button=function(e){const t=this;i.EventSource.call(this);i.extend(!0,this,{tooltip:null,srcRest:null,srcGroup:null,srcHover:null,srcDown:null,clickTimeThreshold:i.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:i.DEFAULT_SETTINGS.clickDistThreshold,fadeDelay:0,fadeLength:2e3,onPress:null,onRelease:null,onClick:null,onEnter:null,onExit:null,onFocus:null,onBlur:null,userData:null},e);this.element=e.element||i.makeNeutralElement("div");if(!e.element){this.imgRest=i.makeTransparentImage(this.srcRest);this.imgGroup=i.makeTransparentImage(this.srcGroup);this.imgHover=i.makeTransparentImage(this.srcHover);this.imgDown=i.makeTransparentImage(this.srcDown);this.imgRest.alt=this.imgGroup.alt=this.imgHover.alt=this.imgDown.alt=this.tooltip;i.setElementPointerEventsNone(this.imgRest);i.setElementPointerEventsNone(this.imgGroup);i.setElementPointerEventsNone(this.imgHover);i.setElementPointerEventsNone(this.imgDown);this.element.style.position="relative";i.setElementTouchActionNone(this.element);this.imgGroup.style.position=this.imgHover.style.position=this.imgDown.style.position="absolute";this.imgGroup.style.top=this.imgHover.style.top=this.imgDown.style.top="0px";this.imgGroup.style.left=this.imgHover.style.left=this.imgDown.style.left="0px";this.imgHover.style.visibility=this.imgDown.style.visibility="hidden";this.element.appendChild(this.imgRest);this.element.appendChild(this.imgGroup);this.element.appendChild(this.imgHover);this.element.appendChild(this.imgDown)}this.addHandler("press",this.onPress);this.addHandler("release",this.onRelease);this.addHandler("click",this.onClick);this.addHandler("enter",this.onEnter);this.addHandler("exit",this.onExit);this.addHandler("focus",this.onFocus);this.addHandler("blur",this.onBlur);this.currentState=i.ButtonState.GROUP;this.fadeBeginTime=null;this.shouldFade=!1;this.element.style.display="inline-block";this.element.style.position="relative";this.element.title=this.tooltip;this.tracker=new i.MouseTracker({userData:"Button.tracker",element:this.element,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,enterHandler:function(e){if(e.insideElementPressed){r(t,i.ButtonState.DOWN);t.raiseEvent("enter",{originalEvent:e.originalEvent})}else e.buttonDownAny||r(t,i.ButtonState.HOVER)},focusHandler:function(e){t.tracker.enterHandler(e);t.raiseEvent("focus",{originalEvent:e.originalEvent})},leaveHandler:function(e){o(t,i.ButtonState.GROUP);e.insideElementPressed&&t.raiseEvent("exit",{originalEvent:e.originalEvent})},blurHandler:function(e){t.tracker.leaveHandler(e);t.raiseEvent("blur",{originalEvent:e.originalEvent})},pressHandler:function(e){r(t,i.ButtonState.DOWN);t.raiseEvent("press",{originalEvent:e.originalEvent})},releaseHandler:function(e){if(e.insideElementPressed&&e.insideElementReleased){o(t,i.ButtonState.HOVER);t.raiseEvent("release",{originalEvent:e.originalEvent})}else e.insideElementPressed?o(t,i.ButtonState.GROUP):r(t,i.ButtonState.HOVER)},clickHandler:function(e){e.quick&&t.raiseEvent("click",{originalEvent:e.originalEvent})},keyHandler:function(e){if(13===e.keyCode){t.raiseEvent("click",{originalEvent:e.originalEvent});t.raiseEvent("release",{originalEvent:e.originalEvent});e.preventDefault=!0}else e.preventDefault=!1}});o(this,i.ButtonState.REST)};i.extend(i.Button.prototype,i.EventSource.prototype,{notifyGroupEnter:function(){r(this,i.ButtonState.GROUP)},notifyGroupExit:function(){o(this,i.ButtonState.REST)},disable:function(){this.notifyGroupExit();this.element.disabled=!0;this.tracker.setTracking(!1);i.setElementOpacity(this.element,.2,!0)},enable:function(){this.element.disabled=!1;this.tracker.setTracking(!0);i.setElementOpacity(this.element,1,!0);this.notifyGroupEnter()},destroy:function(){if(this.imgRest){this.element.removeChild(this.imgRest);this.imgRest=null}if(this.imgGroup){this.element.removeChild(this.imgGroup);this.imgGroup=null}if(this.imgHover){this.element.removeChild(this.imgHover);this.imgHover=null}if(this.imgDown){this.element.removeChild(this.imgDown);this.imgDown=null}this.removeAllHandlers();this.tracker.destroy();this.element=null}});function n(e){i.requestAnimationFrame(function(){!function(e){var t;if(e.shouldFade){t=i.now();t=t-e.fadeBeginTime;t=1-t/e.fadeLength;t=Math.min(1,t);t=Math.max(0,t);e.imgGroup&&i.setElementOpacity(e.imgGroup,t,!0);0=i.ButtonState.GROUP&&e.currentState===i.ButtonState.REST){!function(e){e.shouldFade=!1;e.imgGroup&&i.setElementOpacity(e.imgGroup,1,!0)}(e);e.currentState=i.ButtonState.GROUP}if(t>=i.ButtonState.HOVER&&e.currentState===i.ButtonState.GROUP){e.imgHover&&(e.imgHover.style.visibility="");e.currentState=i.ButtonState.HOVER}if(t>=i.ButtonState.DOWN&&e.currentState===i.ButtonState.HOVER){e.imgDown&&(e.imgDown.style.visibility="");e.currentState=i.ButtonState.DOWN}}}function o(e,t){if(!e.element.disabled){if(t<=i.ButtonState.HOVER&&e.currentState===i.ButtonState.DOWN){e.imgDown&&(e.imgDown.style.visibility="hidden");e.currentState=i.ButtonState.HOVER}if(t<=i.ButtonState.GROUP&&e.currentState===i.ButtonState.HOVER){e.imgHover&&(e.imgHover.style.visibility="hidden");e.currentState=i.ButtonState.GROUP}if(t<=i.ButtonState.REST&&e.currentState===i.ButtonState.GROUP){!function(e){e.shouldFade=!0;e.fadeBeginTime=i.now()+e.fadeDelay;window.setTimeout(function(){n(e)},e.fadeDelay)}(e);e.currentState=i.ButtonState.REST}}}}(OpenSeadragon);!function(r){r.ButtonGroup=function(e){r.extend(!0,this,{buttons:[],clickTimeThreshold:r.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:r.DEFAULT_SETTINGS.clickDistThreshold,labelText:""},e);let t=this.buttons.concat([]),i=this,n;this.element=e.element||r.makeNeutralElement("div");if(!e.group){this.element.style.display="inline-block";for(n=0;nh&&(h=d.x);d.yu&&(u=d.y)}return new p.Rect(l,c,h-l,u-c)},_getSegments:function(){var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();return[[e,t],[t,n],[n,i],[i,e]]},rotate:function(e,t){if(0===(e=p.positiveModulo(e,360)))return this.clone();t=t||this.getCenter();var i=this.getTopLeft().rotate(e,t);const n=this.getTopRight().rotate(e,t);let r=n.minus(i);r=r.apply(function(e){return Math.abs(e)<1e-15?0:e});let o=Math.atan(r.y/r.x);r.x<0?o+=Math.PI:r.y<0&&(o+=2*Math.PI);return new p.Rect(i.x,i.y,this.width,this.height,o/Math.PI*180)},getBoundingBox:function(){if(0===this.degrees)return this.clone();var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();var r=Math.min(e.x,t.x,i.x,n.x);var o=Math.max(e.x,t.x,i.x,n.x);var s=Math.min(e.y,t.y,i.y,n.y);n=Math.max(e.y,t.y,i.y,n.y);return new p.Rect(r,s,o-r,n-s)},getIntegerBoundingBox:function(){var e=this.getBoundingBox();var t=Math.floor(e.x);var i=Math.floor(e.y);var n=Math.ceil(e.width+e.x-t);e=Math.ceil(e.height+e.y-i);return new p.Rect(t,i,n,e)},containsPoint:function(e,t){t=t||0;var i=this.getTopLeft();const n=this.getTopRight();const r=this.getBottomLeft();var o=n.minus(i);var s=r.minus(i);return(e.x-i.x)*o.x+(e.y-i.y)*o.y>=-t&&(e.x-n.x)*o.x+(e.y-n.y)*o.y<=t&&(e.x-i.x)*s.x+(e.y-i.y)*s.y>=-t&&(e.x-r.x)*s.x+(e.y-r.y)*s.y<=t},toString:function(){return"["+Math.round(100*this.x)/100+", "+Math.round(100*this.y)/100+", "+Math.round(100*this.width)/100+"x"+Math.round(100*this.height)/100+", "+Math.round(100*this.degrees)/100+"deg]"}}}(OpenSeadragon);!function(c){const s={};c.ReferenceStrip=function(e){const t=e.viewer;var i=c.getElementSize(t.element);let n;let r;if(!e.id){e.id="referencestrip-"+c.now();this.element=c.makeNeutralElement("div");this.element.id=e.id;this.element.className="referencestrip"}e=c.extend(!0,{sizeRatio:c.DEFAULT_SETTINGS.referenceStripSizeRatio,position:c.DEFAULT_SETTINGS.referenceStripPosition,scroll:c.DEFAULT_SETTINGS.referenceStripScroll,clickTimeThreshold:c.DEFAULT_SETTINGS.clickTimeThreshold},e,{element:this.element});c.extend(this,e);s[this.id]={animating:!1};this.minPixelRatio=this.viewer.minPixelRatio;this.element.tabIndex=0;const o=this.element.style;o.marginTop="0px";o.marginRight="0px";o.marginBottom="0px";o.marginLeft="0px";o.left="0px";o.bottom="0px";o.border="0px";o.background="#000";o.position="relative";c.setElementTouchActionNone(this.element);c.setElementOpacity(this.element,.8);this.viewer=t;this.tracker=new c.MouseTracker({userData:"ReferenceStrip.tracker",element:this.element,clickHandler:c.delegate(this,a),dragHandler:c.delegate(this,l),scrollHandler:c.delegate(this,h),enterHandler:c.delegate(this,d),leaveHandler:c.delegate(this,p),keyDownHandler:c.delegate(this,g),keyHandler:c.delegate(this,m),preProcessEventHandler:function(e){"wheel"===e.eventType&&(e.preventDefault=!0)}});if(e.width&&e.height){this.element.style.width=e.width+"px";this.element.style.height=e.height+"px";t.addControl(this.element,{anchor:c.ControlAnchor.BOTTOM_LEFT})}else if("horizontal"===e.scroll){this.element.style.width=i.x*e.sizeRatio*t.tileSources.length+12*t.tileSources.length+"px";this.element.style.height=i.y*e.sizeRatio+"px";t.addControl(this.element,{anchor:c.ControlAnchor.BOTTOM_LEFT})}else{this.element.style.height=i.y*e.sizeRatio*t.tileSources.length+12*t.tileSources.length+"px";this.element.style.width=i.x*e.sizeRatio+"px";t.addControl(this.element,{anchor:c.ControlAnchor.TOP_LEFT})}this.panelWidth=i.x*this.sizeRatio+8;this.panelHeight=i.y*this.sizeRatio+8;this.panels=[];this.miniViewers={};for(r=0;ro+i.x-this.panelWidth){a=Math.min(a,n-i.x);this.element.style.marginLeft=-a+"px";u(this,i.x,-a)}else if(as+i.y-this.panelHeight){a=Math.min(a,r-i.y);this.element.style.marginTop=-a+"px";u(this,i.y,-a)}else if(a-(n-o.x)){this.element.style.marginLeft=t+2*e.delta.x+"px";u(this,o.x,t+2*e.delta.x)}}else if(-e.delta.x<0&&t<0){this.element.style.marginLeft=t+2*e.delta.x+"px";u(this,o.x,t+2*e.delta.x)}}else if(0<-e.delta.y){if(i>-(r-o.y)){this.element.style.marginTop=i+2*e.delta.y+"px";u(this,o.y,i+2*e.delta.y)}}else if(-e.delta.y<0&&i<0){this.element.style.marginTop=i+2*e.delta.y+"px";u(this,o.y,i+2*e.delta.y)}}}function h(e){if(this.element){var t=Number(this.element.style.marginLeft.replace("px",""));var i=Number(this.element.style.marginTop.replace("px",""));var n=Number(this.element.style.width.replace("px",""));var r=Number(this.element.style.height.replace("px",""));var o=c.getElementSize(this.viewer.canvas);if("horizontal"===this.scroll){if(0-(n-o.x)){this.element.style.marginLeft=t-60*e.scroll+"px";u(this,o.x,t-60*e.scroll)}}else if(e.scroll<0&&t<0){this.element.style.marginLeft=t-60*e.scroll+"px";u(this,o.x,t-60*e.scroll)}}else if(e.scroll<0){if(i>o.y-r){this.element.style.marginTop=i+60*e.scroll+"px";u(this,o.y,i+60*e.scroll)}}else if(0=this.target.time)this.current.value=this.target.value;else{i=e+(t-e)*(i=this.springStiffness,n=(this.current.time-this.start.time)/(this.target.time-this.start.time),(1-Math.exp(i*-n))/(1-Math.exp(-i)));this._exponential?this.current.value=Math.exp(i):this.current.value=i}var i,n;return this.current.value!==this.target.value},isAtTargetValue:function(){return this.current.value===this.target.value}}}(OpenSeadragon);!function(r){r.ImageJob=function(e){this.data=null;this.userData={};this.errorMsg=null;this.timeout=r.DEFAULT_SETTINGS.timeout;this.isBatched=!1;r.extend(!0,this,{jobId:null,tries:0},e)};r.ImageJob.prototype={start:function(){this.tries++;const e=this;const t=this.abort;this.jobId=window.setTimeout(function(){e.fail("Image load exceeded timeout ("+e.timeout+" ms)",null)},this.timeout);this.abort=function(){e.source.downloadTileAbort(e);"function"==typeof t&&t();e.fail("Image load aborted.",null)};this.source.downloadTileStart(this)},prepareForBatch:function(){this.tries++;this.jobId=-1},finish:function(e,t,i){if(this.jobId)if(null!=(n=e)&&!1!==n){var n;this.data=e;this.request=t;this.errorMsg=null;this.dataType=i;window.clearTimeout(this.jobId);this.jobId=null;this.callback(this)}else this.fail(i||"[downloadTileStart->finish()] Retrieved data is invalid!",t)},fail:function(e,t){this.data=null;this.request=t;this.errorMsg=e;this.dataType=null;if(this.jobId){window.clearTimeout(this.jobId);this.jobId=null}this.callback(this)}};r.BatchImageJob=function(e){r.extend(!0,this,{timeout:r.DEFAULT_SETTINGS.timeout,jobId:null,data:null,dataType:null,errorMsg:null},e);this.jobs=e.jobs||[];this.source=e.source};r.BatchImageJob.prototype={start:function(){this._finishedJobs=0;const t=this;this.jobId=window.setTimeout(function(){t.fail("Batch image load exceeded timeout ("+t.timeout+" ms)",null)},this.timeout);this.abort=function(){t.source.downloadTileBatchAbort(t);for(var e of this.jobs)e.jobId&&e.abort&&e.abort()};var e=(t,i)=>(...e)=>{if(this.jobId){this._finishedJobs++;t.call(i,...e);if(this._finishedJobs===this.jobs.length){window.clearTimeout(this.jobId);this.jobId=null;this.callback&&this.callback(this)}}};for(var i of this.jobs){i.finish=e(i.finish,i);i.fail=e(i.fail,i);i.prepareForBatch()}this.source.downloadTileBatchStart(this)},finish:function(e,t,i){r.console.error("Finish call on batch job is not desirable: call finish on individual child jobs!",e,t)},fail:function(t,i){this.data=null;this.request=i;this.errorMsg=t;this.dataType=null;for(let e=0;efunction(t,e,i){if(e.errorMsg&&null===e.data&&e.tries<1+t.tileRetryMax){e.isBatched=!1;t.failedTiles.push(e)}e.isBatched||t.jobsInProgress--;if(t.canAcceptNewJob()&&0this._flushBatchBucket(i),i.waitTimeout);this._batchBuckets.push(i)}i.jobs.push(e);if(1<=i.maxJobs&&i.jobs.length>=i.maxJobs){clearTimeout(i.timer);this._flushBatchBucket(i)}},_flushBatchBucket:function(e){e.timer=null;var t=this._batchBuckets.indexOf(e);-1function(e,t){e.jobsInProgress--;t.jobs.length=0}(i,e)});if(!this.jobLimit||this.jobsInProgressthis.addCache(this.cacheKey,e,t.type,!0,!1)))},buildDistinctMainCacheKey:function(){return this.cacheKey===this.originalCacheKey?"mod://"+this.originalCacheKey:this.cacheKey},getCache:function(e=this._cKey){const t=this._caches[e];t&&t.withTileReference(this);return t},addCache:function(e,t,i=void 0,n=!1,r=!0){const o=this.tiledImage;if(!o)return null;if(!i){if(!this.__typeWarningReported){d.console.warn(this,"[Tile.addCache] called without type specification. Automated deduction is potentially unsafe: prefer specification of data type explicitly.");this.__typeWarningReported=!0}"function"==typeof t&&d.console.error("[TileCache.cacheTile] options.data as a callback requires type argument! Current is "+i);i=d.converter.guessType(t)}var s=e===this.cacheKey;if(r&&(s||n)){r=o.getDrawer().getSupportedDataFormats();const l=d.converter.getConversionPath(i,r);d.console.assert(l,"[Tile.addCache] data was set for the default tile cache we are unable"+`to render. Make sure OpenSeadragon.converter was taught to convert ${i} to (one of): `+l.toString())}i=o._tileCache.cacheTile({data:t,dataType:i,tile:this,cacheKey:e,cutoff:o.source.getClosestLevel()});const a=this._caches[e];if(a!==i){this._caches[e]=i;if(a){a.removeTile(this);o._tileCache.safeUnloadCache(a)}}!s&&n&&this._updateMainCacheKey(e);return i},setCache(e,t,i=!1,n=!0){const r=this.tiledImage;if(!r)return null;var o=e===this.cacheKey;if(n){d.console.assert(t instanceof d.CacheRecord,"[Tile.setCache] cache must be a CacheRecord object!");if(o||i){n=r.getDrawer().getSupportedDataFormats();const a=d.converter.getConversionPath(t.type,n);d.console.assert(a,"[Tile.setCache] data was set for the default tile cache we are unable"+`to render. Make sure OpenSeadragon.converter was taught to convert ${t.type} to (one of): `+a.toString())}}const s=this._caches[e];if(s!==t){(this._caches[e]=t).addTile(this);if(s){s.removeTile(this);r._tileCache.safeUnloadCache(s)}}!o&&i&&this._updateMainCacheKey(e);return t},_updateMainCacheKey:function(e){let t=this._caches[this._cKey];t&&t.destroyInternalCache();this._cKey=e},getCacheSize:function(){return Object.keys(this._caches).length},removeCache:function(e,t=!0){var i=this._caches[e];if(i){var n=this.cacheKey,r=this.originalCacheKey,o=n===r;if(o||r!==e){if(n===e){if(o||!this._caches[r]){d.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!","If you want to remove the main cache, first set different cache as main with tile.addCache()");return}this._updateMainCacheKey(r)}this.tiledImage._tileCache.unloadCacheForTile(this,e,t,!1)&&delete this._caches[e];return i}d.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!","If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData().")}else this.tiledImage._tileCache.unloadCacheForTile(this,e,t,!0)},getScaleForEdgeSmoothing:function(){d.console.warn("[Tile.getScaleForEdgeSmoothing] is deprecated, the following error is the consequence:");var e=this.getCanvasContext();if(e)return e.canvas.width/(this.size.x*d.pixelDensityRatio);d.console.warn("[Tile.drawCanvas] attempting to get tile scale %s when tile's not cached",this.toString());return 1},getTranslationForEdgeSmoothing:function(e,t,i){var n=Math.max(1,Math.ceil((i.x-t.x)/2));t=Math.max(1,Math.ceil((i.y-t.y)/2));return new d.Point(n,t).minus(this.position.times(d.pixelDensityRatio).times(e||1).apply(function(e){return e%1}))},reflectCacheRenamed:function(e,t){var i=this._caches[e];if(i){e===this._ocKey&&(this._ocKey=t);e===this._cKey&&(this._cKey=t);this._caches[t]=i;delete this._caches[e]}},equals(e){return this._ocKey===e._ocKey},unload:function(e=!1){this.loaded&&this.tiledImage._tileCache.unloadTile(this,e)},_unload:function(){this.tiledImage=null;this._caches={};this.loaded=!1;this.loading=!1;this._cKey=this._ocKey}}}(OpenSeadragon);!function(h){h.OverlayPlacement=h.Placement;h.OverlayRotationMode=h.freezeObject({NO_ROTATION:1,EXACT:2,BOUNDING_BOX:3});h.Overlay=function(e,t,i){let n;n=h.isPlainObject(e)?e:{element:e,location:t,placement:i};this.elementWrapper=document.createElement("div");this.element=n.element;this.elementWrapper.appendChild(this.element);this.element.id&&(this.elementWrapper.id="overlay-wrapper-"+this.element.id);this.elementWrapper.classList.add("openseadragon-overlay-wrapper");this.style=this.elementWrapper.style;this._init(n)};h.Overlay.prototype={_init:function(e){this.location=e.location;this.placement=void 0===e.placement?h.Placement.TOP_LEFT:e.placement;this.onDraw=e.onDraw;this.checkResize=void 0===e.checkResize||e.checkResize;this.width=void 0===e.width?null:e.width;this.height=void 0===e.height?null:e.height;this.rotationMode=e.rotationMode||h.OverlayRotationMode.EXACT;if(this.location instanceof h.Rect){this.width=this.location.width;this.height=this.location.height;this.location=this.location.getTopLeft();this.placement=h.Placement.TOP_LEFT}this.scales=null!==this.width&&null!==this.height;this.bounds=new h.Rect(this.location.x,this.location.y,this.width,this.height);this.position=this.location},adjust:function(e,t){var i=h.Placement.properties[this.placement];if(i){i.isHorizontallyCentered?e.x-=t.x/2:i.isRight&&(e.x-=t.x);i.isVerticallyCentered?e.y-=t.y/2:i.isBottom&&(e.y-=t.y)}},destroy:function(){const e=this.elementWrapper;const t=this.style;if(e.parentNode){e.parentNode.removeChild(e);if(e.prevElementParent){t.display="none";document.body.appendChild(e)}}this.onDraw=null;t.top="";t.left="";t.position="";null!==this.width&&(t.width="");null!==this.height&&(t.height="");var i=h.getCssPropertyWithVendorPrefix("transformOrigin");var n=h.getCssPropertyWithVendorPrefix("transform");if(i&&n){t[i]="";t[n]=""}},drawHTML:function(e,t){const i=this.elementWrapper;if(i.parentNode!==e){i.prevElementParent=i.parentNode;i.prevNextSibling=i.nextSibling;e.appendChild(i);this.style.position="absolute";this.size=h.getElementSize(this.elementWrapper)}var n=this._getOverlayPositionAndSize(t);var r=n.position;var o=this.size=n.size;let s="";t.overlayPreserveContentDirection&&(s=t.flipped?" scaleX(-1)":" scaleX(1)");e=t.flipped?-n.rotate:n.rotate;n=t.flipped?" scaleX(-1)":"";if(this.onDraw)this.onDraw(r,o,this.element);else{const a=this.style;const l=this.element.style;l.display="block";a.left=r.x+"px";a.top=r.y+"px";null!==this.width&&(l.width=o.x+"px");null!==this.height&&(l.height=o.y+"px");r=h.getCssPropertyWithVendorPrefix("transformOrigin");o=h.getCssPropertyWithVendorPrefix("transform");if(r&&o)if(e&&!t.flipped){l[o]="";a[r]=this._getTransformOrigin();a[o]="rotate("+e+"deg)"}else if(!e&&t.flipped){l[o]=s;a[r]=this._getTransformOrigin();a[o]=n}else if(e&&t.flipped){l[o]=s;a[r]=this._getTransformOrigin();a[o]="rotate("+e+"deg)"+n}else{l[o]="";a[r]="";a[o]=""}a.display="flex"}},_getOverlayPositionAndSize:function(e){let t=e.pixelFromPoint(this.location,!0);let i=this._getSizeInPixels(e);this.adjust(t,i);let n=0;if(e.getRotation(!0)&&this.rotationMode!==h.OverlayRotationMode.NO_ROTATION)if(this.rotationMode===h.OverlayRotationMode.BOUNDING_BOX&&null!==this.width&&null!==this.height){var r=new h.Rect(t.x,t.y,i.x,i.y);const o=this._getBoundingBox(r,e.getRotation(!0));t=o.getTopLeft();i=o.getSize()}else n=e.getRotation(!0);e.flipped&&(t.x=e.getContainerSize().x-t.x);return{position:t,size:i,rotate:n}},_getSizeInPixels:function(e){let t=this.size.x;let i=this.size.y;if(null!==this.width||null!==this.height){var n=e.deltaPixelsFromPointsNoRotate(new h.Point(this.width||0,this.height||0),!0);null!==this.width&&(t=n.x);null!==this.height&&(i=n.y)}if(this.checkResize&&(null===this.width||null===this.height)){n=this.size=h.getElementSize(this.elementWrapper);null===this.width&&(t=n.x);null===this.height&&(i=n.y)}return new h.Point(t,i)},_getBoundingBox:function(e,t){var i=this._getPlacementPoint(e);return e.rotate(t,i).getBoundingBox()},_getPlacementPoint:function(e){const t=new h.Point(e.x,e.y);var i=h.Placement.properties[this.placement];if(i){i.isHorizontallyCentered?t.x+=e.width/2:i.isRight&&(t.x+=e.width);i.isVerticallyCentered?t.y+=e.height/2:i.isBottom&&(t.y+=e.height)}return t},_getTransformOrigin:function(){let e="";var t=h.Placement.properties[this.placement];if(!t)return e;t.isLeft?e="left":t.isRight&&(e="right");t.isTop?e+=" top":t.isBottom&&(e+=" bottom");return e},update:function(e,t){t=h.isPlainObject(e)?e:{location:e,placement:t};this._init({location:t.location||this.location,placement:(void 0!==t.placement?t:this).placement,onDraw:t.onDraw||this.onDraw,checkResize:t.checkResize||this.checkResize,width:(void 0!==t.width?t:this).width,height:(void 0!==t.height?t:this).height,rotationMode:t.rotationMode||this.rotationMode})},getBounds:function(e){h.console.assert(e,"A viewport must now be passed to Overlay.getBounds.");let t=this.width;let i=this.height;if(null===t||null===i){var n=e.deltaPointsFromPixelsNoRotate(this.size,!0);null===t&&(t=n.x);null===i&&(i=n.y)}n=this.location.clone();this.adjust(n,new h.Point(t,i));return this._adjustBoundsForRotation(e,new h.Rect(n.x,n.y,t,i))},_adjustBoundsForRotation:function(e,t){if(!e||0===e.getRotation(!0)||this.rotationMode===h.OverlayRotationMode.EXACT)return t;if(this.rotationMode!==h.OverlayRotationMode.BOUNDING_BOX)return t.rotate(-e.getRotation(!0),this._getPlacementPoint(t));if(null===this.width||null===this.height)return t;t=this._getOverlayPositionAndSize(e);return e.viewerElementToViewportRectangle(new h.Rect(t.position.x,t.position.y,t.size.x,t.size.y))}}}(OpenSeadragon);!function(n){const i=n;i.DrawerBase=class{constructor(e){n.console.assert(e.viewer,"[Drawer] options.viewer is required");n.console.assert(e.viewport,"[Drawer] options.viewport is required");n.console.assert(e.element,"[Drawer] options.element is required");this._id=this.getType()+n.now();this.viewer=e.viewer;this.viewport=e.viewport;this.debugGridColor="string"==typeof e.debugGridColor?[e.debugGridColor]:e.debugGridColor||n.DEFAULT_SETTINGS.debugGridColor;this.options=n.extend({usePrivateCache:!1,preloadCache:!0,offScreen:!1,broadCastTileInvalidation:!0},this.defaultOptions,e.options);this.container=n.getElement(e.element);this._renderingTarget=this._createDrawingElement();if(!this.options.offScreen){this.canvas.style.width="100%";this.canvas.style.height="100%";this.canvas.style.position="absolute";this.canvas.style.left="0";n.setElementOpacity(this.canvas,this.viewer.opacity,!0);n.setElementPointerEventsNone(this.canvas);n.setElementTouchActionNone(this.canvas);this.container.style.textAlign="left";this.container.appendChild(this.canvas);if(this.options.broadCastTileInvalidation){let e=this.viewer;for(;e.viewer;)e=e.viewer;this._parentViewer=e;e._registerDrawer(this)}else{this.viewer._registerDrawer(this);this._parentViewer=this.viewer}}this._checkInterfaceImplementation();this.setInternalCacheNeedsRefresh()}get defaultOptions(){return{}}get canvas(){return this._renderingTarget}get element(){n.console.error("Drawer.element is deprecated. Use Drawer.container instead.");return this.container}getId(){return this._id}getType(){n.console.error("Drawer.getType must be implemented by child class")}getRequiredDataFormats(){return this.getSupportedDataFormats()}getSupportedDataFormats(){throw"Drawer.getSupportedDataFormats must define its supported rendering data types!"}getDataToDraw(e){const t=e.getCache(e.cacheKey);if(t){var i=t.getDataForRendering(this,e);return i&&i.data}n.console.warn("Attempt to draw tile %s when not cached!",e)}static isSupported(){n.console.error("Drawer.isSupported must be implemented by child class")}_createDrawingElement(){n.console.error("Drawer._createDrawingElement must be implemented by child class");return null}draw(e){n.console.error("Drawer.draw must be implemented by child class")}canRotate(){n.console.error("Drawer.canRotate must be implemented by child class")}destroy(){this._parentViewer._unregisterDrawer(this)}destroyInternalCache(){this.viewer.tileCache.clearDrawerInternalCache(this)}minimumOverlapRequired(e){return!1}setImageSmoothingEnabled(e){n.console.error("Drawer.setImageSmoothingEnabled must be implemented by child class")}drawDebuggingRect(e){n.console.warn("[drawer].drawDebuggingRect is not implemented by this drawer")}clear(){n.console.warn("[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.")}internalCacheCreate(e,t){}internalCacheFree(e){}setInternalCacheNeedsRefresh(){this._dataNeedsRefresh=n.now()}tiledImageCreated(e){}_checkInterfaceImplementation(){if(this._createDrawingElement===n.DrawerBase.prototype._createDrawingElement)throw new Error("[drawer]._createDrawingElement must be implemented by child class");if(this.draw===n.DrawerBase.prototype.draw)throw new Error("[drawer].draw must be implemented by child class");if(this.canRotate===n.DrawerBase.prototype.canRotate)throw new Error("[drawer].canRotate must be implemented by child class");if(this.destroy===n.DrawerBase.prototype.destroy)throw new Error("[drawer].destroy must be implemented by child class");if(this.setImageSmoothingEnabled===n.DrawerBase.prototype.setImageSmoothingEnabled)throw new Error("[drawer].setImageSmoothingEnabled must be implemented by child class")}viewportToDrawerRectangle(e){var t=this.viewport.pixelFromPointNoRotate(e.getTopLeft(),!0);e=this.viewport.deltaPixelsFromPointsNoRotate(e.getSize(),!0);return new n.Rect(t.x*n.pixelDensityRatio,t.y*n.pixelDensityRatio,e.x*n.pixelDensityRatio,e.y*n.pixelDensityRatio)}viewportCoordToDrawerCoord(e){e=this.viewport.pixelFromPointNoRotate(e,!0);return new n.Point(e.x*n.pixelDensityRatio,e.y*n.pixelDensityRatio)}_calculateCanvasSize(){var e=n.pixelDensityRatio;var t=this.viewport.getContainerSize();return new i.Point(Math.round(t.x*e),Math.round(t.y*e))}_raiseTiledImageDrawnEvent(e,t){this.viewer&&this.viewer.raiseEvent("tiled-image-drawn",{tiledImage:e,tiles:t})}_raiseDrawerErrorEvent(e,t){this.viewer&&this.viewer.raiseEvent("drawer-error",{tiledImage:e,drawer:this,error:t})}}}(OpenSeadragon);!function(o){var e=o;class t extends e.DrawerBase{constructor(e){super(e);this.viewer.rejectEventHandler("tile-drawing","The HTMLDrawer does not raise the tile-drawing event");this.viewer.allowEventHandler("tile-drawn");o.converter.learn("image",t.imageCacheType,function(e,t){var i=o.makeNeutralElement("div");const n=t.cloneNode();n.style.msInterpolationMode="nearest-neighbor";n.style.width="100%";n.style.height="100%";const r=i.style;r.position="absolute";return{element:i,imgElement:n,style:r,data:t}},1,1);o.converter.learn(t.imageCacheType,"image",(e,t)=>t.data,1,3);o.converter.learnDestroy(t.imageCacheType,function(e){e.imgElement&&e.imgElement.parentNode&&e.imgElement.parentNode.removeChild(e.imgElement);e.element&&e.element.parentNode&&e.element.parentNode.removeChild(e.element)})}static get imageCacheType(){return"htmlDrawer[image]"}static get canvasCacheType(){return"htmlDrawer[canvas]"}static isSupported(){return!0}getType(){return"html"}getSupportedDataFormats(){return[t.imageCacheType,t.canvasCacheType]}minimumOverlapRequired(e){return!0}_createDrawingElement(){return o.makeNeutralElement("div")}draw(e){const t=this;this._prepareNewFrame();e.forEach(function(e){0!==e.opacity&&t._drawTiles(e)})}canRotate(){return!1}destroy(){super.destroy();this.container.removeChild(this.canvas)}setImageSmoothingEnabled(){}_prepareNewFrame(){this.canvas.innerHTML=""}_drawTiles(t){var i=t.getTilesToDraw().map(e=>e.tile);if(0!==t.opacity&&(0!==i.length||t.placeholderFillStyle))for(let e=i.length-1;0<=e;e--){var n=i[e];this._drawTile(n);this.viewer&&this.viewer.raiseEvent("tile-drawn",{tiledImage:t,tile:n})}}_drawTile(e){o.console.assert(e,"[Drawer._drawTile] tile is required");let t=this.canvas;if(e.loaded){const i=this.getDataToDraw(e);if(i){i.element.parentNode!==t&&t.appendChild(i.element);i.imgElement.parentNode!==i.element&&i.element.appendChild(i.imgElement);i.style.top=e.position.y+"px";i.style.left=e.position.x+"px";i.style.height=e.size.y+"px";i.style.width=e.size.x+"px";e.flipped&&(i.style.transform="scaleX(-1)");o.setElementOpacity(i.element,e.opacity)}}else o.console.warn("Attempting to draw tile %s when it's not yet loaded.",e.toString())}}o.HTMLDrawer=t}(OpenSeadragon);!function(d){var e=d;class t extends e.DrawerBase{constructor(e){super(e);this.context=this.canvas.getContext("2d");this.sketchCanvas=null;this.sketchContext=null;this._imageSmoothingEnabled=!0;this.viewer.allowEventHandler("tile-drawn");this.viewer.allowEventHandler("tile-drawing")}static isSupported(){return d.supportsCanvas}getType(){return"canvas"}getSupportedDataFormats(){return["context2d"]}_createDrawingElement(){const e=d.makeNeutralElement("canvas");var t=this._calculateCanvasSize();e.width=t.x;e.height=t.y;return e}draw(e){this._prepareNewFrame();this.viewer.viewport.getFlip()!==this._viewportFlipped&&this._flip();for(const t of e)0!==t.opacity&&this._drawTiles(t)}canRotate(){return!0}destroy(){super.destroy();this.canvas.width=1;this.canvas.height=1;this.sketchCanvas=null;this.sketchContext=null;this.container.removeChild(this.canvas)}minimumOverlapRequired(e){return!0}setImageSmoothingEnabled(e){this._imageSmoothingEnabled=!!e;this._updateImageSmoothingEnabled(this.context);this.viewer.forceRedraw()}drawDebuggingRect(e){const t=this.context;t.save();t.lineWidth=2*d.pixelDensityRatio;t.strokeStyle=this.debugGridColor[0];t.fillStyle=this.debugGridColor[0];t.strokeRect(e.x*d.pixelDensityRatio,e.y*d.pixelDensityRatio,e.width*d.pixelDensityRatio,e.height*d.pixelDensityRatio);t.restore()}get _viewportFlipped(){return this.context.getTransform().a<0}_raiseTileDrawingEvent(e,t,i,n){this.viewer.raiseEvent("tile-drawing",{tiledImage:e,context:t,tile:i,rendered:n})}_prepareNewFrame(){var e=this._calculateCanvasSize();if(this.canvas.width!==e.x||this.canvas.height!==e.y){this.canvas.width=e.x;this.canvas.height=e.y;this._updateImageSmoothingEnabled(this.context);if(null!==this.sketchCanvas){e=this._calculateSketchCanvasSize();this.sketchCanvas.width=e.x;this.sketchCanvas.height=e.y;this._updateImageSmoothingEnabled(this.sketchContext)}}this._clear()}_clear(e,t){const i=this._getContext(e);if(t)i.clearRect(t.x,t.y,t.width,t.height);else{t=i.canvas;i.clearRect(0,0,t.width,t.height)}}_drawTiles(a){var l=a.getTilesToDraw().map(e=>e.tile);if(0!==a.opacity&&(0!==l.length||a.placeholderFillStyle)){let t=l[0];let i;t&&(i=a.opacity<1||a.compositeOperation&&"source-over"!==a.compositeOperation||!a._isBottomItem()&&a.source.hasTransparency(null,t.getUrl(),t.ajaxHeaders,t.postData));let n;let r;var h=this.viewport.getZoom(!0);h=a.viewportToImageZoom(h);if(1a.smoothTileEdgesMinZoom&&!a.iOSDevice&&a.getRotation(!0)%360==0){i=!0;h=t.length&&this.getDataToDraw(t);n=h?h.canvas.width/(t.size.x*d.pixelDensityRatio):1;r=t.getTranslationForEdgeSmoothing(n,this._getCanvasSize(!1),this._getCanvasSize(!0))}let e;if(i){if(!n){e=this.viewport.viewportToViewerElementRectangle(a.getClippedBounds(!0)).getIntegerBoundingBox();e=e.times(d.pixelDensityRatio)}this._clear(!0,e)}n||this._setRotations(a,i);let o=!1;if(a._clip){this._saveContext(i);let e=a.imageToViewportRectangle(a._clip,!0);e=e.rotate(-a.getRotation(!0),a._getRotationPoint(!0));let t=this.viewportToDrawerRectangle(e);n&&(t=t.times(n));r&&(t=t.translate(r));this._setClip(t,i);o=!0}if(a._croppingPolygons){const u=this;o||this._saveContext(i);try{var c=a._croppingPolygons.map(function(e){return e.map(function(e){e=a.imageToViewportCoordinates(e.x,e.y,!0).rotate(-a.getRotation(!0),a._getRotationPoint(!0));let t=u.viewportCoordToDrawerCoord(e);n&&(t=t.times(n));r&&(t=t.plus(r));return t})});this._clipWithPolygons(c,i)}catch(e){d.console.error(e)}o=!0}a._hasOpaqueTile=!1;if(a.placeholderFillStyle&&!1===a._hasOpaqueTile){let e=this.viewportToDrawerRectangle(a.getBoundsNoRotate(!0));n&&(e=e.times(n));r&&(e=e.translate(r));let t=null;t="function"==typeof a.placeholderFillStyle?a.placeholderFillStyle(a,this.context):a.placeholderFillStyle;this._drawRectangle(e,t,i)}c=function(e){if("number"==typeof e)return m(e);if(!e||!d.Browser)return p;let t=e[d.Browser.vendor];g(t)&&(t=e["*"]);return m(t)}(a.subPixelRoundingForTransparency);let s=!1;c===d.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS?s=!0:c===d.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST&&(s=!(this.viewer&&this.viewer.isAnimating()));for(let e=0;ethis.canvas.width&&(e.width=this.canvas.width-e.x);if(e.y<0){e.height+=e.y;e.y=0}e.y+e.height>this.canvas.height&&(e.height=this.canvas.height-e.y);this.context.drawImage(this.sketchCanvas,e.x,e.y,e.width,e.height,e.x,e.y,e.width,e.height)}else{n=s.scale||1;i=(r=s.translate)instanceof d.Point?r:new d.Point(0,0);let e=0;let t=0;if(r){o=this.sketchCanvas.width-this.canvas.width;r=this.sketchCanvas.height-this.canvas.height;e=Math.round(o/2);t=Math.round(r/2)}this.context.drawImage(this.sketchCanvas,i.x-e*n,i.y-t*n,(this.canvas.width+2*e)*n,(this.canvas.height+2*t)*n,-e,-t,this.canvas.width+2*e,this.canvas.height+2*t)}this.context.restore()}_drawDebugInfoOnTile(e,t,i,n){var r=this.viewer.world.getIndexOfItem(n)%this.debugGridColor.length;const o=this.context;o.save();o.lineWidth=2*d.pixelDensityRatio;o.font="small-caps bold "+13*d.pixelDensityRatio+"px arial";o.strokeStyle=this.debugGridColor[r];o.fillStyle=this.debugGridColor[r];this._setRotations(n);this._viewportFlipped&&this._flip({point:e.position.plus(e.size.divide(2))});o.strokeRect(e.position.x*d.pixelDensityRatio,e.position.y*d.pixelDensityRatio,e.size.x*d.pixelDensityRatio,e.size.y*d.pixelDensityRatio);var s=(e.position.x+e.size.x/2)*d.pixelDensityRatio;var a=(e.position.y+e.size.y/2)*d.pixelDensityRatio;o.translate(s,a);r=this.viewport.getRotation(!0);o.rotate(Math.PI/180*-r);o.translate(-s,-a);if(0===e.x&&0===e.y){o.fillText("Zoom: "+this.viewport.getZoom(),e.position.x*d.pixelDensityRatio,(e.position.y-30)*d.pixelDensityRatio);o.fillText("Pan: "+this.viewport.getBounds().toString(),e.position.x*d.pixelDensityRatio,(e.position.y-20)*d.pixelDensityRatio)}o.fillText("Level: "+e.level,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+20)*d.pixelDensityRatio);o.fillText("Column: "+e.x,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+30)*d.pixelDensityRatio);o.fillText("Row: "+e.y,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+40)*d.pixelDensityRatio);o.fillText("Order: "+i+" of "+t,(e.position.x+10)*d.pixelDensityRatio,(e.position.y+50)*d.pixelDensityRatio);o.fillText("Size: "+e.size.toString(),(e.position.x+10)*d.pixelDensityRatio,(e.position.y+60)*d.pixelDensityRatio);o.fillText("Position: "+e.position.toString(),(e.position.x+10)*d.pixelDensityRatio,(e.position.y+70)*d.pixelDensityRatio);this.viewport.getRotation(!0)%360!=0&&this._restoreRotationChanges();n.getRotation(!0)%360!=0&&this._restoreRotationChanges();o.restore()}_updateImageSmoothingEnabled(e){e.msImageSmoothingEnabled=this._imageSmoothingEnabled;e.imageSmoothingEnabled=this._imageSmoothingEnabled}_getCanvasSize(e){e=this._getContext(e).canvas;return new d.Point(e.width,e.height)}_getCanvasCenter(){return new d.Point(this.canvas.width/2,this.canvas.height/2)}_setRotations(e,t=!1){let i=!1;if(this.viewport.getRotation(!0)%360!=0){this._offsetForRotation({degrees:this.viewport.getRotation(!0),useSketch:t,saveContext:i});i=!1}e.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:e.getRotation(!0),point:this.viewport.pixelFromPointNoRotate(e._getRotationPoint(!0),!0),useSketch:t,saveContext:i})}_offsetForRotation(e){var t=e.point?e.point.times(d.pixelDensityRatio):this._getCanvasCenter();const i=this._getContext(e.useSketch);i.save();i.translate(t.x,t.y);i.rotate(Math.PI/180*e.degrees);i.translate(-t.x,-t.y)}_flip(e){var t=(e=e||{}).point?e.point.times(d.pixelDensityRatio):this._getCanvasCenter();const i=this._getContext(e.useSketch);i.translate(t.x,0);i.scale(-1,1);i.translate(-t.x,0)}_restoreRotationChanges(e){const t=this._getContext(e);t.restore()}_calculateCanvasSize(){var e=d.pixelDensityRatio;var t=this.viewport.getContainerSize();return{x:Math.round(t.x*e),y:Math.round(t.y*e)}}_calculateSketchCanvasSize(){var e=this._calculateCanvasSize();if(0===this.viewport.getRotation())return e;e=Math.ceil(Math.sqrt(e.x*e.x+e.y*e.y));return{x:e,y:e}}}d.CanvasDrawer=t;const p=d.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;function g(e){return e!==d.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS&&e!==d.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST&&e!==d.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER}function m(e){return g(e)?p:e}}(OpenSeadragon);!function(C){const l=C;class d{constructor(e){this._renderingCanvas=e.renderingCanvas;this._unpackWithPremultipliedAlpha=!!e.unpackWithPremultipliedAlpha;this._imageSmoothingEnabled=void 0===e.imageSmoothingEnabled||e.imageSmoothingEnabled;this._initShaderProgram=e.initShaderProgram;this._gl=null;this._isWebGL2=!1;this._extTextureFilterAnisotropic=null;this._maxAnisotropy=0;this._firstPass=null;this._secondPass=null;this._glFrameBuffer=null;this._renderToTexture=null;this._glNumTextures=0;this._unitQuad=null;this._destroyed=!1;this._gl=this._renderingCanvas.getContext("webgl2");if(this._gl){this._isWebGL2=!0;this._setupWebGLExtensions()}else{this._gl=this._renderingCanvas.getContext("webgl");this._isWebGL2=!1;this._gl&&this._setupWebGLExtensions()}this._gl&&this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,this._unpackWithPremultipliedAlpha)}getContext(){return this._gl}isWebGL2(){return this._isWebGL2}getMaxTextures(){return this._gl?this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS):0}getRenderingCanvas(){return this._renderingCanvas}getFirstPass(){return this._firstPass}getSecondPass(){return this._secondPass}getFrameBuffer(){return this._glFrameBuffer}getRenderToTexture(){return this._renderToTexture}getUnitQuad(){return this._unitQuad}_setupWebGLExtensions(){const e=this._gl;this._extTextureFilterAnisotropic=e.getExtension("EXT_texture_filter_anisotropic")||e.getExtension("WEBKIT_EXT_texture_filter_anisotropic")||e.getExtension("MOZ_EXT_texture_filter_anisotropic");this._extTextureFilterAnisotropic&&(this._maxAnisotropy=e.getParameter(this._extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT))}getTextureFilter(){var e=this._gl;return this._imageSmoothingEnabled?e.LINEAR:e.NEAREST}_applyAnisotropy(){if(this._imageSmoothingEnabled&&this._extTextureFilterAnisotropic&&!(this._maxAnisotropy<=0)){const e=this._gl;e.texParameterf(e.TEXTURE_2D,this._extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(4,this._maxAnisotropy))}}setupRenderer(e,t){const i=this._gl;if(i){this._unitQuad=this.makeQuadVertexBuffer(0,1,0,1);this._makeFirstPassShaderProgram();this._makeSecondPassShaderProgram();this._renderToTexture=i.createTexture();i.activeTexture(i.TEXTURE0);i.bindTexture(i.TEXTURE_2D,this._renderToTexture);i.texImage2D(i.TEXTURE_2D,0,i.RGBA,e,t,0,i.RGBA,i.UNSIGNED_BYTE,null);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter());this._applyAnisotropy();i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE);this._glFrameBuffer=i.createFramebuffer();i.bindFramebuffer(i.FRAMEBUFFER,this._glFrameBuffer);i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,this._renderToTexture,0);i.enable(i.BLEND);i.blendFunc(i.ONE,i.ONE_MINUS_SRC_ALPHA)}else C.console.error("WebGL context not available for setupRenderer")}resizeRenderer(e,t){const i=this._gl;if(i){i.viewport(0,0,e,t);i.deleteTexture(this._renderToTexture);this._renderToTexture=i.createTexture();i.activeTexture(i.TEXTURE0);i.bindTexture(i.TEXTURE_2D,this._renderToTexture);i.texImage2D(i.TEXTURE_2D,0,i.RGBA,e,t,0,i.RGBA,i.UNSIGNED_BYTE,null);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter());this._applyAnisotropy();i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE);i.bindFramebuffer(i.FRAMEBUFFER,this._glFrameBuffer);i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,this._renderToTexture,0)}}createTexture(e,t={}){const i=this._gl;if(!i)return null;var n=i.createTexture();i.activeTexture(i.TEXTURE0);i.bindTexture(i.TEXTURE_2D,n);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE);i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter());i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,this.getTextureFilter());this._applyAnisotropy();try{var r=void 0!==t.unpackWithPremultipliedAlpha?t.unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha;i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,r);i.texImage2D(i.TEXTURE_2D,0,i.RGBA,i.RGBA,i.UNSIGNED_BYTE,e);return n}catch(e){i.deleteTexture(n);return null}}deleteTexture(e){this._gl&&e&&this._gl.deleteTexture(e)}setImageSmoothingEnabled(e){this._imageSmoothingEnabled=!!e}setUnpackWithPremultipliedAlpha(e){this._unpackWithPremultipliedAlpha=!!e;this._gl&&this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,this._unpackWithPremultipliedAlpha)}makeQuadVertexBuffer(e,t,i,n){return new Float32Array([e,n,t,n,e,i,e,i,t,n,t,i])}_makeFirstPassShaderProgram(){const t=this._glNumTextures=this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS);var e=` - attribute vec2 a_output_position; - attribute vec2 a_texture_position; - attribute float a_index; - - ${[...Array(t).keys()].map(e=>`uniform mat3 u_matrix_${e};`).join("\n")} // create a uniform mat3 for each potential tile to draw - - varying vec2 v_texture_position; - varying float v_image_index; - - void main() { - - mat3 transform_matrix; // value will be set by the if/elses in makeConditional() - - ${[...Array(t).keys()].map(e=>`${0n.getUniformLocation(r,"u_matrix_"+e)),uImages:n.getUniformLocation(r,"u_images"),uOpacities:n.getUniformLocation(r,"u_opacities"),bufferOutputPosition:n.createBuffer(),bufferTexturePosition:n.createBuffer(),bufferIndex:n.createBuffer()};n.uniform1iv(this._firstPass.uImages,[...Array(t).keys()]);const o=new Float32Array(12*t);for(let e=0;eArray(6).fill(e)).flat();n.bufferData(n.ARRAY_BUFFER,new Float32Array(i),n.STATIC_DRAW);n.enableVertexAttribArray(this._firstPass.aIndex)}_makeSecondPassShaderProgram(){const e=this._gl;var t=this._initShaderProgram(e,` - attribute vec2 a_output_position; - attribute vec2 a_texture_position; - - varying vec2 v_texture_position; - - void main() { - // Transform to clip space (0:1 --> -1:1) - gl_Position = vec4(vec3(a_output_position * 2.0 - 1.0, 1), 1); - - v_texture_position = a_texture_position; - } - `,` - precision mediump float; - - // our texture - uniform sampler2D u_image; - - // the texCoords passed in from the vertex shader. - varying vec2 v_texture_position; - - // the opacity multiplier for the image - uniform float u_opacity_multiplier; - - void main() { - gl_FragColor = texture2D(u_image, v_texture_position); - gl_FragColor *= u_opacity_multiplier; - } - `);e.useProgram(t);this._secondPass={shaderProgram:t,aOutputPosition:e.getAttribLocation(t,"a_output_position"),aTexturePosition:e.getAttribLocation(t,"a_texture_position"),uImage:e.getUniformLocation(t,"u_image"),uOpacityMultiplier:e.getUniformLocation(t,"u_opacity_multiplier"),bufferOutputPosition:e.createBuffer(),bufferTexturePosition:e.createBuffer()};e.bindBuffer(e.ARRAY_BUFFER,this._secondPass.bufferOutputPosition);e.bufferData(e.ARRAY_BUFFER,this._unitQuad,e.STATIC_DRAW);e.enableVertexAttribArray(this._secondPass.aOutputPosition);e.bindBuffer(e.ARRAY_BUFFER,this._secondPass.bufferTexturePosition);e.bufferData(e.ARRAY_BUFFER,this._unitQuad,e.DYNAMIC_DRAW);e.enableVertexAttribArray(this._secondPass.aTexturePosition)}destroy(){if(!this._destroyed){this._destroyed=!0;const i=this._gl;if(i){try{var t=i.getParameter(i.MAX_TEXTURE_IMAGE_UNITS);if(t&&00!==e))return!0;C.console.warn("[WebGLDrawer.isSupported] Functional test failed: no non-zero pixels read back.");return!1}catch(e){C.console.warn("[WebGLDrawer.isSupported] Functional test failed:",e&&e.message?e.message:e);return!1}finally{try{t&&e&&e.deleteTexture(t);if(e)e.destroy();else if(i){const u=i.getExtension("WEBGL_lose_context");u&&u.loseContext()}}catch(e){}}}getType(){return"webgl"}isWebGL2(){return!!this._glContext&&this._glContext.isWebGL2()}setContextRecoveryEnabled(e){this._enableContextRecovery=!!e}isContextRecoveryEnabled(){return this._enableContextRecovery}minimumOverlapRequired(e){return e.hasIssue("webgl")}_createDrawingElement(){const e=C.makeNeutralElement("canvas");var t=this._calculateCanvasSize();e.width=t.x;e.height=t.y;return e}_getBackupCanvasDrawer(){if(!this._backupCanvasDrawer){this._backupCanvasDrawer=this.viewer.requestDrawer("canvas",{mainDrawer:!1});this._backupCanvasDrawer.canvas.style.setProperty("visibility","hidden");this._backupCanvasDrawer.getSupportedDataFormats=()=>this._supportedFormats;this._backupCanvasDrawer.getDataToDraw=this.getDataToDraw.bind(this)}return this._backupCanvasDrawer}_draw(e,t=0){const w=this._glContext?this._glContext.getContext():null;if(w){const _=this._glContext.getFirstPass();const T=this._glContext.getSecondPass();const x=this._glContext.getFrameBuffer();const S=this._glContext.getRenderToTexture();var i=this.viewport.getBoundsNoRotateWithMargins(!0);const r=i,o=new l.Point(i.x+i.width/2,i.y+i.height/2),s=this.viewport.getRotation(!0)*Math.PI/180;var n=this.viewport.flipped?-1:1;i=C.Mat3.makeTranslation(-o.x,-o.y);const a=C.Mat3.makeScaling(2/r.width*n,-2/r.height);n=C.Mat3.makeRotation(-s);const E=a.multiply(n).multiply(i);w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT);this._outputContext.clearRect(0,0,this._outputCanvas.width,this._outputCanvas.height);let y=!1;e.forEach((i,e)=>{if(i.getIssue("webgl")){if(y){this._outputContext.drawImage(this._renderingCanvas,0,0);w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT);y=!1}if(this._canvasFallbackAllowed){const t=this._getBackupCanvasDrawer();t.draw([i]);this._outputContext.drawImage(t.canvas,0,0)}}else{const m=i.getTilesToDraw();i.placeholderFillStyle&&!1===i._hasOpaqueTile&&this._drawPlaceholder(i);if(0!==m.length&&0!==i.getOpacity()){var n=m[0];var r=i.compositeOperation||this.viewer.compositeOperation||i._clip||i._croppingPolygons||i.debugMode;var o=r||i.opacity<1||n.tile.hasTransparency;if(r){y&&this._outputContext.drawImage(this._renderingCanvas,0,0);w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT)}w.useProgram(_.shaderProgram);if(o){w.bindFramebuffer(w.FRAMEBUFFER,x);w.clear(w.COLOR_BUFFER_BIT)}else w.bindFramebuffer(w.FRAMEBUFFER,null);let t=E;var s=i.getRotation(!0);if(s%360!=0){n=C.Mat3.makeRotation(-s*Math.PI/180);s=i.getBoundsNoRotate(!0).getCenter();const v=C.Mat3.makeTranslation(s.x,s.y);s=C.Mat3.makeTranslation(-s.x,-s.y);s=v.multiply(n).multiply(s);t=E.multiply(s)}var a=this._glContext.getMaxTextures();if(a<=0||null==a)throw new Error(`WebGL error: bad value for gl parameter MAX_TEXTURE_IMAGE_UNITS (${a}). This could happen - if too many contexts have been created and not released, or there is another problem with the graphics card.`);var l=new Float32Array(12*a);var h=new Array(a);const f=new Array(a);var c=new Array(a);for(let e=0;e{w.uniformMatrix3fv(_.uTransformMatrices[t],!1,e)});w.uniform1fv(_.uOpacities,new Float32Array(c));w.bindBuffer(w.ARRAY_BUFFER,_.bufferOutputPosition);w.vertexAttribPointer(_.aOutputPosition,2,w.FLOAT,!1,0,0);w.bindBuffer(w.ARRAY_BUFFER,_.bufferTexturePosition);w.vertexAttribPointer(_.aTexturePosition,2,w.FLOAT,!1,0,0);w.bindBuffer(w.ARRAY_BUFFER,_.bufferIndex);w.vertexAttribPointer(_.aIndex,1,w.FLOAT,!1,0,0);w.drawArrays(w.TRIANGLES,0,6*p)}}if(o){w.useProgram(T.shaderProgram);w.bindFramebuffer(w.FRAMEBUFFER,null);w.activeTexture(w.TEXTURE0);w.bindTexture(w.TEXTURE_2D,S);w.uniform1f(T.uOpacityMultiplier,i.opacity);w.bindBuffer(w.ARRAY_BUFFER,T.bufferTexturePosition);w.vertexAttribPointer(T.aTexturePosition,2,w.FLOAT,!1,0,0);w.bindBuffer(w.ARRAY_BUFFER,T.bufferOutputPosition);w.vertexAttribPointer(T.aOutputPosition,2,w.FLOAT,!1,0,0);w.drawArrays(w.TRIANGLES,0,6)}y=!0;if(r){this._applyContext2dPipeline(i,m,e);y=!1;w.bindFramebuffer(w.FRAMEBUFFER,null);w.clear(w.COLOR_BUFFER_BIT)}0===e&&this._raiseTiledImageDrawnEvent(i,m.map(e=>e.tile))}}});y&&this._outputContext.drawImage(this._renderingCanvas,0,0)}}draw(t,i=!1){try{this._draw(t,i)}catch(e){if(!this._isWebGLContextError(e))throw e;if(this._enableContextRecovery&&!i){C.console.warn("WebGL context error detected during draw operation, attempting to recreate context...",e);if(this._recreateContext()){C.console.info("WebGL context recreated successfully, retrying draw operation");this.viewer&&this.viewer.raiseEvent("webgl-context-recovered",{drawer:this,error:e});this.draw(t,!0)}else this._fallbackToCanvasDrawer(e,t)}else{if(!this._enableContextRecovery)throw e;this._fallbackToCanvasDrawer(e,t)}}}setImageSmoothingEnabled(e){if(this._imageSmoothingEnabled!==e){this._imageSmoothingEnabled=e;this._glContext&&this._glContext.setImageSmoothingEnabled(e);this.setInternalCacheNeedsRefresh();this.viewer.forceRedraw()}}setUnpackWithPremultipliedAlpha(e){if(this._unpackWithPremultipliedAlpha!==e){this._unpackWithPremultipliedAlpha=e;this._glContext&&this._glContext.setUnpackWithPremultipliedAlpha(e);this.setInternalCacheNeedsRefresh();this.viewer.forceRedraw()}}drawDebuggingRect(e){const t=this._outputContext;t.save();t.lineWidth=2*C.pixelDensityRatio;t.strokeStyle=this.debugGridColor[0];t.fillStyle=this.debugGridColor[0];t.strokeRect(e.x*C.pixelDensityRatio,e.y*C.pixelDensityRatio,e.width*C.pixelDensityRatio,e.height*C.pixelDensityRatio);t.restore()}_applyContext2dPipeline(e,t,i){this._outputContext.save();this._outputContext.globalCompositeOperation=0===i?null:e.compositeOperation||this.viewer.compositeOperation;if(e._croppingPolygons||e._clip){this._renderToClippingCanvas(e);this._outputContext.drawImage(this._clippingCanvas,0,0)}else this._outputContext.drawImage(this._renderingCanvas,0,0);this._outputContext.restore();if(e.debugMode){i=this.viewer.viewport.getFlip();i&&this._flip();this._drawDebugInfo(t,e,i);i&&this._flip()}}_getTileData(e,t,i,n,r,o,s,a,l){var h=i.texture;var c=i.position;var u=i.overlapFraction;o.set(c,12*r);i=e.positionedBounds.width*u.x;o=e.positionedBounds.height*u.y;c=e.positionedBounds.x+(0===e.x?0:i);u=e.positionedBounds.y+(0===e.y?0:o);i=e.positionedBounds.x+e.positionedBounds.width-(e.isRightMost?0:i);o=e.positionedBounds.y+e.positionedBounds.height-(e.isBottomMost?0:o);const d=new C.Mat3([i-c,0,0,0,o-u,0,c,u,1]);e.flipped&&d.scaleAndTranslateSelf(-1,1,1,0);d.scaleAndTranslateOtherSetSelf(n);l[r]=e.opacity;s[r]=h;a[r]=d.values}_setupRenderer(){this._glContext&&this._glContext.getContext()?this._glContext.setupRenderer(this._renderingCanvas.width,this._renderingCanvas.height):C.console.error("_setupCanvases must be called before _setupRenderer")}_resizeRenderer(){this._glContext&&this._glContext.resizeRenderer(this._renderingCanvas.width,this._renderingCanvas.height)}_setupCanvases(){const t=this;this._outputCanvas=this.canvas;this._outputContext=this._outputCanvas.getContext("2d");this._renderingCanvas=document.createElement("canvas");this._clippingCanvas=document.createElement("canvas");this._clippingContext=this._clippingCanvas.getContext("2d");this._renderingCanvas.width=this._clippingCanvas.width=this._outputCanvas.width;this._renderingCanvas.height=this._clippingCanvas.height=this._outputCanvas.height;this._glContext=new d({renderingCanvas:this._renderingCanvas,unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha,imageSmoothingEnabled:this._imageSmoothingEnabled,initShaderProgram:this.constructor.initShaderProgram});this._resizeHandler=function(){if(t._outputCanvas!==t.viewer.drawer.canvas){t._outputCanvas.style.width=t.viewer.drawer.canvas.clientWidth+"px";t._outputCanvas.style.height=t.viewer.drawer.canvas.clientHeight+"px"}var e=t._calculateCanvasSize();if(t._outputCanvas.width!==e.x||t._outputCanvas.height!==e.y){t._outputCanvas.width=e.x;t._outputCanvas.height=e.y}t._renderingCanvas.style.width=t._outputCanvas.clientWidth+"px";t._renderingCanvas.style.height=t._outputCanvas.clientHeight+"px";t._renderingCanvas.width=t._clippingCanvas.width=t._outputCanvas.width;t._renderingCanvas.height=t._clippingCanvas.height=t._outputCanvas.height;t._resizeRenderer()};this.viewer.addHandler("resize",this._resizeHandler)}_isWebGLContextError(e){if(!e||!e.message)return!1;const t=e.message.toLowerCase();return t.includes("max_texture_image_units")||t.includes("webgl")&&(t.includes("context")||t.includes("lost")||t.includes("invalid"))}_recreateContext(){if(this._destroyed)return null;try{var e=this._renderingCanvas;var t=e.width;var i=e.height;var n=e.style.width;var r=e.style.height;this.destroyInternalCache();if(this._glContext){this._glContext.destroy();this._glContext=null}this._renderingCanvas=document.createElement("canvas");this._renderingCanvas.width=t;this._renderingCanvas.height=i;n&&(this._renderingCanvas.style.width=n);r&&(this._renderingCanvas.style.height=r);this._glContext=new d({renderingCanvas:this._renderingCanvas,unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha,imageSmoothingEnabled:this._imageSmoothingEnabled,initShaderProgram:this.constructor.initShaderProgram});if(!this._glContext.getContext()){C.console.error("Failed to recreate WebGL context: no GL context");return null}try{var o=this._glContext.getMaxTextures();if(!o||o<=0){C.console.error("Failed to recreate WebGL context: invalid MAX_TEXTURE_IMAGE_UNITS");return null}}catch(e){C.console.error("Failed to verify new WebGL context:",e);return null}this._setupRenderer();this.setInternalCacheNeedsRefresh();return this}catch(e){C.console.error("Failed to recreate WebGL context:",e);return null}}_fallbackToCanvasDrawer(e,t){if(!this._canvasFallbackAllowed){this._raiseContextRecoveryFailedEvent(e,null);throw e}var i=this.viewer.requestDrawer("canvas",{mainDrawer:!0,redrawImmediately:!1});if(!i){C.console.error("Failed to create canvas drawer as fallback");this._raiseContextRecoveryFailedEvent(e,null);throw e}C.console.error("Failed to recreate WebGL context, switching to canvas drawer");this._raiseContextRecoveryFailedEvent(e,i);this.viewer.world.requestInvalidate(!0)}_raiseContextRecoveryFailedEvent(e,t=null){this.viewer&&this.viewer.raiseEvent("webgl-context-recovery-failed",{drawer:this,canvasDrawer:t,error:e})}internalCacheCreate(i,n){const r=n.tiledImage;if(!(this._glContext?this._glContext.getContext():null)){C.console.error("WebGL context not available in internalCacheCreate");return{}}let o;let s=i.data;let e=!1;if(s instanceof CanvasRenderingContext2D){s=s.canvas;e=!0}if(!r.getIssue("webgl"))if(e&&C.isCanvasTainted(s)){r.setIssue("webgl","WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?");this._raiseDrawerErrorEvent(r,this._canvasFallbackAllowed?"Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.":"Tainted data cannot be used by the WebGLDrawer, and canvas fallback is not enabled.");this.setInternalCacheNeedsRefresh()}else{let e,t;if(n.sourceBounds){e=Math.min(n.sourceBounds.width,s.width)/s.width;t=Math.min(n.sourceBounds.height,s.height)/s.height}else{e=1;t=1}var a=r.source.tileOverlap;var l=this._calculateOverlapFraction(n,r);if(0{e=t.imageToViewportCoordinates(e.x,e.y,!0).rotate(this.viewer.viewport.getRotation(!0),this.viewer.viewport.getCenter(!0));return this.viewportCoordToDrawerCoord(e)});this._clippingContext.beginPath();n.forEach((e,t)=>{this._clippingContext[0===t?"moveTo":"lineTo"](e.x,e.y)});this._clippingContext.clip();this._setClip()}if(t._croppingPolygons){const r=t._croppingPolygons.map(e=>e.map(e=>{e=t.imageToViewportCoordinates(e.x,e.y,!0).rotate(this.viewer.viewport.getRotation(!0),this.viewer.viewport.getCenter(!0));return this.viewportCoordToDrawerCoord(e)}));this._clippingContext.beginPath();r.forEach(e=>{e.forEach((e,t)=>{this._clippingContext[0===t?"moveTo":"lineTo"](e.x,e.y)})});this._clippingContext.clip()}if(this.viewer.viewport.getFlip()){e=new C.Point(this.canvas.width/2,this.canvas.height/2);this._clippingContext.translate(e.x,0);this._clippingContext.scale(-1,1);this._clippingContext.translate(-e.x,0)}this._clippingContext.drawImage(this._renderingCanvas,0,0);this._clippingContext.restore()}_setRotations(e){let t=!1;if(this.viewport.getRotation(!0)%360!=0){this._offsetForRotation({degrees:this.viewport.getRotation(!0),saveContext:t});t=!1}e.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:e.getRotation(!0),point:this.viewport.pixelFromPointNoRotate(e._getRotationPoint(!0),!0),saveContext:t})}_offsetForRotation(e){var t=e.point?e.point.times(C.pixelDensityRatio):this._getCanvasCenter();const i=this._outputContext;i.save();i.translate(t.x,t.y);i.rotate(Math.PI/180*e.degrees);i.translate(-t.x,-t.y)}_flip(e){e=(e=e||{}).point?e.point.times(C.pixelDensityRatio):this._getCanvasCenter();const t=this._outputContext;t.translate(e.x,0);t.scale(-1,1);t.translate(-e.x,0)}_drawDebugInfo(t,i,n){for(let e=t.length-1;0<=e;e--){var r=t[e].tile;try{this._drawDebugInfoOnTile(r,t.length,e,i,n)}catch(e){C.console.error(e)}}}_drawDebugInfoOnTile(e,t,i,n,r){var o=this.viewer.world.getIndexOfItem(n)%this.debugGridColor.length;const s=this.context;s.save();s.lineWidth=2*C.pixelDensityRatio;s.font="small-caps bold "+13*C.pixelDensityRatio+"px arial";s.strokeStyle=this.debugGridColor[o];s.fillStyle=this.debugGridColor[o];this._setRotations(n);r&&this._flip({point:e.position.plus(e.size.divide(2))});s.strokeRect(e.position.x*C.pixelDensityRatio,e.position.y*C.pixelDensityRatio,e.size.x*C.pixelDensityRatio,e.size.y*C.pixelDensityRatio);var a=(e.position.x+e.size.x/2)*C.pixelDensityRatio;o=(e.position.y+e.size.y/2)*C.pixelDensityRatio;s.translate(a,o);r=this.viewport.getRotation(!0);s.rotate(Math.PI/180*-r);s.translate(-a,-o);if(0===e.x&&0===e.y){s.fillText("Zoom: "+this.viewport.getZoom(),e.position.x*C.pixelDensityRatio,(e.position.y-30)*C.pixelDensityRatio);s.fillText("Pan: "+this.viewport.getBounds().toString(),e.position.x*C.pixelDensityRatio,(e.position.y-20)*C.pixelDensityRatio)}s.fillText("Level: "+e.level,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+20)*C.pixelDensityRatio);s.fillText("Column: "+e.x,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+30)*C.pixelDensityRatio);s.fillText("Row: "+e.y,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+40)*C.pixelDensityRatio);s.fillText("Order: "+i+" of "+t,(e.position.x+10)*C.pixelDensityRatio,(e.position.y+50)*C.pixelDensityRatio);s.fillText("Size: "+e.size.toString(),(e.position.x+10)*C.pixelDensityRatio,(e.position.y+60)*C.pixelDensityRatio);s.fillText("Position: "+e.position.toString(),(e.position.x+10)*C.pixelDensityRatio,(e.position.y+70)*C.pixelDensityRatio);this.viewport.getRotation(!0)%360!=0&&this._restoreRotationChanges();n.getRotation(!0)%360!=0&&this._restoreRotationChanges();s.restore()}_drawPlaceholder(e){var t=e.getBounds(!0);var i=this.viewportToDrawerRectangle(e.getBounds(!0));const n=this._outputContext;let r;r="function"==typeof e.placeholderFillStyle?e.placeholderFillStyle(e,n):e.placeholderFillStyle;this._offsetForRotation({degrees:this.viewer.viewport.getRotation(!0)});n.fillStyle=r;n.translate(i.x,i.y);n.rotate(Math.PI/180*t.degrees);n.translate(-i.x,-i.y);n.fillRect(i.x,i.y,i.width,i.height);this._restoreRotationChanges()}_getCanvasCenter(){return new C.Point(this.canvas.width/2,this.canvas.height/2)}_restoreRotationChanges(){const e=this._outputContext;e.restore()}static initShaderProgram(e,t,i){function n(e,t,i){t=e.createShader(t);e.shaderSource(t,i);e.compileShader(t);if(e.getShaderParameter(t,e.COMPILE_STATUS))return t;C.console.error("An error occurred compiling the shaders: "+e.getShaderInfoLog(t));e.deleteShader(t);return null}var r=n(e,e.VERTEX_SHADER,t);t=n(e,e.FRAGMENT_SHADER,i);i=e.createProgram();e.attachShader(i,r);e.attachShader(i,t);e.linkProgram(i);if(e.getProgramParameter(i,e.LINK_STATUS))return i;C.console.error("Unable to initialize the shader program: "+e.getProgramInfoLog(i));return null}}}(OpenSeadragon);!function(c){c.Viewport=function(e){var t=arguments;if((e=t.length&&t[0]instanceof c.Point?{containerSize:t[0],contentSize:t[1],config:t[2]}:e).config){c.extend(!0,e,e.config);delete e.config}this._margins=c.extend({left:0,top:0,right:0,bottom:0},e.margins||{});delete e.margins;e.initialDegrees=e.degrees;delete e.degrees;c.extend(!0,this,{containerSize:null,contentSize:null,zoomPoint:null,rotationPivot:null,viewer:null,springStiffness:c.DEFAULT_SETTINGS.springStiffness,animationTime:c.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:c.DEFAULT_SETTINGS.minZoomImageRatio,maxZoomPixelRatio:c.DEFAULT_SETTINGS.maxZoomPixelRatio,visibilityRatio:c.DEFAULT_SETTINGS.visibilityRatio,wrapHorizontal:c.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:c.DEFAULT_SETTINGS.wrapVertical,defaultZoomLevel:c.DEFAULT_SETTINGS.defaultZoomLevel,minZoomLevel:c.DEFAULT_SETTINGS.minZoomLevel,maxZoomLevel:c.DEFAULT_SETTINGS.maxZoomLevel,initialDegrees:c.DEFAULT_SETTINGS.degrees,flipped:c.DEFAULT_SETTINGS.flipped,homeFillsViewer:c.DEFAULT_SETTINGS.homeFillsViewer,silenceMultiImageWarnings:c.DEFAULT_SETTINGS.silenceMultiImageWarnings},e);this._updateContainerInnerSize();this.centerSpringX=new c.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.centerSpringY=new c.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.zoomSpring=new c.Spring({exponential:!0,initial:1,springStiffness:this.springStiffness,animationTime:this.animationTime});this.degreesSpring=new c.Spring({initial:e.initialDegrees,springStiffness:this.springStiffness,animationTime:this.animationTime});this._oldCenterX=this.centerSpringX.current.value;this._oldCenterY=this.centerSpringY.current.value;this._oldZoom=this.zoomSpring.current.value;this._oldDegrees=this.degreesSpring.current.value;this._sizeChanged=!1;this._setContentBounds(new c.Rect(0,0,1,1),1);this.goHome(!0);this.update()};c.Viewport.prototype={get degrees(){c.console.warn("Accessing [Viewport.degrees] is deprecated. Use viewport.getRotation instead.");return this.getRotation()},set degrees(e){c.console.warn("Setting [Viewport.degrees] is deprecated. Use viewport.rotateTo, viewport.rotateBy, or viewport.setRotation instead.");this.rotateTo(e)},resetContentSize:function(e){c.console.assert(e,"[Viewport.resetContentSize] contentSize is required");c.console.assert(e instanceof c.Point,"[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point");c.console.assert(0r.width?this.visibilityRatio*r.width:this.visibilityRatio*n.width;t=r.x-a+e;i=l-n.x-e;if(e>r.width){n.x+=(t+i)/2;o=!0}else if(i<0){n.x+=i;o=!0}else if(0r.height?this.visibilityRatio*r.height:this.visibilityRatio*n.height;t=r.y-a+e;i=l-n.y-e;if(e>r.height){n.y+=(t+i)/2;s=!0}else if(i<0){n.y+=i;s=!0}else if(0=r?s.height=s.width/r:s.width=s.height*r;s.x=o.x-s.width/2;s.y=o.y-s.height/2;let a=1/s.width;if(i){this.panTo(o,!0);this.zoomTo(a,null,!0);n&&this.applyConstraints(!0);return this}t=this.getCenter(!0);e=this.getZoom(!0);this.panTo(t,!0);this.zoomTo(e,null,!0);const l=this.getBounds();r=this.getZoom();if(0===r||Math.abs(a/r-1)<1e-8){this.zoomTo(a,null,!0);this.panTo(o,i);n&&this.applyConstraints(!1);return this}if(n){this.panTo(o,!1);a=this._applyZoomConstraints(a);this.zoomTo(a,null,!1);o=this.getConstrainedBounds();this.panTo(t,!0);this.zoomTo(e,null,!0);this.fitBounds(o)}else{const h=s.rotate(-this.getRotation());r=h.getTopLeft().times(a).minus(l.getTopLeft().times(r)).divide(a-r);this.zoomTo(a,r,i)}return this},fitBounds:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!1})},fitBoundsWithConstraints:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!0})},fitVertically:function(e){var t=new c.Rect(this._contentBounds.x+this._contentBounds.width/2,this._contentBounds.y,0,this._contentBounds.height);return this.fitBounds(t,e)},fitHorizontally:function(e){var t=new c.Rect(this._contentBounds.x,this._contentBounds.y+this._contentBounds.height/2,this._contentBounds.width,0);return this.fitBounds(t,e)},getConstrainedBounds:function(e){e=this.getBounds(e);return this._applyBoundaryConstraints(e)},panBy:function(e,t){const i=new c.Point;if(t){i.x=this.centerSpringX.current.value;i.y=this.centerSpringY.current.value}else{i.x=this.centerSpringX.target.value;i.y=this.centerSpringY.target.value}return this.panTo(i.plus(e),t)},panTo:function(e,t){if(t){this.centerSpringX.resetTo(e.x);this.centerSpringY.resetTo(e.y)}else{this.centerSpringX.springTo(e.x);this.centerSpringY.springTo(e.y)}this.viewer&&this.viewer.raiseEvent("pan",{center:e,immediately:t});return this},zoomBy:function(e,t,i){return this.zoomTo(this.zoomSpring.target.value*e,t,i)},zoomTo:function(e,t,i){const n=this;this.zoomPoint=t instanceof c.Point&&!isNaN(t.x)&&!isNaN(t.y)?t:null;i?this._adjustCenterSpringsForZoomPoint(function(){n.zoomSpring.resetTo(e)}):this.zoomSpring.springTo(e);this.viewer&&this.viewer.raiseEvent("zoom",{zoom:e,refPoint:t,immediately:i});return this},setRotation:function(e,t){return this.rotateTo(e,null,t)},getRotation:function(e){return(e?this.degreesSpring.current:this.degreesSpring.target).value},setRotationWithPivot:function(e,t,i){return this.rotateTo(e,t,i)},rotateTo:function(t,i,e){if(!this.viewer||!this.viewer.drawer.canRotate())return this;if(this.degreesSpring.target.value===t&&this.degreesSpring.isAtTargetValue())return this;this.rotationPivot=i instanceof c.Point&&!isNaN(i.x)&&!isNaN(i.y)?i:null;if(e)if(this.rotationPivot){if(!(t-this._oldDegrees)){this.rotationPivot=null;return this}this._rotateAboutPivot(t)}else this.degreesSpring.resetTo(t);else{var n=c.positiveModulo(this.degreesSpring.current.value,360);let e=c.positiveModulo(t,360);i=e-n;180this.getMaxZoom()&&this.applyConstraints(i)}}}}(OpenSeadragon);!function(v){v.TiledImage=function(e){this._initialized=!1;v.console.assert(e.tileCache,"[TiledImage] options.tileCache is required");v.console.assert(e.drawer,"[TiledImage] options.drawer is required");v.console.assert(e.viewer,"[TiledImage] options.viewer is required");v.console.assert(e.imageLoader,"[TiledImage] options.imageLoader is required");v.console.assert(e.source,"[TiledImage] options.source is required");v.console.assert(!e.clip||e.clip instanceof v.Rect,"[TiledImage] options.clip must be an OpenSeadragon.Rect if present");v.EventSource.call(this);this._optimalWorldIndex=void 0;this._tileCache=e.tileCache;delete e.tileCache;this._drawer=e.drawer;delete e.drawer;this._imageLoader=e.imageLoader;delete e.imageLoader;e.clip instanceof v.Rect&&(this._clip=e.clip.clone());delete e.clip;var t=e.x||0;delete e.x;var i=e.y||0;delete e.y;this.normHeight=e.source.dimensions.y/e.source.dimensions.x;this.contentAspectX=e.source.dimensions.x/e.source.dimensions.y;let n=1;if(e.width){n=e.width;delete e.width;if(e.height){v.console.error("specifying both width and height to a tiledImage is not supported");delete e.height}}else if(e.height){n=e.height/this.normHeight;delete e.height}var r=e.fitBounds;delete e.fitBounds;var o=e.fitBoundsPlacement||OpenSeadragon.Placement.CENTER;delete e.fitBoundsPlacement;var s=e.degrees||0;delete e.degrees;var a=e.ajaxHeaders;delete e.ajaxHeaders;this.crossOriginPolicy=e.crossOriginPolicy;delete e.crossOriginPolicy;v.extend(!0,this,{viewer:null,tilesMatrix:{},coverage:{},loadingCoverage:{},lastResetTime:0,_needsDraw:!0,_needsUpdate:!0,_hasOpaqueTile:!1,_tilesLoading:0,_zombieCache:!1,_tilesToDraw:[],_lastDrawn:[],_arrayCacheMap:[],_isBlending:!1,_wasBlending:!1,_issues:{},springStiffness:v.DEFAULT_SETTINGS.springStiffness,animationTime:v.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:v.DEFAULT_SETTINGS.minZoomImageRatio,wrapHorizontal:v.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:v.DEFAULT_SETTINGS.wrapVertical,immediateRender:v.DEFAULT_SETTINGS.immediateRender,loadDestinationTilesOnAnimation:v.DEFAULT_SETTINGS.loadDestinationTilesOnAnimation,blendTime:v.DEFAULT_SETTINGS.blendTime,alwaysBlend:v.DEFAULT_SETTINGS.alwaysBlend,minPixelRatio:v.DEFAULT_SETTINGS.minPixelRatio,smoothTileEdgesMinZoom:v.DEFAULT_SETTINGS.smoothTileEdgesMinZoom,iOSDevice:v.DEFAULT_SETTINGS.iOSDevice,debugMode:v.DEFAULT_SETTINGS.debugMode,ajaxWithCredentials:v.DEFAULT_SETTINGS.ajaxWithCredentials,placeholderFillStyle:v.DEFAULT_SETTINGS.placeholderFillStyle,opacity:v.DEFAULT_SETTINGS.opacity,preload:v.DEFAULT_SETTINGS.preload,compositeOperation:v.DEFAULT_SETTINGS.compositeOperation,subPixelRoundingForTransparency:v.DEFAULT_SETTINGS.subPixelRoundingForTransparency,maxTilesPerFrame:v.DEFAULT_SETTINGS.maxTilesPerFrame,originalDataType:void 0,_currentMaxTilesPerFrame:10*(e.maxTilesPerFrame||v.DEFAULT_SETTINGS.maxTilesPerFrame)},e);this._preload=this.preload;delete this.preload;this._fullyLoaded=!1;this._xSpring=new v.Spring({initial:t,springStiffness:this.springStiffness,animationTime:this.animationTime});this._ySpring=new v.Spring({initial:i,springStiffness:this.springStiffness,animationTime:this.animationTime});this._scaleSpring=new v.Spring({initial:n,springStiffness:this.springStiffness,animationTime:this.animationTime});this._degreesSpring=new v.Spring({initial:s,springStiffness:this.springStiffness,animationTime:this.animationTime});this._updateForScale();r&&this.fitBounds(r,o,!0);this._ownAjaxHeaders={};this.setAjaxHeaders(a,!1);this._initialized=!0};v.extend(v.TiledImage.prototype,v.EventSource.prototype,{needsDraw:function(){return this._needsDraw},redraw:function(){this._needsDraw=!0},getFullyLoaded:function(){return this._fullyLoaded},whenFullyLoaded:function(e){this.getFullyLoaded()?setTimeout(e,1):this.addOnceHandler("fully-loaded-change",function(){e()})},_setFullyLoaded:function(e){if(e!==this._fullyLoaded){this._fullyLoaded=e;this.raiseEvent("fully-loaded-change",{fullyLoaded:this._fullyLoaded})}},requestInvalidate:function(e=!0,t=!1,i=v.now()){t=t?this._lastDrawn.map(e=>e.tile):this._tileCache.getLoadedTilesFor(this);return this.viewer.world.requestTileInvalidateEvent(t,i,e)},reset:function(){this._tileCache.clearTilesFor(this);this._currentMaxTilesPerFrame=10*this.maxTilesPerFrame;this.lastResetTime=v.now();this._needsDraw=!0;this._fullyLoaded=!1},update:function(e){var t=this._xSpring.update();var i=this._ySpring.update();var n=this._scaleSpring.update();var r=this._degreesSpring.update();r=t||i||n||r||this._needsUpdate;if(r||e||!this._fullyLoaded){e=this._updateLevelsForViewport();this._setFullyLoaded(e)}this._needsUpdate=!1;if(r){this._updateForScale();this._raiseBoundsChange();return this._needsDraw=!0}return!1},setDrawn:function(){this._needsDraw=this._isBlending||this._wasBlending||0r){o=this._clip.x/this._clip.height*t.height;s=this._clip.y/this._clip.height*t.height}else{o=this._clip.x/this._clip.width*t.width;s=this._clip.y/this._clip.width*t.width}}if(t.getAspectRatio()>r){var h=t.height/l;let e=0;i.isHorizontallyCentered?e=(t.width-t.height*r)/2:i.isRight&&(e=t.width-t.height*r);this.setPosition(new v.Point(t.x-o+e,t.y-s),n);this.setHeight(h,n)}else{h=t.width/a;let e=0;i.isVerticallyCentered?e=(t.height-t.width/r)/2:i.isBottom&&(e=t.height-t.width/r);this.setPosition(new v.Point(t.x-o,t.y-s+e),n);this.setWidth(h,n)}},getClip:function(){return this._clip?this._clip.clone():null},setClip:function(e){v.console.assert(!e||e instanceof v.Rect,"[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null");e instanceof v.Rect?this._clip=e.clone():this._clip=null;this._needsUpdate=!0;this._needsDraw=!0;this.raiseEvent("clip-change")},getFlip:function(){return this.flipped},setFlip:function(e){this.flipped=e},get flipped(){return this._flipped},set flipped(e){var t=this._flipped!==!!e;this._flipped=!!e;if(t&&this._initialized){this.update(!0);this._needsDraw=!0;this._raiseBoundsChange()}},get wrapHorizontal(){return this._wrapHorizontal},set wrapHorizontal(e){var t=this._wrapHorizontal!==!!e;this._wrapHorizontal=!!e;if(this._initialized&&t){this.update(!0);this._needsDraw=!0}},get wrapVertical(){return this._wrapVertical},set wrapVertical(e){var t=this._wrapVertical!==!!e;this._wrapVertical=!!e;if(this._initialized&&t){this.update(!0);this._needsDraw=!0}},get debugMode(){return this._debugMode},set debugMode(e){this._debugMode=!!e;this._needsDraw=!0},getOpacity:function(){return this.opacity},setOpacity:function(e){this.opacity=e},get opacity(){return this._opacity},set opacity(e){if(e!==this.opacity){this._opacity=e;this._needsDraw=!0;this._needsUpdate=!0;this.raiseEvent("opacity-change",{opacity:this.opacity})}},getPreload:function(){return this._preload},setPreload:function(e){this._preload=!!e;this._needsDraw=!0},getRotation:function(e){return(e?this._degreesSpring.current:this._degreesSpring.target).value},setRotation:function(e,t){if(this._degreesSpring.target.value!==e||!this._degreesSpring.isAtTargetValue()){t?this._degreesSpring.resetTo(e):this._degreesSpring.springTo(e);this._needsDraw=!0;this._needsUpdate=!0;this._raiseBoundsChange()}},getDrawArea:function(){if(0===this._opacity&&!this._preload)return!1;let e=this._viewportToTiledImageRectangle(this.viewport.getBoundsWithMargins(!0));if(!this.wrapHorizontal&&!this.wrapVertical){var t=this._viewportToTiledImageRectangle(this.getClippedBounds(!0));e=e.intersection(t)}return e},getLoadArea:function(){let e=this._viewportToTiledImageRectangle(this.viewport.getBoundsWithMargins(!1));if(!this.wrapHorizontal&&!this.wrapVertical){var t=this._viewportToTiledImageRectangle(this.getClippedBounds(!1));e=e.intersection(t)}return e},getTilesToDraw:function(){const e=this._lastDrawn;let t=0;for(const i of this._tilesToDraw)if(Array.isArray(i))for(const n of i)e[t++]=n;else i&&(e[t++]=i);e.length=t;this._updateTilesInViewport(e);t=0;for(const r of this._tilesToDraw)if(Array.isArray(r)){for(const o of r)if(o.tile.loaded){o.tile.beingDrawn=!0;e[t++]=o}}else if(r&&r.tile.loaded){r.tile.beingDrawn=!0;e[t++]=r}e.length=t;return e},_getRotationPoint:function(e){return this.getBoundsNoRotate(e).getCenter()},get compositeOperation(){return this._compositeOperation},set compositeOperation(e){if(e!==this._compositeOperation){this._compositeOperation=e;this._needsDraw=!0;this.raiseEvent("composite-operation-change",{compositeOperation:this._compositeOperation})}},getCompositeOperation:function(){return this._compositeOperation},setCompositeOperation:function(e){this.compositeOperation=e},setAjaxHeaders:function(e,t){if(v.isPlainObject(e=null===e?{}:e)){this._ownAjaxHeaders=e;this._updateAjaxHeaders(t)}else v.console.error("[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object")},_updateAjaxHeaders:function(e){void 0===e&&(e=!0);v.isPlainObject(this.viewer.ajaxHeaders)?this.ajaxHeaders=v.extend({},this.viewer.ajaxHeaders,this._ownAjaxHeaders):this.ajaxHeaders=this._ownAjaxHeaders;if(e){let e,t,i,n;for(const s in this.tilesMatrix){e=this.source.getNumTiles(s);var r=this.tilesMatrix[s];for(const a in r){t=(e.x+a%e.x)%e.x;for(const l in r[a]){i=(e.y+l%e.y)%e.y;n=r[a][l];n.loadWithAjax=this.loadTilesWithAjax;if(n.loadWithAjax){var o=this.source.getTileAjaxHeaders(s,t,i);n.ajaxHeaders=v.extend({},this.ajaxHeaders,o)}else n.ajaxHeaders=null}}}for(let e=0;e=n;t--,e++)a[e]=t;for(let e=i+1;e<=this.source.maxLevel;e++){var l=this.tilesMatrix[e]&&this.tilesMatrix[e][0]&&this.tilesMatrix[e][0][0];if(l&&l.isBottomMost&&l.isRightMost&&l.loaded){a.push(e);break}}let h=!1;for(let e=0;e=this.minPixelRatio)h=!0;else if(!h)continue;var d=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(c),!1).x*this._scaleSpring.current.value;var p=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(Math.max(this.source.getClosestLevel(),0)),!1).x*this._scaleSpring.current.value;p=this.immediateRender?1:p;u=Math.min(1,(u-.5)/.5);d=p/Math.abs(p-d);d=this._updateLevel(c,u,d,t,r,s,o);this.viewer.world.ensureTilesUpToDate(d.tilesToDraw);o=d.bestLoadTileCandidates;this._tilesToDraw[c]=d.tilesToDraw;if(this._providesCoverage(this.coverage,c))break}if(o&&0{var n=this._getTile(t,i,r,l,c);d=d||this._getCachedArray(r,e);this.viewer&&this.viewer.raiseEvent("update-tile",{tiledImage:this,tile:n});this._setCoverage(this.coverage,r,t,i,!1);if(n.exists){if(n.loaded){1===n.opacity&&this._setCoverage(this.coverage,r,t,i,!0);d[p++]={tile:n,level:r,levelOpacity:o,currentTime:l};this._setCoverage(this.loadingCoverage,r,t,i,!0)}this._positionTile(n,this.source.tileOverlap,this.viewport,u,s)}if(a&&!n.loaded){let e=n.loading||this._isCovered(this.loadingCoverage,r,t,i);this._setCoverage(this.loadingCoverage,r,t,i,e);if(n.exists){!n.loading&&this._tryFindTileCacheRecord(n)&&(e=!0);n.loading?this._tilesLoading++:e||(h=this._compareTiles(h,n,this._currentMaxTilesPerFrame))}}});this._currentMaxTilesPerFrame>this.maxTilesPerFrame&&(this._currentMaxTilesPerFrame=Math.max(Math.ceil(this._currentMaxTilesPerFrame/2),this.maxTilesPerFrame));d&&(d.length=p);return{bestLoadTileCandidates:h,tilesToDraw:d||[]}},_visitTiles:function(n,r,o){const e=r.getBoundingBox();var t=this._getCornerTiles(n,e.getTopLeft(),e.getBottomRight());var s=t.topLeft;const a=t.bottomRight;var l=this.source.getNumTiles(n);if(this.getFlip()){a.x+=1;this.wrapHorizontal||(a.x=Math.min(a.x,l.x-1))}var h=Math.max(0,(a.x-s.x)*(a.y-s.y));for(let i=s.x;i<=a.x;i++)for(let t=s.y;t<=a.y;t++){let e;if(this.getFlip()){var c=(l.x+i%l.x)%l.x;e=i+l.x-c-c-1}else e=i;null!==r.intersection(this.getTileBounds(n,e,t))&&o(e,t,h)}},_positionTile:function(e,t,i,n,r){const o=e.bounds.getTopLeft();o.x*=this._scaleSpring.current.value;o.y*=this._scaleSpring.current.value;o.x+=this._xSpring.current.value;o.y+=this._ySpring.current.value;const s=e.bounds.getSize();s.x*=this._scaleSpring.current.value;s.y*=this._scaleSpring.current.value;e.positionedBounds.x=o.x;e.positionedBounds.y=o.y;e.positionedBounds.width=s.x;e.positionedBounds.height=s.y;var a=i.pixelFromPointNoRotate(o,!0);const l=i.pixelFromPointNoRotate(o,!1);let h=i.deltaPixelsFromPointsNoRotate(s,!0);const c=i.deltaPixelsFromPointsNoRotate(s,!1);i=l.plus(c.divide(2));i=n.squaredDistanceTo(i);if(this.getDrawer().minimumOverlapRequired(this)){t||(h=h.plus(new v.Point(1,1)));e.isRightMost&&this.wrapHorizontal&&(h.x+=.75);e.isBottomMost&&this.wrapVertical&&(h.y+=.75)}e.position=a;e.size=h;e.squaredDistance=i;e.visibility=r},_getCornerTiles:function(e,t,i){let n;let r;if(this.wrapHorizontal){n=v.positiveModulo(t.x,1);r=v.positiveModulo(i.x,1)}else{n=Math.max(0,t.x);r=Math.min(1,i.x)}let o;let s;var a=1/this.source.aspectRatio;if(this.wrapVertical){o=v.positiveModulo(t.y,a);s=v.positiveModulo(i.y,a)}else{o=Math.max(0,t.y);s=Math.min(a,i.y)}const l=this.source.getTileAtPoint(e,new v.Point(n,o));const h=this.source.getTileAtPoint(e,new v.Point(r,s));e=this.source.getNumTiles(e);if(this.wrapHorizontal){l.x+=e.x*Math.floor(t.x);h.x+=e.x*Math.floor(i.x)}if(this.wrapVertical){l.y+=e.y*Math.floor(t.y/a);h.y+=e.y*Math.floor(i.y/a)}return{topLeft:l,bottomRight:h}},_tryFindTileCacheRecord:function(e){var t=this._tileCache.getCacheRecord(e.originalCacheKey);if(!t)return!1;e.loading=!0;this._setTileLoaded(e,t.data,null,null,t.type);return!0},_getTile:function(e,t,i,n,r){let o,s,a,l,h,c,u,d,p,g=this.tilesMatrix,m=this.source;let f=g[i];f||(g[i]=f={});f[e]||(f[e]={});if(f[e][t]&&!f[e][t].flipped==!this.flipped)p=f[e][t];else{o=(r.x+e%r.x)%r.x;s=(r.y+t%r.y)%r.y;a=this.getTileBounds(i,e,t);l=m.getTileBounds(i,o,s,!0);h=m.tileExists(i,o,s);c=m.getTileUrl(i,o,s);u=m.getTilePostData(i,o,s);if(this.loadTilesWithAjax){d=m.getTileAjaxHeaders(i,o,s);v.isPlainObject(this.ajaxHeaders)&&(d=v.extend({},this.ajaxHeaders,d))}else d=null;p=new v.Tile(i,e,t,a,h,c,void 0,this.loadTilesWithAjax,d,l,u,m.getTileHashKey(i,o,s,c,d,u));this.getFlip()?0==o&&(p.isRightMost=!0):o==r.x-1&&(p.isRightMost=!0);s==r.y-1&&(p.isBottomMost=!0);p.flipped=this.flipped;f[e][t]=p}p.lastTouchTime=n;return p},_loadTile:function(o,s){const a=this;o.loading=!0;(o.tiledImage=this)._imageLoader.addJob({src:o.getUrl(),tile:o,source:this.source,postData:o.postData,loadWithAjax:o.loadWithAjax,ajaxHeaders:o.ajaxHeaders,crossOriginPolicy:this.crossOriginPolicy,ajaxWithCredentials:this.ajaxWithCredentials,callback:function(e,t,i,n,r){a._onTileLoad(o,s,e,t,i,n,r)},abort:function(){o.loading=!1}})||this.viewer.raiseEvent("job-queue-full",{tile:o,tiledImage:this,time:s})},_onTileLoad:function(t,e,i,n,r,o,s){if(null!=i){t.exists=!0;if(e{this._setTileLoaded(t,e,null,r,l)}).catch(e=>{v.console.warn("Failed to satisfy original type [%s] %s from %s: %s",l,t,o,e);this._setTileLoaded(t,i,null,r,o)})}else{v.console.warn("Ignoring default base tile data type %s: no conversion possible from %s",this.originalDataType,o);this._setTileLoaded(t,i,null,r,o)}}else this._setTileLoaded(t,i,null,r,o)}else{v.console.error("Tile %s failed to load: %s - error: %s",t,t.getUrl(),n);this.viewer.raiseEvent("tile-load-failed",{tile:t,tiledImage:this,time:e,message:n,tileRequest:r,tries:s,maxReached:0===this.viewer.tileRetryMax||s>=this.viewer.tileRetryMax});t.loading=!1;t.exists=!1}},_setTileLoaded:function(i,t,e,n,r){i.tiledImage=this;v.console.assert(void 0!==r,"TileSource::downloadTileStart must return a dataType.");let o=!1;i.addCache(i.cacheKey,()=>{o=!0;return t},r,!1,!1);let s=null,a=0,l=!1;const h=this;function c(){a--;if(!(0{s=e}),get image(){v.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile-invalidated' event to modify data instead.");return t},get data(){v.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile-invalidated' event to modify data instead.");return t},getCompletionCallback:function(){v.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: use async event handlers instead, execution order is deducted by addHandler(...) priority argument.");return u()}}).catch(()=>{v.console.error("[tile-loaded] event finished with failure: there might be a problem with a plugin you are using.")}).then(e)}if(o)this.viewer.world.requestTileInvalidateEvent([i],void 0,!1,!0,!0).then(d).catch(d);else{r=i.getCache(i.originalCacheKey);const p=e=>{if(this.viewer.isDestroyed())return v.Promise.resolve();var t=this.getDrawer();return e.isUsableForDrawer(t)?v.Promise.resolve():e.prepareForRendering(t)};for(const g of r._tiles){if(g.cacheKey!==i.cacheKey){const m=g.getCache();p(m).then(()=>i.setCache(g.cacheKey,m,!0,!1)).then(d);return}if(g.processing){g.processingPromise.then(e=>{const t=e.getCache();p(t).then(()=>{i.setCache(e.cacheKey,t,!0,!1);return t.loaded?null:t.await()}).then(d)});return}}p(r).then(d)}},_compareTiles:function(t,i,e){if(!t)return[i];let n=!1;for(let e=0;ee&&t.pop();return t},_sortTilesComparator:function(e,t){return null===e?1:null===t?-1:e.visibility===t.visibility?e.squaredDistance-t.squaredDistance:t.visibility-e.visibility},_getCachedArray:function(e,t=void 0){let i=this._arrayCacheMap[e];i?void 0!==t&&(i.length=t):i=this._arrayCacheMap[e]=void 0!==t?new Array(t):[];return i},_providesCoverage:function(e,t,i,n){var r;var o;let s,a;if(!e[t])return!1;if(void 0!==i&&void 0!==n)return void 0===e[t][i]||void 0===e[t][i][n]||!0===e[t][i][n];for(s in r=e[t])if(Object.prototype.hasOwnProperty.call(r,s))for(a in o=r[s])if(Object.prototype.hasOwnProperty.call(o,a)&&!o[a])return!1;return!0},_isCovered:function(e,t,i,n){return void 0===i||void 0===n?this._providesCoverage(e,t+1):this._providesCoverage(e,t+1,2*i,2*n)&&this._providesCoverage(e,t+1,2*i,2*n+1)&&this._providesCoverage(e,t+1,2*i+1,2*n)&&this._providesCoverage(e,t+1,2*i+1,2*n+1)},_setCoverage:function(e,t,i,n,r){if(e[t]){e[t][i]||(e[t][i]={});e[t][i][n]=r}else v.console.warn("Setting coverage for a tile before its level's coverage has been reset: %s",t)},_resetCoverage:function(e,t){e[t]={}}})}(OpenSeadragon);!function(c){const e=c;const r=Symbol("DRAWER_INTERNAL_CACHE");e.CacheRecord=class{constructor(){this.revive()}get data(){return this._data}get type(){return this._type}await(){return this._promise||c.Promise.resolve(this._data)}getImage(){c.console.error("[CacheRecord.getImage] options.image is deprecated. Moreover, it might not work correctly as the cache system performs conversion asynchronously in case the type needs to be converted.");this.transformTo("image");return this.data}getRenderedContext(){c.console.error("[CacheRecord.getRenderedContext] options.getRenderedContext is deprecated. Moreover, it might not work correctly as the cache system performs conversion asynchronously in case the type needs to be converted.");this.transformTo("context2d");return this.data}setDataAs(e,t){c.console.assert(null!=e,"[CacheRecord.setDataAs] needs valid data to set!");if(this._conversionJobQueue){let i=null;var n=new c.Promise((e,t)=>{i=e});this._conversionJobQueue.push(()=>i(this._overwriteData(e,t)));return n}return this._overwriteData(e,t)}getDataAs(t=void 0,i=!0){return this.loaded?t===this._type?i?c.converter.copy(this._tRef,this._data,t||this._type):this._promise:this._transformDataIfNeeded(this._tRef,this._data,t||this._type,i)||this._promise:this._promise.then(e=>this._transformDataIfNeeded(this._tRef,e,t||this._type,i)||e)}_transformDataIfNeeded(e,t,i,n){if(this._destroyed)return c.Promise.resolve();let r;i!==this._type?r=c.converter.convert(e,t,this._type,i):n&&(r=c.converter.copy(e,t,i));return!!r&&r.then(e=>{if(!this._destroyed)return e;c.converter.destroy(e,i)}).catch(e=>{this._handleConversionError(e)})}getDataForRendering(e,t){if(this._destroyed){c.console.error(`Attempt to draw tile with destroyed main cache ${this}!`);t._unload()}else if(this.loaded)if(this._destroyed){c.console.error(`Attempt to draw tile with destroyed main cache ${this}!`);t._unload()}else{const i=e.getSupportedDataFormats();if(i.includes(this.type)){if(!e.options.usePrivateCache)return this;if(!e.options.preloadCache)return this.prepareInternalCacheSync(e);t=this._getInternalCacheRef(e);if(t&&t.loaded)return t;c.console.error(`Attempt to draw tile cache ${this} with internal cache non-ready state!`)}else{c.console.error(`Attempt to draw tile cache ${this} with unsupported type '${this.type}' for the target drawer!`);this.prepareForRendering(e)}}else this._promise||c.console.error(`Attempt to draw cache ${this} when not loaded!`)}isUsableForDrawer(e){const t=e.getSupportedDataFormats();if(!t.includes(this.type))return!1;if(e.options.usePrivateCache)if(!this._getInternalCacheRef(e))return!1;return!0}prepareForRendering(t){const e=t.getRequiredDataFormats();if(!this.loaded)return this.await().then(e=>this.prepareForRendering(t));let i;i=e.includes(this.type)?this.await():this.transformTo(e);var n=e=>e.catch(e=>{this._handleConversionError(e);return null});return t.options.usePrivateCache&&t.options.preloadCache?n(i.then(e=>this.prepareInternalCacheAsync(t))):n(i)}prepareInternalCacheAsync(t){let e=this._getInternalCacheRef(t);if(this._checkInternalCacheUpToDate(e,t))return e.await();e&&!e.loaded&&e.await().then(()=>e.destroy());c.console.assert(this._tRef,"Data Create called from invalidation routine needs tile reference!");var i=t.internalCacheCreate(this,this._tRef);c.console.assert(void 0!==i,"[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");var n=t.getId();e=this[r][n]=new c.InternalCacheRecord(i,n,e=>t.internalCacheFree(e));return e.await()}prepareInternalCacheSync(t){let e=this._getInternalCacheRef(t);if(this._checkInternalCacheUpToDate(e,t))return e;e&&e.destroy();c.console.assert(this._tRef,"Data Create called from drawing loop needs tile reference!");var i=t.internalCacheCreate(this,this._tRef);c.console.assert(void 0!==i,"[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");var n=t.getId();e=this[r][n]=new c.InternalCacheRecord(i,n,e=>t.internalCacheFree(e));return e}_getInternalCacheRef(t){if(t.options.usePrivateCache){let e=this[r];e=e||(this[r]={});return e[t.getId()]}c.console.error("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false.")}_checkInternalCacheUpToDate(e,t){return e&&e.tstamp>=t._dataNeedsRefresh}transformTo(e=this._type){if(!this.loaded){this._conversionJobQueue=this._conversionJobQueue||[];let i=null;var t=new c.Promise((e,t)=>{i=e});this._conversionJobQueue.push(()=>{if(!this._destroyed)if("string"==typeof e&&e!==this._type||Array.isArray(e)&&!e.includes(this._type)){this._convert(this._type,e);this._promise.then(e=>i(e))}else this._promise.then(e=>{this._checkAwaitsConvert();return i(e)})});return t}("string"==typeof e&&e!==this._type||Array.isArray(e)&&!e.includes(this._type))&&this._convert(this._type,e);return this._promise}destroyInternalCache(e=void 0){const t=this[r];if(t)if(e){const i=t[e];if(i){i.destroy();delete t[e]}}else{for(const n in t)t[n].destroy();delete this[r]}}withTileReference(e){this._tRef=e;return this}toString(){const e=this._tRef||this._tiles.length&&this._tiles[0];return e?`Cache ${this.type} [used e.g. by ${e.toString()}]`:"Orphan cache!"}revive(){c.console.assert(!this.loaded&&!this._type,"[CacheRecord::revive] must not be called when loaded!");this._tiles=[];this._data=null;this._type=null;this.loaded=!1;this._promise=null;this._destroyed=!1;this._ownerTileCache=null;this.cacheKey=null}destroy(){if(!this._destroyed){delete this._conversionJobQueue;this._destroyed=!0;if(this.loaded)this._destroySelfUnsafe(this._data,this._type);else if(this._promise){const t=this._type;this._promise.then(e=>this._destroySelfUnsafe(e,t)).catch(c.console.error)}}}_destroySelfUnsafe(e,t){c.converter.destroy(e,t);this.destroyInternalCache();if(this._destroyed){this.loaded=!1;this._tiles=null;this._data=null;this._type=null;this._tRef=null;this._promise=null}}addTile(e,t,i){if(!this._destroyed){c.console.assert(e,"[CacheRecord.addTile] tile is required");if(null!=t&&this._tiles.length<1){"function"==typeof t&&(t=t());if(this.type&&this._promise)t instanceof c.Promise?this._promise=t.then(e=>{this._overwriteData(e,i)}):this._overwriteData(t,i);else{if(t instanceof c.Promise){this._promise=t.then(e=>{if(!this._destroyed){this.loaded=!0;return this._data=e}try{c.converter.destroy(e,this._type)}catch(e){}}).catch(e=>{this._handleConversionError(e)});this._data=null}else{this._promise=c.Promise.resolve(t);this._data=t;this.loaded=!0}this._type=i}this._tiles.push(e)}else{t=this._tiles.includes(e);!t&&this.type&&this._promise?this._tiles.push(e):t||c.console.warn("Tile %s caching attempt without data argument on uninitialized cache entry!",e)}}}removeTile(t){if(this._destroyed)return!1;for(let e=0;e{if(this._conversionJobQueue&&!this._destroyed){const e=this._conversionJobQueue[0];this._conversionJobQueue.splice(0,1);0===this._conversionJobQueue.length&&delete this._conversionJobQueue;e()}})}_triggerNeedsDraw(){0{if(this._data===i&&this._type===n)return this._data;c.converter.destroy(this._data,this._type);this._type=n;this._data=i;this._promise=c.Promise.resolve(i);const e=this[r];if(e)for(const t in e)e[t].setDataAs(i,n);this._triggerNeedsDraw();return this._data})}_convert(e,t){const o=c.converter,s=o.getConversionPath(e,t);if(s){var i=this._data;const a=s.length;const l=this;const h=(t,i)=>{if(i>=a){l._data=t;l.loaded=!0;l._checkAwaitsConvert();return c.Promise.resolve(t)}const n=s[i];let e;try{e=n.transform(l._tRef,t)}catch(e){o.destroy(t,n.origin.value);return c.Promise.reject(`[CacheRecord._convert] sync failure (while converting using ${n.target.value}, ${n.origin.value})`)}if(void 0===e){l.loaded=!1;o.destroy(t,n.origin.value);return c.Promise.reject(`[CacheRecord._convert] data mid result undefined value (while converting using ${n.target.value}, ${n.origin.value})`)}o.destroy(t,n.origin.value);const r="promise"===c.type(e)?e:c.Promise.resolve(e);return r.then(e=>h(e,i+1))};this.loaded=!1;this._data=void 0;this._type=s[a-1].target.value;this._promise=h(i,0).catch(e=>{this._handleConversionError(e)})}else c.console.error(`[CacheRecord._convert] Conversion ${e} ---> ${t} cannot be done!`)}_handleConversionError(e){c.console.error("[CacheRecord] Conversion/preparation error:",e);this._destroyed=!0;this.loaded=!1;this._data=null;if(this.cacheKey&&this._ownerTileCache)this._ownerTileCache._handleBrokenCacheRecord(this);else{this._promise=c.Promise.resolve(void 0);this._tiles=[];this._tRef=null}}};e.InternalCacheRecord=class{constructor(e,t,i){this.tstamp=c.now();this._ondestroy=i;this._type=t;if(e instanceof c.Promise)(this._promise=e).then(e=>{this.loaded=!0;this._data=e});else{this._promise=null;this.loaded=!0;this._data=e}}get data(){return this._data}get type(){return this._type}await(){return this._promise||c.Promise.resolve(this._data)}withTileReference(e){this._temporaryTileRef=e;return this}destroy(){if(this.loaded){this._ondestroy&&this._ondestroy(this._data);this._data=null;this.loaded=!1}}};e.TileCache=class{constructor(e){this._maxCacheItemCount=(e=e||{}).maxImageCacheCount||c.DEFAULT_SETTINGS.maxImageCacheCount;this._tilesLoaded=[];this._zombiesLoaded=[];this._zombiesLoadedCount=0;this._cachesLoaded=[];this._cachesLoadedCount=0}numTilesLoaded(){return this._tilesLoaded.length}numCachesLoaded(){return this._zombiesLoadedCount+this._cachesLoadedCount}cacheTile(e){c.console.assert(e,"[TileCache.cacheTile] options is required");var t=e.tile;c.console.assert(t,"[TileCache.cacheTile] options.tile is required");c.console.assert(t.cacheKey,"[TileCache.cacheTile] options.tile.cacheKey is required");if(e.image instanceof Image){c.console.warn("[TileCache.cacheTile] options.image is deprecated!");e.data=e.image;e.dataType="image"}var i=e.cacheKey||t.cacheKey;let n=this._cachesLoaded[i];if(!n){if(void 0===e.data){c.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute has been deprecated and will be removed in the future.");e.data=e.image}n=this._zombiesLoaded[i];if(n){if(n._destroyed)n.revive();else{"function"==typeof e.data&&e.data();delete e.data}delete this._zombiesLoaded[i];this._zombiesLoadedCount--;this._cachesLoaded[i]=n;this._cachesLoadedCount++}else{var r=void 0!==e.data&&null!==e.data&&!1!==e.data;c.console.assert(r,"[TileCache.cacheTile] options.data is required to create an CacheRecord");n=this._cachesLoaded[i]=new c.CacheRecord;this._cachesLoadedCount++}}if(!e.dataType){c.console.error("[TileCache.cacheTile] options.dataType is newly required. For easier use of the cache system, use the tile instance API.");"function"==typeof e.data&&c.console.error("[TileCache.cacheTile] options.dataType is mandatory when data item is a callback!");e.dataType=c.converter.guessType(e.data)}n._ownerTileCache=this;n.cacheKey=i;n.addTile(t,e.data,e.dataType);this._freeOldRecordRoutine(t,e.cutoff||0);return n}renameCache(e){var t=e.newCacheKey,i=e.oldCacheKey;let n=this._cachesLoaded[i];if(n){if(this._cachesLoaded[t]){c.console.error("Cannot rename cache %s to %s: the target cache is occupied!",i,t);return null}this._cachesLoaded[t]=n;delete this._cachesLoaded[i]}else{n=this._zombiesLoaded[i];c.console.assert(n,"[TileCache.renameCache] oldCacheKey must reference existing cache!");if(this._zombiesLoaded[t]){c.console.error("Cannot rename zombie cache %s to %s: the target cache is occupied!",i,t);return null}this._zombiesLoaded[t]=n;delete this._zombiesLoaded[i]}n._ownerTileCache=this;n.cacheKey=t;for(const r of n._tiles)r.reflectCacheRenamed(i,t);return n}cloneCache(i){const n=i.tile;var e=i.copyTargetKey;const r=this._cachesLoaded[e]||this._zombiesLoaded[e];c.console.assert(r,"[TileCache.cloneCache] attempt to clone non-existent cache %s!",e);c.console.assert(!this._cachesLoaded[i.newCacheKey],"[TileCache.cloneCache] attempt to copy clone to existing cache %s!",i.newCacheKey);e=i.desiredType||void 0;return r.getDataAs(e,!0).then(e=>{const t=this._cachesLoaded[i.newCacheKey]=new c.CacheRecord;t.addTile(n,e,r.type);this._cachesLoadedCount++;this._freeOldRecordRoutine(n,i.cutoff||0);return t})}injectCache(e){const t=e.targetKey,i=e.tile;if(e.tileAllowNotLoaded||i.loaded||i.loading){var n=this._cachesLoaded[t];if(n)for(const i of[...n._tiles])this.unloadCacheForTile(i,t,!0,!1);this._cachesLoaded[t]&&c.console.error("The inject routine should've freed cache!");const r=e.cache;this._cachesLoaded[t]=r;r._ownerTileCache=this;r.cacheKey=t;for(const o of i.getCache(i.originalCacheKey)._tiles)o.setCache(t,r,e.setAsMainCache,!1)}else c.console.warn("Attempt to inject cache on tile in invalid state: this is probably a bug!")}replaceCache(e){const t=e.victimKey,i=e.consumerKey,n=this._cachesLoaded[t],r=e.tile;if(n&&(e.tileAllowNotLoaded||r.loaded||r.loading)){var o=this._cachesLoaded[i];if(o)for(const r of[...o._tiles])this.unloadCacheForTile(r,i,!0,!1);this._cachesLoaded[i]&&c.console.error("The consume routine should've freed cache!");var s=this.renameCache({oldCacheKey:t,newCacheKey:i});if(s)for(const a of r.getCache(r.originalCacheKey)._tiles)a.setCache(i,s,e.setAsMainCache,!1)}else c.console.warn("Attempt to consume cache on tile in invalid state: this is probably a bug!")}restoreTilesThatShareOriginalCache(e,t,i){for(const n of t._tiles)if(n.cacheKey!==n.originalCacheKey){this.unloadCacheForTile(n,n.cacheKey,i,!0);delete n._caches[n.cacheKey];n.cacheKey=n.originalCacheKey}}_freeOldRecordRoutine(e,i){let n=this._tilesLoaded.length,r=-1;if(this._cachesLoadedCount+this._zombiesLoadedCount>this._maxCacheItemCount)if(0this._maxCacheItemCount;if(t._zombieCache&&n&&0this._maxCacheItemCount}for(let e=this._tilesLoaded.length-1;0<=e;e--)(i=this._tilesLoaded[e]).tiledImage===t&&(i.loaded?i.tiledImage===t&&this._unloadTile(i,!t._zombieCache||n,e):this._tilesLoaded.splice(e,1))}clear(e=0){for(const t in this._zombiesLoaded)this._zombiesLoaded[t].destroy();for(const i in this._tilesLoaded)this._unloadTile(i,!0);this._tilesLoaded=[];this._zombiesLoaded=[];this._zombiesLoadedCount=0;this._cachesLoaded=[];this._cachesLoadedCount=0}clearDrawerInternalCache(e){var t=e.getId();for(const i of this._zombiesLoaded)i&&i.destroyInternalCache(t);for(const n of this._cachesLoaded)n&&n.destroyInternalCache(t)}getLoadedTilesFor(t){return t?this._tilesLoaded.filter(e=>e.tiledImage===t):[...this._tilesLoaded]}getCacheRecord(e){c.console.assert(e,"[TileCache.getCacheRecord] cacheKey is required");return this._cachesLoaded[e]||this._zombiesLoaded[e]}safeUnloadCache(e){if(e&&!e._destroyed&&e.getTileCount()<1){for(const t in this._zombiesLoaded){const i=this._zombiesLoaded[t];if(i===e){delete this._zombiesLoaded[t];i.destroy();return}}c.console.error("Attempt to delete an orphan cache that is not in zombie list: this could be a bug!",e);e.destroy()}}unloadCacheForTile(e,t,i,n){const r=this._cachesLoaded[t];if(r){if(r.removeTile(e)){if(!r.getTileCount()){if(i)r.destroy();else{this._zombiesLoaded[t]=r;this._zombiesLoadedCount++}delete this._cachesLoaded[t];this._cachesLoadedCount--}return!0}c.console.error("[TileCache.unloadCacheForTile] System tried to delete tile from cache it does not belong to! This could mean a bug in the cache system.");return!1}n||c.console.warn("[TileCache.unloadCacheForTile] Attempting to delete missing cache!");return!1}unloadTile(t,e=!1){if(t.loaded){var i=this._tilesLoaded.findIndex(e=>e===t);this._unloadTile(t,e,i)}else c.console.warn("Attempt to unload already unloaded tile.")}_unloadTile(e,t,i=void 0){c.console.assert(e,"[TileCache._unloadTile] tile is required");for(const n in e._caches)this.unloadCacheForTile(e,n,t,!1);void 0!==i&&this._tilesLoaded.splice(i,1);if(e.loaded){const r=e.tiledImage;e._unload();r.viewer.raiseEvent("tile-unloaded",{tile:e,tiledImage:r,destroyed:t})}}}}(OpenSeadragon);!function(v){v.World=function(e){const t=this;v.console.assert(e.viewer,"[World] options.viewer is required");v.EventSource.call(this);this.viewer=e.viewer;this._items=[];this._needsDraw=!1;this.__invalidatedAt=1;this._autoRefigureSizes=!0;this._needsSizesFigured=!1;this._delegatedFigureSizes=function(e){t._autoRefigureSizes?t._figureSizes():t._needsSizesFigured=!0};this._figureSizes()};v.extend(v.World.prototype,v.EventSource.prototype,{addItem:function(e,t){v.console.assert(e,"[World.addItem] item is required");v.console.assert(e instanceof v.TiledImage,"[World.addItem] only TiledImages supported at this time");if(void 0!==(t=t||{}).index){t=Math.max(0,Math.min(this._items.length,t.index));this._items.splice(t,0,e)}else this._items.push(e);this._autoRefigureSizes?this._figureSizes():this._needsSizesFigured=!0;this._needsDraw=!0;e.addHandler("bounds-change",this._delegatedFigureSizes);e.addHandler("clip-change",this._delegatedFigureSizes);this.raiseEvent("add-item",{item:e})},getItemAt:function(e){v.console.assert(void 0!==e,"[World.getItemAt] index is required");return this._items[e]},getIndexOfItem:function(e){v.console.assert(e,"[World.getIndexOfItem] item is required");return v.indexOf(this._items,e)},getItemCount:function(){return this._items.length},setItemIndex:function(e,t){v.console.assert(e,"[World.setItemIndex] item is required");v.console.assert(void 0!==t,"[World.setItemIndex] index is required");var i=this.getIndexOfItem(e);if(t>=this._items.length)throw new Error("Index bigger than number of layers.");this._items.splice(i,1);this._items.splice(t,0,e);this._needsDraw=!0;this.raiseEvent("item-index-change",{item:e,previousIndex:i,newIndex:t})},removeItem:function(e){v.console.assert(e,"[World.removeItem] item is required");var t=v.indexOf(this._items,e);if(-1!==t){e.removeHandler("bounds-change",this._delegatedFigureSizes);e.removeHandler("clip-change",this._delegatedFigureSizes);e.destroy();this._items.splice(t,1);this._figureSizes();this._needsDraw=!0;this._raiseRemoveItem(e)}},removeAll:function(){this.viewer._cancelPendingImages();let t;for(let e=0;e=i;var h=d.level<=(d.tiledImage.source.getClosestLevel()||0);if(l||h)r[o++]=d;else{a._unloadTile(d,!1,e-s);s++}}r.length=o;return this.requestTileInvalidateEvent(r,t,e)},requestTileInvalidateEvent:function(e,d,p=!0,g=!1,m=!1){if(!this.viewer.isOpen())return v.Promise.resolve();void 0===d&&(d=this.__invalidatedAt);const f=[];e=e.map(o=>{if(!o||!g&&!o.loaded&&!o.processing)return Promise.resolve();const t=o.tiledImage;const s=t.getDrawer();const e=s._parentViewer||this.viewer;const a=o.getCache(o.originalCacheKey);var i=o.getCache(o.originalCacheKey);if(i.__invStamp&&i.__invStamp>=d)return Promise.resolve();let l=!1;a.__finishProcessing&&a.__finishProcessing(!0);let n;a.__resolve||(n=new v.Promise(e=>{a.__resolve=e}));a.__finishProcessing=e=>{l=l||e;o.processing=!1;a.__finishProcessing=null;if(!e){a.__resolve(o);a.__resolve=null}};for(const r of a._tiles){r.processing=d;n&&(r.processingPromise=n)}a.__invStamp=d;a.__wasRestored=p;let h=null;const c=()=>{if(h){var e=o.buildDistinctMainCacheKey();t._tileCache.injectCache({tile:o,cache:h,targetKey:e,setAsMainCache:!0,tileAllowNotLoaded:o.loading})}else p&&t._tileCache.restoreTilesThatShareOriginalCache(o,o.getCache(o.originalCacheKey),!0)};const u=()=>l||"number"==typeof a.__invStamp&&a.__invStamp{if(h)return h.getDataAs(t,!1);var e=p?o.originalCacheKey:o.cacheKey;const i=o.getCache(e);if(!i){v.console.error("[Tile::getData] There is no cache available for tile with key %s",e);return v.Promise.reject()}t=t||i.type;h=(new v.CacheRecord).withTileReference(o);return i.getDataAs(t,!0).then(e=>{if(null==e)return v.Promise.reject(new Error("[World.getData] Working cache source data unavailable"));h.addTile(o,e,t);return h.data})},setData:(e,t)=>{if(h)return h.setDataAs(e,t);h=(new v.CacheRecord).withTileReference(o);h.addTile(o,e,t);return v.Promise.resolve()},resetData:()=>{if(h){h.destroy();h=null}},stopPropagation:()=>{return u()}}).catch(e=>{v.console.error("Update routine error:",e);if(h){try{h.destroy()}catch(e){}h=null}l=!0;a.__finishProcessing&&a.__finishProcessing(!0);return null}).then(e=>{if(this.viewer.isDestroyed()){a.__finishProcessing&&a.__finishProcessing(!0);return null}if(l)return null;if(a.__finishProcessing){if(!l&&(o.loaded||o.loading)){if(a.__invStamp{if(l){h.destroy();h=null}else{if(!u()&&e)c();else{h.destroy();h=null}a.__finishProcessing()}});if(p){var t=o.getCache();const i=o.getCache(o.originalCacheKey);return t!==i?i.prepareForRendering(s).then(e=>{if(!l){!u()&&e&&c();a.__finishProcessing()}}):null}}else v.console.error("Invalidation flow error: tile processing state is invalid. "+`Tile: ${o?o.toString():"null"}, `+`loaded: ${o?o.loaded:"n/a"}, loading: ${o?o.loading:"n/a"}, `+`originalCache.__invStamp: ${a.__invStamp}, `+`this.__invalidatedAt: ${this.__invalidatedAt}, `+`tStamp: ${d}, wasOutdatedRun: `+l);if(m){const n=o.getCache();return n.prepareForRendering(s).then(()=>{!l&&a.__finishProcessing&&a.__finishProcessing()})}a.__finishProcessing();return null}l||a.__finishProcessing(!0)}if(m){const r=o.getCache();return r.prepareForRendering(s).then(()=>{!l&&a.__finishProcessing&&a.__finishProcessing()})}if(h){h.destroy();h=null}return null}).catch(e=>{v.console.error("Update routine error:",e);if(h){h.destroy();h=null}a.__finishProcessing()})});return v.Promise.all(e).then(()=>{f.length&&this.requestTileInvalidateEvent(f,void 0,p,!0);g||this.viewer.isDestroyed()||this.draw()})},ensureTilesUpToDate:function(e){let t;let i;for(var n of e){n=n.tile||n;if(n.loaded&&!n.processing){var r=n.getCache(n.originalCacheKey);i=r.__wasRestored;r.__invStampu.height?o:o*(u.width/u.height);p=d*(u.height/u.width);g=new v.Point(l+(o-d)/2,h+(o-p)/2);c.setPosition(g,t);c.setWidth(d,t);"horizontal"===i?l+=s:h+=s}this.setAutoRefigureSizes(!0)},_figureSizes:function(){var e=this._homeBounds?this._homeBounds.clone():null;var t=this._contentSize?this._contentSize.clone():null;var i=this._contentFactor||0;if(this._items.length){let t=this._items[0];var s=t.getBounds();this._contentFactor=t.getContentSize().x/s.width;var a=t.getClippedBounds().getBoundingBox();let i=a.x;let n=a.y;let r=a.x+a.width;let o=a.y+a.height;for(let e=1;e=3}(),e.supportsAsync=!0,e.getCurrentPixelDensityRatio=function(){if(e.supportsCanvas){const e=document.createElement("canvas").getContext("2d"),t=window.devicePixelRatio||1,i=e.webkitBackingStorePixelRatio||e.mozBackingStorePixelRatio||e.msBackingStorePixelRatio||e.oBackingStorePixelRatio||e.backingStorePixelRatio||1;return Math.max(t,1)/i}return 1},e.pixelDensityRatio=e.getCurrentPixelDensityRatio()}(OpenSeadragon),function(e){e.extend=function(){let t,i,n,o,s,r,a=arguments[0]||{};const l=arguments.length;let h=!1,c=1;for("boolean"==typeof a&&(h=a,a=arguments[1]||{},c=2),"object"==typeof a||OpenSeadragon.isFunction(a)||(a={}),l===c&&(a=this,--c);c=n.x&&i.x=n.y},getMousePosition:function(t){if("number"==typeof t.pageX)e.getMousePosition=function(t){const i=new e.Point;return i.x=t.pageX,i.y=t.pageY,i};else{if("number"!=typeof t.clientX)throw new Error("Unknown event mouse position, no known technique.");e.getMousePosition=function(t){const i=new e.Point;return i.x=t.clientX+document.body.scrollLeft+document.documentElement.scrollLeft,i.y=t.clientY+document.body.scrollTop+document.documentElement.scrollTop,i}}return e.getMousePosition(t)},getPageScroll:function(){const t=document.documentElement||{},i=document.body||{};if("number"==typeof window.pageXOffset)e.getPageScroll=function(){return new e.Point(window.pageXOffset,window.pageYOffset)};else if(i.scrollLeft||i.scrollTop)e.getPageScroll=function(){return new e.Point(document.body.scrollLeft,document.body.scrollTop)};else{if(!t.scrollLeft&&!t.scrollTop)return new e.Point(0,0);e.getPageScroll=function(){return new e.Point(document.documentElement.scrollLeft,document.documentElement.scrollTop)}}return e.getPageScroll()},setPageScroll:function(t){if(void 0!==window.scrollTo)e.setPageScroll=function(e){window.scrollTo(e.x,e.y)};else{const i=e.getPageScroll();if(i.x===t.x&&i.y===t.y)return;document.body.scrollLeft=t.x,document.body.scrollTop=t.y;let n=e.getPageScroll();if(n.x!==i.x&&n.y!==i.y)return void(e.setPageScroll=function(e){document.body.scrollLeft=e.x,document.body.scrollTop=e.y});if(document.documentElement.scrollLeft=t.x,document.documentElement.scrollTop=t.y,n=e.getPageScroll(),n.x!==i.x&&n.y!==i.y)return void(e.setPageScroll=function(e){document.documentElement.scrollLeft=e.x,document.documentElement.scrollTop=e.y});e.setPageScroll=function(e){}}e.setPageScroll(t)},getWindowSize:function(){const t=document.documentElement||{},i=document.body||{};if("number"==typeof window.innerWidth)e.getWindowSize=function(){return new e.Point(window.innerWidth,window.innerHeight)};else if(t.clientWidth||t.clientHeight)e.getWindowSize=function(){return new e.Point(document.documentElement.clientWidth,document.documentElement.clientHeight)};else{if(!i.clientWidth&&!i.clientHeight)throw new Error("Unknown window size, no known technique.");e.getWindowSize=function(){return new e.Point(document.body.clientWidth,document.body.clientHeight)}}return e.getWindowSize()},makeCenteredNode:function(t){t=e.getElement(t);const i=[e.makeNeutralElement("div"),e.makeNeutralElement("div"),e.makeNeutralElement("div")];return e.extend(i[0].style,{display:"table",height:"100%",width:"100%"}),e.extend(i[1].style,{display:"table-row"}),e.extend(i[2].style,{display:"table-cell",verticalAlign:"middle",textAlign:"center"}),i[0].appendChild(i[1]),i[1].appendChild(i[2]),i[2].appendChild(t),i[0]},trace:function(e,t=!1){this.__traceLogs=[],setInterval(()=>{this.__traceLogs.length&&(console.log(this.__traceLogs.join("\n")),this.__traceLogs=[])},2e3),this.trace=function(e,t=!1){if("string"==typeof e)return this.__traceLogs.push(e),void(t&&this.__traceLogs.push(...(new Error).stack.split("\n").slice(1)));e instanceof OpenSeadragon.Tile&&(e=e.getCache(e.originalCacheKey));const i=e._tiles[0];this.__traceLogs.push(`Cache ${i.toString()} loaded ${i.loaded} loading ${i.loading} cacheCount ${Object.keys(i._caches).length} - CACHE ${e.__invStamp}`),t&&this.__traceLogs.push(...(new Error).stack.split("\n").slice(1))},this.trace(e,t)},makeNeutralElement:function(e){const t=document.createElement(e),i=t.style;return i.background="transparent none",i.border="none",i.margin="0px",i.padding="0px",i.position="static",t},now:function(){return Date.now?e.now=Date.now:e.now=function(){return(new Date).getTime()},e.now()},makeTransparentImage:function(t){const i=e.makeNeutralElement("img");return i.src=t,i},setElementOpacity:function(t,i,n){let o,s;t=e.getElement(t),n&&!e.Browser.alpha&&(i=Math.round(i)),e.Browser.opacity?t.style.opacity=i<1?i:"":i<1?(o=Math.round(100*i),s="alpha(opacity="+o+")",t.style.filter=s):t.style.filter=""},setElementTouchActionNone:function(t){void 0!==(t=e.getElement(t)).style.touchAction?t.style.touchAction="none":void 0!==t.style.msTouchAction&&(t.style.msTouchAction="none")},setElementPointerEvents:function(t,i){void 0!==(t=e.getElement(t)).style&&void 0!==t.style.pointerEvents&&(t.style.pointerEvents=i)},setElementPointerEventsNone:function(t){e.setElementPointerEvents(t,"none")},addClass:function(t,i){(t=e.getElement(t)).className?-1===(" "+t.className+" ").indexOf(" "+i+" ")&&(t.className+=" "+i):t.className=i},indexOf:function(e,t,i){return Array.prototype.indexOf?this.indexOf=function(e,t,i){return e.indexOf(t,i)}:this.indexOf=function(e,t,i){let n=i||0;if(!e)throw new TypeError;const o=e.length;if(0===o||n>=o)return-1;n<0&&(n=o-Math.abs(n));for(let i=n;i=200&&h.status<300||0===h.status&&"http:"!==l&&"https:"!==l?i(h):e.isFunction(n)?n(h):e.console.error("AJAX request returned %d: %s",h.status,t))};const c=a?"POST":"GET";try{if(h.open(c,t,!0),r&&(h.responseType=r),s)for(const e in s)Object.prototype.hasOwnProperty.call(s,e)&&s[e]&&h.setRequestHeader(e,s[e]);o&&(h.withCredentials=!0),h.send(a)}catch(t){e.console.error("%s while making AJAX request: %s",t.name,t.message),h.onreadystatechange=function(){},e.isFunction(n)&&n(h,t)}return h},jsonp:function(t){let i,n=t.url;const o=document.head||document.getElementsByTagName("head")[0]||document.documentElement,s=t.callbackName||"openseadragon"+e.now(),r=window[s],a="$1"+s+"$2",l=t.param||"callback",h=t.callback;n=n.replace(/(=)\?(&|$)|\?\?/i,a),n+=(/\?/.test(n)?"&":"?")+l+"="+s,window[s]=function(t){if(r)window[s]=r;else try{delete window[s]}catch(e){}h&&e.isFunction(h)&&h(t)},i=document.createElement("script"),void 0===t.async&&!1===t.async||(i.async="async"),t.scriptCharset&&(i.charset=t.scriptCharset),i.src=n,i.onload=i.onreadystatechange=function(e,t){(t||!i.readyState||/loaded|complete/.test(i.readyState))&&(i.onload=i.onreadystatechange=null,o&&i.parentNode&&o.removeChild(i),i=void 0)},o.insertBefore(i,o.firstChild)},createFromDZI:function(){throw"OpenSeadragon.createFromDZI is deprecated, use Viewer.open."},parseXml:function(t){if(!window.DOMParser)throw new Error("Browser doesn't support XML DOM.");return e.parseXml=function(e){let t=null;return t=(new DOMParser).parseFromString(e,"text/xml"),t},e.parseXml(t)},parseJSON:function(t){return e.parseJSON=window.JSON.parse,e.parseJSON(t)},imageFormatSupported:function(e){return!!i[(e=e||"").toLowerCase()]},setImageFormatsSupported:function(t){e.extend(i,t)}});const t=function(e){};e.console=window.console||{log:t,debug:t,info:t,warn:t,error:t,assert:t},e.Browser={vendor:e.BROWSERS.UNKNOWN,version:0,alpha:!0};const i={avif:!0,bmp:!1,jpeg:!0,jpg:!0,png:!0,tif:!1,wdp:!1,webp:!0},n={};function o(e,t){return t&&e!==document.body?document.body:e.offsetParent}!function(){const t=navigator.appVersion,i=navigator.userAgent;let o;switch(navigator.appName){case"Microsoft Internet Explorer":window.attachEvent&&window.ActiveXObject&&(e.Browser.vendor=e.BROWSERS.IE,e.Browser.version=parseFloat(i.substring(i.indexOf("MSIE")+5,i.indexOf(";",i.indexOf("MSIE")))));break;case"Netscape":window.addEventListener&&(i.indexOf("Edge")>=0?(e.Browser.vendor=e.BROWSERS.EDGE,e.Browser.version=parseFloat(i.substring(i.indexOf("Edge")+5))):i.indexOf("Edg")>=0?(e.Browser.vendor=e.BROWSERS.CHROMEEDGE,e.Browser.version=parseFloat(i.substring(i.indexOf("Edg")+4))):i.indexOf("Firefox")>=0?(e.Browser.vendor=e.BROWSERS.FIREFOX,e.Browser.version=parseFloat(i.substring(i.indexOf("Firefox")+8))):i.indexOf("Safari")>=0?(e.Browser.vendor=i.indexOf("Chrome")>=0?e.BROWSERS.CHROME:e.BROWSERS.SAFARI,e.Browser.version=parseFloat(i.substring(i.substring(0,i.indexOf("Safari")).lastIndexOf("/")+1,i.indexOf("Safari")))):(o=new RegExp("Trident/.*rv:([0-9]{1,}[.0-9]{0,})"),null!==o.exec(i)&&(e.Browser.vendor=e.BROWSERS.IE,e.Browser.version=parseFloat(RegExp.$1))));break;case"Opera":e.Browser.vendor=e.BROWSERS.OPERA,e.Browser.version=parseFloat(t)}const s=window.location.search.substring(1).split("&");for(let t=0;t0){const t=i.substring(0,o),s=i.substring(o+1);try{n[t]=decodeURIComponent(s)}catch(i){e.console.error("Ignoring malformed URL parameter: %s=%s",t,s)}}}e.Browser.alpha=!(e.Browser.vendor===e.BROWSERS.CHROME&&e.Browser.version<2),e.Browser.opacity=!0,e.Browser.vendor===e.BROWSERS.IE&&e.console.error("Internet Explorer is not supported by OpenSeadragon")}(),function(t){const i=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame,n=t.cancelAnimationFrame||t.mozCancelAnimationFrame||t.webkitCancelAnimationFrame||t.msCancelAnimationFrame;if(i&&n)e.requestAnimationFrame=function(){return i.apply(t,arguments)},e.cancelAnimationFrame=function(){return n.apply(t,arguments)};else{let t,i=[],n=[],o=0;e.requestAnimationFrame=function(s){return i.push([++o,s]),t||(t=setInterval(function(){if(i.length){const t=e.now(),o=n;for(n=i,i=o;n.length;)n.shift()[1](t)}else clearInterval(t),t=void 0},20)),o},e.cancelAnimationFrame=function(e){let t,o;for(t=0,o=i.length;t{for(;t instanceof e.Promise;)t=t._value;this._value=t},t=>{for(;t instanceof e.Promise;)t=t._value;this._value=t,this._error=!0})}catch(e){this._value=e,this._error=!0}}then(e){if(!this._error)try{this._value=e(this._value)}catch(e){this._value=e,this._error=!0}return this}catch(e){if(this._error)try{this._value=e(this._value),this._error=!1}catch(e){this._value=e,this._error=!0}return this}get _value(){return this.__value}set _value(e){e&&e.constructor===this.constructor&&(e=e._value),this.__value=e}static resolve(e){return new this(t=>t(e))}static reject(e){return new this((t,i)=>i(e))}static all(e){return new this(t=>t(e.map(e=>e())))}static race(e){return e.length<1?this.resolve():new this(t=>t(e[0]()))}}}(OpenSeadragon),function(e,t){"function"==typeof define&&define.amd?define([],function(){return t}):"object"==typeof module&&module.exports?module.exports=t:(e||(e="object"==typeof window&&window)||t.console.error("OpenSeadragon must run in browser environment!"),e.OpenSeadragon=t)}(this,OpenSeadragon),function(e){class t{constructor(e){e||(e=[0,0,0,0,0,0,0,0,0]),this.values=e}static makeIdentity(){return new t([1,0,0,0,1,0,0,0,1])}static makeTranslation(e,i){return new t([1,0,0,0,1,0,e,i,1])}static makeRotation(e){const i=Math.cos(e),n=Math.sin(e);return new t([i,-n,0,n,i,0,0,0,1])}static makeScaling(e,i){return new t([e,0,0,0,i,0,0,0,1])}multiply(e){let i=this.values,n=e.values;const o=i[0],s=i[1],r=i[2],a=i[3],l=i[4],h=i[5],c=i[6],u=i[7],d=i[8],p=n[0],g=n[1],m=n[2],f=n[3],v=n[4],y=n[5],w=n[6],_=n[7],T=n[8];return new t([p*o+g*a+m*c,p*s+g*l+m*u,p*r+g*h+m*d,f*o+v*a+y*c,f*s+v*l+y*u,f*r+v*h+y*d,w*o+_*a+T*c,w*s+_*l+T*u,w*r+_*h+T*d])}setValues(e,t,i,n,o,s,r,a,l){this.values[0]=e,this.values[1]=t,this.values[2]=i,this.values[3]=n,this.values[4]=o,this.values[5]=s,this.values[6]=r,this.values[7]=a,this.values[8]=l}scaleAndTranslate(e,i,n,o){const s=this.values,r=s[0],a=s[1],l=s[2],h=s[3],c=s[4],u=s[5];return new t([e*r,e*a,e*l,i*h,i*c,i*u,n*r+o*h,n*a+o*c,n*l+o*u])}scaleAndTranslateSelf(e,t,i,n){const o=this.values,s=o[0],r=o[1],a=o[2],l=o[3],h=o[4],c=o[5];o[0]=e*s,o[1]=e*r,o[2]=e*a,o[3]=t*l,o[4]=t*h,o[5]=t*c,o[6]=i*s+n*l+o[6],o[7]=i*r+n*h+o[7],o[8]=i*a+n*c+o[8]}scaleAndTranslateOtherSetSelf(e){const t=e.values,i=this.values,n=i[0],o=i[4],s=i[6],r=i[7];i[0]=n*t[0],i[1]=n*t[1],i[2]=n*t[2],i[3]=o*t[3],i[4]=o*t[4],i[5]=o*t[5],i[6]=s*t[0]+r*t[3]+t[6],i[7]=s*t[1]+r*t[4]+t[7],i[8]=s*t[2]+r*t[5]+t[8]}}e.Mat3=t}(OpenSeadragon),function(e){const t={supportsFullScreen:!1,isFullScreen:function(){return!1},getFullScreenElement:function(){return null},requestFullScreen:function(){},exitFullScreen:function(){},cancelFullScreen:function(){},fullScreenEventName:"",fullScreenErrorEventName:""};document.exitFullscreen?(t.supportsFullScreen=!0,t.getFullScreenElement=function(){return document.fullscreenElement},t.requestFullScreen=function(t){return t.requestFullscreen().catch(function(t){e.console.error("Fullscreen request failed: ",t)})},t.exitFullScreen=function(){document.exitFullscreen().catch(function(t){e.console.error("Error while exiting fullscreen: ",t)})},t.fullScreenEventName="fullscreenchange",t.fullScreenErrorEventName="fullscreenerror"):document.msExitFullscreen?(t.supportsFullScreen=!0,t.getFullScreenElement=function(){return document.msFullscreenElement},t.requestFullScreen=function(e){return e.msRequestFullscreen()},t.exitFullScreen=function(){document.msExitFullscreen()},t.fullScreenEventName="MSFullscreenChange",t.fullScreenErrorEventName="MSFullscreenError"):document.webkitExitFullscreen?(t.supportsFullScreen=!0,t.getFullScreenElement=function(){return document.webkitFullscreenElement},t.requestFullScreen=function(e){return e.webkitRequestFullscreen()},t.exitFullScreen=function(){document.webkitExitFullscreen()},t.fullScreenEventName="webkitfullscreenchange",t.fullScreenErrorEventName="webkitfullscreenerror"):document.webkitCancelFullScreen?(t.supportsFullScreen=!0,t.getFullScreenElement=function(){return document.webkitCurrentFullScreenElement},t.requestFullScreen=function(e){return e.webkitRequestFullScreen()},t.exitFullScreen=function(){document.webkitCancelFullScreen()},t.fullScreenEventName="webkitfullscreenchange",t.fullScreenErrorEventName="webkitfullscreenerror"):document.mozCancelFullScreen&&(t.supportsFullScreen=!0,t.getFullScreenElement=function(){return document.mozFullScreenElement},t.requestFullScreen=function(e){return e.mozRequestFullScreen()},t.exitFullScreen=function(){document.mozCancelFullScreen()},t.fullScreenEventName="mozfullscreenchange",t.fullScreenErrorEventName="mozfullscreenerror"),t.isFullScreen=function(){return null!==t.getFullScreenElement()},t.cancelFullScreen=function(){e.console.error("cancelFullScreen is deprecated. Use exitFullScreen instead."),t.exitFullScreen()},e.extend(e,t)}(OpenSeadragon),function(e){e.EventSource=function(){this.events={},this._rejectedEventList={}},e.EventSource.prototype={addOnceHandler:function(e,t,i,n,o){const s=this;n=n||1;let r=0;const a=function(i){return r++,r===n&&s.removeHandler(e,a),t(i)};return this.addHandler(e,a,i,o)},addHandler:function(t,i,n,o){if(Object.prototype.hasOwnProperty.call(this._rejectedEventList,t))return e.console.error(`Error adding handler for ${t}. ${this._rejectedEventList[t]}`),!1;let s=this.events[t];if(s||(this.events[t]=s=[]),i&&e.isFunction(i)){let e=s.length,t={handler:i,userData:n||null,priority:o||0};for(s[e]=t;e>0&&s[e-1].priority{const a=n.length;(function l(h){if(h>=a||!n[h])return s(i),null;let c;o.eventSource=t,o.userData=n[h].userData;try{c=n[h].handler(o)}catch(e){return r(e)}return c=c&&"promise"===e.type(c)?c:e.Promise.resolve(),c.then(()=>!o.stopPropagation||"function"==typeof o.stopPropagation&&!1===o.stopPropagation()?l(h+1):l(a))})(0).catch(r)})}):null},raiseEvent:function(t,i){if(Object.prototype.hasOwnProperty.call(this._rejectedEventList,t))return e.console.error(`Error adding handler for ${t}. ${this._rejectedEventList[t]}`),!1;const n=this.getHandler(t);return n&&n(this,i||{}),!0},raiseEventAwaiting:function(t,i,n=null){const o=this.getAwaitingHandler(t,n);return o?o(this,i||{}):e.Promise.resolve(n)},rejectEventHandler(e,t=""){this._rejectedEventList[e]=t},allowEventHandler(e){delete this._rejectedEventList[e]}}}(OpenSeadragon),function(e){const t=[],i={};e.MouseTracker=function(n){t.push(this);const o=arguments;e.isPlainObject(n)||(n={element:o[0],clickTimeThreshold:o[1],clickDistThreshold:o[2]}),this.hash=function(){let e=Date.now().toString(36)+Math.random().toString(36).substring(2);for(;e in i;)e=Date.now().toString(36)+Math.random().toString(36).substring(2);return e}(),this.element=e.getElement(n.element),this.clickTimeThreshold=n.clickTimeThreshold||e.DEFAULT_SETTINGS.clickTimeThreshold,this.clickDistThreshold=n.clickDistThreshold||e.DEFAULT_SETTINGS.clickDistThreshold,this.dblClickTimeThreshold=n.dblClickTimeThreshold||e.DEFAULT_SETTINGS.dblClickTimeThreshold,this.dblClickDistThreshold=n.dblClickDistThreshold||e.DEFAULT_SETTINGS.dblClickDistThreshold,this.userData=n.userData||null,this.stopDelay=n.stopDelay||50,this.preProcessEventHandler=n.preProcessEventHandler||null,this.contextMenuHandler=n.contextMenuHandler||null,this.enterHandler=n.enterHandler||null,this.leaveHandler=n.leaveHandler||null,this.exitHandler=n.exitHandler||null,this.overHandler=n.overHandler||null,this.outHandler=n.outHandler||null,this.pressHandler=n.pressHandler||null,this.nonPrimaryPressHandler=n.nonPrimaryPressHandler||null,this.releaseHandler=n.releaseHandler||null,this.nonPrimaryReleaseHandler=n.nonPrimaryReleaseHandler||null,this.moveHandler=n.moveHandler||null,this.scrollHandler=n.scrollHandler||null,this.clickHandler=n.clickHandler||null,this.dblClickHandler=n.dblClickHandler||null,this.dragHandler=n.dragHandler||null,this.dragEndHandler=n.dragEndHandler||null,this.pinchHandler=n.pinchHandler||null,this.stopHandler=n.stopHandler||null,this.keyDownHandler=n.keyDownHandler||null,this.keyUpHandler=n.keyUpHandler||null,this.keyHandler=n.keyHandler||null,this.focusHandler=n.focusHandler||null,this.blurHandler=n.blurHandler||null;const s=this;i[this.hash]={click:function(t){!function(t,i){const n={originalEvent:i,eventType:"click",pointerType:"mouse",isEmulated:!1};D(t,n),n.preventDefault&&!n.defaultPrevented&&e.cancelEvent(i);n.stopPropagation&&e.stopEvent(i)}(s,t)},dblclick:function(t){!function(t,i){const n={originalEvent:i,eventType:"dblclick",pointerType:"mouse",isEmulated:!1};D(t,n),n.preventDefault&&!n.defaultPrevented&&e.cancelEvent(i);n.stopPropagation&&e.stopEvent(i)}(s,t)},keydown:function(t){!function(t,i){let n=null;const o={originalEvent:i,eventType:"keydown",pointerType:"",isEmulated:!1};D(t,o),!t.keyDownHandler||o.preventGesture||o.defaultPrevented||(n={eventSource:t,keyCode:i.keyCode?i.keyCode:i.charCode,ctrl:i.ctrlKey,shift:i.shiftKey,alt:i.altKey,meta:i.metaKey,originalEvent:i,preventDefault:o.preventDefault||o.defaultPrevented,userData:t.userData},t.keyDownHandler(n));(n&&n.preventDefault||o.preventDefault&&!o.defaultPrevented)&&e.cancelEvent(i);o.stopPropagation&&e.stopEvent(i)}(s,t)},keyup:function(t){!function(t,i){let n=null;const o={originalEvent:i,eventType:"keyup",pointerType:"",isEmulated:!1};D(t,o),!t.keyUpHandler||o.preventGesture||o.defaultPrevented||(n={eventSource:t,keyCode:i.keyCode?i.keyCode:i.charCode,ctrl:i.ctrlKey,shift:i.shiftKey,alt:i.altKey,meta:i.metaKey,originalEvent:i,preventDefault:o.preventDefault||o.defaultPrevented,userData:t.userData},t.keyUpHandler(n));(n&&n.preventDefault||o.preventDefault&&!o.defaultPrevented)&&e.cancelEvent(i);o.stopPropagation&&e.stopEvent(i)}(s,t)},keypress:function(t){!function(t,i){let n=null;const o={originalEvent:i,eventType:"keypress",pointerType:"",isEmulated:!1};D(t,o),!t.keyHandler||o.preventGesture||o.defaultPrevented||(n={eventSource:t,keyCode:i.keyCode?i.keyCode:i.charCode,ctrl:i.ctrlKey,shift:i.shiftKey,alt:i.altKey,meta:i.metaKey,originalEvent:i,preventDefault:o.preventDefault||o.defaultPrevented,userData:t.userData},t.keyHandler(n));(n&&n.preventDefault||o.preventDefault&&!o.defaultPrevented)&&e.cancelEvent(i);o.stopPropagation&&e.stopEvent(i)}(s,t)},focus:function(e){!function(e,t){const i={originalEvent:t,eventType:"focus",pointerType:"",isEmulated:!1};D(e,i),e.focusHandler&&!i.preventGesture&&e.focusHandler({eventSource:e,originalEvent:t,userData:e.userData})}(s,e)},blur:function(e){!function(e,t){const i={originalEvent:t,eventType:"blur",pointerType:"",isEmulated:!1};D(e,i),e.blurHandler&&!i.preventGesture&&e.blurHandler({eventSource:e,originalEvent:t,userData:e.userData})}(s,e)},contextmenu:function(t){!function(t,i){let n=null;const o={originalEvent:i,eventType:"contextmenu",pointerType:"mouse",isEmulated:!1};D(t,o),!t.contextMenuHandler||o.preventGesture||o.defaultPrevented||(n={eventSource:t,position:g(d(i),t.element),originalEvent:o.originalEvent,preventDefault:o.preventDefault||o.defaultPrevented,userData:t.userData},t.contextMenuHandler(n));(n&&n.preventDefault||o.preventDefault&&!o.defaultPrevented)&&e.cancelEvent(i);o.stopPropagation&&e.stopEvent(i)}(s,t)},wheel:function(e){!function(e,t){v(e,t,t)}(s,e)},mousewheel:function(e){f(s,e)},DOMMouseScroll:function(e){f(s,e)},MozMousePixelScroll:function(e){f(s,e)},losecapture:function(t){!function(t,i){const n={id:e.MouseTracker.mousePointerId,type:"mouse"},o={originalEvent:i,eventType:"lostpointercapture",pointerType:"mouse",isEmulated:!1};D(t,o),i.target===t.element&&I(t,n,!1);o.stopPropagation&&e.stopEvent(i)}(s,t)},mouseenter:function(e){y(s,e)},mouseleave:function(e){w(s,e)},mouseover:function(e){_(s,e)},mouseout:function(e){T(s,e)},mousedown:function(e){x(s,e)},mouseup:function(e){S(s,e)},mousemove:function(e){C(s,e)},touchstart:function(t){!function(t,i){const n=i.changedTouches.length,o=t.getActivePointersListByType("touch"),s=e.now();o.getLength()>i.touches.length-n&&e.console.warn("Tracked touch contact count doesn't match event.touches.length");const r={originalEvent:i,eventType:"pointerdown",pointerType:"touch",isEmulated:!1};D(t,r);for(let e=0;e0){const t=[],i=n.asArray();for(let e=0;e1&&("mouse"===this.type||"pen"===this.type)&&(e.console.warn("GesturePointList.addContact() Implausible contacts value"),this.contacts=1)},removeContact:function(){--this.contacts,this.contacts<0&&(this.contacts=0)}}}(OpenSeadragon),function(e){e.ControlAnchor={NONE:0,TOP_LEFT:1,TOP_RIGHT:2,BOTTOM_RIGHT:3,BOTTOM_LEFT:4,ABSOLUTE:5},e.Control=function(t,i,n){const o=t.parentNode;"number"==typeof i&&(e.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"),i={anchor:i}),i.attachToViewer=void 0===i.attachToViewer||i.attachToViewer,this.autoFade=void 0===i.autoFade||i.autoFade,this.element=t,this.anchor=i.anchor,this.container=n,this.anchor===e.ControlAnchor.ABSOLUTE?(this.wrapper=e.makeNeutralElement("div"),this.wrapper.style.position="absolute",this.wrapper.style.top="number"==typeof i.top?i.top+"px":i.top,this.wrapper.style.left="number"==typeof i.left?i.left+"px":i.left,this.wrapper.style.height="number"==typeof i.height?i.height+"px":i.height,this.wrapper.style.width="number"==typeof i.width?i.width+"px":i.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%"):(this.wrapper=e.makeNeutralElement("div"),this.wrapper.style.display="inline-block",this.anchor===e.ControlAnchor.NONE&&(this.wrapper.style.width=this.wrapper.style.height="100%")),this.wrapper.appendChild(this.element),i.attachToViewer?this.anchor===e.ControlAnchor.TOP_RIGHT||this.anchor===e.ControlAnchor.BOTTOM_RIGHT?this.container.insertBefore(this.wrapper,this.container.firstChild):this.container.appendChild(this.wrapper):o.appendChild(this.wrapper)},e.Control.prototype={destroy:function(){this.wrapper.removeChild(this.element),this.anchor!==e.ControlAnchor.NONE&&this.container.removeChild(this.wrapper)},isVisible:function(){return"none"!==this.wrapper.style.display},setVisible:function(t){this.wrapper.style.display=t?this.anchor===e.ControlAnchor.ABSOLUTE?"block":"inline-block":"none"},setOpacity:function(t){e.setElementOpacity(this.wrapper,t,!0)}}}(OpenSeadragon),function(e){function t(e,t){const i=e.controls;for(let e=i.length-1;e>=0;e--)if(i[e].element===t)return e;return-1}e.ControlDock=function(t){const i=["topleft","topright","bottomright","bottomleft"];e.extend(!0,this,{id:"controldock-"+e.now()+"-"+Math.floor(1e6*Math.random()),container:e.makeNeutralElement("div"),controls:[]},t),this.container.onsubmit=function(){return!1},this.element&&(this.element=e.getElement(this.element),this.element.appendChild(this.container),"static"===e.getElementStyle(this.element).position&&(this.element.style.position="relative"),this.container.style.width="100%",this.container.style.height="100%");for(let t=0;t=0)){switch(n.anchor){case e.ControlAnchor.TOP_RIGHT:o=this.controls.topright,i.style.position="relative",i.style.paddingRight="0px",i.style.paddingTop="0px";break;case e.ControlAnchor.BOTTOM_RIGHT:o=this.controls.bottomright,i.style.position="relative",i.style.paddingRight="0px",i.style.paddingBottom="0px";break;case e.ControlAnchor.BOTTOM_LEFT:o=this.controls.bottomleft,i.style.position="relative",i.style.paddingLeft="0px",i.style.paddingBottom="0px";break;case e.ControlAnchor.TOP_LEFT:o=this.controls.topleft,i.style.position="relative",i.style.paddingLeft="0px",i.style.paddingTop="0px";break;case e.ControlAnchor.ABSOLUTE:default:case e.ControlAnchor.NONE:o=this.container,i.style.margin="0px",i.style.padding="0px"}this.controls.push(new e.Control(i,n,o)),i.style.display="inline-block"}},removeControl:function(i){const n=t(this,i=e.getElement(i));return n>=0&&(this.controls[n].destroy(),this.controls.splice(n,1)),this},clearControls:function(){for(;this.controls.length>0;)this.controls.pop().destroy();return this},areControlsEnabled:function(){for(let e=this.controls.length-1;e>=0;e--)if(this.controls[e].isVisible())return!0;return!1},setControlsEnabled:function(e){for(let t=this.controls.length-1;t>=0;t--)this.controls[t].setVisible(e);return this}}}(OpenSeadragon),function(e){e.Placement=e.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:!1,isHorizontallyCentered:!0,isRight:!1,isTop:!1,isVerticallyCentered:!0,isBottom:!1},1:{isLeft:!0,isHorizontallyCentered:!1,isRight:!1,isTop:!0,isVerticallyCentered:!1,isBottom:!1},2:{isLeft:!1,isHorizontallyCentered:!0,isRight:!1,isTop:!0,isVerticallyCentered:!1,isBottom:!1},3:{isLeft:!1,isHorizontallyCentered:!1,isRight:!0,isTop:!0,isVerticallyCentered:!1,isBottom:!1},4:{isLeft:!1,isHorizontallyCentered:!1,isRight:!0,isTop:!1,isVerticallyCentered:!0,isBottom:!1},5:{isLeft:!1,isHorizontallyCentered:!1,isRight:!0,isTop:!1,isVerticallyCentered:!1,isBottom:!0},6:{isLeft:!1,isHorizontallyCentered:!0,isRight:!1,isTop:!1,isVerticallyCentered:!1,isBottom:!0},7:{isLeft:!0,isHorizontallyCentered:!1,isRight:!1,isTop:!1,isVerticallyCentered:!1,isBottom:!0},8:{isLeft:!0,isHorizontallyCentered:!1,isRight:!1,isTop:!1,isVerticallyCentered:!0,isBottom:!1}}})}(OpenSeadragon),function(e){const t={};let i=1;function n(t){return t=e.getElement(t),new e.Point(0===t.clientWidth?1:t.clientWidth,0===t.clientHeight?1:t.clientHeight)}function o(t,i){if(i instanceof e.Overlay)return i;let n=null;if(i.element)n=e.getElement(i.element);else{const t=i.id?i.id:"openseadragon-overlay-"+Math.floor(1e7*Math.random());n=e.getElement(i.id),n||(n=document.createElement("a"),n.href="#/overlay/"+t),n.id=t,e.addClass(n,i.className?i.className:"openseadragon-overlay")}let o=i.location,s=i.width,r=i.height;if(!o){let n=i.x,a=i.y;if(void 0!==i.px){const o=t.viewport.imageToViewportRectangle(new e.Rect(i.px,i.py,s||0,r||0));n=o.x,a=o.y,s=void 0!==s?o.width:void 0,r=void 0!==r?o.height:void 0}o=new e.Point(n,a)}let a=i.placement;return a&&"string"===e.type(a)&&(a=e.Placement[i.placement.toUpperCase()]),new e.Overlay({element:n,location:o,placement:a,onDraw:i.onDraw,checkResize:i.checkResize,width:s,height:r,rotationMode:i.rotationMode})}function s(e,t){for(let i=e.length-1;i>=0;i--)if(e[i].element===t)return i;return-1}function r(t,i){return e.requestAnimationFrame(function(){i(t)})}function a(t){e.requestAnimationFrame(function(){!function(t){if(t.controlsShouldFade){let i=1-(e.now()-t.controlsFadeBeginTime)/t.controlsFadeLength;i=Math.min(1,i),i=Math.max(0,i);for(let e=t.controls.length-1;e>=0;e--)t.controls[e].autoFade&&t.controls[e].setOpacity(i);i>0&&a(t)}}(t)})}function l(t){t.autoHideControls&&(t.controlsShouldFade=!0,t.controlsFadeBeginTime=e.now()+t.controlsFadeDelay,window.setTimeout(function(){a(t)},t.controlsFadeDelay))}function h(e){e.controlsShouldFade=!1;for(let t=e.controls.length-1;t>=0;t--)e.controls[t].setOpacity(1)}function c(){h(this)}function u(){l(this)}function d(e){const t={tracker:e.eventSource,position:e.position,originalEvent:e.originalEvent,preventDefault:e.preventDefault};this.raiseEvent("canvas-contextmenu",t),e.preventDefault=t.preventDefault}function p(e,t){switch(e){case"ArrowUp":return t?"zoomIn":"panUp";case"ArrowDown":return t?"zoomOut":"panDown";case"ArrowLeft":return"panLeft";case"ArrowRight":return"panRight";case"Equal":return"zoomIn";case"Minus":return"zoomOut";default:return null}}function g(e){const t=(e,t)=>{const i=p(e,t);i&&this._activeActions[i]&&(this._activeActions[i]=!1,this._navActionFrames[i]=n.flickMinSpeed){let t=0;this.panHorizontal&&(t=n.flickMomentum*i.speed*Math.cos(i.direction));let o=0;this.panVertical&&(o=n.flickMomentum*i.speed*Math.sin(i.direction));const s=this.viewport.pixelFromPoint(this.viewport.getCenter(!0)),r=this.viewport.pointFromPixel(new e.Point(s.x-t,s.y-o));this.viewport.panTo(r,!1)}this.viewport.applyConstraints()}n.dblClickDragToZoom&&!0===t[this.hash].draggingToZoom&&(t[this.hash].draggingToZoom=!1)}function T(e){this.raiseEvent("canvas-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function x(e){this.raiseEvent("canvas-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function S(i){this.raiseEvent("canvas-press",{tracker:i.eventSource,pointerType:i.pointerType,position:i.position,insideElementPressed:i.insideElementPressed,insideElementReleased:i.insideElementReleased,originalEvent:i.originalEvent});if(this.gestureSettingsByDeviceType(i.pointerType).dblClickDragToZoom){const i=t[this.hash].lastClickTime,n=e.now();if(null===i)return;n-ithis.minScrollDeltaTime?(this._lastScrollTime=s,i={tracker:t.eventSource,position:t.position,scroll:t.scroll,shift:t.shift,originalEvent:t.originalEvent,preventDefaultAction:!1,preventDefault:!0},this.raiseEvent("canvas-scroll",i),!i.preventDefaultAction&&this.viewport&&(this.viewport.flipped&&(t.position.x=this.viewport.getContainerSize().x-t.position.x),n=this.gestureSettingsByDeviceType(t.pointerType),n.scrollToZoom&&(o=Math.pow(this.zoomPerScroll,t.scroll),this.viewport.zoomBy(o,n.zoomToRefPoint?this.viewport.pointFromPixel(t.position,!0):null),this.viewport.applyConstraints())),t.preventDefault=i.preventDefault):t.preventDefault=!0}function A(e){t[this.hash].mouseInside=!0,h(this),this.raiseEvent("container-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function F(e){e.pointers<1&&(t[this.hash].mouseInside=!1,t[this.hash].animating||l(this)),this.raiseEvent("container-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function O(i){!function(i){if(function(e){for(const t in e._activeActions)(e._activeActions[t]||e._navActionVirtuallyHeld[t])&&(e._navActionFrames[t]++,e._navActionFrames[t]>=e._minNavActionFrames&&(e._navActionVirtuallyHeld[t]=!1));function t(t){return e._activeActions[t]||e._navActionVirtuallyHeld[t]}const i=e.pixelsPerArrowPress/10,n=e.viewport.deltaPointsFromPixels(new OpenSeadragon.Point(i,i));if(t("zoomIn"))return e.viewport.zoomBy(1.01,null,!0),void e.viewport.applyConstraints();if(t("zoomOut"))return e.viewport.zoomBy(.99,null,!0),void e.viewport.applyConstraints();let o=0,s=0;e.preventVerticalPan||(t("panUp")&&(s-=n.y),t("panDown")&&(s+=n.y));e.preventHorizontalPan||(t("panLeft")&&(o-=n.x),t("panRight")&&(o+=n.x));0===o&&0===s||(e.viewport.panBy(new OpenSeadragon.Point(o,s),!0),e.viewport.applyConstraints())}(i),i._opening||!t[i.hash])return;let o=!1;if(i.autoResize||t[i.hash].forceResize){let s;if(i._autoResizePolling){s=n(i.container);const e=t[i.hash].prevContainerSize;s.equals(e)||(t[i.hash].needsResize=!0)}t[i.hash].needsResize&&(!function(i,n){const o=i.viewport,s=o.getZoom(),r=o.getCenter();let a;if(o.resize(n,i.preserveImageSizeOnResize),o.panTo(r,!0),i.preserveImageSizeOnResize)a=t[i.hash].prevContainerSize.x/n.x;else{const o=new e.Point(0,0),s=new e.Point(t[i.hash].prevContainerSize.x,t[i.hash].prevContainerSize.y).distanceTo(o);a=new e.Point(n.x,n.y).distanceTo(o)/s*t[i.hash].prevContainerSize.x/n.x}o.zoomTo(s*a,null,!0),t[i.hash].prevContainerSize=n,t[i.hash].forceRedraw=!0,t[i.hash].needsResize=!1,t[i.hash].forceResize=!1}(i,s||n(i.container)),o=!0)}const s=i.viewport.update()||o;let r=i.world.update(s)||s;s&&i.raiseEvent("viewport-change");i.referenceStrip&&(r=i.referenceStrip.update(i.viewport)||r);const a=t[i.hash].animating;!a&&r&&(i.raiseEvent("animation-start"),h(i));const c=a&&!r;c&&(t[i.hash].animating=!1);(r||c||t[i.hash].forceRedraw||i.world.needsDraw())&&(!function(e){e.imageLoader.clear(),e.world.draw(),e.raiseEvent("update-viewport",{})}(i),i._drawOverlays(),i.navigator&&i.navigator.update(i.viewport),t[i.hash].forceRedraw=!1,r&&i.raiseEvent("animation"));c&&(i.raiseEvent("animation-finish"),t[i.hash].mouseInside||l(i));t[i.hash].animating=r}(i),i.isOpen()?i._updateRequestId=r(i,O):i._updateRequestId=!1}function L(e,t){return e?e+t:t}function k(t){e.requestAnimationFrame(e.delegate(t,B))}function B(){if(t[this.hash].zooming&&this.viewport){const i=e.now(),n=i-t[this.hash].lastZoomTime,o=Math.pow(t[this.hash].zoomFactor,n/1e3);this.viewport.zoomBy(o),this.viewport.applyConstraints(),t[this.hash].lastZoomTime=i,k(this)}}function M(){this.buttonGroup&&(this.buttonGroup.emulateEnter(),this.buttonGroup.emulateLeave())}function z(){this.viewport&&this.viewport.goHome()}function H(){this.isFullPage()&&!e.isFullScreen()?this.setFullPage(!1):this.setFullScreen(!this.isFullPage()),this.buttonGroup&&this.buttonGroup.emulateLeave(),this.fullPageButton.element.focus(),this.viewport&&this.viewport.applyConstraints()}function N(){if(this.viewport){let e=this.viewport.getRotation();this.viewport.flipped?e+=this.rotationIncrement:e-=this.rotationIncrement,this.viewport.setRotation(e)}}function U(){if(this.viewport){let e=this.viewport.getRotation();this.viewport.flipped?e-=this.rotationIncrement:e+=this.rotationIncrement,this.viewport.setRotation(e)}}function W(){this.viewport.toggleFlip()}function j(t){if("string"==typeof t)return t;const i=t&&t.prototype;return i&&i instanceof OpenSeadragon.DrawerBase&&e.isFunction(i.getType)?i.getType.call(t):void 0}function G(){const e=window.matchMedia("(pointer: coarse)").matches;return/iPad|iPhone|iPod|Mac/.test(navigator.userAgent)&&e?["canvas"]:["webgl","canvas"]}e.Viewer=function(o){const s=arguments,a=this;let h;e.isPlainObject(o)||(o={id:s[0],xmlPath:s.length>1?s[1]:void 0,prefixUrl:s.length>2?s[2]:void 0,controls:s.length>3?s[3]:void 0,overlays:s.length>4?s[4]:void 0}),o.config&&(e.extend(!0,o,o.config),delete o.config);if(o.drawerOptions=Object.assign({},["useCanvas"].reduce((e,t)=>(e[t]=o[t],delete o[t],e),{}),o.drawerOptions),e.extend(!0,this,{id:o.id,hash:o.hash||i++,viewer:null,initialPage:0,element:null,container:null,canvas:null,overlays:[],overlaysContainer:null,previousDisplayValuesOfBodyChildren:[],customControls:[],source:null,drawer:null,drawerCandidates:null,world:null,viewport:null,navigator:null,collectionViewport:null,collectionDrawer:null,navImages:null,buttonGroup:null,profiler:null},e.DEFAULT_SETTINGS,o),void 0===this.hash)throw new Error("A hash must be defined, either by specifying options.id or options.hash.");if(void 0!==t[this.hash]&&e.console.warn("Hash "+this.hash+" has already been used."),t[this.hash]={fsBoundsDelta:new e.Point(1,1),prevContainerSize:null,animating:!1,forceRedraw:!1,needsResize:!1,forceResize:!1,mouseInside:!1,group:null,zooming:!1,zoomFactor:null,lastZoomTime:null,fullPage:!1,onfullscreenchange:null,lastClickTime:null,draggingToZoom:!1},this._sequenceIndex=0,this._firstOpen=!0,this._updateRequestId=null,this._loadQueue=[],this.currentOverlays=[],this._updatePixelDensityRatioBind=null,this._lastScrollTime=e.now(),this._fullyLoaded=!1,this._navActionFrames={},this._navActionVirtuallyHeld={},this._minNavActionFrames=10,this._activeActions={panUp:!1,panDown:!1,panLeft:!1,panRight:!1,zoomIn:!1,zoomOut:!1},e.EventSource.call(this),this.addHandler("open-failed",function(t){const i=e.getString("Errors.OpenFailed",t.eventSource,t.message);a._showMessage(i)}),e.ControlDock.call(this,o),this.xmlPath&&(this.tileSources=[this.xmlPath]),this.element=this.element||document.getElementById(this.id),this.canvas=e.makeNeutralElement("div"),this.canvas.className="openseadragon-canvas",!document.querySelector("style[data-openseadragon-mobile-css]")){const e=document.createElement("style");e.setAttribute("data-openseadragon-mobile-css","true"),e.textContent="@media (hover: none) { .openseadragon-canvas:focus { outline: none !important; }}",document.head.appendChild(e)}var c;(c=this.canvas.style).width="100%",c.height="100%",c.overflow="hidden",c.position="absolute",c.top="0px",c.left="0px",e.setElementTouchActionNone(this.canvas),""!==o.tabIndex&&(this.canvas.tabIndex=void 0===o.tabIndex?0:o.tabIndex),this.container.className="openseadragon-container",function(e){e.width="100%",e.height="100%",e.position="relative",e.overflow="hidden",e.left="0px",e.top="0px",e.textAlign="left"}(this.container.style),e.setElementTouchActionNone(this.container),this.container.insertBefore(this.canvas,this.container.firstChild),this.element.appendChild(this.container),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 e.MouseTracker({userData:"Viewer.innerTracker",element:this.canvas,startDisabled:!this.mouseNavEnabled,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,dblClickTimeThreshold:this.dblClickTimeThreshold,dblClickDistThreshold:this.dblClickDistThreshold,contextMenuHandler:e.delegate(this,d),keyDownHandler:e.delegate(this,m),keyUpHandler:e.delegate(this,g),keyHandler:e.delegate(this,f),clickHandler:e.delegate(this,v),dblClickHandler:e.delegate(this,y),dragHandler:e.delegate(this,w),dragEndHandler:e.delegate(this,_),enterHandler:e.delegate(this,T),leaveHandler:e.delegate(this,x),pressHandler:e.delegate(this,S),releaseHandler:e.delegate(this,E),nonPrimaryPressHandler:e.delegate(this,C),nonPrimaryReleaseHandler:e.delegate(this,b),scrollHandler:e.delegate(this,I),pinchHandler:e.delegate(this,P),focusHandler:e.delegate(this,R),blurHandler:e.delegate(this,D)}),this.outerTracker=new e.MouseTracker({userData:"Viewer.outerTracker",element:this.container,startDisabled:!this.mouseNavEnabled,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,dblClickTimeThreshold:this.dblClickTimeThreshold,dblClickDistThreshold:this.dblClickDistThreshold,enterHandler:e.delegate(this,A),leaveHandler:e.delegate(this,F)}),this.toolbar&&(this.toolbar=new e.ControlDock({element:this.toolbar})),this.bindStandardControls(),t[this.hash].prevContainerSize=n(this.container),window.ResizeObserver?(this._autoResizePolling=!1,this._resizeObserver=new ResizeObserver(function(){t[a.hash].needsResize=!0}),this._resizeObserver.observe(this.container,{})):this._autoResizePolling=!0,this.world=new e.World({viewer:this}),this.world.addHandler("add-item",function(e){a.source=a.world.getItemAt(0).source,t[a.hash].forceRedraw=!0,a._updateRequestId||(a._updateRequestId=r(a,O));const i=e.item,n=function(){const e=a._areAllFullyLoaded();e!==a._fullyLoaded&&(a._fullyLoaded=e,a.raiseEvent("fully-loaded-change",{fullyLoaded:e}))};i._fullyLoadedHandlerForViewer=n,i.addHandler("fully-loaded-change",n)}),this.world.addHandler("remove-item",function(e){const i=e.item;i._fullyLoadedHandlerForViewer&&(i.removeHandler("fully-loaded-change",i._fullyLoadedHandlerForViewer),delete i._fullyLoadedHandlerForViewer),a.world.getItemCount()?a.source=a.world.getItemAt(0).source:a.source=null,t[a.hash].forceRedraw=!0}),this.world.addHandler("metrics-change",function(e){a.viewport&&a.viewport._setContentBounds(a.world.getHomeBounds(),a.world.getContentFactor())}),this.world.addHandler("item-index-change",function(e){a.source=a.world.getItemAt(0).source}),this.viewport=new e.Viewport({containerSize:t[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()),this.imageLoader=new e.ImageLoader({jobLimit:this.imageLoaderLimit,timeout:o.timeout,tileRetryMax:this.tileRetryMax,tileRetryDelay:this.tileRetryDelay}),this.tileCache=new e.TileCache({viewer:this,maxImageCacheCount:this.maxImageCacheCount}),Object.prototype.hasOwnProperty.call(this.drawerOptions,"useCanvas")&&(e.console.error('useCanvas is deprecated, use the "drawer" option to indicate preferred drawer(s)'),this.drawerOptions.useCanvas||(this.drawer=e.HTMLDrawer),delete this.drawerOptions.useCanvas);let u=Array.isArray(this.drawer)?this.drawer:[this.drawer];0===u.length&&(u=[e.DEFAULT_SETTINGS.drawer].flat(),e.console.warn("No valid drawers were selected. Using the default value.")),u=u.flatMap(function(e){return"auto"===e?G():[e]}),u=u.filter(function(e,t,i){return i.indexOf(e)===t}),this.drawerCandidates=u.map(j).filter(Boolean),this.drawer=null;for(const e of u){if(this.requestDrawer(e,{mainDrawer:!0,redrawImmediately:!1}))break}if(!this.drawer)throw e.console.error("No drawer could be created!"),"Error with creating the selected drawer(s)";this.drawer.setImageSmoothingEnabled(this.imageSmoothingEnabled),this.overlaysContainer=e.makeNeutralElement("div"),this.canvas.appendChild(this.overlaysContainer),this.drawer.canRotate()||(this.rotateLeft&&(h=this.buttonGroup.buttons.indexOf(this.rotateLeft),this.buttonGroup.buttons.splice(h,1),this.buttonGroup.element.removeChild(this.rotateLeft.element)),this.rotateRight&&(h=this.buttonGroup.buttons.indexOf(this.rotateRight),this.buttonGroup.buttons.splice(h,1),this.buttonGroup.element.removeChild(this.rotateRight.element))),this._addUpdatePixelDensityRatioEvent(),"navigatorAutoResize"in this&&e.console.warn("navigatorAutoResize is deprecated, this value will be ignored."),this.showNavigator&&(this.navigator=new e.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})),this.sequenceMode&&this.bindSequenceControls(),this.tileSources&&this.open(this.tileSources);for(h=0;he.viewer.world.requestInvalidate(i,n)))},close:function(){return t[this.hash]?(this._opening=!1,this.navigator&&this.navigator.close(),this.preserveOverlays||(this.clearOverlays(),this.overlaysContainer.innerHTML=""),t[this.hash].animating=!1,this.world.removeAll(),this.tileCache.clear(),this.imageLoader.clear(),this.raiseEvent("close"),this):this},destroy:function(){if(t[this.hash]){if(this.raiseEvent("before-destroy"),this._removeUpdatePixelDensityRatioEvent(),this.close(),this.clearOverlays(),this.overlaysContainer.innerHTML="",this._resizeObserver&&this._resizeObserver.disconnect(),this.referenceStrip&&(this.referenceStrip.destroy(),this.referenceStrip=null),null!==this._updateRequestId&&(e.cancelAnimationFrame(this._updateRequestId),this._updateRequestId=null),this.drawer&&this.drawer.destroy(),this.navigator&&(this.navigator.destroy(),t[this.navigator.hash]=null,delete t[this.navigator.hash],this.navigator=null),this.buttonGroup)this.buttonGroup.destroy();else if(this.customButtons)for(;this.customButtons.length;)this.customButtons.pop().destroy();this.paging&&this.paging.destroy(),this.container&&this.container.parentNode===this.element&&this.element.removeChild(this.container),this.container.onsubmit=null,this.clearControls(),this.innerTracker&&this.innerTracker.destroy(),this.outerTracker&&this.outerTracker.destroy(),t[this.hash]=null,delete t[this.hash],this.canvas=null,this.container=null,e._viewers.delete(this.element),this.element=null,this.raiseEvent("destroy"),this.removeAllHandlers()}},isDestroyed(){return!t[this.hash]},requestDrawer(t,i){const n=(i=e.extend(!0,{mainDrawer:!0,redrawImmediately:!0,drawerOptions:null},i)).mainDrawer,o=i.redrawImmediately,s=i.drawerOptions,r=this.drawer;let a=null;t&&t.prototype instanceof e.DrawerBase?(a=t,t="custom"):"string"==typeof t&&(a=e.determineDrawer(t)),a||e.console.warn("Unsupported drawer %s! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.",t);let l=!1;if(a)try{l=a.isSupported()}catch(i){e.console.warn("Error in %s isSupported(); treating this drawer as unsupported:",t,i&&i.message?i.message:i)}if(l){r&&n&&r.destroy();const e=new a({viewer:this,viewport:this.viewport,element:this.canvas,debugGridColor:this.debugGridColor,options:s||this.drawerOptions[t]});return n&&(this.drawer=e,o&&this.forceRedraw()),e}return!1},isMouseNavEnabled:function(){return this.innerTracker.tracking},setMouseNavEnabled:function(e){return this.innerTracker.setTracking(e),this.outerTracker.setTracking(e),this.raiseEvent("mouse-enabled",{enabled:e}),this},isKeyboardNavEnabled:function(){return this.keyboardNavEnabled},setKeyboardNavEnabled:function(e){return this.keyboardNavEnabled=e,this.raiseEvent("keyboard-enabled",{enabled:e}),this},areControlsEnabled:function(){let e=this.controls.length;for(let t=0;t-1&&t.index{this.collectionMode&&(this.world.arrange({immediately:e.options.collectionImmediately,rows:this.collectionRows,columns:this.collectionColumns,layout:this.collectionLayout,tileSize:this.collectionTileSize,tileMargin:this.collectionTileMargin}),this.world.setAutoRefigureSizes(!0))},r=e=>{for(let e=0;e{let n,r;for(o.tiledImage=t.item,o.originalSuccess=i;this._loadQueue.length;){n=this._loadQueue[0];const t=n.tiledImage;if(!t)break;this._loadQueue.splice(0,1);const i=t.source;if(n.options.replace){const e=n.options.replaceItem,t=this.world.getIndexOfItem(e);-1!==t&&(n.options.index=t),!e._zombieCache&&e.source.equals(i)&&e.allowZombieCache(!0),this.world.removeItem(e)}this.collectionMode&&this.world.setAutoRefigureSizes(!1),this.navigator&&(r=e.extend({},n.options,{replace:!1,originalTiledImage:t,tileSource:i}),this.navigator.addTiledImage(r)),this.world.addItem(t,{index:n.options.index}),0===this._loadQueue.length&&s(n),1!==this.world.getItemCount()||this.preserveViewport||this.viewport.goHome(!0),n.originalSuccess&&n.originalSuccess({item:t}),this.drawer&&this.drawer.tiledImageCreated(t)}},t.error=r,this.instantiateTiledImageClass(t))},instantiateTiledImageClass:function(t){return this.instantiateTileSourceClass(t).then(i=>{const n=new e.TiledImage({viewer:this,source:i.source,viewport:this.viewport,drawer:this.drawer,tileCache:this.tileCache,imageLoader:this.imageLoader,x:t.x,y:t.y,width:t.width,height:t.height,fitBounds:t.fitBounds,fitBoundsPlacement:t.fitBoundsPlacement,clip:t.clip,placeholderFillStyle:t.placeholderFillStyle,opacity:t.opacity,preload:t.preload,degrees:t.degrees,flipped:t.flipped,compositeOperation:t.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:t.crossOriginPolicy,ajaxWithCredentials:t.ajaxWithCredentials,loadTilesWithAjax:t.loadTilesWithAjax,ajaxHeaders:t.ajaxHeaders,debugMode:this.debugMode,subPixelRoundingForTransparency:this.subPixelRoundingForTransparency,callTileLoadedWithCachedData:this.callTileLoadedWithCachedData,originalDataType:t.originalDataType});return t.success({item:n}),n}).catch(e=>{if(t.error)return t.error(e),e;throw e})},instantiateTileSourceClass(t){return new e.Promise((i,n)=>{void 0===t.placeholderFillStyle&&(t.placeholderFillStyle=this.placeholderFillStyle),void 0===t.opacity&&(t.opacity=this.opacity),void 0===t.preload&&(t.preload=this.preload),void 0===t.compositeOperation&&(t.compositeOperation=this.compositeOperation),void 0===t.crossOriginPolicy&&(t.crossOriginPolicy=void 0!==t.tileSource.crossOriginPolicy?t.tileSource.crossOriginPolicy:this.crossOriginPolicy),void 0===t.ajaxWithCredentials&&(t.ajaxWithCredentials=this.ajaxWithCredentials),void 0===t.loadTilesWithAjax&&(t.loadTilesWithAjax=this.loadTilesWithAjax),e.isPlainObject(t.ajaxHeaders)||(t.ajaxHeaders={});let o=t.tileSource;if("string"===e.type(o))if(o.match(/^\s*<.*>\s*$/))o=e.parseXml(o);else if(o.match(/^\s*[{[].*[}\]]\s*$/))try{o=e.parseJSON(o)}catch(e){}function s(e,t){e.ready?i({source:e}):(e.addHandler("ready",function(e){i({source:e.tileSource})}),e.addHandler("open-failed",function(e){n({message:e.message,source:t})}))}setTimeout(()=>{if("string"===e.type(o))o=new e.TileSource({url:o,crossOriginPolicy:void 0!==t.crossOriginPolicy?t.crossOriginPolicy:this.crossOriginPolicy,ajaxWithCredentials:this.ajaxWithCredentials,ajaxHeaders:e.extend({},this.ajaxHeaders,t.ajaxHeaders),splitHashDataForPost:this.splitHashDataForPost}),s(o,o);else if(e.isPlainObject(o)||o.nodeType)if(void 0!==o.crossOriginPolicy||void 0===t.crossOriginPolicy&&void 0===this.crossOriginPolicy||(o.crossOriginPolicy=void 0!==t.crossOriginPolicy?t.crossOriginPolicy:this.crossOriginPolicy),void 0===o.ajaxWithCredentials&&(o.ajaxWithCredentials=this.ajaxWithCredentials),e.isFunction(o.getTileUrl)){const t=new e.TileSource(o);t.getTileUrl=o.getTileUrl,o.ready=!1,s(t,o)}else{const t=e.TileSource.determineType(this,o,null);if(!t)return void n({message:"Unable to load TileSource",source:o,error:!0});const i=t.prototype.configure.apply(this,[o]);i.ready=!1,s(new t(i),o)}else s(o,o)})})},addSimpleImage:function(t){e.console.assert(t,"[Viewer.addSimpleImage] options is required"),e.console.assert(t.url,"[Viewer.addSimpleImage] options.url is required");const i=e.extend({},t,{tileSource:{type:"image",url:t.url}});delete i.url,this.addTiledImage(i)},addLayer:function(t){const i=this;e.console.error("[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead.");const n=e.extend({},t,{success:function(e){i.raiseEvent("add-layer",{options:t,drawer:e.item})},error:function(e){i.raiseEvent("add-layer-failed",e)}});return this.addTiledImage(n),this},getLayerAtLevel:function(t){return e.console.error("[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead."),this.world.getItemAt(t)},getLevelOfLayer:function(t){return e.console.error("[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead."),this.world.getIndexOfItem(t)},getLayersCount:function(){return e.console.error("[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead."),this.world.getItemCount()},setLayerLevel:function(t,i){return e.console.error("[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead."),this.world.setItemIndex(t,i)},removeLayer:function(t){return e.console.error("[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead."),this.world.removeItem(t)},forceRedraw:function(){return t[this.hash].forceRedraw=!0,this},forceResize:function(){t[this.hash].needsResize=!0,t[this.hash].forceResize=!0},bindSequenceControls:function(){const t=e.delegate(this,c),i=e.delegate(this,u),n=e.delegate(this,this.goToNextPage),o=e.delegate(this,this.goToPreviousPage),s=this.navImages;let r=!0;return this.showSequenceControl&&((this.previousButton||this.nextButton)&&(r=!1),this.previousButton=new e.Button({element:this.previousButton?e.getElement(this.previousButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.PreviousPage"),srcRest:L(this.prefixUrl,s.previous.REST),srcGroup:L(this.prefixUrl,s.previous.GROUP),srcHover:L(this.prefixUrl,s.previous.HOVER),srcDown:L(this.prefixUrl,s.previous.DOWN),onRelease:o,onFocus:t,onBlur:i}),this.nextButton=new e.Button({element:this.nextButton?e.getElement(this.nextButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.NextPage"),srcRest:L(this.prefixUrl,s.next.REST),srcGroup:L(this.prefixUrl,s.next.GROUP),srcHover:L(this.prefixUrl,s.next.HOVER),srcDown:L(this.prefixUrl,s.next.DOWN),onRelease:n,onFocus:t,onBlur:i}),this.navPrevNextWrap||this.previousButton.disable(),this.tileSources&&this.tileSources.length||this.nextButton.disable(),r&&(this.paging=new e.ButtonGroup({buttons:[this.previousButton,this.nextButton],clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold}),this.pagingControl=this.paging.element,this.toolbar?this.toolbar.addControl(this.pagingControl,{anchor:e.ControlAnchor.BOTTOM_RIGHT}):this.addControl(this.pagingControl,{anchor:this.sequenceControlAnchor||e.ControlAnchor.TOP_LEFT}))),this},bindStandardControls:function(){const t=e.delegate(this,this.startZoomInAction),i=e.delegate(this,this.endZoomAction),n=e.delegate(this,this.singleZoomInAction),o=e.delegate(this,this.startZoomOutAction),s=e.delegate(this,this.singleZoomOutAction),r=e.delegate(this,z),a=e.delegate(this,H),l=e.delegate(this,N),h=e.delegate(this,U),d=e.delegate(this,W),p=e.delegate(this,c),g=e.delegate(this,u),m=this.navImages,f=[];let v=!0;return this.showNavigationControl&&((this.zoomInButton||this.zoomOutButton||this.homeButton||this.fullPageButton||this.rotateLeftButton||this.rotateRightButton||this.flipButton)&&(v=!1),this.showZoomControl&&(f.push(this.zoomInButton=new e.Button({element:this.zoomInButton?e.getElement(this.zoomInButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.ZoomIn"),srcRest:L(this.prefixUrl,m.zoomIn.REST),srcGroup:L(this.prefixUrl,m.zoomIn.GROUP),srcHover:L(this.prefixUrl,m.zoomIn.HOVER),srcDown:L(this.prefixUrl,m.zoomIn.DOWN),onPress:t,onRelease:i,onClick:n,onEnter:t,onExit:i,onFocus:p,onBlur:g})),f.push(this.zoomOutButton=new e.Button({element:this.zoomOutButton?e.getElement(this.zoomOutButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.ZoomOut"),srcRest:L(this.prefixUrl,m.zoomOut.REST),srcGroup:L(this.prefixUrl,m.zoomOut.GROUP),srcHover:L(this.prefixUrl,m.zoomOut.HOVER),srcDown:L(this.prefixUrl,m.zoomOut.DOWN),onPress:o,onRelease:i,onClick:s,onEnter:o,onExit:i,onFocus:p,onBlur:g}))),this.showHomeControl&&f.push(this.homeButton=new e.Button({element:this.homeButton?e.getElement(this.homeButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.Home"),srcRest:L(this.prefixUrl,m.home.REST),srcGroup:L(this.prefixUrl,m.home.GROUP),srcHover:L(this.prefixUrl,m.home.HOVER),srcDown:L(this.prefixUrl,m.home.DOWN),onRelease:r,onFocus:p,onBlur:g})),this.showFullPageControl&&f.push(this.fullPageButton=new e.Button({element:this.fullPageButton?e.getElement(this.fullPageButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.FullPage"),srcRest:L(this.prefixUrl,m.fullpage.REST),srcGroup:L(this.prefixUrl,m.fullpage.GROUP),srcHover:L(this.prefixUrl,m.fullpage.HOVER),srcDown:L(this.prefixUrl,m.fullpage.DOWN),onRelease:a,onFocus:p,onBlur:g})),this.showRotationControl&&(f.push(this.rotateLeftButton=new e.Button({element:this.rotateLeftButton?e.getElement(this.rotateLeftButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.RotateLeft"),srcRest:L(this.prefixUrl,m.rotateleft.REST),srcGroup:L(this.prefixUrl,m.rotateleft.GROUP),srcHover:L(this.prefixUrl,m.rotateleft.HOVER),srcDown:L(this.prefixUrl,m.rotateleft.DOWN),onRelease:l,onFocus:p,onBlur:g})),f.push(this.rotateRightButton=new e.Button({element:this.rotateRightButton?e.getElement(this.rotateRightButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.RotateRight"),srcRest:L(this.prefixUrl,m.rotateright.REST),srcGroup:L(this.prefixUrl,m.rotateright.GROUP),srcHover:L(this.prefixUrl,m.rotateright.HOVER),srcDown:L(this.prefixUrl,m.rotateright.DOWN),onRelease:h,onFocus:p,onBlur:g}))),this.showFlipControl&&f.push(this.flipButton=new e.Button({element:this.flipButton?e.getElement(this.flipButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:e.getString("Tooltips.Flip"),srcRest:L(this.prefixUrl,m.flip.REST),srcGroup:L(this.prefixUrl,m.flip.GROUP),srcHover:L(this.prefixUrl,m.flip.HOVER),srcDown:L(this.prefixUrl,m.flip.DOWN),onRelease:d,onFocus:p,onBlur:g})),v?(this.buttonGroup=new e.ButtonGroup({buttons:f,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold}),this.navControl=this.buttonGroup.element,this.addHandler("open",e.delegate(this,M)),this.toolbar?this.toolbar.addControl(this.navControl,{anchor:this.navigationControlAnchor||e.ControlAnchor.TOP_LEFT}):this.addControl(this.navControl,{anchor:this.navigationControlAnchor||e.ControlAnchor.TOP_LEFT})):this.customButtons=f),this},currentPage:function(){return this._sequenceIndex},goToPage:function(e){return this.tileSources&&e>=0&&e=0)return this;const l=o(this,a);return this.currentOverlays.push(l),l.drawHTML(this.overlaysContainer,this.viewport),this.raiseEvent("add-overlay",{element:t,location:a.location,placement:a.placement}),this},updateOverlay:function(i,n,o){i=e.getElement(i);const r=s(this.currentOverlays,i);return r>=0&&(this.currentOverlays[r].update(n,o),t[this.hash].forceRedraw=!0,this.raiseEvent("update-overlay",{element:i,location:n,placement:o})),this},removeOverlay:function(i){i=e.getElement(i);const n=s(this.currentOverlays,i);return n>=0&&(this.currentOverlays[n].destroy(),this.currentOverlays.splice(n,1),t[this.hash].forceRedraw=!0,this.raiseEvent("remove-overlay",{element:i})),this},clearOverlays:function(){for(;this.currentOverlays.length>0;)this.currentOverlays.pop().destroy();return t[this.hash].forceRedraw=!0,this.raiseEvent("clear-overlay",{}),this},getOverlayById:function(t){t=e.getElement(t);const i=s(this.currentOverlays,t);return i>=0?this.currentOverlays[i]:null},_registerDrawer:function(e){this._drawerList||(this._drawerList=[]),this._drawerList.push(e)},_unregisterDrawer:function(t){this._drawerList?this._drawerList.splice(this._drawerList.indexOf(t),1):e.console.warn("Viewer._unregisterDrawer: cannot unregister on viewer that is not meant to share updates.")},_updateSequenceButtons:function(e){this.nextButton&&(this.tileSources&&this.tileSources.length-1!==e?this.nextButton.enable():this.navPrevNextWrap||this.nextButton.disable()),this.previousButton&&(e>0?this.previousButton.enable():this.navPrevNextWrap||this.previousButton.disable())},_showMessage:function(t){this._hideMessage();const i=e.makeNeutralElement("div");i.appendChild(document.createTextNode(t)),this.messageDiv=e.makeCenteredNode(i),e.addClass(this.messageDiv,"openseadragon-message"),this.container.appendChild(this.messageDiv)},_hideMessage:function(){const e=this.messageDiv;e&&(e.parentNode.removeChild(e),delete this.messageDiv)},gestureSettingsByDeviceType:function(e){switch(e){case"mouse":return this.gestureSettingsMouse;case"touch":return this.gestureSettingsTouch;case"pen":return this.gestureSettingsPen;default:return this.gestureSettingsUnknown}},_drawOverlays:function(){const e=this.currentOverlays.length;for(let t=0;t1&&(this.referenceStrip=new e.ReferenceStrip({id:this.referenceStripElement,position:this.referenceStripPosition,sizeRatio:this.referenceStripSizeRatio,scroll:this.referenceStripScroll,height:this.referenceStripHeight,width:this.referenceStripWidth,tileSources:this.tileSources,prefixUrl:this.prefixUrl,viewer:this}),this.referenceStrip.setFocus(this._sequenceIndex))}else e.console.warn('Attempting to display a reference strip while "sequenceMode" is off.')},_addUpdatePixelDensityRatioEvent:function(){this._updatePixelDensityRatioBind=this._updatePixelDensityRatio.bind(this),e.addEvent(window,"resize",this._updatePixelDensityRatioBind)},_removeUpdatePixelDensityRatioEvent:function(){e.removeEvent(window,"resize",this._updatePixelDensityRatioBind)},_updatePixelDensityRatio:function(){const t=e.pixelDensityRatio,i=e.getCurrentPixelDensityRatio();t!==i&&(e.pixelDensityRatio=i,this.forceResize())},goToPreviousPage:function(){let e=this._sequenceIndex-1;this.navPrevNextWrap&&e<0&&(e+=this.tileSources.length),this.goToPage(e)},goToNextPage:function(){let e=this._sequenceIndex+1;this.navPrevNextWrap&&e>=this.tileSources.length&&(e=0),this.goToPage(e)},isAnimating:function(){return t[this.hash].animating},startZoomInAction:function(){t[this.hash].lastZoomTime=e.now(),t[this.hash].zoomFactor=this.zoomPerSecond,t[this.hash].zooming=!0,k(this)},startZoomOutAction:function(){t[this.hash].lastZoomTime=e.now(),t[this.hash].zoomFactor=1/this.zoomPerSecond,t[this.hash].zooming=!0,k(this)},endZoomAction:function(){t[this.hash].zooming=!1},singleZoomInAction:function(){this.viewport&&(t[this.hash].zooming=!1,this.viewport.zoomBy(this.zoomPerClick/1),this.viewport.applyConstraints())},singleZoomOutAction:function(){this.viewport&&(t[this.hash].zooming=!1,this.viewport.zoomBy(1/this.zoomPerClick),this.viewport.applyConstraints())}}),e.determineDrawer=function(t){"auto"===t&&(t=G()[0]);for(const i in OpenSeadragon){const n=OpenSeadragon[i],o=n.prototype;if(o&&o instanceof OpenSeadragon.DrawerBase&&e.isFunction(o.getType)&&o.getType.call(n)===t)return n}return null}}(OpenSeadragon),function(e){function t(e){const t={tracker:e.eventSource,position:e.position,quick:e.quick,shift:e.shift,originalEvent:e.originalEvent,preventDefaultAction:!1};if(this.viewer.raiseEvent("navigator-click",t),!t.preventDefaultAction&&e.quick&&this.viewer.viewport&&(this.panVertical||this.panHorizontal)){this.viewer.viewport.flipped&&(e.position.x=this.viewport.getContainerSize().x-e.position.x);const t=this.viewport.pointFromPixel(e.position);this.panVertical?this.panHorizontal||(t.x=this.viewer.viewport.getCenter(!0).x):t.y=this.viewer.viewport.getCenter(!0).y,this.viewer.viewport.panTo(t),this.viewer.viewport.applyConstraints()}}function i(e){const t={tracker:e.eventSource,position:e.position,delta:e.delta,speed:e.speed,direction:e.direction,shift:e.shift,originalEvent:e.originalEvent,preventDefaultAction:!1};this.viewer.raiseEvent("navigator-drag",t),!t.preventDefaultAction&&this.viewer.viewport&&(this.panHorizontal||(e.delta.x=0),this.panVertical||(e.delta.y=0),this.viewer.viewport.flipped&&(e.delta.x=-e.delta.x),this.viewer.viewport.panBy(this.viewport.deltaPointsFromPixels(e.delta)),this.viewer.constrainDuringPan&&this.viewer.viewport.applyConstraints())}function n(e){e.insideElementPressed&&this.viewer.viewport&&this.viewer.viewport.applyConstraints()}function o(e){const t={tracker:e.eventSource,position:e.position,scroll:e.scroll,shift:e.shift,originalEvent:e.originalEvent,preventDefault:e.preventDefault};this.viewer.raiseEvent("navigator-scroll",t),e.preventDefault=t.preventDefault}function s(e,t){r(e,"rotate("+t+"deg)")}function r(e,t){e.style.webkitTransform=t,e.style.mozTransform=t,e.style.msTransform=t,e.style.oTransform=t,e.style.transform=t}e.Navigator=function(r){const a=r.viewer,l=this;let h,c;var u,d;function p(e,t){s(l.displayRegionContainer,e),s(l.displayRegion,-e),l.viewport.setRotation(e,t)}if(r.element||r.id?(r.element?(r.id&&e.console.warn("Given option.id for Navigator was ignored since option.element was provided and is being used instead."),r.element.id?r.id=r.element.id:r.id="navigator-"+e.now(),this.element=r.element):this.element=document.getElementById(r.id),r.controlOptions={anchor:e.ControlAnchor.NONE,attachToViewer:!1,autoFade:!1}):(r.id="navigator-"+e.now(),this.element=e.makeNeutralElement("div"),r.controlOptions={anchor:e.ControlAnchor.TOP_RIGHT,attachToViewer:!0,autoFade:r.autoFade},r.position&&("BOTTOM_RIGHT"===r.position?r.controlOptions.anchor=e.ControlAnchor.BOTTOM_RIGHT:"BOTTOM_LEFT"===r.position?r.controlOptions.anchor=e.ControlAnchor.BOTTOM_LEFT:"TOP_RIGHT"===r.position?r.controlOptions.anchor=e.ControlAnchor.TOP_RIGHT:"TOP_LEFT"===r.position?r.controlOptions.anchor=e.ControlAnchor.TOP_LEFT:"ABSOLUTE"===r.position&&(r.controlOptions.anchor=e.ControlAnchor.ABSOLUTE,r.controlOptions.top=r.top,r.controlOptions.left=r.left,r.controlOptions.height=r.height,r.controlOptions.width=r.width))),this.element.id=r.id,this.element.className+=" navigator",(r=e.extend(!0,{sizeRatio:e.DEFAULT_SETTINGS.navigatorSizeRatio},r,{element:this.element,tabIndex:-1,showNavigator:!1,mouseNavEnabled:!1,showNavigationControl:!1,showSequenceControl:!1,immediateRender:!0,blendTime:0,animationTime:r.animationTime,autoResize:!1,minZoomImageRatio:1,background:r.background,opacity:r.opacity,borderColor:r.borderColor,displayRegionColor:r.displayRegionColor})).minPixelRatio=this.minPixelRatio=a.minPixelRatio,e.setElementTouchActionNone(this.element),this.borderWidth=2,this.fudge=new e.Point(1,1),this.totalBorderWidths=new e.Point(2*this.borderWidth,2*this.borderWidth).minus(this.fudge),r.controlOptions.anchor!==e.ControlAnchor.NONE&&(u=this.element.style,d=this.borderWidth,u.margin="0px",u.border=d+"px solid "+r.borderColor,u.padding="0px",u.background=r.background,u.opacity=r.opacity,u.overflow="hidden"),this.displayRegion=e.makeNeutralElement("div"),this.displayRegion.id=this.element.id+"-displayregion",this.displayRegion.className="displayregion",function(e,t){e.position="relative",e.top="0px",e.left="0px",e.fontSize="0px",e.overflow="hidden",e.border=t+"px solid "+r.displayRegionColor,e.margin="0px",e.padding="0px",e.background="transparent",e.float="left",e.cssFloat="left",e.zIndex=999999999,e.cursor="default",e.boxSizing="content-box"}(this.displayRegion.style,this.borderWidth),e.setElementPointerEventsNone(this.displayRegion),e.setElementTouchActionNone(this.displayRegion),this.displayRegionContainer=e.makeNeutralElement("div"),this.displayRegionContainer.id=this.element.id+"-displayregioncontainer",this.displayRegionContainer.className="displayregioncontainer",this.displayRegionContainer.style.width="100%",this.displayRegionContainer.style.height="100%",e.setElementPointerEventsNone(this.displayRegionContainer),e.setElementTouchActionNone(this.displayRegionContainer),a.addControl(this.element,r.controlOptions),this._resizeWithViewer=r.controlOptions.anchor!==e.ControlAnchor.ABSOLUTE&&r.controlOptions.anchor!==e.ControlAnchor.NONE,r.width&&r.height?(this.setWidth(r.width),this.setHeight(r.height)):this._resizeWithViewer&&(h=e.getElementSize(a.element),this.element.style.height=Math.round(h.y*r.sizeRatio)+"px",this.element.style.width=Math.round(h.x*r.sizeRatio)+"px",this.oldViewerSize=h,c=e.getElementSize(this.element),this.elementArea=c.x*c.y),this.oldContainerSize=new e.Point(0,0),e.Viewer.apply(this,[r]),this.displayRegionContainer.appendChild(this.displayRegion),this.element.getElementsByTagName("div")[0].appendChild(this.displayRegionContainer),r.navigatorRotate){p(r.viewer.viewport?r.viewer.viewport.getRotation():r.viewer.degrees||0,!0),r.viewer.addHandler("rotate",function(e){p(e.degrees,e.immediately)})}this.innerTracker.destroy(),this.innerTracker=new e.MouseTracker({userData:"Navigator.innerTracker",element:this.element,dragHandler:e.delegate(this,i),clickHandler:e.delegate(this,t),releaseHandler:e.delegate(this,n),scrollHandler:e.delegate(this,o),preProcessEventHandler:function(e){"wheel"===e.eventType&&(e.preventDefault=!0)}}),this.outerTracker.userData="Navigator.outerTracker",e.setElementPointerEventsNone(this.canvas),e.setElementPointerEventsNone(this.container),this.addHandler("reset-size",function(){l.viewport&&l.viewport.goHome(!0)}),a.world.addHandler("item-index-change",function(e){window.setTimeout(function(){const t=l.world.getItemAt(e.previousIndex);l.world.setItemIndex(t,e.newIndex)},1)}),a.world.addHandler("remove-item",function(e){const t=e.item,i=l._getMatchingItem(t);i&&l.world.removeItem(i)}),this.update(a.viewport)},e.extend(e.Navigator.prototype,e.EventSource.prototype,e.Viewer.prototype,{updateSize:function(){if(this.viewport){const t=new e.Point(0===this.container.clientWidth?1:this.container.clientWidth,0===this.container.clientHeight?1:this.container.clientHeight);t.equals(this.oldContainerSize)||(this.viewport.resize(t,!0),this.viewport.goHome(!0),this.oldContainerSize=t,this.world.update(),this.world.draw(),this.update(this.viewer.viewport))}},setWidth:function(e){this.width=e,this.element.style.width="number"==typeof e?e+"px":e,this._resizeWithViewer=!1,this.updateSize()},setHeight:function(e){this.height=e,this.element.style.height="number"==typeof e?e+"px":e,this._resizeWithViewer=!1,this.updateSize()},setFlip:function(e){return this.viewport.setFlip(e),this.setDisplayTransform(this.viewer.viewport.getFlip()?"scale(-1,1)":"scale(1,1)"),this},setDisplayTransform:function(e){r(this.canvas,e),r(this.element,e)},update:function(t){let i,n,o,r,a,l;if(t||(t=this.viewer.viewport),i=e.getElementSize(this.viewer.element),this._resizeWithViewer&&i.x&&i.y&&!i.equals(this.oldViewerSize)&&(this.oldViewerSize=i,this.maintainSizeRatio||!this.elementArea?(n=i.x*this.sizeRatio,o=i.y*this.sizeRatio):(n=Math.sqrt(this.elementArea*(i.x/i.y)),o=this.elementArea/n),this.element.style.width=Math.round(n)+"px",this.element.style.height=Math.round(o)+"px",this.elementArea||(this.elementArea=n*o),this.updateSize()),t&&this.viewport){if(r=t.getBoundsNoRotate(!0),a=this.viewport.pixelFromPointNoRotate(r.getTopLeft(),!1),l=this.viewport.pixelFromPointNoRotate(r.getBottomRight(),!1).minus(this.totalBorderWidths),!this.navigatorRotate){const e=t.getRotation(!0);s(this.displayRegion,-e)}const e=this.displayRegion.style;e.display=this.world.getItemCount()?"block":"none",e.top=a.y.toFixed(2)+"px",e.left=a.x.toFixed(2)+"px";const i=l.x-a.x,n=l.y-a.y;e.width=Math.round(Math.max(i,0))+"px",e.height=Math.round(Math.max(n,0))+"px"}},addTiledImage:function(t){const i=this,n=t.originalTiledImage;delete t.original;const o=e.extend({},t,{success:function(e){const t=e.item;function o(){i._matchBounds(t,n)}t._originalForNavigator=n,i._matchBounds(t,n,!0),i._matchOpacity(t,n),i._matchCompositeOperation(t,n),n.addHandler("bounds-change",o),n.addHandler("clip-change",o),n.addHandler("opacity-change",function(){i._matchOpacity(t,n)}),n.addHandler("composite-operation-change",function(){i._matchCompositeOperation(t,n)})}});return e.Viewer.prototype.addTiledImage.apply(this,[o])},destroy:function(){return e.Viewer.prototype.destroy.apply(this)},_getMatchingItem:function(e){const t=this.world.getItemCount();for(let i=0;i{const i=t.tileSource;this.ready=!0,this.aspectRatio=i.width&&i.height?i.width/i.height:1,this.dimensions=new e.Point(i.width,i.height),i.tileSize?(this._tileWidth=this._tileHeight=i.tileSize,delete this.tileSize):(i.tileWidth?(this._tileWidth=i.tileWidth,delete this.tileWidth):this._tileWidth=0,i.tileHeight?(this._tileHeight=i.tileHeight,delete this.tileHeight):this._tileHeight=0),this.tileOverlap=i.tileOverlap?i.tileOverlap:0,this.minLevel=i.minLevel?i.minLevel:0,this.maxLevel=void 0!==i.maxLevel&&null!==i.maxLevel?i.maxLevel:i.width&&i.height?Math.ceil(Math.log(Math.max(i.width,i.height))/Math.log(2)):0,i.success&&e.isFunction(i.success)&&i.success(this)},null,1/0),"string"===e.type(t)?(this.url=t,t=void 0):e.extend(!0,this,t),this.url&&!this.ready?(this.aspectRatio=1,this.dimensions=new e.Point(10,10),this._tileWidth=0,this._tileHeight=0,this.tileOverlap=0,this.minLevel=0,this.maxLevel=0,this.ready=!1,this._uniqueIdentifier=this.url,setTimeout(()=>this.getImageInfo(this.url))):(this._uniqueIdentifier=Math.floor(1e10*Math.random()).toString(36),this.ready||void 0===this.ready?this.raiseEvent("ready",{tileSource:this}):setTimeout(()=>this.raiseEvent("ready",{tileSource:this}))),this},e.TileSource.prototype={getTileSize:function(t){return e.console.error("[TileSource.getTileSize] is deprecated. Use TileSource.getTileWidth() and TileSource.getTileHeight() instead"),this._tileWidth},getTileWidth:function(e){return this._tileWidth?this._tileWidth:this.getTileSize(e)},getTileHeight:function(e){return this._tileHeight?this._tileHeight:this.getTileSize(e)},setMaxLevel:function(e){this.maxLevel=e,this._memoizeLevelScale()},getLevelScale:function(e){return this._memoizeLevelScale(),this.getLevelScale(e)},_memoizeLevelScale:function(){const e={};let t;for(t=0;t<=this.maxLevel;t++)e[t]=1/Math.pow(2,this.maxLevel-t);this.getLevelScale=function(t){return e[t]}},getNumTiles:function(t){const i=this.getLevelScale(t),n=Math.ceil(i*this.dimensions.x/this.getTileWidth(t)),o=Math.ceil(i*this.dimensions.y/this.getTileHeight(t));return new e.Point(n,o)},getPixelRatio:function(t){const i=this.dimensions.times(this.getLevelScale(t)),n=1/i.x*e.pixelDensityRatio,o=1/i.y*e.pixelDensityRatio;return new e.Point(n,o)},getClosestLevel:function(){let e,t;for(e=this.minLevel+1;e<=this.maxLevel&&(t=this.getNumTiles(e),!(t.x>1||t.y>1));e++);return e-1},getTileAtPoint:function(t,i){const n=i.x>=0&&i.x<=1&&i.y>=0&&i.y<=1/this.aspectRatio;e.console.assert(n,"[TileSource.getTileAtPoint] must be called with a valid point.");const o=this.dimensions.x*this.getLevelScale(t),s=i.x*o,r=i.y*o;let a=Math.floor(s/this.getTileWidth(t)),l=Math.floor(r/this.getTileHeight(t));i.x>=1&&(a=this.getNumTiles(t).x-1);return i.y>=1/this.aspectRatio-1e-15&&(l=this.getNumTiles(t).y-1),new e.Point(a,l)},getTileBounds:function(t,i,n,o){const s=this.dimensions.times(this.getLevelScale(t)),r=this.getTileWidth(t),a=this.getTileHeight(t),l=0===i?0:r*i-this.tileOverlap,h=0===n?0:a*n-this.tileOverlap;let c=r+(0===i?1:2)*this.tileOverlap,u=a+(0===n?1:2)*this.tileOverlap;const d=1/s.x;return c=Math.min(c,s.x-l),u=Math.min(u,s.y-h),o?new e.Rect(0,0,c,u):new e.Rect(l*d,h*d,c*d,u*d)},getImageInfo:function(t){const i=this;let n,o,s,r,a,l,h;t&&(a=t.split("/"),l=a[a.length-1],h=l.lastIndexOf("."),h>-1&&(a[a.length-1]=l.slice(0,h)));let c=null;if(this.splitHashDataForPost){const e=t.indexOf("#");-1!==e&&(c=t.substring(e+1),t=t.substr(0,e))}o=function(n){"string"==typeof n&&(n=e.parseXml(n));const o=e.TileSource.determineType(i,n,t);o?(r=o.prototype.configure.apply(i,[n,t,c]),void 0===r.ajaxWithCredentials&&(r.ajaxWithCredentials=i.ajaxWithCredentials),r.ready=!0,s=new o(r),i.ready=!0,i.raiseEvent("ready",{tileSource:s})):i.raiseEvent("open-failed",{message:"Unable to load TileSource",source:t})},t.match(/\.js$/)?(n=t.split("/").pop().replace(".js",""),e.jsonp({url:t,async:!1,callbackName:n,callback:o})):e.makeAjaxRequest({url:t,postData:c,withCredentials:this.ajaxWithCredentials,headers:this.ajaxHeaders,success:function(t){const i=function(t){const i=t.responseText;let n,o,s=t.status;if(!t)throw new Error(e.getString("Errors.Security"));if(200!==t.status&&0!==t.status)throw s=t.status,n=404===s?"Not Found":t.statusText,new Error(e.getString("Errors.Status",s,n));if(i.match(/^\s*<.*/))try{o=t.responseXML&&t.responseXML.documentElement?t.responseXML:e.parseXml(i)}catch(e){o=t.responseText}else if(i.match(/\s*[{[].*/))try{o=e.parseJSON(i)}catch(e){o=i}else o=i;return o}(t);o(i)},error:function(n,o){let s;try{s="HTTP "+n.status+" attempting to load TileSource: "+t}catch(e){let i;i=void 0!==o&&o.toString?o.toString():"Unknown error",s=i+" attempting to load TileSource: "+t}e.console.error(s),i.raiseEvent("open-failed",{message:s,source:t,postData:c})}})},supports:function(e,t){return!1},equals:function(e){return this===e},batchEnabled:()=>!1,batchCompatible:e=>!1,batchMaxJobs:()=>-1,batchTimeout:()=>5,configure:function(e,t,i){throw new Error("Method not implemented.")},destroy:function(e){},getTileUrl:function(e,t,i){throw new Error("Method not implemented.")},getTilePostData:function(e,t,i){return null},getTileAjaxHeaders:function(e,t,i){return{}},getTileHashKey:function(e,t,i,n,o,s){function r(e){return o?e+"+"+JSON.stringify(o):e}return r("string"!=typeof n?this._uniqueIdentifier+":"+e+"/"+t+"_"+i:n)},tileExists:function(e,t,i){const n=this.getNumTiles(e);return e>=this.minLevel&&e<=this.maxLevel&&t>=0&&i>=0&&t=0;e--){const t=this.displayRects[e];for(c=t.minLevel;c<=t.maxLevel;c++)this._levelRects[c]||(this._levelRects[c]=[]),this._levelRects[c].push(t)}e.TileSource.apply(this,[u])},e.extend(e.DziTileSource.prototype,e.TileSource.prototype,{supports:function(e,t){let i;return e.Image?i=e.Image.xmlns:e.documentElement&&("Image"!==e.documentElement.localName&&"Image"!==e.documentElement.tagName||(i=e.documentElement.namespaceURI)),i=(i||"").toLowerCase(),-1!==i.indexOf("schemas.microsoft.com/deepzoom/2008")||-1!==i.indexOf("schemas.microsoft.com/deepzoom/2009")},configure:function(i,n,o){let s;return s=e.isPlainObject(i)?t(this,i):function(i,n){if(!n||!n.documentElement)throw new Error(e.getString("Errors.Xml"));const o=n.documentElement,s=o.localName||o.tagName,r=n.documentElement.namespaceURI;let a=null;const l=[];let h,c,u,d,p;if("Image"===s)try{if(d=o.getElementsByTagName("Size")[0],void 0===d&&(d=o.getElementsByTagNameNS(r,"Size")[0]),a={Image:{xmlns:"http://schemas.microsoft.com/deepzoom/2008",Url:o.getAttribute("Url"),Format:o.getAttribute("Format"),DisplayRect:null,Overlap:parseInt(o.getAttribute("Overlap"),10),TileSize:parseInt(o.getAttribute("TileSize"),10),Size:{Height:parseInt(d.getAttribute("Height"),10),Width:parseInt(d.getAttribute("Width"),10)}}},!e.imageFormatSupported(a.Image.Format))throw new Error(e.getString("Errors.ImageFormat",a.Image.Format.toUpperCase()));for(h=o.getElementsByTagName("DisplayRect"),void 0===h&&(h=o.getElementsByTagNameNS(r,"DisplayRect")[0]),p=0;pthis.maxLevel)return!1;if(!n||!n.length)return!0;for(let h=n.length-1;h>=0;h--){const c=n[h];if(!(ec.maxLevel)&&(o=this.getLevelScale(e),s=c.x*o,r=c.y*o,a=s+c.width*o,l=r+c.height*o,s=Math.floor(s/this._tileWidth),r=Math.floor(r/this._tileWidth),a=Math.ceil(a/this._tileWidth),l=Math.ceil(l/this._tileWidth),s<=t&&t1&&e.profile[1].supports&&(n=-1!==e.profile[1].supports.indexOf("sizeByW")),3===e.version&&e.extraFeatures&&(n=-1!==e.extraFeatures.indexOf("sizeByWh")),!i||n}function n(e){const t=[];for(let i=0;i0?o.tileSize=Math.max.apply(null,i):o.tileSize=e}else this.sizes&&this.sizes.length>0?(this.emulateLegacyImagePyramid=!0,o.levels=n(this),e.extend(!0,o,{width:o.levels[o.levels.length-1].width,height:o.levels[o.levels.length-1].height,tileSize:Math.max(o.height,o.width),tileOverlap:0,minLevel:0,maxLevel:o.levels.length-1}),this.levels=o.levels):e.console.error("Nothing in the info.json to construct image pyramids from");if(!o.maxLevel&&!this.emulateLegacyImagePyramid)if(this.scale_factors){const e=Math.max.apply(null,this.scale_factors);o.maxLevel=Math.round(Math.log(e)*Math.LOG2E)}else o.maxLevel=Number(Math.round(Math.log(Math.max(this.width,this.height),2)));if(this.sizes){let e=this.sizes.length;const t=this.sizes.slice().sort((e,t)=>e.width-t.width);if(t[e-1].width0&&t>=this.minLevel&&t<=this.maxLevel&&(e=this.levels[t].width/this.levels[this.maxLevel].width),e}return e.TileSource.prototype.getLevelScale.call(this,t)},getNumTiles:function(t){if(this.emulateLegacyImagePyramid){return this.getLevelScale(t)?new e.Point(1,1):new e.Point(0,0)}if(this.levelSizes){const i=this.levelSizes[t],n=Math.ceil(i.width/this.getTileWidth(t)),o=Math.ceil(i.height/this.getTileHeight(t));return new e.Point(n,o)}return e.TileSource.prototype.getNumTiles.call(this,t)},getTileAtPoint:function(t,i){if(this.emulateLegacyImagePyramid)return new e.Point(0,0);if(this.levelSizes){const n=i.x>=0&&i.x<=1&&i.y>=0&&i.y<=1/this.aspectRatio;e.console.assert(n,"[TileSource.getTileAtPoint] must be called with a valid point.");const o=this.levelSizes[t].width,s=i.x*o,r=i.y*o;let a=Math.floor(s/this.getTileWidth(t)),l=Math.floor(r/this.getTileHeight(t));i.x>=1&&(a=this.getNumTiles(t).x-1);const h=1e-15;return i.y>=1/this.aspectRatio-h&&(l=this.getNumTiles(t).y-1),new e.Point(a,l)}return e.TileSource.prototype.getTileAtPoint.call(this,t,i)},getTileUrl:function(e,t,i){if(this.emulateLegacyImagePyramid){let t=null;return this.levels.length>0&&e>=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].url),t}const n=Math.pow(.5,this.maxLevel-e);let o,s,r,a,l,h,c,u,d,p,g,m,f,v,y;this.levelSizes?(o=this.levelSizes[e].width,s=this.levelSizes[e].height):(o=Math.ceil(this.width*n),s=Math.ceil(this.height*n)),r=this.getTileWidth(e),a=this.getTileHeight(e),l=Math.round(r/n),h=Math.round(a/n),y=1===this.version?"native."+this.tileFormat:"default."+this.tileFormat,o({width:Math.ceil(e.x_tiles*this._tileWidth),height:Math.ceil(e.y_tiles*this._tileHeight),xTiles:Math.ceil(e.x_tiles),yTiles:Math.ceil(e.y_tiles)})),this.levelScales=i.map(e=>e.scale/n),this.minLevel=0,this.maxLevel=Math.ceil(this.levelSizes.length-1)},getImageInfo:function(t){const i=this;e.makeAjaxRequest({url:t,type:"GET",async:!0,success:function(n){try{const e=JSON.parse(n.responseText);i.parseMetadata(e),i.ready=!0,i.raiseEvent("ready",{tileSource:i})}catch(n){const o="IrisTileSource: Error parsing metadata: "+n.message;e.console.error(o),i.raiseEvent("open-failed",{message:o,source:t})}},error:function(n,o){const s="IrisTileSource: Unable to get metadata from "+t;e.console.error(s),i.raiseEvent("open-failed",{message:s,source:t})}})},getNumTiles:function(t){return tthis.maxLevel||!this.levelSizes[t]?new e.Point(0,0):new e.Point(Math.ceil(this.levelSizes[t].xTiles),Math.ceil(this.levelSizes[t].yTiles))},getTileUrl:function(e,t,i){const n=i*this.levelSizes[e].xTiles+t;return`${this.serverUrl}/slides/${this.slideId}/layers/${e}/tiles/${n}`},getLevelScale:function(e){return this.levelScales[e]},configure:function(e){return e}}),e.extend(!0,e.IrisTileSource.prototype,e.EventSource.prototype)}(OpenSeadragon),function(e){e.OsmTileSource=function(t,i,n,o,s){let r;r=e.isPlainObject(t)?t:{width:arguments[0],height:arguments[1],tileSize:arguments[2],tileOverlap:arguments[3],tilesUrl:arguments[4]},r.width&&r.height||(r.width=67108864,r.height=67108864),r.tileSize||(r.tileSize=256,r.tileOverlap=0),r.tilesUrl||(r.tilesUrl="http://tile.openstreetmap.org/"),r.minLevel=8,e.TileSource.apply(this,[r])},e.extend(e.OsmTileSource.prototype,e.TileSource.prototype,{supports:function(e,t){return e.type&&"openstreetmaps"===e.type},configure:function(e,t,i){return e},getTileUrl:function(e,t,i){return this.tilesUrl+(e-8)+"/"+t+"/"+i+".png"},equals:function(e){return e&&this.tilesUrl===e.tilesUrl}})}(OpenSeadragon),function(e){e.TmsTileSource=function(t,i,n,o,s){let r;r=e.isPlainObject(t)?t:{width:arguments[0],height:arguments[1],tileSize:arguments[2],tileOverlap:arguments[3],tilesUrl:arguments[4]};const a=256*Math.ceil(r.width/256),l=256*Math.ceil(r.height/256);let h;h=a>l?a/256:l/256,r.maxLevel=Math.ceil(Math.log(h)/Math.log(2))-1,r.tileSize=256,r.width=a,r.height=l,e.TileSource.apply(this,[r])},e.extend(e.TmsTileSource.prototype,e.TileSource.prototype,{supports:function(e,t){return e.type&&"tiledmapservice"===e.type},configure:function(e,t,i){return e},getTileUrl:function(e,t,i){const n=this.getNumTiles(e).y-1;return this.tilesUrl+e+"/"+t+"/"+(n-i)+".png"},equals:function(e){return e&&this.tilesUrl===e.tilesUrl}})}(OpenSeadragon),function(e){e.ZoomifyTileSource=function(t){void 0===t.tileSize&&(t.tileSize=256),void 0===t.fileFormat&&(t.fileFormat="jpg",this.fileFormat=t.fileFormat);const i={x:t.width,y:t.height};for(t.imageSizes=[{x:t.width,y:t.height}],t.gridSize=[this._getGridSize(t.width,t.height,t.tileSize)];parseInt(i.x,10)>t.tileSize||parseInt(i.y,10)>t.tileSize;)i.x=Math.floor(i.x/2),i.y=Math.floor(i.y/2),t.imageSizes.push({x:i.x,y:i.y}),t.gridSize.push(this._getGridSize(i.x,i.y,t.tileSize));t.imageSizes.reverse(),t.gridSize.reverse(),t.minLevel=0,t.maxLevel=t.gridSize.length-1,e.TileSource.apply(this,[t])},e.extend(e.ZoomifyTileSource.prototype,e.TileSource.prototype,{_getGridSize:function(e,t,i){return{x:Math.ceil(e/i),y:Math.ceil(t/i)}},_calculateAbsoluteTileNumber:function(e,t,i){let n=0,o={};for(let t=0;t");return i.sort(function(e,t){return e.height-t.height})}(i.levels),i.levels.length>0?(n=i.levels[i.levels.length-1].width,o=i.levels[i.levels.length-1].height):(n=0,o=0,e.console.error("No supported image formats found")),e.extend(!0,i,{width:n,height:o,tileSize:Math.max(o,n),tileOverlap:0,minLevel:0,maxLevel:i.levels.length>0?i.levels.length-1:0}),e.TileSource.apply(this,[i]),this.levels=i.levels},e.extend(e.LegacyTileSource.prototype,e.TileSource.prototype,{supports:function(e,t){return e.type&&"legacy-image-pyramid"===e.type||e.documentElement&&"legacy-image-pyramid"===e.documentElement.getAttribute("type")},configure:function(i,n,o){let s;return s=e.isPlainObject(i)?t(this,i):function(i,n){if(!n||!n.documentElement)throw new Error(e.getString("Errors.Xml"));const o=n.documentElement,s=o.tagName;let r,a=null,l=[];if("image"===s)try{a={type:o.getAttribute("type"),levels:[]},l=o.getElementsByTagName("level");for(let e=0;e0&&e>=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width),t},getNumTiles:function(t){return this.getLevelScale(t)?new e.Point(1,1):new e.Point(0,0)},getTileUrl:function(e,t,i){let n=null;return this.levels.length>0&&e>=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url),n},equals:function(e){if(!e||!e.levels||e.levels.length!==this.levels.length)return!1;for(let t=this.minLevel;t<=this.maxLevel;t++)if(this.levels[t].url!==e.levels[t].url)return!1;return!0}})}(OpenSeadragon),function(e){e.ImageTileSource=class extends e.TileSource{constructor(t){super(e.extend({buildPyramid:!0,crossOriginPolicy:!1,ajaxWithCredentials:!1},t))}supports(e,t){return e.type&&"image"===e.type}configure(e,t,i){return e}getImageInfo(t){const i=new Image,n=this;this.crossOriginPolicy&&(i.crossOrigin=this.crossOriginPolicy),e.addEvent(i,"load",function(){n.width=i.naturalWidth,n.height=i.naturalHeight,n.tileWidth=n.width,n.tileHeight=n.height,n.tileOverlap=0,n.minLevel=0,n.image=i,n.levels=n._buildLevels(i),n.maxLevel=n.levels.length-1,n.raiseEvent("ready",{tileSource:n})}),e.addEvent(i,"error",function(){n.image=null,n.raiseEvent("open-failed",{message:"Error loading image at "+t,source:t})}),i.src=t}getLevelScale(e){let t=NaN;return e>=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width),t}getNumTiles(t){return this.getLevelScale(t)?new e.Point(1,1):new e.Point(0,0)}getTileUrl(e,t,i){return e===this.maxLevel?this.url:`${this.url}?l=${e}&x=${t}&y=${i}`}equals(e){return this.url===e.url}getTilePostData(e,t,i){return{level:e,x:t,y:i}}getContext2D(t,i,n){return e.console.error("Using [TiledImage.getContext2D] (for plain images only) is deprecated. Use overridden downloadTileStart (https://openseadragon.github.io/examples/advanced-data-model/) instead."),this._createContext2D()}downloadTileStart(e){const t=e.postData;if(t.level!==this.maxLevel){if(t.level>=this.minLevel&&t.level<=this.maxLevel){const i=this.levels[t.level],n=this._createContext2D(this.image,i.width,i.height);return void e.finish(n,null,"context2d")}e.fail(`Invalid level ${t.level} for plain image source. Did you forget to set buildPyramid=true?`)}else e.finish(this.image,null,"image")}downloadTileAbort(e){}_buildLevels(t){const i=[{url:t.src,width:t.naturalWidth,height:t.naturalHeight}];if(!this.buildPyramid||!e.supportsCanvas||!this.useCanvas)return i;let n=t.naturalWidth,o=t.naturalHeight;for(;n>=2&&o>=2;)n=Math.floor(n/2),o=Math.floor(o/2),i.push({width:n,height:o});return i.reverse()}_createContext2D(e,t,i){const n=document.createElement("canvas"),o=n.getContext("2d");return n.width=t,n.height=i,o.drawImage(e,0,0,t,i),o}}}(OpenSeadragon),function(e){e.TileSourceCollection=function(t,i,n,o){e.console.error("TileSourceCollection is deprecated; use World instead")}}(OpenSeadragon),function(e){const t=e;t.PriorityQueue=class{constructor(e=void 0){this.nodes_=[],e&&this.insertAll(e)}insert(e,t){this.insertNode(new Node(e,t))}insertNode(e){const t=this.nodes_;e.index=t.length,t.push(e),this.moveUp_(e.index)}insertAll(t){let i,n;if(!(t instanceof e.PriorityQueue))throw"insertAll supports only OpenSeadragon.PriorityQueue object!";if(i=t.getKeys(),n=t.getValues(),this.getCount()<=0){const e=this.nodes_;for(let t=0;t>1;){const o=this.getLeftChildIndex_(e),s=this.getRightChildIndex_(e),r=sn.key)break;t[e]=t[r],t[e].index=e,e=r}t[e]=n,n&&(n.index=e)}moveUp_(e){const t=this.nodes_,i=t[e];for(;e>0;){const n=this.getParentIndex_(e);if(!(t[n].key>i.key))break;t[e]=t[n],t[e].index=e,e=n}t[e]=i,i&&(i.index=e)}getLeftChildIndex_(e){return 2*e+1}getRightChildIndex_(e){return 2*e+2}getParentIndex_(e){return e-1>>1}getValues(){return this.nodes_.map(e=>e.value)}getKeys(){return this.nodes_.map(e=>e.key)}containsValue(e){return this.nodes_.some(t=>t.value==e)}containsKey(e){return this.nodes_.some(t=>t.value==e)}clone(){return new e.PriorityQueue(this)}getCount(){return this.nodes_.length}isEmpty(){return 0===this.nodes_.length}clear(){this.nodes_.length=0}},t.PriorityQueue.Node=class e{constructor(e,t){this.key=e,this.value=t,this.index=0}clone(){return new e(this.key,this.value)}}}(OpenSeadragon),function(e){const t=e;class i{constructor(){this.adjacencyList={},this.vertices={}}addVertex(t){return!this.vertices[t]&&(this.vertices[t]=new e.PriorityQueue.Node(0,t),this.adjacencyList[t]=[],!0)}addEdge(t,i,n,o){n<0&&e.console.error("WeightedGraph: negative weights will make for invalid shortest path computation!");const s=this.adjacencyList[t].findIndex(e=>e.target===this.vertices[i]),r={target:this.vertices[i],origin:this.vertices[t],weight:n,transform:o};return s<0?(this.adjacencyList[t].push(r),!0):(this.adjacencyList[t][s]=r,!1)}dijkstra(e,i){const n=[];if(e===i)return{path:n,cost:0};const o=new t.PriorityQueue;let s;for(let t in this.vertices)t=this.vertices[t],t.value===e?(t.key=0,o.insertNode(t)):(t.key=1/0,delete t.index),t._previous=null;for(;o.getCount()>0&&(s=o.remove(),s.value!==i);){const e=this.adjacencyList[s.value];for(const t in e){const i=e[t],n=s.key+i.weight,r=i.target;nt.target.value===e)),s=t}return{path:n.reverse(),cost:r}}}let n,o=0;const s=new Map;let r=!1;const a="undefined"!=typeof SharedArrayBuffer&&!0===self.crossOriginIsolated;function l(t,i,{timeoutMs:l=15e3}={}){const h=function(){if(n)return n;const e=URL.createObjectURL(new Blob(["\nself.onmessage = async (e) => {\n const { id, op, } = e.data;\n let error;\n try {\n if (op === 'decodeFromBlob') {\n const bmp = await createImageBitmap(e.data.blob, { colorSpaceConversion: 'none' });\n postMessage({ id, ok: true, bmp }, [bmp]);\n return;\n }\n if (op === 'decodeFromBytes') {\n const u8 = new Uint8Array(e.data.bytes);\n const b = new Blob([u8], { type: e.data.mime || '' });\n const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' });\n postMessage({ id, ok: true, bmp }, [bmp]);\n return;\n }\n if (op === 'fetchDecode') {\n const res = await fetch(e.data.url, e.data.setup);\n if (!res.ok) throw new Error('HTTP ' + res.status);\n const b = await res.blob();\n const bmp = await createImageBitmap(b, { colorSpaceConversion: 'none' });\n postMessage({ id, ok: true, bmp }, [bmp]);\n return;\n }\n error = 'Unknown op: ' + op;\n } catch (err) {\n error = String(err && err.message || err);\n }\n postMessage({ id, ok: false, err: error });\n};\n"],{type:"text/javascript"}));return n=new Worker(e),n.onmessage=e=>{const{id:t,ok:i,bmp:n,err:o}=e.data||{},r=s.get(t);r&&(s.delete(t),r.timer&&(clearTimeout(r.timer),r.timer=null),i?r.resolve(n):r.reject(new Error(o)))},n.onerror=e=>{for(const[,e]of s)e.timer&&(clearTimeout(e.timer),e.timer=null),e.reject(new Error("Worker error"));s.clear()},n}(),c=++o;return new e.Promise((e,n)=>{i.id=c,i.op=t;const o={resolve:e,reject:n,timer:null};if(l>0&&(o.timer=setTimeout(()=>{o.timer=null,s.delete(c),n(new Error(`Worker timeout (${t})`))},l)),s.set(c,o),"decodeFromBytes"!==t)h.postMessage(i);else if(a){const e=i.u8,n=new SharedArrayBuffer(e.byteLength);new Uint8Array(n).set(e),h.postMessage({id:c,op:t,bytes:n,mime:i.mime})}else{r||(r=!0,console.warn("[Converter] SharedArrayBuffer unavailable; falling back to ArrayBuffer."));const e=i.u8,n=0===e.byteOffset&&e.byteLength===e.buffer.byteLength?e:e.slice();h.postMessage({id:c,op:t,bytes:n.buffer,mime:i.mime},[n.buffer])}})}t.DataTypeConverter=class{constructor(){this.graph=new i,this.destructors={},this.copyings={};const t=(e,t)=>{const i=document.createElement("canvas");i.width=t.width,i.height=t.height;const n=i.getContext("2d",{willReadFrequently:!0});return n.drawImage(t,0,0),n};this.learn("rasterBlob","image",(t,i)=>new e.Promise((t,n)=>{const o=(window.URL||window.webkitURL).createObjectURL(i);if(!e.supportsAsync)return n("Not supported in sync mode!");const s=new Image;s.onerror=s.onabort=e=>{(window.URL||window.webkitURL).revokeObjectURL(i),n(e)},s.onload=()=>{(window.URL||window.webkitURL).revokeObjectURL(i),t(s)},s.decoding="async",s.src=o}),1,2),this.learn("context2d","rasterBlob",(t,i)=>new e.Promise((t,n)=>{if(!e.supportsAsync)return n("Not supported in sync mode!");i.canvas.toBlob(t)}),1,2),this.learn("rasterBlob","imageBitmap",(t,i)=>new e.Promise((t,o)=>{if(!e.supportsAsync)return o("Not supported in sync mode!");n?l("decodeFromBlob",{blob:i}).then(t).catch(o):createImageBitmap(i,{colorSpaceConversion:"none"}).then(t).catch(o)}),1,1),this.learn("imageBitmap","context2d",(e,t)=>{const i=document.createElement("canvas");i.width=t.width,i.height=t.height;const n=i.getContext("2d",{willReadFrequently:!0});return n.drawImage(t,0,0),n},1,2),this.learn("image","imageBitmap",(e,t)=>createImageBitmap(t,{colorSpaceConversion:"none"}),1,2),this.learn("image","context2d",t,1,2),this.learn("image","image",(t,i)=>((t,i)=>new e.Promise((n,o)=>{if(!e.supportsAsync)return o("Not supported in sync mode!");const s=new Image;s.onerror=s.onabort=e=>o(`Failed to load image: ${i}`),s.onload=()=>n(s),t.tiledImage&&t.tiledImage.crossOriginPolicy&&(s.crossOrigin=t.tiledImage.crossOriginPolicy),s.src=i}))(t,i.src),1,1),this.learn("context2d","context2d",(e,i)=>t(0,i.canvas)),this.learn("rasterBlob","rasterBlob",(e,t)=>t,0,1),this.learn("imageBitmap","imageBitmap",(t,i)=>new e.Promise((t,n)=>{try{if(!e.supportsAsync)return n("Not supported in sync mode!");if(!i)return n(new Error("No ImageBitmap to copy"));if("undefined"!=typeof OffscreenCanvas&&i.width&&i.height){const e=new OffscreenCanvas(i.width,i.height);if(e.getContext("2d",{willReadFrequently:!1}).drawImage(i,0,0),"function"==typeof e.transferToImageBitmap){return t(e.transferToImageBitmap())}return createImageBitmap(e,{colorSpaceConversion:"none"}).then(t)}return createImageBitmap(i,{colorSpaceConversion:"none"}).then(t)}catch(e){return n(e)}}),1,1),this.learnDestroy("context2d",e=>{e.canvas.width=0,e.canvas.height=0})}guessType(t){if(Array.isArray(t)){const e=[];for(const i of t){if(null==i)continue;const t=this.guessType(i);e.includes(t)||e.push(t)}return e.sort(),`Array [${e.join(",")}]`}const i=e.type(t);return"dom-node"===i?i.nodeName.toLowerCase():"object"===i&&e.isFunction(t.getType)?t.getType():i}learn(t,i,n,o=0,s=1){e.console.assert(o>=0&&o<=7,"[DataTypeConverter] Conversion costPower must be between <0, 7>."),e.console.assert(e.isFunction(n),"[DataTypeConverter:learn] Callback must be a valid function!"),t===i?this.copyings[i]=n:(o++,s=Math.min(Math.max(s,1),15),this.graph.addVertex(t),this.graph.addVertex(i),this.graph.addEdge(t,i,10*o^5+s,n),this._known={})}learnDestroy(e,t){this.destructors[e]=t}convert(t,i,n,...o){const s=this.getConversionPath(n,o);if(!s)return e.console.error(`[OpenSeadragon.converter.convert] Conversion ${n} ---\x3e ${o} cannot be done!`),e.Promise.resolve();const r=s.length,a=this,l=(i,n,o=!0)=>{if(n>=r)return e.Promise.resolve(i);const h=s[n];let c;try{c=h.transform(t,i)}catch(t){return o&&a.destroy(i,h.origin.value),e.Promise.reject(`[OpenSeadragon.converter.convert] sync failure (while converting using ${h.origin.value} -> ${h.target.value})`)}if(void 0===c)return o&&a.destroy(i,h.origin.value),e.Promise.reject(`[OpenSeadragon.converter.convert] data mid result undefined value (while converting using ${h.origin.value} -> ${h.target.value})`);o&&a.destroy(i,h.origin.value);return("promise"===e.type(c)?c:e.Promise.resolve(c)).then(e=>l(e,n+1))};return l(i,0,!1)}copy(t,i,n){const o=this.copyings[n];if(o){const n=o(t,i);return"promise"===e.type(n)?n:e.Promise.resolve(n)}return e.console.warn("[OpenSeadragon.converter.copy] is not supported with type %s",n),e.Promise.resolve(void 0)}destroy(t,i){const n=this.destructors[i];if(n){const i=n(t);return"promise"===e.type(i)?i:e.Promise.resolve(i)}}getConversionPath(t,i){let n,o=this._known[t];if(o||(this._known[t]=o={}),Array.isArray(i)){e.console.assert(i.length>0,"[getConversionPath] conversion 'to' type must be defined.");let s=1/0;for(const e of i){let i=o[e];void 0===i&&(o[e]=i=this.graph.dijkstra(t,e)),i&&s>i.cost&&(n=i,s=i.cost)}}else e.console.assert("string"==typeof i,"[getConversionPath] conversion 'to' type must be defined."),n=o[i],void 0===n&&(n=this.graph.dijkstra(t,i),this._known[t][i]=n);return n?n.path:void 0}getConversionPathFinalType(e){if(e&&e.length)return e[e.length-1].target.value}getKnownTypes(){return Object.keys(this.graph.vertices)}existsType(e){return!!this.graph.vertices[e]}},e.converter=new e.DataTypeConverter,e.converter.learn("__private__imageUrl","imageBitmap",(t,i)=>new e.Promise((o,s)=>{if(!e.supportsAsync)return s("Not supported in sync mode!");let r;if(t.tiledImage&&t.tiledImage.crossOriginPolicy){const i=t.tiledImage.crossOriginPolicy;"anonymous"===i?r={mode:"cors",credentials:"omit"}:"use-credentials"===i?r={mode:"cors",credentials:"include"}:i&&e.console.error(`Unsupported crossOriginPolicy ${i}. Ignoring the property.`)}return n?l("fetchDecode",{url:i,setup:r}).then(o).catch(s):fetch(i,r).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status} loading ${i}`);return e.blob()}).then(e=>createImageBitmap(e,{colorSpaceConversion:"none"})).then(o).catch(s)}),1,1),e.converter.learn("__private__imageUrl","__private__imageUrl",(e,t)=>t,0,1)}(OpenSeadragon),function(e){function t(i){e.requestAnimationFrame(function(){!function(i){let n,o,s;i.shouldFade&&(n=e.now(),o=n-i.fadeBeginTime,s=1-o/i.fadeLength,s=Math.min(1,s),s=Math.max(0,s),i.imgGroup&&e.setElementOpacity(i.imgGroup,s,!0),s>0&&t(i))}(i)})}function i(t,i){t.element.disabled||(i>=e.ButtonState.GROUP&&t.currentState===e.ButtonState.REST&&(!function(t){t.shouldFade=!1,t.imgGroup&&e.setElementOpacity(t.imgGroup,1,!0)}(t),t.currentState=e.ButtonState.GROUP),i>=e.ButtonState.HOVER&&t.currentState===e.ButtonState.GROUP&&(t.imgHover&&(t.imgHover.style.visibility=""),t.currentState=e.ButtonState.HOVER),i>=e.ButtonState.DOWN&&t.currentState===e.ButtonState.HOVER&&(t.imgDown&&(t.imgDown.style.visibility=""),t.currentState=e.ButtonState.DOWN))}function n(i,n){i.element.disabled||(n<=e.ButtonState.HOVER&&i.currentState===e.ButtonState.DOWN&&(i.imgDown&&(i.imgDown.style.visibility="hidden"),i.currentState=e.ButtonState.HOVER),n<=e.ButtonState.GROUP&&i.currentState===e.ButtonState.HOVER&&(i.imgHover&&(i.imgHover.style.visibility="hidden"),i.currentState=e.ButtonState.GROUP),n<=e.ButtonState.REST&&i.currentState===e.ButtonState.GROUP&&(!function(i){i.shouldFade=!0,i.fadeBeginTime=e.now()+i.fadeDelay,window.setTimeout(function(){t(i)},i.fadeDelay)}(i),i.currentState=e.ButtonState.REST))}e.ButtonState={REST:0,GROUP:1,HOVER:2,DOWN:3},e.Button=function(t){const o=this;e.EventSource.call(this),e.extend(!0,this,{tooltip:null,srcRest:null,srcGroup:null,srcHover:null,srcDown:null,clickTimeThreshold:e.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:e.DEFAULT_SETTINGS.clickDistThreshold,fadeDelay:0,fadeLength:2e3,onPress:null,onRelease:null,onClick:null,onEnter:null,onExit:null,onFocus:null,onBlur:null,userData:null},t),this.element=t.element||e.makeNeutralElement("div"),t.element||(this.imgRest=e.makeTransparentImage(this.srcRest),this.imgGroup=e.makeTransparentImage(this.srcGroup),this.imgHover=e.makeTransparentImage(this.srcHover),this.imgDown=e.makeTransparentImage(this.srcDown),this.imgRest.alt=this.imgGroup.alt=this.imgHover.alt=this.imgDown.alt=this.tooltip,e.setElementPointerEventsNone(this.imgRest),e.setElementPointerEventsNone(this.imgGroup),e.setElementPointerEventsNone(this.imgHover),e.setElementPointerEventsNone(this.imgDown),this.element.style.position="relative",e.setElementTouchActionNone(this.element),this.imgGroup.style.position=this.imgHover.style.position=this.imgDown.style.position="absolute",this.imgGroup.style.top=this.imgHover.style.top=this.imgDown.style.top="0px",this.imgGroup.style.left=this.imgHover.style.left=this.imgDown.style.left="0px",this.imgHover.style.visibility=this.imgDown.style.visibility="hidden",this.element.appendChild(this.imgRest),this.element.appendChild(this.imgGroup),this.element.appendChild(this.imgHover),this.element.appendChild(this.imgDown)),this.addHandler("press",this.onPress),this.addHandler("release",this.onRelease),this.addHandler("click",this.onClick),this.addHandler("enter",this.onEnter),this.addHandler("exit",this.onExit),this.addHandler("focus",this.onFocus),this.addHandler("blur",this.onBlur),this.currentState=e.ButtonState.GROUP,this.fadeBeginTime=null,this.shouldFade=!1,this.element.style.display="inline-block",this.element.style.position="relative",this.element.title=this.tooltip,this.tracker=new e.MouseTracker({userData:"Button.tracker",element:this.element,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,enterHandler:function(t){t.insideElementPressed?(i(o,e.ButtonState.DOWN),o.raiseEvent("enter",{originalEvent:t.originalEvent})):t.buttonDownAny||i(o,e.ButtonState.HOVER)},focusHandler:function(e){o.tracker.enterHandler(e),o.raiseEvent("focus",{originalEvent:e.originalEvent})},leaveHandler:function(t){n(o,e.ButtonState.GROUP),t.insideElementPressed&&o.raiseEvent("exit",{originalEvent:t.originalEvent})},blurHandler:function(e){o.tracker.leaveHandler(e),o.raiseEvent("blur",{originalEvent:e.originalEvent})},pressHandler:function(t){i(o,e.ButtonState.DOWN),o.raiseEvent("press",{originalEvent:t.originalEvent})},releaseHandler:function(t){t.insideElementPressed&&t.insideElementReleased?(n(o,e.ButtonState.HOVER),o.raiseEvent("release",{originalEvent:t.originalEvent})):t.insideElementPressed?n(o,e.ButtonState.GROUP):i(o,e.ButtonState.HOVER)},clickHandler:function(e){e.quick&&o.raiseEvent("click",{originalEvent:e.originalEvent})},keyHandler:function(e){13===e.keyCode?(o.raiseEvent("click",{originalEvent:e.originalEvent}),o.raiseEvent("release",{originalEvent:e.originalEvent}),e.preventDefault=!0):e.preventDefault=!1}}),n(this,e.ButtonState.REST)},e.extend(e.Button.prototype,e.EventSource.prototype,{notifyGroupEnter:function(){i(this,e.ButtonState.GROUP)},notifyGroupExit:function(){n(this,e.ButtonState.REST)},disable:function(){this.notifyGroupExit(),this.element.disabled=!0,this.tracker.setTracking(!1),e.setElementOpacity(this.element,.2,!0)},enable:function(){this.element.disabled=!1,this.tracker.setTracking(!0),e.setElementOpacity(this.element,1,!0),this.notifyGroupEnter()},destroy:function(){this.imgRest&&(this.element.removeChild(this.imgRest),this.imgRest=null),this.imgGroup&&(this.element.removeChild(this.imgGroup),this.imgGroup=null),this.imgHover&&(this.element.removeChild(this.imgHover),this.imgHover=null),this.imgDown&&(this.element.removeChild(this.imgDown),this.imgDown=null),this.removeAllHandlers(),this.tracker.destroy(),this.element=null}})}(OpenSeadragon),function(e){e.ButtonGroup=function(t){e.extend(!0,this,{buttons:[],clickTimeThreshold:e.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:e.DEFAULT_SETTINGS.clickDistThreshold,labelText:""},t);let i,n=this.buttons.concat([]),o=this;if(this.element=t.element||e.makeNeutralElement("div"),!t.group)for(this.element.style.display="inline-block",i=0;i=270?(r=this.getTopRight(),this.x=r.x,this.y=r.y,a=this.height,this.height=this.width,this.width=a,this.degrees-=270):this.degrees>=180?(r=this.getBottomRight(),this.x=r.x,this.y=r.y,this.degrees-=180):this.degrees>=90&&(r=this.getBottomLeft(),this.x=r.x,this.y=r.y,a=this.height,this.height=this.width,this.width=a,this.degrees-=90)},e.Rect.fromSummits=function(t,i,n){const o=t.distanceTo(i),s=t.distanceTo(n),r=i.minus(t);let a=Math.atan(r.y/r.x);return r.x<0?a+=Math.PI:r.y<0&&(a+=2*Math.PI),new e.Rect(t.x,t.y,o,s,a/Math.PI*180)},e.Rect.prototype={clone:function(){return new e.Rect(this.x,this.y,this.width,this.height,this.degrees)},getAspectRatio:function(){return this.width/this.height},getTopLeft:function(){return new e.Point(this.x,this.y)},getBottomRight:function(){return new e.Point(this.x+this.width,this.y+this.height).rotate(this.degrees,this.getTopLeft())},getTopRight:function(){return new e.Point(this.x+this.width,this.y).rotate(this.degrees,this.getTopLeft())},getBottomLeft:function(){return new e.Point(this.x,this.y+this.height).rotate(this.degrees,this.getTopLeft())},getCenter:function(){return new e.Point(this.x+this.width/2,this.y+this.height/2).rotate(this.degrees,this.getTopLeft())},getSize:function(){return new e.Point(this.width,this.height)},equals:function(t){return t instanceof e.Rect&&this.x===t.x&&this.y===t.y&&this.width===t.width&&this.height===t.height&&this.degrees===t.degrees},times:function(t){return new e.Rect(this.x*t,this.y*t,this.width*t,this.height*t,this.degrees)},translate:function(t){return new e.Rect(this.x+t.x,this.y+t.y,this.width,this.height,this.degrees)},union:function(t){const i=this.getBoundingBox(),n=t.getBoundingBox(),o=Math.min(i.x,n.x),s=Math.min(i.y,n.y),r=Math.max(i.x+i.width,n.x+n.width),a=Math.max(i.y+i.height,n.y+n.height);return new e.Rect(o,s,r-o,a-s)},intersection:function(t){const i=1e-10,n=[],o=this.getTopLeft();t.containsPoint(o,i)&&n.push(o);const s=this.getTopRight();t.containsPoint(s,i)&&n.push(s);const r=this.getBottomLeft();t.containsPoint(r,i)&&n.push(r);const a=this.getBottomRight();t.containsPoint(a,i)&&n.push(a);const l=t.getTopLeft();this.containsPoint(l,i)&&n.push(l);const h=t.getTopRight();this.containsPoint(h,i)&&n.push(h);const c=t.getBottomLeft();this.containsPoint(c,i)&&n.push(c);const u=t.getBottomRight();this.containsPoint(u,i)&&n.push(u);const d=this._getSegments(),p=t._getSegments();for(let e=0;ef&&(f=t.x),t.yy&&(y=t.y)}return new e.Rect(m,v,f-m,y-v)},_getSegments:function(){const e=this.getTopLeft(),t=this.getTopRight(),i=this.getBottomLeft(),n=this.getBottomRight();return[[e,t],[t,n],[n,i],[i,e]]},rotate:function(t,i){if(0===(t=e.positiveModulo(t,360)))return this.clone();i=i||this.getCenter();const n=this.getTopLeft().rotate(t,i);let o=this.getTopRight().rotate(t,i).minus(n);o=o.apply(function(e){return Math.abs(e)<1e-15?0:e});let s=Math.atan(o.y/o.x);return o.x<0?s+=Math.PI:o.y<0&&(s+=2*Math.PI),new e.Rect(n.x,n.y,this.width,this.height,s/Math.PI*180)},getBoundingBox:function(){if(0===this.degrees)return this.clone();const t=this.getTopLeft(),i=this.getTopRight(),n=this.getBottomLeft(),o=this.getBottomRight(),s=Math.min(t.x,i.x,n.x,o.x),r=Math.max(t.x,i.x,n.x,o.x),a=Math.min(t.y,i.y,n.y,o.y),l=Math.max(t.y,i.y,n.y,o.y);return new e.Rect(s,a,r-s,l-a)},getIntegerBoundingBox:function(){const t=this.getBoundingBox(),i=Math.floor(t.x),n=Math.floor(t.y),o=Math.ceil(t.width+t.x-i),s=Math.ceil(t.height+t.y-n);return new e.Rect(i,n,o,s)},containsPoint:function(e,t){t=t||0;const i=this.getTopLeft(),n=this.getTopRight(),o=this.getBottomLeft(),s=n.minus(i),r=o.minus(i);return(e.x-i.x)*s.x+(e.y-i.y)*s.y>=-t&&(e.x-n.x)*s.x+(e.y-n.y)*s.y<=t&&(e.x-i.x)*r.x+(e.y-i.y)*r.y>=-t&&(e.x-o.x)*r.x+(e.y-o.y)*r.y<=t},toString:function(){return"["+Math.round(100*this.x)/100+", "+Math.round(100*this.y)/100+", "+Math.round(100*this.width)/100+"x"+Math.round(100*this.height)/100+", "+Math.round(100*this.degrees)/100+"deg]"}}}(OpenSeadragon),function(e){const t={};function i(e){if(e.quick){let t;t="horizontal"===this.scroll?Math.floor(e.position.x/(this.panelWidth+4)):Math.floor(e.position.y/this.panelHeight),this.viewer.goToPage(t)}this.element.focus()}function n(t){if(this.dragging=!0,this.element){const i=Number(this.element.style.marginLeft.replace("px","")),n=Number(this.element.style.marginTop.replace("px","")),o=Number(this.element.style.width.replace("px","")),r=Number(this.element.style.height.replace("px","")),a=e.getElementSize(this.viewer.canvas);"horizontal"===this.scroll?-t.delta.x>0?i>-(o-a.x)&&(this.element.style.marginLeft=i+2*t.delta.x+"px",s(this,a.x,i+2*t.delta.x)):-t.delta.x<0&&i<0&&(this.element.style.marginLeft=i+2*t.delta.x+"px",s(this,a.x,i+2*t.delta.x)):-t.delta.y>0?n>-(r-a.y)&&(this.element.style.marginTop=n+2*t.delta.y+"px",s(this,a.y,n+2*t.delta.y)):-t.delta.y<0&&n<0&&(this.element.style.marginTop=n+2*t.delta.y+"px",s(this,a.y,n+2*t.delta.y))}}function o(t){if(this.element){const i=Number(this.element.style.marginLeft.replace("px","")),n=Number(this.element.style.marginTop.replace("px","")),o=Number(this.element.style.width.replace("px","")),r=Number(this.element.style.height.replace("px","")),a=e.getElementSize(this.viewer.canvas);"horizontal"===this.scroll?t.scroll>0?i>-(o-a.x)&&(this.element.style.marginLeft=i-60*t.scroll+"px",s(this,a.x,i-60*t.scroll)):t.scroll<0&&i<0&&(this.element.style.marginLeft=i-60*t.scroll+"px",s(this,a.x,i-60*t.scroll)):t.scroll<0?n>a.y-r&&(this.element.style.marginTop=n+60*t.scroll+"px",s(this,a.y,n+60*t.scroll)):t.scroll>0&&n<0&&(this.element.style.marginTop=n+60*t.scroll+"px",s(this,a.y,n+60*t.scroll)),t.preventDefault=!0}}function s(t,i,n){let o,s,r,a,l,h;for(o="horizontal"===t.scroll?t.panelWidth:t.panelHeight,s=Math.ceil(i/o)+5,r=Math.ceil((Math.abs(n)+i)/o)+1,s=r-s,s=s<0?0:s,l=s;ll+n.x-this.panelWidth?(c=Math.min(c,o-n.x),this.element.style.marginLeft=-c+"px",s(this,n.x,-c)):ch+n.y-this.panelHeight?(c=Math.min(c,a-n.y),this.element.style.marginTop=-c+"px",s(this,n.y,-c)):c1?i[1].springStiffness:5,animationTime:i.length>1?i[1].animationTime:1.5}),e.console.assert("number"==typeof t.springStiffness&&0!==t.springStiffness,"[OpenSeadragon.Spring] options.springStiffness must be a non-zero number"),e.console.assert("number"==typeof t.animationTime&&t.animationTime>=0,"[OpenSeadragon.Spring] options.animationTime must be a number greater than or equal to 0"),t.exponential&&(this._exponential=!0,delete t.exponential),e.extend(!0,this,t),this.current={value:"number"==typeof this.initial?this.initial:this._exponential?0:1,time:e.now()},e.console.assert(!this._exponential||0!==this.current.value,"[OpenSeadragon.Spring] value must be non-zero for exponential springs"),this.start={value:this.current.value,time:this.current.time},this.target={value:this.current.value,time:this.current.time},this._exponential&&(this.start._logValue=Math.log(this.start.value),this.target._logValue=Math.log(this.target.value),this.current._logValue=Math.log(this.current.value))},e.Spring.prototype={resetTo:function(t){e.console.assert(!this._exponential||0!==t,"[OpenSeadragon.Spring.resetTo] target must be non-zero for exponential springs"),this.start.value=this.target.value=this.current.value=t,this.start.time=this.target.time=this.current.time=e.now(),this._exponential&&(this.start._logValue=Math.log(this.start.value),this.target._logValue=Math.log(this.target.value),this.current._logValue=Math.log(this.current.value))},springTo:function(t){e.console.assert(!this._exponential||0!==t,"[OpenSeadragon.Spring.springTo] target must be non-zero for exponential springs"),this.start.value=this.current.value,this.start.time=this.current.time,this.target.value=t,this.target.time=this.start.time+1e3*this.animationTime,this._exponential&&(this.start._logValue=Math.log(this.start.value),this.target._logValue=Math.log(this.target.value))},shiftBy:function(t){this.start.value+=t,this.target.value+=t,this._exponential&&(e.console.assert(0!==this.target.value&&0!==this.start.value,"[OpenSeadragon.Spring.shiftBy] spring value must be non-zero for exponential springs"),this.start._logValue=Math.log(this.start.value),this.target._logValue=Math.log(this.target.value))},setExponential:function(t){this._exponential=t,this._exponential&&(e.console.assert(0!==this.current.value&&0!==this.target.value&&0!==this.start.value,"[OpenSeadragon.Spring.setExponential] spring value must be non-zero for exponential springs"),this.start._logValue=Math.log(this.start.value),this.target._logValue=Math.log(this.target.value),this.current._logValue=Math.log(this.current.value))},update:function(){let t,i;if(this.current.time=e.now(),this._exponential?(t=this.start._logValue,i=this.target._logValue):(t=this.start.value,i=this.target.value),this.current.time>=this.target.time)this.current.value=this.target.value;else{let e=t+(i-t)*(n=this.springStiffness,o=(this.current.time-this.start.time)/(this.target.time-this.start.time),(1-Math.exp(n*-o))/(1-Math.exp(-n)));this._exponential?this.current.value=Math.exp(e):this.current.value=e}var n,o;return this.current.value!==this.target.value},isAtTargetValue:function(){return this.current.value===this.target.value}}}(OpenSeadragon),function(e){e.ImageJob=function(t){this.data=null,this.userData={},this.errorMsg=null,this.timeout=e.DEFAULT_SETTINGS.timeout,this.isBatched=!1,e.extend(!0,this,{jobId:null,tries:0},t)},e.ImageJob.prototype={start:function(){this.tries++;const e=this,t=this.abort;this.jobId=window.setTimeout(function(){e.fail("Image load exceeded timeout ("+e.timeout+" ms)",null)},this.timeout),this.abort=function(){e.source.downloadTileAbort(e),"function"==typeof t&&t(),e.fail("Image load aborted.",null)},this.source.downloadTileStart(this)},prepareForBatch:function(){this.tries++,this.jobId=-1},finish:function(e,t,i){var n;this.jobId&&(null!=(n=e)&&!1!==n?(this.data=e,this.request=t,this.errorMsg=null,this.dataType=i,window.clearTimeout(this.jobId),this.jobId=null,this.callback(this)):this.fail(i||"[downloadTileStart->finish()] Retrieved data is invalid!",t))},fail:function(e,t){this.data=null,this.request=t,this.errorMsg=e,this.dataType=null,this.jobId&&(window.clearTimeout(this.jobId),this.jobId=null),this.callback(this)}},e.BatchImageJob=function(t){e.extend(!0,this,{timeout:e.DEFAULT_SETTINGS.timeout,jobId:null,data:null,dataType:null,errorMsg:null},t),this.jobs=t.jobs||[],this.source=t.source},e.BatchImageJob.prototype={start:function(){this._finishedJobs=0;const e=this;this.jobId=window.setTimeout(function(){e.fail("Batch image load exceeded timeout ("+e.timeout+" ms)",null)},this.timeout),this.abort=function(){e.source.downloadTileBatchAbort(e);for(let e of this.jobs)e.jobId&&e.abort&&e.abort()};const t=(e,t)=>(...i)=>{this.jobId&&(this._finishedJobs++,e.call(t,...i),this._finishedJobs===this.jobs.length&&(window.clearTimeout(this.jobId),this.jobId=null,this.callback&&this.callback(this)))};for(let e of this.jobs)e.finish=t(e.finish,e),e.fail=t(e.fail,e),e.prepareForBatch();this.source.downloadTileBatchStart(this)},finish:function(t,i,n){e.console.error("Finish call on batch job is not desirable: call finish on individual child jobs!",t,i)},fail:function(e,t){this.data=null,this.request=t,this.errorMsg=e,this.dataType=null;for(let i=0;ifunction(e,t,i){t.errorMsg&&null===t.data&&t.tries<1+e.tileRetryMax&&(t.isBatched=!1,e.failedTiles.push(t));t.isBatched||e.jobsInProgress--;if(e.canAcceptNewJob()&&e.jobQueue.length>0){e.jobQueue.shift().start(),e.jobsInProgress++}if(e.tileRetryMax>0&&0===e.jobQueue.length&&e.canAcceptNewJob()&&e.failedTiles.length>0){let t=e.failedTiles.shift();setTimeout(function(){t.start()},e.tileRetryDelay),e.jobsInProgress++}i&&i(t.data,t.errorMsg,t.request,t.dataType,t.tries)}(i,e,t.callback),abort:t.abort,timeout:this.timeout},o=new e.ImageJob(n);return t.source&&t.source.batchEnabled()?(o.isBatched=!0,this._stageJobForBatching(o,t.source),!1):!this.jobLimit||this.jobsInProgressthis._flushBatchBucket(n),n.waitTimeout),this._batchBuckets.push(n)),n.jobs.push(t),n.maxJobs>=1&&n.jobs.length>=n.maxJobs&&(clearTimeout(n.timer),this._flushBatchBucket(n))},_flushBatchBucket:function(t){t.timer=null;const i=this._batchBuckets.indexOf(t);if(i>-1&&this._batchBuckets.splice(i,1),0===t.jobs.length)return;const n=this,o=new e.BatchImageJob({source:t.source,jobs:t.jobs,timeout:this.timeout,callback:e=>function(e,t){e.jobsInProgress--,t.jobs.length=0}(n,e)});!this.jobLimit||this.jobsInProgressthis.addCache(this.cacheKey,e,t.type,!0,!1)))},buildDistinctMainCacheKey:function(){return this.cacheKey===this.originalCacheKey?"mod://"+this.originalCacheKey:this.cacheKey},getCache:function(e=this._cKey){const t=this._caches[e];return t&&t.withTileReference(this),t},addCache:function(t,i,n=void 0,o=!1,s=!0){const r=this.tiledImage;if(!r)return null;n||(this.__typeWarningReported||(e.console.warn(this,"[Tile.addCache] called without type specification. Automated deduction is potentially unsafe: prefer specification of data type explicitly."),this.__typeWarningReported=!0),"function"==typeof i&&e.console.error("[TileCache.cacheTile] options.data as a callback requires type argument! Current is "+n),n=e.converter.guessType(i));const a=t===this.cacheKey;if(s&&(a||o)){const t=r.getDrawer().getSupportedDataFormats(),i=e.converter.getConversionPath(n,t);e.console.assert(i,`[Tile.addCache] data was set for the default tile cache we are unableto render. Make sure OpenSeadragon.converter was taught to convert ${n} to (one of): ${i.toString()}`)}const l=r._tileCache.cacheTile({data:i,dataType:n,tile:this,cacheKey:t,cutoff:r.source.getClosestLevel()}),h=this._caches[t];return h!==l&&(this._caches[t]=l,h&&(h.removeTile(this),r._tileCache.safeUnloadCache(h))),!a&&o&&this._updateMainCacheKey(t),l},setCache(t,i,n=!1,o=!0){const s=this.tiledImage;if(!s)return null;const r=t===this.cacheKey;if(o&&(e.console.assert(i instanceof e.CacheRecord,"[Tile.setCache] cache must be a CacheRecord object!"),r||n)){const t=s.getDrawer().getSupportedDataFormats(),n=e.converter.getConversionPath(i.type,t);e.console.assert(n,`[Tile.setCache] data was set for the default tile cache we are unableto render. Make sure OpenSeadragon.converter was taught to convert ${i.type} to (one of): ${n.toString()}`)}const a=this._caches[t];return a!==i&&(this._caches[t]=i,i.addTile(this),a&&(a.removeTile(this),s._tileCache.safeUnloadCache(a))),!r&&n&&this._updateMainCacheKey(t),i},_updateMainCacheKey:function(e){let t=this._caches[this._cKey];t&&t.destroyInternalCache(),this._cKey=e},getCacheSize:function(){return Object.keys(this._caches).length},removeCache:function(t,i=!0){const n=this._caches[t];if(!n)return void this.tiledImage._tileCache.unloadCacheForTile(this,t,i,!0);const o=this.cacheKey,s=this.originalCacheKey,r=o===s;if(r||s!==t){if(o===t){if(r||!this._caches[s])return void e.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!","If you want to remove the main cache, first set different cache as main with tile.addCache()");this._updateMainCacheKey(s)}return this.tiledImage._tileCache.unloadCacheForTile(this,t,i,!1)&&delete this._caches[t],n}e.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!","If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData().")},getScaleForEdgeSmoothing:function(){e.console.warn("[Tile.getScaleForEdgeSmoothing] is deprecated, the following error is the consequence:");const t=this.getCanvasContext();return t?t.canvas.width/(this.size.x*e.pixelDensityRatio):(e.console.warn("[Tile.drawCanvas] attempting to get tile scale %s when tile's not cached",this.toString()),1)},getTranslationForEdgeSmoothing:function(t,i,n){const o=Math.max(1,Math.ceil((n.x-i.x)/2)),s=Math.max(1,Math.ceil((n.y-i.y)/2));return new e.Point(o,s).minus(this.position.times(e.pixelDensityRatio).times(t||1).apply(function(e){return e%1}))},reflectCacheRenamed:function(e,t){let i=this._caches[e];i&&(e===this._ocKey&&(this._ocKey=t),e===this._cKey&&(this._cKey=t),this._caches[t]=i,delete this._caches[e])},equals(e){return this._ocKey===e._ocKey},unload:function(e=!1){this.loaded&&this.tiledImage._tileCache.unloadTile(this,e)},_unload:function(){this.tiledImage=null,this._caches={},this.loaded=!1,this.loading=!1,this._cKey=this._ocKey}}}(OpenSeadragon),function(e){e.OverlayPlacement=e.Placement,e.OverlayRotationMode=e.freezeObject({NO_ROTATION:1,EXACT:2,BOUNDING_BOX:3}),e.Overlay=function(t,i,n){let o;o=e.isPlainObject(t)?t:{element:t,location:i,placement:n},this.elementWrapper=document.createElement("div"),this.element=o.element,this.elementWrapper.appendChild(this.element),this.element.id&&(this.elementWrapper.id="overlay-wrapper-"+this.element.id),this.elementWrapper.classList.add("openseadragon-overlay-wrapper"),this.style=this.elementWrapper.style,this._init(o)},e.Overlay.prototype={_init:function(t){this.location=t.location,this.placement=void 0===t.placement?e.Placement.TOP_LEFT:t.placement,this.onDraw=t.onDraw,this.checkResize=void 0===t.checkResize||t.checkResize,this.width=void 0===t.width?null:t.width,this.height=void 0===t.height?null:t.height,this.rotationMode=t.rotationMode||e.OverlayRotationMode.EXACT,this.location instanceof e.Rect&&(this.width=this.location.width,this.height=this.location.height,this.location=this.location.getTopLeft(),this.placement=e.Placement.TOP_LEFT),this.scales=null!==this.width&&null!==this.height,this.bounds=new e.Rect(this.location.x,this.location.y,this.width,this.height),this.position=this.location},adjust:function(t,i){const n=e.Placement.properties[this.placement];n&&(n.isHorizontallyCentered?t.x-=i.x/2:n.isRight&&(t.x-=i.x),n.isVerticallyCentered?t.y-=i.y/2:n.isBottom&&(t.y-=i.y))},destroy:function(){const t=this.elementWrapper,i=this.style;t.parentNode&&(t.parentNode.removeChild(t),t.prevElementParent&&(i.display="none",document.body.appendChild(t))),this.onDraw=null,i.top="",i.left="",i.position="",null!==this.width&&(i.width=""),null!==this.height&&(i.height="");const n=e.getCssPropertyWithVendorPrefix("transformOrigin"),o=e.getCssPropertyWithVendorPrefix("transform");n&&o&&(i[n]="",i[o]="")},drawHTML:function(t,i){const n=this.elementWrapper;n.parentNode!==t&&(n.prevElementParent=n.parentNode,n.prevNextSibling=n.nextSibling,t.appendChild(n),this.style.position="absolute",this.size=e.getElementSize(this.elementWrapper));const o=this._getOverlayPositionAndSize(i),s=o.position,r=this.size=o.size;let a="";i.overlayPreserveContentDirection&&(a=i.flipped?" scaleX(-1)":" scaleX(1)");const l=i.flipped?-o.rotate:o.rotate,h=i.flipped?" scaleX(-1)":"";if(this.onDraw)this.onDraw(s,r,this.element);else{const t=this.style,n=this.element.style;n.display="block",t.left=s.x+"px",t.top=s.y+"px",null!==this.width&&(n.width=r.x+"px"),null!==this.height&&(n.height=r.y+"px");const o=e.getCssPropertyWithVendorPrefix("transformOrigin"),c=e.getCssPropertyWithVendorPrefix("transform");o&&c&&(l&&!i.flipped?(n[c]="",t[o]=this._getTransformOrigin(),t[c]="rotate("+l+"deg)"):!l&&i.flipped?(n[c]=a,t[o]=this._getTransformOrigin(),t[c]=h):l&&i.flipped?(n[c]=a,t[o]=this._getTransformOrigin(),t[c]="rotate("+l+"deg)"+h):(n[c]="",t[o]="",t[c]="")),t.display="flex"}},_getOverlayPositionAndSize:function(t){let i=t.pixelFromPoint(this.location,!0),n=this._getSizeInPixels(t);this.adjust(i,n);let o=0;if(t.getRotation(!0)&&this.rotationMode!==e.OverlayRotationMode.NO_ROTATION)if(this.rotationMode===e.OverlayRotationMode.BOUNDING_BOX&&null!==this.width&&null!==this.height){const o=new e.Rect(i.x,i.y,n.x,n.y),s=this._getBoundingBox(o,t.getRotation(!0));i=s.getTopLeft(),n=s.getSize()}else o=t.getRotation(!0);return t.flipped&&(i.x=t.getContainerSize().x-i.x),{position:i,size:n,rotate:o}},_getSizeInPixels:function(t){let i=this.size.x,n=this.size.y;if(null!==this.width||null!==this.height){const o=t.deltaPixelsFromPointsNoRotate(new e.Point(this.width||0,this.height||0),!0);null!==this.width&&(i=o.x),null!==this.height&&(n=o.y)}if(this.checkResize&&(null===this.width||null===this.height)){const t=this.size=e.getElementSize(this.elementWrapper);null===this.width&&(i=t.x),null===this.height&&(n=t.y)}return new e.Point(i,n)},_getBoundingBox:function(e,t){const i=this._getPlacementPoint(e);return e.rotate(t,i).getBoundingBox()},_getPlacementPoint:function(t){const i=new e.Point(t.x,t.y),n=e.Placement.properties[this.placement];return n&&(n.isHorizontallyCentered?i.x+=t.width/2:n.isRight&&(i.x+=t.width),n.isVerticallyCentered?i.y+=t.height/2:n.isBottom&&(i.y+=t.height)),i},_getTransformOrigin:function(){let t="";const i=e.Placement.properties[this.placement];return i?(i.isLeft?t="left":i.isRight&&(t="right"),i.isTop?t+=" top":i.isBottom&&(t+=" bottom"),t):t},update:function(t,i){const n=e.isPlainObject(t)?t:{location:t,placement:i};this._init({location:n.location||this.location,placement:void 0!==n.placement?n.placement:this.placement,onDraw:n.onDraw||this.onDraw,checkResize:n.checkResize||this.checkResize,width:void 0!==n.width?n.width:this.width,height:void 0!==n.height?n.height:this.height,rotationMode:n.rotationMode||this.rotationMode})},getBounds:function(t){e.console.assert(t,"A viewport must now be passed to Overlay.getBounds.");let i=this.width,n=this.height;if(null===i||null===n){const e=t.deltaPointsFromPixelsNoRotate(this.size,!0);null===i&&(i=e.x),null===n&&(n=e.y)}const o=this.location.clone();return this.adjust(o,new e.Point(i,n)),this._adjustBoundsForRotation(t,new e.Rect(o.x,o.y,i,n))},_adjustBoundsForRotation:function(t,i){if(!t||0===t.getRotation(!0)||this.rotationMode===e.OverlayRotationMode.EXACT)return i;if(this.rotationMode===e.OverlayRotationMode.BOUNDING_BOX){if(null===this.width||null===this.height)return i;const n=this._getOverlayPositionAndSize(t);return t.viewerElementToViewportRectangle(new e.Rect(n.position.x,n.position.y,n.size.x,n.size.y))}return i.rotate(-t.getRotation(!0),this._getPlacementPoint(i))}}}(OpenSeadragon),function(e){const t=e;t.DrawerBase=class{constructor(t){if(e.console.assert(t.viewer,"[Drawer] options.viewer is required"),e.console.assert(t.viewport,"[Drawer] options.viewport is required"),e.console.assert(t.element,"[Drawer] options.element is required"),this._id=this.getType()+e.now(),this.viewer=t.viewer,this.viewport=t.viewport,this.debugGridColor="string"==typeof t.debugGridColor?[t.debugGridColor]:t.debugGridColor||e.DEFAULT_SETTINGS.debugGridColor,this.options=e.extend({usePrivateCache:!1,preloadCache:!0,offScreen:!1,broadCastTileInvalidation:!0},this.defaultOptions,t.options),this.container=e.getElement(t.element),this._renderingTarget=this._createDrawingElement(),!this.options.offScreen)if(this.canvas.style.width="100%",this.canvas.style.height="100%",this.canvas.style.position="absolute",this.canvas.style.left="0",e.setElementOpacity(this.canvas,this.viewer.opacity,!0),e.setElementPointerEventsNone(this.canvas),e.setElementTouchActionNone(this.canvas),this.container.style.textAlign="left",this.container.appendChild(this.canvas),this.options.broadCastTileInvalidation){let e=this.viewer;for(;e.viewer;)e=e.viewer;this._parentViewer=e,e._registerDrawer(this)}else this.viewer._registerDrawer(this),this._parentViewer=this.viewer;this._checkInterfaceImplementation(),this.setInternalCacheNeedsRefresh()}get defaultOptions(){return{}}get canvas(){return this._renderingTarget}get element(){return e.console.error("Drawer.element is deprecated. Use Drawer.container instead."),this.container}getId(){return this._id}getType(){e.console.error("Drawer.getType must be implemented by child class")}getRequiredDataFormats(){return this.getSupportedDataFormats()}getSupportedDataFormats(){throw"Drawer.getSupportedDataFormats must define its supported rendering data types!"}getDataToDraw(t){const i=t.getCache(t.cacheKey);if(!i)return void e.console.warn("Attempt to draw tile %s when not cached!",t);const n=i.getDataForRendering(this,t);return n&&n.data}static isSupported(){e.console.error("Drawer.isSupported must be implemented by child class")}_createDrawingElement(){return e.console.error("Drawer._createDrawingElement must be implemented by child class"),null}draw(t){e.console.error("Drawer.draw must be implemented by child class")}canRotate(){e.console.error("Drawer.canRotate must be implemented by child class")}destroy(){this._parentViewer._unregisterDrawer(this)}destroyInternalCache(){this.viewer.tileCache.clearDrawerInternalCache(this)}minimumOverlapRequired(e){return!1}setImageSmoothingEnabled(t){e.console.error("Drawer.setImageSmoothingEnabled must be implemented by child class")}drawDebuggingRect(t){e.console.warn("[drawer].drawDebuggingRect is not implemented by this drawer")}clear(){e.console.warn("[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.")}internalCacheCreate(e,t){}internalCacheFree(e){}setInternalCacheNeedsRefresh(){this._dataNeedsRefresh=e.now()}tiledImageCreated(e){}_checkInterfaceImplementation(){if(this._createDrawingElement===e.DrawerBase.prototype._createDrawingElement)throw new Error("[drawer]._createDrawingElement must be implemented by child class");if(this.draw===e.DrawerBase.prototype.draw)throw new Error("[drawer].draw must be implemented by child class");if(this.canRotate===e.DrawerBase.prototype.canRotate)throw new Error("[drawer].canRotate must be implemented by child class");if(this.destroy===e.DrawerBase.prototype.destroy)throw new Error("[drawer].destroy must be implemented by child class");if(this.setImageSmoothingEnabled===e.DrawerBase.prototype.setImageSmoothingEnabled)throw new Error("[drawer].setImageSmoothingEnabled must be implemented by child class")}viewportToDrawerRectangle(t){const i=this.viewport.pixelFromPointNoRotate(t.getTopLeft(),!0),n=this.viewport.deltaPixelsFromPointsNoRotate(t.getSize(),!0);return new e.Rect(i.x*e.pixelDensityRatio,i.y*e.pixelDensityRatio,n.x*e.pixelDensityRatio,n.y*e.pixelDensityRatio)}viewportCoordToDrawerCoord(t){const i=this.viewport.pixelFromPointNoRotate(t,!0);return new e.Point(i.x*e.pixelDensityRatio,i.y*e.pixelDensityRatio)}_calculateCanvasSize(){const i=e.pixelDensityRatio,n=this.viewport.getContainerSize();return new t.Point(Math.round(n.x*i),Math.round(n.y*i))}_raiseTiledImageDrawnEvent(e,t){this.viewer&&this.viewer.raiseEvent("tiled-image-drawn",{tiledImage:e,tiles:t})}_raiseDrawerErrorEvent(e,t){this.viewer&&this.viewer.raiseEvent("drawer-error",{tiledImage:e,drawer:this,error:t})}}}(OpenSeadragon),function(e){const t=e;class i extends t.DrawerBase{constructor(t){super(t),this.viewer.rejectEventHandler("tile-drawing","The HTMLDrawer does not raise the tile-drawing event"),this.viewer.allowEventHandler("tile-drawn"),e.converter.learn("image",i.imageCacheType,function(t,i){const n=e.makeNeutralElement("div"),o=i.cloneNode();o.style.msInterpolationMode="nearest-neighbor",o.style.width="100%",o.style.height="100%";const s=n.style;return s.position="absolute",{element:n,imgElement:o,style:s,data:i}},1,1),e.converter.learn(i.imageCacheType,"image",(e,t)=>t.data,1,3),e.converter.learnDestroy(i.imageCacheType,function(e){e.imgElement&&e.imgElement.parentNode&&e.imgElement.parentNode.removeChild(e.imgElement),e.element&&e.element.parentNode&&e.element.parentNode.removeChild(e.element)})}static get imageCacheType(){return"htmlDrawer[image]"}static get canvasCacheType(){return"htmlDrawer[canvas]"}static isSupported(){return!0}getType(){return"html"}getSupportedDataFormats(){return[i.imageCacheType,i.canvasCacheType]}minimumOverlapRequired(e){return!0}_createDrawingElement(){return e.makeNeutralElement("div")}draw(e){const t=this;this._prepareNewFrame(),e.forEach(function(e){0!==e.opacity&&t._drawTiles(e)})}canRotate(){return!1}destroy(){super.destroy(),this.container.removeChild(this.canvas)}setImageSmoothingEnabled(){}_prepareNewFrame(){this.canvas.innerHTML=""}_drawTiles(e){const t=e.getTilesToDraw().map(e=>e.tile);if(0!==e.opacity&&(0!==t.length||e.placeholderFillStyle))for(let i=t.length-1;i>=0;i--){const n=t[i];this._drawTile(n),this.viewer&&this.viewer.raiseEvent("tile-drawn",{tiledImage:e,tile:n})}}_drawTile(t){e.console.assert(t,"[Drawer._drawTile] tile is required");let i=this.canvas;if(!t.loaded)return void e.console.warn("Attempting to draw tile %s when it's not yet loaded.",t.toString());const n=this.getDataToDraw(t);n&&(n.element.parentNode!==i&&i.appendChild(n.element),n.imgElement.parentNode!==n.element&&n.element.appendChild(n.imgElement),n.style.top=t.position.y+"px",n.style.left=t.position.x+"px",n.style.height=t.size.y+"px",n.style.width=t.size.x+"px",t.flipped&&(n.style.transform="scaleX(-1)"),e.setElementOpacity(n.element,t.opacity))}}e.HTMLDrawer=i}(OpenSeadragon),function(e){const t=e;class i extends t.DrawerBase{constructor(e){super(e),this.context=this.canvas.getContext("2d"),this.sketchCanvas=null,this.sketchContext=null,this._imageSmoothingEnabled=!0,this.viewer.allowEventHandler("tile-drawn"),this.viewer.allowEventHandler("tile-drawing")}static isSupported(){return e.supportsCanvas}getType(){return"canvas"}getSupportedDataFormats(){return["context2d"]}_createDrawingElement(){const t=e.makeNeutralElement("canvas"),i=this._calculateCanvasSize();return t.width=i.x,t.height=i.y,t}draw(e){this._prepareNewFrame(),this.viewer.viewport.getFlip()!==this._viewportFlipped&&this._flip();for(const t of e)0!==t.opacity&&this._drawTiles(t)}canRotate(){return!0}destroy(){super.destroy(),this.canvas.width=1,this.canvas.height=1,this.sketchCanvas=null,this.sketchContext=null,this.container.removeChild(this.canvas)}minimumOverlapRequired(e){return!0}setImageSmoothingEnabled(e){this._imageSmoothingEnabled=!!e,this._updateImageSmoothingEnabled(this.context),this.viewer.forceRedraw()}drawDebuggingRect(t){const i=this.context;i.save(),i.lineWidth=2*e.pixelDensityRatio,i.strokeStyle=this.debugGridColor[0],i.fillStyle=this.debugGridColor[0],i.strokeRect(t.x*e.pixelDensityRatio,t.y*e.pixelDensityRatio,t.width*e.pixelDensityRatio,t.height*e.pixelDensityRatio),i.restore()}get _viewportFlipped(){return this.context.getTransform().a<0}_raiseTileDrawingEvent(e,t,i,n){this.viewer.raiseEvent("tile-drawing",{tiledImage:e,context:t,tile:i,rendered:n})}_prepareNewFrame(){const e=this._calculateCanvasSize();if((this.canvas.width!==e.x||this.canvas.height!==e.y)&&(this.canvas.width=e.x,this.canvas.height=e.y,this._updateImageSmoothingEnabled(this.context),null!==this.sketchCanvas)){const e=this._calculateSketchCanvasSize();this.sketchCanvas.width=e.x,this.sketchCanvas.height=e.y,this._updateImageSmoothingEnabled(this.sketchContext)}this._clear()}_clear(e,t){const i=this._getContext(e);if(t)i.clearRect(t.x,t.y,t.width,t.height);else{const e=i.canvas;i.clearRect(0,0,e.width,e.height)}}_drawTiles(t){const i=t.getTilesToDraw().map(e=>e.tile);if(0===t.opacity||0===i.length&&!t.placeholderFillStyle)return;let r,a,l,h=i[0];h&&(r=t.opacity<1||t.compositeOperation&&"source-over"!==t.compositeOperation||!t._isBottomItem()&&t.source.hasTransparency(null,h.getUrl(),h.ajaxHeaders,h.postData));const c=this.viewport.getZoom(!0),u=t.viewportToImageZoom(c);if(i.length>1&&u>t.smoothTileEdgesMinZoom&&!t.iOSDevice&&t.getRotation(!0)%360==0){r=!0;const t=h.length&&this.getDataToDraw(h);a=t?t.canvas.width/(h.size.x*e.pixelDensityRatio):1,l=h.getTranslationForEdgeSmoothing(a,this._getCanvasSize(!1),this._getCanvasSize(!0))}let d;r&&(a||(d=this.viewport.viewportToViewerElementRectangle(t.getClippedBounds(!0)).getIntegerBoundingBox(),d=d.times(e.pixelDensityRatio)),this._clear(!0,d)),a||this._setRotations(t,r);let p=!1;if(t._clip){this._saveContext(r);let e=t.imageToViewportRectangle(t._clip,!0);e=e.rotate(-t.getRotation(!0),t._getRotationPoint(!0));let i=this.viewportToDrawerRectangle(e);a&&(i=i.times(a)),l&&(i=i.translate(l)),this._setClip(i,r),p=!0}if(t._croppingPolygons){const i=this;p||this._saveContext(r);try{const e=t._croppingPolygons.map(function(e){return e.map(function(e){const n=t.imageToViewportCoordinates(e.x,e.y,!0).rotate(-t.getRotation(!0),t._getRotationPoint(!0));let o=i.viewportCoordToDrawerCoord(n);return a&&(o=o.times(a)),l&&(o=o.plus(l)),o})});this._clipWithPolygons(e,r)}catch(t){e.console.error(t)}p=!0}if(t._hasOpaqueTile=!1,t.placeholderFillStyle&&!1===t._hasOpaqueTile){let e=this.viewportToDrawerRectangle(t.getBoundsNoRotate(!0));a&&(e=e.times(a)),l&&(e=e.translate(l));let i=null;i="function"==typeof t.placeholderFillStyle?t.placeholderFillStyle(t,this.context):t.placeholderFillStyle,this._drawRectangle(e,i,r)}const g=function(t){if("number"==typeof t)return s(t);if(!t||!e.Browser)return n;let i=t[e.Browser.vendor];o(i)&&(i=t["*"]);return s(i)}(t.subPixelRoundingForTransparency);let m=!1;g===e.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS?m=!0:g===e.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST&&(m=!(this.viewer&&this.viewer.isAnimating()));for(let e=0;e=0;n--){const o=i[n];try{this._drawDebugInfoOnTile(o,i.length,n,t)}catch(t){e.console.error(t)}}}_clipWithPolygons(e,t){const i=this._getContext(t);i.beginPath();for(const t of e)for(const[e,n]of t.entries())i[0===e?"moveTo":"lineTo"](n.x,n.y);i.clip()}_drawTile(t,i,n,o,s,r,a){if(e.console.assert(t,"[Drawer._drawTile] tile is required"),e.console.assert(i,"[Drawer._drawTile] drawingHandler is required"),!t.loaded)return void e.console.warn("Attempting to draw tile %s when it's not yet loaded.",t.toString());const l=this.getDataToDraw(t);if(!l)return;const h=this._getContext(n);o=o||1;let c,u,d=t.position.times(e.pixelDensityRatio),p=t.size.times(e.pixelDensityRatio);h.save(),"number"==typeof o&&1!==o&&(d=d.times(o),p=p.times(o)),s instanceof e.Point&&(d=d.plus(s)),1===h.globalAlpha&&t.hasTransparency&&(r&&(d.x=Math.round(d.x),d.y=Math.round(d.y),p.x=Math.round(p.x),p.y=Math.round(p.y)),h.clearRect(d.x,d.y,p.x,p.y)),this._raiseTileDrawingEvent(i,h,t,l),t.sourceBounds?(c=Math.min(t.sourceBounds.width,l.canvas.width),u=Math.min(t.sourceBounds.height,l.canvas.height)):(c=l.canvas.width,u=l.canvas.height),h.translate(d.x+p.x/2,0),t.flipped&&h.scale(-1,1),h.drawImage(l.canvas,0,0,c,u,-p.x/2,d.y,p.x,p.y),h.restore()}_getContext(e){let t=this.context;if(e){if(null===this.sketchCanvas){this.sketchCanvas=document.createElement("canvas");const e=this._calculateSketchCanvasSize();if(this.sketchCanvas.width=e.x,this.sketchCanvas.height=e.y,this.sketchContext=this.sketchCanvas.getContext("2d"),0===this.viewport.getRotation()){const e=this;this.viewer.addHandler("rotate",function t(){if(0===e.viewport.getRotation())return;e.viewer.removeHandler("rotate",t);const i=e._calculateSketchCanvasSize();e.sketchCanvas.width=i.x,e.sketchCanvas.height=i.y})}this._updateImageSmoothingEnabled(this.sketchContext)}t=this.sketchContext}return t}_saveContext(e){this._getContext(e).save()}_restoreContext(e){this._getContext(e).restore()}_setClip(e,t){const i=this._getContext(t);i.beginPath(),i.rect(e.x,e.y,e.width,e.height),i.clip()}_drawRectangle(e,t,i){const n=this._getContext(i);n.save(),n.fillStyle=t,n.fillRect(e.x,e.y,e.width,e.height),n.restore()}blendSketch(t,i,n,o){let s=t;e.isPlainObject(s)||(s={opacity:t,scale:i,translate:n,compositeOperation:o}),t=s.opacity,o=s.compositeOperation;const r=s.bounds;if(this.context.save(),this.context.globalAlpha=t,o&&(this.context.globalCompositeOperation=o),r)r.x<0&&(r.width+=r.x,r.x=0),r.x+r.width>this.canvas.width&&(r.width=this.canvas.width-r.x),r.y<0&&(r.height+=r.y,r.y=0),r.y+r.height>this.canvas.height&&(r.height=this.canvas.height-r.y),this.context.drawImage(this.sketchCanvas,r.x,r.y,r.width,r.height,r.x,r.y,r.width,r.height);else{i=s.scale||1;const t=(n=s.translate)instanceof e.Point?n:new e.Point(0,0);let o=0,r=0;if(n){const e=this.sketchCanvas.width-this.canvas.width,t=this.sketchCanvas.height-this.canvas.height;o=Math.round(e/2),r=Math.round(t/2)}this.context.drawImage(this.sketchCanvas,t.x-o*i,t.y-r*i,(this.canvas.width+2*o)*i,(this.canvas.height+2*r)*i,-o,-r,this.canvas.width+2*o,this.canvas.height+2*r)}this.context.restore()}_drawDebugInfoOnTile(t,i,n,o){const s=this.viewer.world.getIndexOfItem(o)%this.debugGridColor.length,r=this.context;r.save(),r.lineWidth=2*e.pixelDensityRatio,r.font="small-caps bold "+13*e.pixelDensityRatio+"px arial",r.strokeStyle=this.debugGridColor[s],r.fillStyle=this.debugGridColor[s],this._setRotations(o),this._viewportFlipped&&this._flip({point:t.position.plus(t.size.divide(2))}),r.strokeRect(t.position.x*e.pixelDensityRatio,t.position.y*e.pixelDensityRatio,t.size.x*e.pixelDensityRatio,t.size.y*e.pixelDensityRatio);const a=(t.position.x+t.size.x/2)*e.pixelDensityRatio,l=(t.position.y+t.size.y/2)*e.pixelDensityRatio;r.translate(a,l);const h=this.viewport.getRotation(!0);r.rotate(Math.PI/180*-h),r.translate(-a,-l),0===t.x&&0===t.y&&(r.fillText("Zoom: "+this.viewport.getZoom(),t.position.x*e.pixelDensityRatio,(t.position.y-30)*e.pixelDensityRatio),r.fillText("Pan: "+this.viewport.getBounds().toString(),t.position.x*e.pixelDensityRatio,(t.position.y-20)*e.pixelDensityRatio)),r.fillText("Level: "+t.level,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+20)*e.pixelDensityRatio),r.fillText("Column: "+t.x,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+30)*e.pixelDensityRatio),r.fillText("Row: "+t.y,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+40)*e.pixelDensityRatio),r.fillText("Order: "+n+" of "+i,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+50)*e.pixelDensityRatio),r.fillText("Size: "+t.size.toString(),(t.position.x+10)*e.pixelDensityRatio,(t.position.y+60)*e.pixelDensityRatio),r.fillText("Position: "+t.position.toString(),(t.position.x+10)*e.pixelDensityRatio,(t.position.y+70)*e.pixelDensityRatio),this.viewport.getRotation(!0)%360!=0&&this._restoreRotationChanges(),o.getRotation(!0)%360!=0&&this._restoreRotationChanges(),r.restore()}_updateImageSmoothingEnabled(e){e.msImageSmoothingEnabled=this._imageSmoothingEnabled,e.imageSmoothingEnabled=this._imageSmoothingEnabled}_getCanvasSize(t){const i=this._getContext(t).canvas;return new e.Point(i.width,i.height)}_getCanvasCenter(){return new e.Point(this.canvas.width/2,this.canvas.height/2)}_setRotations(e,t=!1){let i=!1;this.viewport.getRotation(!0)%360!=0&&(this._offsetForRotation({degrees:this.viewport.getRotation(!0),useSketch:t,saveContext:i}),i=!1),e.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:e.getRotation(!0),point:this.viewport.pixelFromPointNoRotate(e._getRotationPoint(!0),!0),useSketch:t,saveContext:i})}_offsetForRotation(t){const i=t.point?t.point.times(e.pixelDensityRatio):this._getCanvasCenter(),n=this._getContext(t.useSketch);n.save(),n.translate(i.x,i.y),n.rotate(Math.PI/180*t.degrees),n.translate(-i.x,-i.y)}_flip(t){const i=(t=t||{}).point?t.point.times(e.pixelDensityRatio):this._getCanvasCenter(),n=this._getContext(t.useSketch);n.translate(i.x,0),n.scale(-1,1),n.translate(-i.x,0)}_restoreRotationChanges(e){this._getContext(e).restore()}_calculateCanvasSize(){const t=e.pixelDensityRatio,i=this.viewport.getContainerSize();return{x:Math.round(i.x*t),y:Math.round(i.y*t)}}_calculateSketchCanvasSize(){const e=this._calculateCanvasSize();if(0===this.viewport.getRotation())return e;const t=Math.ceil(Math.sqrt(e.x*e.x+e.y*e.y));return{x:t,y:t}}}e.CanvasDrawer=i;const n=e.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;function o(t){return t!==e.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS&&t!==e.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST&&t!==e.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER}function s(e){return o(e)?n:e}}(OpenSeadragon),function(e){const t=e;class i{constructor(e){this._renderingCanvas=e.renderingCanvas,this._unpackWithPremultipliedAlpha=!!e.unpackWithPremultipliedAlpha,this._imageSmoothingEnabled=void 0===e.imageSmoothingEnabled||e.imageSmoothingEnabled,this._initShaderProgram=e.initShaderProgram,this._gl=null,this._isWebGL2=!1,this._extTextureFilterAnisotropic=null,this._maxAnisotropy=0,this._firstPass=null,this._secondPass=null,this._glFrameBuffer=null,this._renderToTexture=null,this._glNumTextures=0,this._unitQuad=null,this._destroyed=!1,this._gl=this._renderingCanvas.getContext("webgl2"),this._gl?(this._isWebGL2=!0,this._setupWebGLExtensions()):(this._gl=this._renderingCanvas.getContext("webgl"),this._isWebGL2=!1,this._gl&&this._setupWebGLExtensions()),this._gl&&this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,this._unpackWithPremultipliedAlpha)}getContext(){return this._gl}isWebGL2(){return this._isWebGL2}getMaxTextures(){return this._gl?this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS):0}getRenderingCanvas(){return this._renderingCanvas}getFirstPass(){return this._firstPass}getSecondPass(){return this._secondPass}getFrameBuffer(){return this._glFrameBuffer}getRenderToTexture(){return this._renderToTexture}getUnitQuad(){return this._unitQuad}_setupWebGLExtensions(){const e=this._gl;this._extTextureFilterAnisotropic=e.getExtension("EXT_texture_filter_anisotropic")||e.getExtension("WEBKIT_EXT_texture_filter_anisotropic")||e.getExtension("MOZ_EXT_texture_filter_anisotropic"),this._extTextureFilterAnisotropic&&(this._maxAnisotropy=e.getParameter(this._extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT))}getTextureFilter(){const e=this._gl;return this._imageSmoothingEnabled?e.LINEAR:e.NEAREST}_applyAnisotropy(){if(!this._imageSmoothingEnabled||!this._extTextureFilterAnisotropic||this._maxAnisotropy<=0)return;const e=this._gl;e.texParameterf(e.TEXTURE_2D,this._extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(4,this._maxAnisotropy))}setupRenderer(t,i){const n=this._gl;n?(this._unitQuad=this.makeQuadVertexBuffer(0,1,0,1),this._makeFirstPassShaderProgram(),this._makeSecondPassShaderProgram(),this._renderToTexture=n.createTexture(),n.activeTexture(n.TEXTURE0),n.bindTexture(n.TEXTURE_2D,this._renderToTexture),n.texImage2D(n.TEXTURE_2D,0,n.RGBA,t,i,0,n.RGBA,n.UNSIGNED_BYTE,null),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MIN_FILTER,this.getTextureFilter()),this._applyAnisotropy(),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_S,n.CLAMP_TO_EDGE),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_T,n.CLAMP_TO_EDGE),this._glFrameBuffer=n.createFramebuffer(),n.bindFramebuffer(n.FRAMEBUFFER,this._glFrameBuffer),n.framebufferTexture2D(n.FRAMEBUFFER,n.COLOR_ATTACHMENT0,n.TEXTURE_2D,this._renderToTexture,0),n.enable(n.BLEND),n.blendFunc(n.ONE,n.ONE_MINUS_SRC_ALPHA)):e.console.error("WebGL context not available for setupRenderer")}resizeRenderer(e,t){const i=this._gl;i&&(i.viewport(0,0,e,t),i.deleteTexture(this._renderToTexture),this._renderToTexture=i.createTexture(),i.activeTexture(i.TEXTURE0),i.bindTexture(i.TEXTURE_2D,this._renderToTexture),i.texImage2D(i.TEXTURE_2D,0,i.RGBA,e,t,0,i.RGBA,i.UNSIGNED_BYTE,null),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter()),this._applyAnisotropy(),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.bindFramebuffer(i.FRAMEBUFFER,this._glFrameBuffer),i.framebufferTexture2D(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,this._renderToTexture,0))}createTexture(e,t={}){const i=this._gl;if(!i)return null;const n=i.createTexture();i.activeTexture(i.TEXTURE0),i.bindTexture(i.TEXTURE_2D,n),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,this.getTextureFilter()),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,this.getTextureFilter()),this._applyAnisotropy();try{const o=void 0!==t.unpackWithPremultipliedAlpha?t.unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha;return i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,o),i.texImage2D(i.TEXTURE_2D,0,i.RGBA,i.RGBA,i.UNSIGNED_BYTE,e),n}catch(e){return i.deleteTexture(n),null}}deleteTexture(e){this._gl&&e&&this._gl.deleteTexture(e)}setImageSmoothingEnabled(e){this._imageSmoothingEnabled=!!e}setUnpackWithPremultipliedAlpha(e){this._unpackWithPremultipliedAlpha=!!e,this._gl&&this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,this._unpackWithPremultipliedAlpha)}makeQuadVertexBuffer(e,t,i,n){return new Float32Array([e,n,t,n,e,i,e,i,t,n,t,i])}_makeFirstPassShaderProgram(){const e=this._glNumTextures=this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS),t=`\n attribute vec2 a_output_position;\n attribute vec2 a_texture_position;\n attribute float a_index;\n\n ${[...Array(e).keys()].map(e=>`uniform mat3 u_matrix_${e};`).join("\n")} // create a uniform mat3 for each potential tile to draw\n\n varying vec2 v_texture_position;\n varying float v_image_index;\n\n void main() {\n\n mat3 transform_matrix; // value will be set by the if/elses in makeConditional()\n\n ${[...Array(e).keys()].map(e=>`${e>0?"else ":""}if(int(a_index) == ${e}) { transform_matrix = u_matrix_${e}; }`).join("\n")}\n\n gl_Position = vec4(transform_matrix * vec3(a_output_position, 1), 1);\n\n v_texture_position = a_texture_position;\n v_image_index = a_index;\n }\n `,i=`\n precision mediump float;\n\n // our textures\n uniform sampler2D u_images[${e}];\n // our opacities\n uniform float u_opacities[${e}];\n\n // the varyings passed in from the vertex shader.\n varying vec2 v_texture_position;\n varying float v_image_index;\n\n void main() {\n // can't index directly with a variable, need to use a loop iterator hack\n for(int i = 0; i < ${e}; ++i){\n if(i == int(v_image_index)){\n gl_FragColor = texture2D(u_images[i], v_texture_position) * u_opacities[i];\n }\n }\n }\n `,n=this._gl,o=this._initShaderProgram(n,t,i);n.useProgram(o),this._firstPass={shaderProgram:o,aOutputPosition:n.getAttribLocation(o,"a_output_position"),aTexturePosition:n.getAttribLocation(o,"a_texture_position"),aIndex:n.getAttribLocation(o,"a_index"),uTransformMatrices:[...Array(this._glNumTextures).keys()].map(e=>n.getUniformLocation(o,`u_matrix_${e}`)),uImages:n.getUniformLocation(o,"u_images"),uOpacities:n.getUniformLocation(o,"u_opacities"),bufferOutputPosition:n.createBuffer(),bufferTexturePosition:n.createBuffer(),bufferIndex:n.createBuffer()},n.uniform1iv(this._firstPass.uImages,[...Array(e).keys()]);const s=new Float32Array(12*e);for(let t=0;tArray(6).fill(e)).flat();n.bufferData(n.ARRAY_BUFFER,new Float32Array(r),n.STATIC_DRAW),n.enableVertexAttribArray(this._firstPass.aIndex)}_makeSecondPassShaderProgram(){const e=this._gl,t=this._initShaderProgram(e,"\n attribute vec2 a_output_position;\n attribute vec2 a_texture_position;\n\n varying vec2 v_texture_position;\n\n void main() {\n // Transform to clip space (0:1 --\x3e -1:1)\n gl_Position = vec4(vec3(a_output_position * 2.0 - 1.0, 1), 1);\n\n v_texture_position = a_texture_position;\n }\n ","\n precision mediump float;\n\n // our texture\n uniform sampler2D u_image;\n\n // the texCoords passed in from the vertex shader.\n varying vec2 v_texture_position;\n\n // the opacity multiplier for the image\n uniform float u_opacity_multiplier;\n\n void main() {\n gl_FragColor = texture2D(u_image, v_texture_position);\n gl_FragColor *= u_opacity_multiplier;\n }\n ");e.useProgram(t),this._secondPass={shaderProgram:t,aOutputPosition:e.getAttribLocation(t,"a_output_position"),aTexturePosition:e.getAttribLocation(t,"a_texture_position"),uImage:e.getUniformLocation(t,"u_image"),uOpacityMultiplier:e.getUniformLocation(t,"u_opacity_multiplier"),bufferOutputPosition:e.createBuffer(),bufferTexturePosition:e.createBuffer()},e.bindBuffer(e.ARRAY_BUFFER,this._secondPass.bufferOutputPosition),e.bufferData(e.ARRAY_BUFFER,this._unitQuad,e.STATIC_DRAW),e.enableVertexAttribArray(this._secondPass.aOutputPosition),e.bindBuffer(e.ARRAY_BUFFER,this._secondPass.bufferTexturePosition),e.bufferData(e.ARRAY_BUFFER,this._unitQuad,e.DYNAMIC_DRAW),e.enableVertexAttribArray(this._secondPass.aTexturePosition)}destroy(){if(this._destroyed)return;this._destroyed=!0;const t=this._gl;if(t){try{const e=t.getParameter(t.MAX_TEXTURE_IMAGE_UNITS);if(e&&e>0)for(let i=0;i0!==e)||(e.console.warn("[WebGLDrawer.isSupported] Functional test failed: no non-zero pixels read back."),!1)}catch(t){return e.console.warn("[WebGLDrawer.isSupported] Functional test failed:",t&&t.message?t.message:t),!1}finally{try{if(o&&t&&t.deleteTexture(o),t)t.destroy();else if(s){const e=s.getExtension("WEBGL_lose_context");e&&e.loseContext()}}catch(e){}}}getType(){return"webgl"}isWebGL2(){return!!this._glContext&&this._glContext.isWebGL2()}setContextRecoveryEnabled(e){this._enableContextRecovery=!!e}isContextRecoveryEnabled(){return this._enableContextRecovery}minimumOverlapRequired(e){return e.hasIssue("webgl")}_createDrawingElement(){const t=e.makeNeutralElement("canvas"),i=this._calculateCanvasSize();return t.width=i.x,t.height=i.y,t}_getBackupCanvasDrawer(){return this._backupCanvasDrawer||(this._backupCanvasDrawer=this.viewer.requestDrawer("canvas",{mainDrawer:!1}),this._backupCanvasDrawer.canvas.style.setProperty("visibility","hidden"),this._backupCanvasDrawer.getSupportedDataFormats=()=>this._supportedFormats,this._backupCanvasDrawer.getDataToDraw=this.getDataToDraw.bind(this)),this._backupCanvasDrawer}_draw(i,n=!1){const o=this._glContext?this._glContext.getContext():null;if(!o)return;const s=this._glContext.getFirstPass(),r=this._glContext.getSecondPass(),a=this._glContext.getFrameBuffer(),l=this._glContext.getRenderToTexture(),h=this.viewport.getBoundsNoRotateWithMargins(!0),c=h,u=new t.Point(h.x+h.width/2,h.y+h.height/2),d=this.viewport.getRotation(!0)*Math.PI/180,p=this.viewport.flipped?-1:1,g=e.Mat3.makeTranslation(-u.x,-u.y),m=e.Mat3.makeScaling(2/c.width*p,-2/c.height),f=e.Mat3.makeRotation(-d),v=m.multiply(f).multiply(g);o.bindFramebuffer(o.FRAMEBUFFER,null),o.clear(o.COLOR_BUFFER_BIT),this._outputContext.clearRect(0,0,this._outputCanvas.width,this._outputCanvas.height);let y=!1;i.forEach((t,i)=>{if(t.getIssue("webgl")){if(y&&(this._outputContext.drawImage(this._renderingCanvas,0,0),o.bindFramebuffer(o.FRAMEBUFFER,null),o.clear(o.COLOR_BUFFER_BIT),y=!1),this._canvasFallbackAllowed){const e=this._getBackupCanvasDrawer();e.draw([t]),this._outputContext.drawImage(e.canvas,0,0)}}else{const n=t.getTilesToDraw();if(t.placeholderFillStyle&&!1===t._hasOpaqueTile&&this._drawPlaceholder(t),0===n.length||0===t.getOpacity())return;const h=n[0],c=t.compositeOperation||this.viewer.compositeOperation||t._clip||t._croppingPolygons||t.debugMode,u=c||t.opacity<1||h.tile.hasTransparency;c&&(y&&this._outputContext.drawImage(this._renderingCanvas,0,0),o.bindFramebuffer(o.FRAMEBUFFER,null),o.clear(o.COLOR_BUFFER_BIT)),o.useProgram(s.shaderProgram),u?(o.bindFramebuffer(o.FRAMEBUFFER,a),o.clear(o.COLOR_BUFFER_BIT)):o.bindFramebuffer(o.FRAMEBUFFER,null);let d=v;const p=t.getRotation(!0);if(p%360!=0){const i=e.Mat3.makeRotation(-p*Math.PI/180),n=t.getBoundsNoRotate(!0).getCenter(),o=e.Mat3.makeTranslation(n.x,n.y),s=e.Mat3.makeTranslation(-n.x,-n.y),r=o.multiply(i).multiply(s);d=v.multiply(r)}const g=this._glContext.getMaxTextures();if(g<=0||null==g)throw new Error(`WebGL error: bad value for gl parameter MAX_TEXTURE_IMAGE_UNITS (${g}). This could happen\n if too many contexts have been created and not released, or there is another problem with the graphics card.`);const m=new Float32Array(12*g),f=new Array(g),w=new Array(g),_=new Array(g);for(let e=0;e{o.uniformMatrix3fv(s.uTransformMatrices[t],!1,e)}),o.uniform1fv(s.uOpacities,new Float32Array(_)),o.bindBuffer(o.ARRAY_BUFFER,s.bufferOutputPosition),o.vertexAttribPointer(s.aOutputPosition,2,o.FLOAT,!1,0,0),o.bindBuffer(o.ARRAY_BUFFER,s.bufferTexturePosition),o.vertexAttribPointer(s.aTexturePosition,2,o.FLOAT,!1,0,0),o.bindBuffer(o.ARRAY_BUFFER,s.bufferIndex),o.vertexAttribPointer(s.aIndex,1,o.FLOAT,!1,0,0),o.drawArrays(o.TRIANGLES,0,6*a)}}u&&(o.useProgram(r.shaderProgram),o.bindFramebuffer(o.FRAMEBUFFER,null),o.activeTexture(o.TEXTURE0),o.bindTexture(o.TEXTURE_2D,l),o.uniform1f(r.uOpacityMultiplier,t.opacity),o.bindBuffer(o.ARRAY_BUFFER,r.bufferTexturePosition),o.vertexAttribPointer(r.aTexturePosition,2,o.FLOAT,!1,0,0),o.bindBuffer(o.ARRAY_BUFFER,r.bufferOutputPosition),o.vertexAttribPointer(r.aOutputPosition,2,o.FLOAT,!1,0,0),o.drawArrays(o.TRIANGLES,0,6)),y=!0,c&&(this._applyContext2dPipeline(t,n,i),y=!1,o.bindFramebuffer(o.FRAMEBUFFER,null),o.clear(o.COLOR_BUFFER_BIT)),0===i&&this._raiseTiledImageDrawnEvent(t,n.map(e=>e.tile))}}),y&&this._outputContext.drawImage(this._renderingCanvas,0,0)}draw(t,i=!1){try{this._draw(t,i)}catch(n){if(!this._isWebGLContextError(n))throw n;if(this._enableContextRecovery&&!i){e.console.warn("WebGL context error detected during draw operation, attempting to recreate context...",n);this._recreateContext()?(e.console.info("WebGL context recreated successfully, retrying draw operation"),this.viewer&&this.viewer.raiseEvent("webgl-context-recovered",{drawer:this,error:n}),this.draw(t,!0)):this._fallbackToCanvasDrawer(n,t)}else{if(!this._enableContextRecovery)throw n;this._fallbackToCanvasDrawer(n,t)}}}setImageSmoothingEnabled(e){this._imageSmoothingEnabled!==e&&(this._imageSmoothingEnabled=e,this._glContext&&this._glContext.setImageSmoothingEnabled(e),this.setInternalCacheNeedsRefresh(),this.viewer.forceRedraw())}setUnpackWithPremultipliedAlpha(e){this._unpackWithPremultipliedAlpha!==e&&(this._unpackWithPremultipliedAlpha=e,this._glContext&&this._glContext.setUnpackWithPremultipliedAlpha(e),this.setInternalCacheNeedsRefresh(),this.viewer.forceRedraw())}drawDebuggingRect(t){const i=this._outputContext;i.save(),i.lineWidth=2*e.pixelDensityRatio,i.strokeStyle=this.debugGridColor[0],i.fillStyle=this.debugGridColor[0],i.strokeRect(t.x*e.pixelDensityRatio,t.y*e.pixelDensityRatio,t.width*e.pixelDensityRatio,t.height*e.pixelDensityRatio),i.restore()}_applyContext2dPipeline(e,t,i){if(this._outputContext.save(),this._outputContext.globalCompositeOperation=0===i?null:e.compositeOperation||this.viewer.compositeOperation,e._croppingPolygons||e._clip?(this._renderToClippingCanvas(e),this._outputContext.drawImage(this._clippingCanvas,0,0)):this._outputContext.drawImage(this._renderingCanvas,0,0),this._outputContext.restore(),e.debugMode){const i=this.viewer.viewport.getFlip();i&&this._flip(),this._drawDebugInfo(t,e,i),i&&this._flip()}}_getTileData(t,i,n,o,s,r,a,l,h){const c=n.texture,u=n.position,d=n.overlapFraction;r.set(u,12*s);const p=t.positionedBounds.width*d.x,g=t.positionedBounds.height*d.y,m=t.positionedBounds.x+(0===t.x?0:p),f=t.positionedBounds.y+(0===t.y?0:g),v=t.positionedBounds.x+t.positionedBounds.width-(t.isRightMost?0:p),y=t.positionedBounds.y+t.positionedBounds.height-(t.isBottomMost?0:g),w=new e.Mat3([v-m,0,0,0,y-f,0,m,f,1]);t.flipped&&w.scaleAndTranslateSelf(-1,1,1,0),w.scaleAndTranslateOtherSetSelf(o),h[s]=t.opacity,a[s]=c,l[s]=w.values}_setupRenderer(){this._glContext&&this._glContext.getContext()?this._glContext.setupRenderer(this._renderingCanvas.width,this._renderingCanvas.height):e.console.error("_setupCanvases must be called before _setupRenderer")}_resizeRenderer(){this._glContext&&this._glContext.resizeRenderer(this._renderingCanvas.width,this._renderingCanvas.height)}_setupCanvases(){const e=this;this._outputCanvas=this.canvas,this._outputContext=this._outputCanvas.getContext("2d"),this._renderingCanvas=document.createElement("canvas"),this._clippingCanvas=document.createElement("canvas"),this._clippingContext=this._clippingCanvas.getContext("2d"),this._renderingCanvas.width=this._clippingCanvas.width=this._outputCanvas.width,this._renderingCanvas.height=this._clippingCanvas.height=this._outputCanvas.height,this._glContext=new i({renderingCanvas:this._renderingCanvas,unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha,imageSmoothingEnabled:this._imageSmoothingEnabled,initShaderProgram:this.constructor.initShaderProgram}),this._resizeHandler=function(){e._outputCanvas!==e.viewer.drawer.canvas&&(e._outputCanvas.style.width=e.viewer.drawer.canvas.clientWidth+"px",e._outputCanvas.style.height=e.viewer.drawer.canvas.clientHeight+"px");const t=e._calculateCanvasSize();e._outputCanvas.width===t.x&&e._outputCanvas.height===t.y||(e._outputCanvas.width=t.x,e._outputCanvas.height=t.y),e._renderingCanvas.style.width=e._outputCanvas.clientWidth+"px",e._renderingCanvas.style.height=e._outputCanvas.clientHeight+"px",e._renderingCanvas.width=e._clippingCanvas.width=e._outputCanvas.width,e._renderingCanvas.height=e._clippingCanvas.height=e._outputCanvas.height,e._resizeRenderer()},this.viewer.addHandler("resize",this._resizeHandler)}_isWebGLContextError(e){if(!e||!e.message)return!1;const t=e.message.toLowerCase();return t.includes("max_texture_image_units")||t.includes("webgl")&&(t.includes("context")||t.includes("lost")||t.includes("invalid"))}_recreateContext(){if(this._destroyed)return null;try{const t=this._renderingCanvas,n=t.width,o=t.height,s=t.style.width,r=t.style.height;if(this.destroyInternalCache(),this._glContext&&(this._glContext.destroy(),this._glContext=null),this._renderingCanvas=document.createElement("canvas"),this._renderingCanvas.width=n,this._renderingCanvas.height=o,s&&(this._renderingCanvas.style.width=s),r&&(this._renderingCanvas.style.height=r),this._glContext=new i({renderingCanvas:this._renderingCanvas,unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha,imageSmoothingEnabled:this._imageSmoothingEnabled,initShaderProgram:this.constructor.initShaderProgram}),!this._glContext.getContext())return e.console.error("Failed to recreate WebGL context: no GL context"),null;try{const t=this._glContext.getMaxTextures();if(!t||t<=0)return e.console.error("Failed to recreate WebGL context: invalid MAX_TEXTURE_IMAGE_UNITS"),null}catch(t){return e.console.error("Failed to verify new WebGL context:",t),null}return this._setupRenderer(),this.setInternalCacheNeedsRefresh(),this}catch(t){return e.console.error("Failed to recreate WebGL context:",t),null}}_fallbackToCanvasDrawer(t,i){const n=this;if(!this._canvasFallbackAllowed)throw n._raiseContextRecoveryFailedEvent(t,null),t;const o=this.viewer.requestDrawer("canvas",{mainDrawer:!0,redrawImmediately:!1});if(!o)throw e.console.error("Failed to create canvas drawer as fallback"),n._raiseContextRecoveryFailedEvent(t,null),t;e.console.error("Failed to recreate WebGL context, switching to canvas drawer"),n._raiseContextRecoveryFailedEvent(t,o),this.viewer.world.requestInvalidate(!0)}_raiseContextRecoveryFailedEvent(e,t=null){this.viewer&&this.viewer.raiseEvent("webgl-context-recovery-failed",{drawer:this,canvasDrawer:t,error:e})}internalCacheCreate(t,i){const n=i.tiledImage;if(!(this._glContext?this._glContext.getContext():null))return e.console.error("WebGL context not available in internalCacheCreate"),{};let o,s,r=t.data,a=!1;if(r instanceof CanvasRenderingContext2D&&(r=r.canvas,a=!0),!n.getIssue("webgl"))if(a&&e.isCanvasTainted(r))n.setIssue("webgl","WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?"),this._raiseDrawerErrorEvent(n,this._canvasFallbackAllowed?"Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.":"Tainted data cannot be used by the WebGLDrawer, and canvas fallback is not enabled."),this.setInternalCacheNeedsRefresh();else{let e,t;i.sourceBounds?(e=Math.min(i.sourceBounds.width,r.width)/r.width,t=Math.min(i.sourceBounds.height,r.height)/r.height):(e=1,t=1);const a=n.source.tileOverlap,l=this._calculateOverlapFraction(i,n);if(a>0){const n=(0===i.x?0:l.x)*e,o=(0===i.y?0:l.y)*t,r=(i.isRightMost?1:1-l.x)*e,a=(i.isBottomMost?1:1-l.y)*t;s=this._glContext.makeQuadVertexBuffer(n,r,o,a)}else s=1===e&&1===t?this._glContext.getUnitQuad():this._glContext.makeQuadVertexBuffer(0,e,0,t);if(o=this._glContext.createTexture(r,{unpackWithPremultipliedAlpha:this._unpackWithPremultipliedAlpha}),o)return{texture:o,position:s,overlapFraction:l,glContext:this._glContext};{n.setIssue("webgl","Error creating texture in WebGL.");const e=this._canvasFallbackAllowed;this._raiseDrawerErrorEvent(n,e?"Unknown error when creating texture. Falling back to CanvasDrawer for this TiledImage.":"Cannot use WebGL for this TiledImage; canvas fallback is not enabled."),this.setInternalCacheNeedsRefresh()}}if(r instanceof Image){const e=document.createElement("canvas");e.width=r.width,e.height=r.height;const t=e.getContext("2d",{willReadFrequently:!0});t.drawImage(r,0,0),r=t}return r instanceof CanvasRenderingContext2D?r:(e.console.error("Unsupported data used for WebGL Drawer - probably a bug!"),{})}internalCacheFree(e){if(e&&e.texture){const t=e.glContext||this._glContext;if(t&&!t.isDestroyed())try{t.deleteTexture(e.texture)}catch(e){}e.texture=null,e.glContext=null}}_calculateOverlapFraction(e,t){const i=t.source.tileOverlap,n=e.sourceBounds.width,o=e.sourceBounds.height;return{x:i/(n+((0===e.x?0:i)+(e.isRightMost?0:i))),y:i/(o+((0===e.y?0:i)+(e.isBottomMost?0:i)))}}_setClip(){}_renderToClippingCanvas(t){if(this._clippingContext.clearRect(0,0,this._clippingCanvas.width,this._clippingCanvas.height),this._clippingContext.save(),this.viewer.viewport.getFlip()){const t=new e.Point(this.canvas.width/2,this.canvas.height/2);this._clippingContext.translate(t.x,0),this._clippingContext.scale(-1,1),this._clippingContext.translate(-t.x,0)}if(t._clip){const e=[{x:t._clip.x,y:t._clip.y},{x:t._clip.x+t._clip.width,y:t._clip.y},{x:t._clip.x+t._clip.width,y:t._clip.y+t._clip.height},{x:t._clip.x,y:t._clip.y+t._clip.height}].map(e=>{const i=t.imageToViewportCoordinates(e.x,e.y,!0).rotate(this.viewer.viewport.getRotation(!0),this.viewer.viewport.getCenter(!0));return this.viewportCoordToDrawerCoord(i)});this._clippingContext.beginPath(),e.forEach((e,t)=>{this._clippingContext[0===t?"moveTo":"lineTo"](e.x,e.y)}),this._clippingContext.clip(),this._setClip()}if(t._croppingPolygons){const e=t._croppingPolygons.map(e=>e.map(e=>{const i=t.imageToViewportCoordinates(e.x,e.y,!0).rotate(this.viewer.viewport.getRotation(!0),this.viewer.viewport.getCenter(!0));return this.viewportCoordToDrawerCoord(i)}));this._clippingContext.beginPath(),e.forEach(e=>{e.forEach((e,t)=>{this._clippingContext[0===t?"moveTo":"lineTo"](e.x,e.y)})}),this._clippingContext.clip()}if(this.viewer.viewport.getFlip()){const t=new e.Point(this.canvas.width/2,this.canvas.height/2);this._clippingContext.translate(t.x,0),this._clippingContext.scale(-1,1),this._clippingContext.translate(-t.x,0)}this._clippingContext.drawImage(this._renderingCanvas,0,0),this._clippingContext.restore()}_setRotations(e){let t=!1;this.viewport.getRotation(!0)%360!=0&&(this._offsetForRotation({degrees:this.viewport.getRotation(!0),saveContext:t}),t=!1),e.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:e.getRotation(!0),point:this.viewport.pixelFromPointNoRotate(e._getRotationPoint(!0),!0),saveContext:t})}_offsetForRotation(t){const i=t.point?t.point.times(e.pixelDensityRatio):this._getCanvasCenter(),n=this._outputContext;n.save(),n.translate(i.x,i.y),n.rotate(Math.PI/180*t.degrees),n.translate(-i.x,-i.y)}_flip(t){const i=(t=t||{}).point?t.point.times(e.pixelDensityRatio):this._getCanvasCenter(),n=this._outputContext;n.translate(i.x,0),n.scale(-1,1),n.translate(-i.x,0)}_drawDebugInfo(t,i,n){for(let o=t.length-1;o>=0;o--){const s=t[o].tile;try{this._drawDebugInfoOnTile(s,t.length,o,i,n)}catch(t){e.console.error(t)}}}_drawDebugInfoOnTile(t,i,n,o,s){const r=this.viewer.world.getIndexOfItem(o)%this.debugGridColor.length,a=this.context;a.save(),a.lineWidth=2*e.pixelDensityRatio,a.font="small-caps bold "+13*e.pixelDensityRatio+"px arial",a.strokeStyle=this.debugGridColor[r],a.fillStyle=this.debugGridColor[r],this._setRotations(o),s&&this._flip({point:t.position.plus(t.size.divide(2))}),a.strokeRect(t.position.x*e.pixelDensityRatio,t.position.y*e.pixelDensityRatio,t.size.x*e.pixelDensityRatio,t.size.y*e.pixelDensityRatio);const l=(t.position.x+t.size.x/2)*e.pixelDensityRatio,h=(t.position.y+t.size.y/2)*e.pixelDensityRatio;a.translate(l,h);const c=this.viewport.getRotation(!0);a.rotate(Math.PI/180*-c),a.translate(-l,-h),0===t.x&&0===t.y&&(a.fillText("Zoom: "+this.viewport.getZoom(),t.position.x*e.pixelDensityRatio,(t.position.y-30)*e.pixelDensityRatio),a.fillText("Pan: "+this.viewport.getBounds().toString(),t.position.x*e.pixelDensityRatio,(t.position.y-20)*e.pixelDensityRatio)),a.fillText("Level: "+t.level,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+20)*e.pixelDensityRatio),a.fillText("Column: "+t.x,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+30)*e.pixelDensityRatio),a.fillText("Row: "+t.y,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+40)*e.pixelDensityRatio),a.fillText("Order: "+n+" of "+i,(t.position.x+10)*e.pixelDensityRatio,(t.position.y+50)*e.pixelDensityRatio),a.fillText("Size: "+t.size.toString(),(t.position.x+10)*e.pixelDensityRatio,(t.position.y+60)*e.pixelDensityRatio),a.fillText("Position: "+t.position.toString(),(t.position.x+10)*e.pixelDensityRatio,(t.position.y+70)*e.pixelDensityRatio),this.viewport.getRotation(!0)%360!=0&&this._restoreRotationChanges(),o.getRotation(!0)%360!=0&&this._restoreRotationChanges(),a.restore()}_drawPlaceholder(e){const t=e.getBounds(!0),i=this.viewportToDrawerRectangle(e.getBounds(!0)),n=this._outputContext;let o;o="function"==typeof e.placeholderFillStyle?e.placeholderFillStyle(e,n):e.placeholderFillStyle,this._offsetForRotation({degrees:this.viewer.viewport.getRotation(!0)}),n.fillStyle=o,n.translate(i.x,i.y),n.rotate(Math.PI/180*t.degrees),n.translate(-i.x,-i.y),n.fillRect(i.x,i.y,i.width,i.height),this._restoreRotationChanges()}_getCanvasCenter(){return new e.Point(this.canvas.width/2,this.canvas.height/2)}_restoreRotationChanges(){this._outputContext.restore()}static initShaderProgram(t,i,n){function o(t,i,n){const o=t.createShader(i);return t.shaderSource(o,n),t.compileShader(o),t.getShaderParameter(o,t.COMPILE_STATUS)?o:(e.console.error(`An error occurred compiling the shaders: ${t.getShaderInfoLog(o)}`),t.deleteShader(o),null)}const s=o(t,t.VERTEX_SHADER,i),r=o(t,t.FRAGMENT_SHADER,n),a=t.createProgram();return t.attachShader(a,s),t.attachShader(a,r),t.linkProgram(a),t.getProgramParameter(a,t.LINK_STATUS)?a:(e.console.error(`Unable to initialize the shader program: ${t.getProgramInfoLog(a)}`),null)}}}(OpenSeadragon),function(e){e.Viewport=function(t){const i=arguments;i.length&&i[0]instanceof e.Point&&(t={containerSize:i[0],contentSize:i[1],config:i[2]}),t.config&&(e.extend(!0,t,t.config),delete t.config),this._margins=e.extend({left:0,top:0,right:0,bottom:0},t.margins||{}),delete t.margins,t.initialDegrees=t.degrees,delete t.degrees,e.extend(!0,this,{containerSize:null,contentSize:null,zoomPoint:null,rotationPivot:null,viewer:null,springStiffness:e.DEFAULT_SETTINGS.springStiffness,animationTime:e.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:e.DEFAULT_SETTINGS.minZoomImageRatio,maxZoomPixelRatio:e.DEFAULT_SETTINGS.maxZoomPixelRatio,visibilityRatio:e.DEFAULT_SETTINGS.visibilityRatio,wrapHorizontal:e.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:e.DEFAULT_SETTINGS.wrapVertical,defaultZoomLevel:e.DEFAULT_SETTINGS.defaultZoomLevel,minZoomLevel:e.DEFAULT_SETTINGS.minZoomLevel,maxZoomLevel:e.DEFAULT_SETTINGS.maxZoomLevel,initialDegrees:e.DEFAULT_SETTINGS.degrees,flipped:e.DEFAULT_SETTINGS.flipped,homeFillsViewer:e.DEFAULT_SETTINGS.homeFillsViewer,silenceMultiImageWarnings:e.DEFAULT_SETTINGS.silenceMultiImageWarnings},t),this._updateContainerInnerSize(),this.centerSpringX=new e.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime}),this.centerSpringY=new e.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime}),this.zoomSpring=new e.Spring({exponential:!0,initial:1,springStiffness:this.springStiffness,animationTime:this.animationTime}),this.degreesSpring=new e.Spring({initial:t.initialDegrees,springStiffness:this.springStiffness,animationTime:this.animationTime}),this._oldCenterX=this.centerSpringX.current.value,this._oldCenterY=this.centerSpringY.current.value,this._oldZoom=this.zoomSpring.current.value,this._oldDegrees=this.degreesSpring.current.value,this._sizeChanged=!1,this._setContentBounds(new e.Rect(0,0,1,1),1),this.goHome(!0),this.update()},e.Viewport.prototype={get degrees(){return e.console.warn("Accessing [Viewport.degrees] is deprecated. Use viewport.getRotation instead."),this.getRotation()},set degrees(t){e.console.warn("Setting [Viewport.degrees] is deprecated. Use viewport.rotateTo, viewport.rotateBy, or viewport.setRotation instead."),this.rotateTo(t)},resetContentSize:function(t){return e.console.assert(t,"[Viewport.resetContentSize] contentSize is required"),e.console.assert(t instanceof e.Point,"[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point"),e.console.assert(t.x>0,"[Viewport.resetContentSize] contentSize.x must be greater than 0"),e.console.assert(t.y>0,"[Viewport.resetContentSize] contentSize.y must be greater than 0"),this._setContentBounds(new e.Rect(0,0,1,t.y/t.x),t.x),this},setHomeBounds:function(t,i){e.console.error("[Viewport.setHomeBounds] this function is deprecated; The content bounds should not be set manually."),this._setContentBounds(t,i)},_setContentBounds:function(t,i){e.console.assert(t,"[Viewport._setContentBounds] bounds is required"),e.console.assert(t instanceof e.Rect,"[Viewport._setContentBounds] bounds must be an OpenSeadragon.Rect"),e.console.assert(t.width>0,"[Viewport._setContentBounds] bounds.width must be greater than 0"),e.console.assert(t.height>0,"[Viewport._setContentBounds] bounds.height must be greater than 0"),this._contentBoundsNoRotate=t.clone(),this._contentSizeNoRotate=this._contentBoundsNoRotate.getSize().times(i),this._contentBounds=t.rotate(this.getRotation()).getBoundingBox(),this._contentSize=this._contentBounds.getSize().times(i),this._contentAspectRatio=this._contentSize.x/this._contentSize.y,this.viewer&&this.viewer.raiseEvent("reset-size",{contentSize:this._contentSizeNoRotate.clone(),contentFactor:i,homeBounds:this._contentBoundsNoRotate.clone(),contentBounds:this._contentBounds.clone()})},getHomeZoom:function(){if(this.defaultZoomLevel)return this.defaultZoomLevel;const e=this._contentAspectRatio/this.getAspectRatio();let t;return t=this.homeFillsViewer?e>=1?e:1:e>=1?1:e,t/this._contentBounds.width},getHomeBounds:function(){return this.getHomeBoundsNoRotate().rotate(-this.getRotation())},getHomeBoundsNoRotate:function(){const t=this._contentBounds.getCenter(),i=1/this.getHomeZoom(),n=i/this.getAspectRatio();return new e.Rect(t.x-i/2,t.y-n/2,i,n)},goHome:function(e){return this.viewer&&this.viewer.raiseEvent("home",{immediately:e}),this.fitBounds(this.getHomeBounds(),e)},getMinZoom:function(){const e=this.getHomeZoom();return this.minZoomLevel?this.minZoomLevel:this.minZoomImageRatio*e},getMaxZoom:function(){let e=this.maxZoomLevel;return e||(e=this._contentSize.x*this.maxZoomPixelRatio/this._containerInnerSize.x,e/=this._contentBounds.width),Math.max(e,this.getHomeZoom())},getAspectRatio:function(){return this._containerInnerSize.x/this._containerInnerSize.y},getContainerSize:function(){return new e.Point(this.containerSize.x,this.containerSize.y)},getMargins:function(){return e.extend({},this._margins)},setMargins:function(t){e.console.assert("object"===e.type(t),"[Viewport.setMargins] margins must be an object"),this._margins=e.extend({left:0,top:0,right:0,bottom:0},t),this._updateContainerInnerSize(),this.viewer&&this.viewer.forceRedraw()},getBounds:function(e){return this.getBoundsNoRotate(e).rotate(-this.getRotation(e))},getBoundsNoRotate:function(t){const i=this.getCenter(t),n=1/this.getZoom(t),o=n/this.getAspectRatio();return new e.Rect(i.x-n/2,i.y-o/2,n,o)},getBoundsWithMargins:function(e){return this.getBoundsNoRotateWithMargins(e).rotate(-this.getRotation(e),this.getCenter(e))},getBoundsNoRotateWithMargins:function(e){const t=this.getBoundsNoRotate(e),i=this._containerInnerSize.x*this.getZoom(e);return t.x-=this._margins.left/i,t.y-=this._margins.top/i,t.width+=(this._margins.left+this._margins.right)/i,t.height+=(this._margins.top+this._margins.bottom)/i,t},getCenter:function(t){const i=new e.Point(this.centerSpringX.current.value,this.centerSpringY.current.value),n=new e.Point(this.centerSpringX.target.value,this.centerSpringY.target.value);if(t)return i;if(!this.zoomPoint)return n;const o=this.pixelFromPoint(this.zoomPoint,!0),s=this.getZoom(),r=1/s,a=r/this.getAspectRatio(),l=new e.Rect(i.x-r/2,i.y-a/2,r,a),h=this._pixelFromPoint(this.zoomPoint,l).minus(o).rotate(-this.getRotation(!0)).divide(this._containerInnerSize.x*s);return n.plus(h)},getZoom:function(e){return e?this.zoomSpring.current.value:this.zoomSpring.target.value},_applyZoomConstraints:function(e){return Math.max(Math.min(e,this.getMaxZoom()),this.getMinZoom())},_applyBoundaryConstraints:function(e){const t=this.viewportToViewerElementRectangle(e).getBoundingBox(),i=this.viewportToViewerElementRectangle(this._contentBoundsNoRotate).getBoundingBox();let n=!1,o=!1;if(this.wrapHorizontal);else{const e=t.x+t.width,o=i.x+i.width;let s,r,a;s=t.width>i.width?this.visibilityRatio*i.width:this.visibilityRatio*t.width,r=i.x-e+s,a=o-t.x-s,s>i.width?(t.x+=(r+a)/2,n=!0):a<0?(t.x+=a,n=!0):r>0&&(t.x+=r,n=!0)}if(this.wrapVertical);else{const e=t.y+t.height,n=i.y+i.height;let s,r,a;s=t.height>i.height?this.visibilityRatio*i.height:this.visibilityRatio*t.height,r=i.y-e+s,a=n-t.y-s,s>i.height?(t.y+=(r+a)/2,o=!0):a<0?(t.y+=a,o=!0):r>0&&(t.y+=r,o=!0)}const s=n||o,r=s?this.viewerElementToViewportRectangle(t):e.clone();return r.xConstrained=n,r.yConstrained=o,r.constraintApplied=s,r},_raiseConstraintsEvent:function(e){this.viewer&&this.viewer.raiseEvent("constrain",{immediately:e})},applyConstraints:function(e){const t=this.getZoom(),i=this._applyZoomConstraints(t);t!==i&&this.zoomTo(i,this.zoomPoint,e);const n=this.getConstrainedBounds(!1);return n.constraintApplied&&(this.fitBounds(n,e),this._raiseConstraintsEvent(e)),this},ensureVisible:function(e){return this.applyConstraints(e)},_fitBounds:function(t,i){const n=(i=i||{}).immediately||!1,o=i.constraints||!1,s=this.getAspectRatio(),r=t.getCenter(),a=new e.Rect(t.x,t.y,t.width,t.height,t.degrees+this.getRotation()).getBoundingBox();a.getAspectRatio()>=s?a.height=a.width/s:a.width=a.height*s,a.x=r.x-a.width/2,a.y=r.y-a.height/2;let l=1/a.width;if(console.log("New center:",r,"zoom:",l,"imm:",n),n)return this.panTo(r,!0),this.zoomTo(l,null,!0),o&&this.applyConstraints(!0),this;const h=this.getCenter(!0),c=this.getZoom(!0);this.panTo(h,!0),this.zoomTo(c,null,!0);const u=this.getBounds(),d=this.getZoom();if(0===d||Math.abs(l/d-1)<1e-8)return this.zoomTo(l,null,!0),this.panTo(r,n),o&&this.applyConstraints(!1),this;if(o){this.panTo(r,!1),l=this._applyZoomConstraints(l),this.zoomTo(l,null,!1);const e=this.getConstrainedBounds();this.panTo(h,!0),this.zoomTo(c,null,!0),this.fitBounds(e)}else{const e=a.rotate(-this.getRotation()).getTopLeft().times(l).minus(u.getTopLeft().times(d)).divide(l-d);this.zoomTo(l,e,n)}return this},fitBounds:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!1})},fitBoundsWithConstraints:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!0})},fitVertically:function(t){const i=new e.Rect(this._contentBounds.x+this._contentBounds.width/2,this._contentBounds.y,0,this._contentBounds.height);return this.fitBounds(i,t)},fitHorizontally:function(t){const i=new e.Rect(this._contentBounds.x,this._contentBounds.y+this._contentBounds.height/2,this._contentBounds.width,0);return this.fitBounds(i,t)},getConstrainedBounds:function(e){const t=this.getBounds(e);return this._applyBoundaryConstraints(t)},panBy:function(t,i){const n=new e.Point;return i?(n.x=this.centerSpringX.current.value,n.y=this.centerSpringY.current.value):(n.x=this.centerSpringX.target.value,n.y=this.centerSpringY.target.value),this.panTo(n.plus(t),i)},panTo:function(e,t){return t?(this.centerSpringX.resetTo(e.x),this.centerSpringY.resetTo(e.y)):(this.centerSpringX.springTo(e.x),this.centerSpringY.springTo(e.y)),this.viewer&&this.viewer.raiseEvent("pan",{center:e,immediately:t}),this},zoomBy:function(e,t,i){return this.zoomTo(this.zoomSpring.target.value*e,t,i)},zoomTo:function(t,i,n){const o=this;return this.zoomPoint=i instanceof e.Point&&!isNaN(i.x)&&!isNaN(i.y)?i:null,n?this._adjustCenterSpringsForZoomPoint(function(){o.zoomSpring.resetTo(t)}):this.zoomSpring.springTo(t),this.viewer&&this.viewer.raiseEvent("zoom",{zoom:t,refPoint:i,immediately:n}),this},setRotation:function(e,t){return this.rotateTo(e,null,t)},getRotation:function(e){return e?this.degreesSpring.current.value:this.degreesSpring.target.value},setRotationWithPivot:function(e,t,i){return this.rotateTo(e,t,i)},rotateTo:function(t,i,n){if(!this.viewer||!this.viewer.drawer.canRotate())return this;if(this.degreesSpring.target.value===t&&this.degreesSpring.isAtTargetValue())return this;if(this.rotationPivot=i instanceof e.Point&&!isNaN(i.x)&&!isNaN(i.y)?i:null,n)if(this.rotationPivot){if(!(t-this._oldDegrees))return this.rotationPivot=null,this;this._rotateAboutPivot(t)}else this.degreesSpring.resetTo(t);else{const i=e.positiveModulo(this.degreesSpring.current.value,360);let n=e.positiveModulo(t,360);const o=n-i;o>180?n-=360:o<-180&&(n+=360);const s=i-n;this.degreesSpring.resetTo(t+s),this.degreesSpring.springTo(t)}return this._setContentBounds(this.viewer.world.getHomeBounds(),this.viewer.world.getContentFactor()),this.viewer.forceRedraw(),this.viewer.raiseEvent("rotate",{degrees:t,immediately:!!n,pivot:this.rotationPivot||this.getCenter()}),this},rotateBy:function(e,t,i){return this.rotateTo(this.degreesSpring.target.value+e,t,i)},resize:function(e,t){const i=this.getBoundsNoRotate(),n=i;let o;this._sizeChanged=!this.containerSize.equals(e),this.containerSize.x=e.x,this.containerSize.y=e.y,this._updateContainerInnerSize(),t&&(o=e.x/this.containerSize.x,n.width=i.width*o,n.height=n.width/this.getAspectRatio()),this.viewer&&this.viewer.raiseEvent("resize",{newContainerSize:e,maintain:t});const s=this.fitBounds(n,!0);return this.viewer&&this.viewer.raiseEvent("after-resize",{newContainerSize:e,maintain:t}),s},_updateContainerInnerSize:function(){this._containerInnerSize=new e.Point(Math.max(1,this.containerSize.x-(this._margins.left+this._margins.right)),Math.max(1,this.containerSize.y-(this._margins.top+this._margins.bottom)))},update:function(){const e=this;this._adjustCenterSpringsForZoomPoint(function(){e.zoomSpring.update()}),this.degreesSpring.isAtTargetValue()&&(this.rotationPivot=null),this.centerSpringX.update(),this.centerSpringY.update(),this.rotationPivot?this._rotateAboutPivot(!0):this.degreesSpring.update();const t=this.centerSpringX.current.value!==this._oldCenterX||this.centerSpringY.current.value!==this._oldCenterY||this.zoomSpring.current.value!==this._oldZoom||this.degreesSpring.current.value!==this._oldDegrees||this._sizeChanged;this._sizeChanged=!1,this._oldCenterX=this.centerSpringX.current.value,this._oldCenterY=this.centerSpringY.current.value,this._oldZoom=this.zoomSpring.current.value,this._oldDegrees=this.degreesSpring.current.value;return t||!this.zoomSpring.isAtTargetValue()||!this.centerSpringX.isAtTargetValue()||!this.centerSpringY.isAtTargetValue()||!this.degreesSpring.isAtTargetValue()},_rotateAboutPivot:function(e){const t=!0===e,i=this.rotationPivot.minus(this.getCenter());this.centerSpringX.shiftBy(i.x),this.centerSpringY.shiftBy(i.y),t?this.degreesSpring.update():this.degreesSpring.resetTo(e);const n=this.degreesSpring.current.value-this._oldDegrees,o=i.rotate(-1*n).times(-1);this.centerSpringX.shiftBy(o.x),this.centerSpringY.shiftBy(o.y)},_adjustCenterSpringsForZoomPoint:function(e){if(this.zoomPoint){const t=this.pixelFromPoint(this.zoomPoint,!0);e();const i=this.pixelFromPoint(this.zoomPoint,!0).minus(t),n=this.deltaPointsFromPixels(i,!0);this.centerSpringX.shiftBy(n.x),this.centerSpringY.shiftBy(n.y),this.zoomSpring.isAtTargetValue()&&(this.zoomPoint=null)}else e()},deltaPixelsFromPointsNoRotate:function(e,t){return e.times(this._containerInnerSize.x*this.getZoom(t))},deltaPixelsFromPoints:function(e,t){return this.deltaPixelsFromPointsNoRotate(e.rotate(this.getRotation(t)),t)},deltaPointsFromPixelsNoRotate:function(e,t){return e.divide(this._containerInnerSize.x*this.getZoom(t))},deltaPointsFromPixels:function(e,t){return this.deltaPointsFromPixelsNoRotate(e,t).rotate(-this.getRotation(t))},pixelFromPointNoRotate:function(e,t){return this._pixelFromPointNoRotate(e,this.getBoundsNoRotate(t))},pixelFromPoint:function(e,t){return this._pixelFromPoint(e,this.getBoundsNoRotate(t))},_pixelFromPointNoRotate:function(t,i){return t.minus(i.getTopLeft()).times(this._containerInnerSize.x/i.width).plus(new e.Point(this._margins.left,this._margins.top))},_pixelFromPoint:function(e,t){return this._pixelFromPointNoRotate(e.rotate(this.getRotation(!0),this.getCenter(!0)),t)},pointFromPixelNoRotate:function(t,i){const n=this.getBoundsNoRotate(i);return t.minus(new e.Point(this._margins.left,this._margins.top)).divide(this._containerInnerSize.x/n.width).plus(n.getTopLeft())},pointFromPixel:function(e,t){return this.pointFromPixelNoRotate(e,t).rotate(-this.getRotation(t),this.getCenter(t))},_viewportToImageDelta:function(t,i){const n=this._contentBoundsNoRotate.width;return new e.Point(t*this._contentSizeNoRotate.x/n,i*this._contentSizeNoRotate.x/n)},viewportToImageCoordinates:function(t,i){if(t instanceof e.Point)return this.viewportToImageCoordinates(t.x,t.y);if(this.viewer){const n=this.viewer.world.getItemCount();if(n>1)this.silenceMultiImageWarnings||e.console.error("[Viewport.viewportToImageCoordinates] is not accurate with multi-image; use TiledImage.viewportToImageCoordinates instead.");else if(1===n){return this.viewer.world.getItemAt(0).viewportToImageCoordinates(t,i,!0)}}return this._viewportToImageDelta(t-this._contentBoundsNoRotate.x,i-this._contentBoundsNoRotate.y)},_imageToViewportDelta:function(t,i){const n=this._contentBoundsNoRotate.width;return new e.Point(t/this._contentSizeNoRotate.x*n,i/this._contentSizeNoRotate.x*n)},imageToViewportCoordinates:function(t,i){if(t instanceof e.Point)return this.imageToViewportCoordinates(t.x,t.y);if(this.viewer){const n=this.viewer.world.getItemCount();if(n>1)this.silenceMultiImageWarnings||e.console.error("[Viewport.imageToViewportCoordinates] is not accurate with multi-image; use TiledImage.imageToViewportCoordinates instead.");else if(1===n){return this.viewer.world.getItemAt(0).imageToViewportCoordinates(t,i,!0)}}const n=this._imageToViewportDelta(t,i);return n.x+=this._contentBoundsNoRotate.x,n.y+=this._contentBoundsNoRotate.y,n},imageToViewportRectangle:function(t,i,n,o){let s=t;if(s instanceof e.Rect||(s=new e.Rect(t,i,n,o)),this.viewer){const s=this.viewer.world.getItemCount();if(s>1)this.silenceMultiImageWarnings||e.console.error("[Viewport.imageToViewportRectangle] is not accurate with multi-image; use TiledImage.imageToViewportRectangle instead.");else if(1===s){return this.viewer.world.getItemAt(0).imageToViewportRectangle(t,i,n,o,!0)}}const r=this.imageToViewportCoordinates(s.x,s.y),a=this._imageToViewportDelta(s.width,s.height);return new e.Rect(r.x,r.y,a.x,a.y,s.degrees)},viewportToImageRectangle:function(t,i,n,o){let s=t;if(s instanceof e.Rect||(s=new e.Rect(t,i,n,o)),this.viewer){const s=this.viewer.world.getItemCount();if(s>1)this.silenceMultiImageWarnings||e.console.error("[Viewport.viewportToImageRectangle] is not accurate with multi-image; use TiledImage.viewportToImageRectangle instead.");else if(1===s){return this.viewer.world.getItemAt(0).viewportToImageRectangle(t,i,n,o,!0)}}const r=this.viewportToImageCoordinates(s.x,s.y),a=this._viewportToImageDelta(s.width,s.height);return new e.Rect(r.x,r.y,a.x,a.y,s.degrees)},viewerElementToImageCoordinates:function(e){const t=this.pointFromPixel(e,!0);return this.viewportToImageCoordinates(t)},imageToViewerElementCoordinates:function(e){const t=this.imageToViewportCoordinates(e);return this.pixelFromPoint(t,!0)},windowToImageCoordinates:function(t){e.console.assert(this.viewer,"[Viewport.windowToImageCoordinates] the viewport must have a viewer.");const i=t.minus(e.getElementPosition(this.viewer.container));return this.viewerElementToImageCoordinates(i)},imageToWindowCoordinates:function(t){e.console.assert(this.viewer,"[Viewport.imageToWindowCoordinates] the viewport must have a viewer.");return this.imageToViewerElementCoordinates(t).plus(e.getElementPosition(this.viewer.container))},viewerElementToViewportCoordinates:function(e){return this.pointFromPixel(e,!0)},viewportToViewerElementCoordinates:function(e){return this.pixelFromPoint(e,!0)},viewerElementToViewportRectangle:function(t){return e.Rect.fromSummits(this.pointFromPixel(t.getTopLeft(),!0),this.pointFromPixel(t.getTopRight(),!0),this.pointFromPixel(t.getBottomLeft(),!0))},viewportToViewerElementRectangle:function(t){return e.Rect.fromSummits(this.pixelFromPoint(t.getTopLeft(),!0),this.pixelFromPoint(t.getTopRight(),!0),this.pixelFromPoint(t.getBottomLeft(),!0))},windowToViewportCoordinates:function(t){e.console.assert(this.viewer,"[Viewport.windowToViewportCoordinates] the viewport must have a viewer.");const i=t.minus(e.getElementPosition(this.viewer.container));return this.viewerElementToViewportCoordinates(i)},viewportToWindowCoordinates:function(t){e.console.assert(this.viewer,"[Viewport.viewportToWindowCoordinates] the viewport must have a viewer.");return this.viewportToViewerElementCoordinates(t).plus(e.getElementPosition(this.viewer.container))},viewportToImageZoom:function(t){if(this.viewer){const i=this.viewer.world.getItemCount();if(i>1)this.silenceMultiImageWarnings||e.console.error("[Viewport.viewportToImageZoom] is not accurate with multi-image.");else if(1===i){return this.viewer.world.getItemAt(0).viewportToImageZoom(t)}}const i=this._contentSizeNoRotate.x;return t*(this._containerInnerSize.x/i*this._contentBoundsNoRotate.width)},imageToViewportZoom:function(t){if(this.viewer){const i=this.viewer.world.getItemCount();if(i>1)this.silenceMultiImageWarnings||e.console.error("[Viewport.imageToViewportZoom] is not accurate with multi-image. Instead, use [TiledImage.imageToViewportZoom] for the specific image of interest");else if(1===i){return this.viewer.world.getItemAt(0).imageToViewportZoom(t)}}return t*(this._contentSizeNoRotate.x/this._containerInnerSize.x/this._contentBoundsNoRotate.width)},toggleFlip:function(){return this.setFlip(!this.getFlip()),this},getFlip:function(){return this.flipped},setFlip:function(e){return this.flipped===e||(this.flipped=e,this.viewer.navigator&&this.viewer.navigator.setFlip(this.getFlip()),this.viewer.forceRedraw(),this.viewer.raiseEvent("flip",{flipped:e})),this},getMaxZoomPixelRatio:function(){return this.maxZoomPixelRatio},setMaxZoomPixelRatio:function(t,i=!0,n=!1){e.console.assert(!isNaN(t),"[Viewport.setMaxZoomPixelRatio] ratio must be a number"),isNaN(t)||(this.maxZoomPixelRatio=t,i&&this.getZoom()>this.getMaxZoom()&&this.applyConstraints(n))}}}(OpenSeadragon),function(e){e.TiledImage=function(t){this._initialized=!1,e.console.assert(t.tileCache,"[TiledImage] options.tileCache is required"),e.console.assert(t.drawer,"[TiledImage] options.drawer is required"),e.console.assert(t.viewer,"[TiledImage] options.viewer is required"),e.console.assert(t.imageLoader,"[TiledImage] options.imageLoader is required"),e.console.assert(t.source,"[TiledImage] options.source is required"),e.console.assert(!t.clip||t.clip instanceof e.Rect,"[TiledImage] options.clip must be an OpenSeadragon.Rect if present"),e.EventSource.call(this),this._optimalWorldIndex=void 0,this._tileCache=t.tileCache,delete t.tileCache,this._drawer=t.drawer,delete t.drawer,this._imageLoader=t.imageLoader,delete t.imageLoader,t.clip instanceof e.Rect&&(this._clip=t.clip.clone()),delete t.clip;const i=t.x||0;delete t.x;const n=t.y||0;delete t.y,this.normHeight=t.source.dimensions.y/t.source.dimensions.x,this.contentAspectX=t.source.dimensions.x/t.source.dimensions.y;let o=1;t.width?(o=t.width,delete t.width,t.height&&(e.console.error("specifying both width and height to a tiledImage is not supported"),delete t.height)):t.height&&(o=t.height/this.normHeight,delete t.height);const s=t.fitBounds;delete t.fitBounds;const r=t.fitBoundsPlacement||OpenSeadragon.Placement.CENTER;delete t.fitBoundsPlacement;const a=t.degrees||0;delete t.degrees;const l=t.ajaxHeaders;delete t.ajaxHeaders,this.crossOriginPolicy=t.crossOriginPolicy,delete t.crossOriginPolicy,e.extend(!0,this,{viewer:null,tilesMatrix:{},coverage:{},loadingCoverage:{},lastResetTime:0,_needsDraw:!0,_needsUpdate:!0,_hasOpaqueTile:!1,_tilesLoading:0,_zombieCache:!1,_tilesToDraw:[],_lastDrawn:[],_arrayCacheMap:[],_isBlending:!1,_wasBlending:!1,_issues:{},springStiffness:e.DEFAULT_SETTINGS.springStiffness,animationTime:e.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:e.DEFAULT_SETTINGS.minZoomImageRatio,wrapHorizontal:e.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:e.DEFAULT_SETTINGS.wrapVertical,immediateRender:e.DEFAULT_SETTINGS.immediateRender,loadDestinationTilesOnAnimation:e.DEFAULT_SETTINGS.loadDestinationTilesOnAnimation,blendTime:e.DEFAULT_SETTINGS.blendTime,alwaysBlend:e.DEFAULT_SETTINGS.alwaysBlend,minPixelRatio:e.DEFAULT_SETTINGS.minPixelRatio,smoothTileEdgesMinZoom:e.DEFAULT_SETTINGS.smoothTileEdgesMinZoom,iOSDevice:e.DEFAULT_SETTINGS.iOSDevice,debugMode:e.DEFAULT_SETTINGS.debugMode,ajaxWithCredentials:e.DEFAULT_SETTINGS.ajaxWithCredentials,placeholderFillStyle:e.DEFAULT_SETTINGS.placeholderFillStyle,opacity:e.DEFAULT_SETTINGS.opacity,preload:e.DEFAULT_SETTINGS.preload,compositeOperation:e.DEFAULT_SETTINGS.compositeOperation,subPixelRoundingForTransparency:e.DEFAULT_SETTINGS.subPixelRoundingForTransparency,maxTilesPerFrame:e.DEFAULT_SETTINGS.maxTilesPerFrame,originalDataType:void 0,_currentMaxTilesPerFrame:10*(t.maxTilesPerFrame||e.DEFAULT_SETTINGS.maxTilesPerFrame)},t),this._preload=this.preload,delete this.preload,this._fullyLoaded=!1,this._xSpring=new e.Spring({initial:i,springStiffness:this.springStiffness,animationTime:this.animationTime}),this._ySpring=new e.Spring({initial:n,springStiffness:this.springStiffness,animationTime:this.animationTime}),this._scaleSpring=new e.Spring({initial:o,springStiffness:this.springStiffness,animationTime:this.animationTime}),this._degreesSpring=new e.Spring({initial:a,springStiffness:this.springStiffness,animationTime:this.animationTime}),this._updateForScale(),s&&this.fitBounds(s,r,!0),this._ownAjaxHeaders={},this.setAjaxHeaders(l,!1),this._initialized=!0},e.extend(e.TiledImage.prototype,e.EventSource.prototype,{needsDraw:function(){return this._needsDraw},redraw:function(){this._needsDraw=!0},getFullyLoaded:function(){return this._fullyLoaded},whenFullyLoaded:function(e){this.getFullyLoaded()?setTimeout(e,1):this.addOnceHandler("fully-loaded-change",function(){e()})},_setFullyLoaded:function(e){e!==this._fullyLoaded&&(this._fullyLoaded=e,this.raiseEvent("fully-loaded-change",{fullyLoaded:this._fullyLoaded}))},requestInvalidate:function(t=!0,i=!1,n=e.now()){const o=i?this._lastDrawn.map(e=>e.tile):this._tileCache.getLoadedTilesFor(this);return this.viewer.world.requestTileInvalidateEvent(o,n,t)},reset:function(){this._tileCache.clearTilesFor(this),this._currentMaxTilesPerFrame=10*this.maxTilesPerFrame,this.lastResetTime=e.now(),this._needsDraw=!0,this._fullyLoaded=!1},update:function(e){const t=this._xSpring.update(),i=this._ySpring.update(),n=this._scaleSpring.update(),o=this._degreesSpring.update(),s=t||i||n||o||this._needsUpdate;if(s||e||!this._fullyLoaded){const e=this._updateLevelsForViewport();this._setFullyLoaded(e)}return this._needsUpdate=!1,!!s&&(this._updateForScale(),this._raiseBoundsChange(),this._needsDraw=!0,!0)},setDrawn:function(){return this._needsDraw=this._isBlending||this._wasBlending||this.opacity>0&&this._lastDrawn.length<1,this._needsDraw},get crossOriginPolicy(){return this._crossOriginPolicy},set crossOriginPolicy(t){this._crossOriginPolicy="string"==typeof t?t.toLowerCase():e.DEFAULT_SETTINGS.crossOriginPolicy},setIssue(t,i=void 0,n=void 0){const o=n?n.message||n:"";this._issues[t]=(i||`TiledImage is ${t}}`)+o,e.console.warn(this._issues[t],n)},getIssue(e){return this._issues[e]},hasIssue(e){return!!this.getIssue(e)},destroy:function(){this.reset(),this.source.destroy(this.viewer)},getBounds:function(e){return this.getBoundsNoRotate(e).rotate(this.getRotation(e),this._getRotationPoint(e))},getBoundsNoRotate:function(t){return t?new e.Rect(this._xSpring.current.value,this._ySpring.current.value,this._worldWidthCurrent,this._worldHeightCurrent):new e.Rect(this._xSpring.target.value,this._ySpring.target.value,this._worldWidthTarget,this._worldHeightTarget)},getWorldBounds:function(){return e.console.error("[TiledImage.getWorldBounds] is deprecated; use TiledImage.getBounds instead"),this.getBounds()},getClippedBounds:function(t){let i=this.getBoundsNoRotate(t);if(this._clip){const n=(t?this._worldWidthCurrent:this._worldWidthTarget)/this.source.dimensions.x,o=this._clip.times(n);i=new e.Rect(i.x+o.x,i.y+o.y,o.width,o.height)}return i.rotate(this.getRotation(t),this._getRotationPoint(t))},getTileBounds:function(e,t,i){const n=this.source.getNumTiles(e),o=(n.x+t%n.x)%n.x,s=(n.y+i%n.y)%n.y,r=this.source.getTileBounds(e,o,s);return this.getFlip()&&(r.x=Math.max(0,1-r.x-r.width)),r.x+=(t-o)/n.x,r.y+=this._worldHeightCurrent/this._worldWidthCurrent*((i-s)/n.y),r},getContentSize:function(){return new e.Point(this.source.dimensions.x,this.source.dimensions.y)},getSizeInWindowCoordinates:function(){const t=this.imageToWindowCoordinates(new e.Point(0,0)),i=this.imageToWindowCoordinates(this.getContentSize());return new e.Point(i.x-t.x,i.y-t.y)},get lastDrawn(){return this._lastDrawn},getDrawer:function(){return this.viewer.drawer},_viewportToImageDelta:function(t,i,n){const o=n?this._scaleSpring.current.value:this._scaleSpring.target.value;return new e.Point(t*(this.source.dimensions.x/o),i*(this.source.dimensions.y*this.contentAspectX/o))},viewportToImageCoordinates:function(t,i,n){let o;return t instanceof e.Point?(n=i,o=t):o=new e.Point(t,i),o=o.rotate(-this.getRotation(n),this._getRotationPoint(n)),n?this._viewportToImageDelta(o.x-this._xSpring.current.value,o.y-this._ySpring.current.value):this._viewportToImageDelta(o.x-this._xSpring.target.value,o.y-this._ySpring.target.value)},_imageToViewportDelta:function(t,i,n){const o=n?this._scaleSpring.current.value:this._scaleSpring.target.value;return new e.Point(t/this.source.dimensions.x*o,i/this.source.dimensions.y/this.contentAspectX*o)},imageToViewportCoordinates:function(t,i,n){t instanceof e.Point&&(n=i,i=t.y,t=t.x);const o=this._imageToViewportDelta(t,i,n);return n?(o.x+=this._xSpring.current.value,o.y+=this._ySpring.current.value):(o.x+=this._xSpring.target.value,o.y+=this._ySpring.target.value),o.rotate(this.getRotation(n),this._getRotationPoint(n))},imageToViewportRectangle:function(t,i,n,o,s){let r=t;r instanceof e.Rect?s=i:r=new e.Rect(t,i,n,o);const a=this.imageToViewportCoordinates(r.getTopLeft(),s),l=this._imageToViewportDelta(r.width,r.height,s);return new e.Rect(a.x,a.y,l.x,l.y,r.degrees+this.getRotation(s))},viewportToImageRectangle:function(t,i,n,o,s){let r=t;t instanceof e.Rect?s=i:r=new e.Rect(t,i,n,o);const a=this.viewportToImageCoordinates(r.getTopLeft(),s),l=this._viewportToImageDelta(r.width,r.height,s);return new e.Rect(a.x,a.y,l.x,l.y,r.degrees-this.getRotation(s))},viewerElementToImageCoordinates:function(e){const t=this.viewport.pointFromPixel(e,!0);return this.viewportToImageCoordinates(t)},imageToViewerElementCoordinates:function(e){const t=this.imageToViewportCoordinates(e);return this.viewport.pixelFromPoint(t,!0)},windowToImageCoordinates:function(e){const t=e.minus(OpenSeadragon.getElementPosition(this.viewer.element));return this.viewerElementToImageCoordinates(t)},imageToWindowCoordinates:function(e){return this.imageToViewerElementCoordinates(e).plus(OpenSeadragon.getElementPosition(this.viewer.element))},_viewportToTiledImageRectangle:function(t){const i=this._scaleSpring.current.value;return t=t.rotate(-this.getRotation(!0),this._getRotationPoint(!0)),new e.Rect((t.x-this._xSpring.current.value)/i,(t.y-this._ySpring.current.value)/i,t.width/i,t.height/i,t.degrees)},viewportToImageZoom:function(e){return this._scaleSpring.current.value*this.viewport._containerInnerSize.x/this.source.dimensions.x*e},imageToViewportZoom:function(e){return e/(this._scaleSpring.current.value*this.viewport._containerInnerSize.x/this.source.dimensions.x)},setPosition:function(e,t){const i=this._xSpring.target.value===e.x&&this._ySpring.target.value===e.y;if(t){if(i&&this._xSpring.current.value===e.x&&this._ySpring.current.value===e.y)return;this._xSpring.resetTo(e.x),this._ySpring.resetTo(e.y),this._needsDraw=!0,this._needsUpdate=!0}else{if(i)return;this._xSpring.springTo(e.x),this._ySpring.springTo(e.y),this._needsDraw=!0,this._needsUpdate=!0}i||this._raiseBoundsChange()},setWidth:function(e,t){this._setScale(e,t)},setHeight:function(e,t){this._setScale(e/this.normHeight,t)},setCroppingPolygons:function(t){const i=function(t){return t.map(function(t){try{if(function(t){return t instanceof e.Point||"number"==typeof t.x&&"number"==typeof t.y}(t))return{x:t.x,y:t.y};throw new Error}catch(e){throw new Error("A Provided cropping polygon point is not supported")}})};try{if(!e.isArray(t))throw new Error("Provided cropping polygon is not an array");this._croppingPolygons=t.map(function(e){return i(e)}),this._needsDraw=!0}catch(t){e.console.error("[TiledImage.setCroppingPolygons] Cropping polygon format not supported"),e.console.error(t),this.resetCroppingPolygons()}},resetCroppingPolygons:function(){this._croppingPolygons=null,this._needsDraw=!0},fitBounds:function(t,i,n){i=i||e.Placement.CENTER;const o=e.Placement.properties[i];let s=this.contentAspectX,r=0,a=0,l=1,h=1;if(this._clip&&(s=this._clip.getAspectRatio(),l=this._clip.width/this.source.dimensions.x,h=this._clip.height/this.source.dimensions.y,t.getAspectRatio()>s?(r=this._clip.x/this._clip.height*t.height,a=this._clip.y/this._clip.height*t.height):(r=this._clip.x/this._clip.width*t.width,a=this._clip.y/this._clip.width*t.width)),t.getAspectRatio()>s){const i=t.height/h;let l=0;o.isHorizontallyCentered?l=(t.width-t.height*s)/2:o.isRight&&(l=t.width-t.height*s),this.setPosition(new e.Point(t.x-r+l,t.y-a),n),this.setHeight(i,n)}else{const i=t.width/l;let h=0;o.isVerticallyCentered?h=(t.height-t.width/s)/2:o.isBottom&&(h=t.height-t.width/s),this.setPosition(new e.Point(t.x-r,t.y-a+h),n),this.setWidth(i,n)}},getClip:function(){return this._clip?this._clip.clone():null},setClip:function(t){e.console.assert(!t||t instanceof e.Rect,"[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"),t instanceof e.Rect?this._clip=t.clone():this._clip=null,this._needsUpdate=!0,this._needsDraw=!0,this.raiseEvent("clip-change")},getFlip:function(){return this.flipped},setFlip:function(e){this.flipped=e},get flipped(){return this._flipped},set flipped(e){const t=this._flipped!==!!e;this._flipped=!!e,t&&this._initialized&&(this.update(!0),this._needsDraw=!0,this._raiseBoundsChange())},get wrapHorizontal(){return this._wrapHorizontal},set wrapHorizontal(e){const t=this._wrapHorizontal!==!!e;this._wrapHorizontal=!!e,this._initialized&&t&&(this.update(!0),this._needsDraw=!0)},get wrapVertical(){return this._wrapVertical},set wrapVertical(e){const t=this._wrapVertical!==!!e;this._wrapVertical=!!e,this._initialized&&t&&(this.update(!0),this._needsDraw=!0)},get debugMode(){return this._debugMode},set debugMode(e){this._debugMode=!!e,this._needsDraw=!0},getOpacity:function(){return this.opacity},setOpacity:function(e){this.opacity=e},get opacity(){return this._opacity},set opacity(e){e!==this.opacity&&(this._opacity=e,this._needsDraw=!0,this._needsUpdate=!0,this.raiseEvent("opacity-change",{opacity:this.opacity}))},getPreload:function(){return this._preload},setPreload:function(e){this._preload=!!e,this._needsDraw=!0},getRotation:function(e){return e?this._degreesSpring.current.value:this._degreesSpring.target.value},setRotation:function(e,t){this._degreesSpring.target.value===e&&this._degreesSpring.isAtTargetValue()||(t?this._degreesSpring.resetTo(e):this._degreesSpring.springTo(e),this._needsDraw=!0,this._needsUpdate=!0,this._raiseBoundsChange())},getDrawArea:function(){if(0===this._opacity&&!this._preload)return!1;let e=this._viewportToTiledImageRectangle(this.viewport.getBoundsWithMargins(!0));if(!this.wrapHorizontal&&!this.wrapVertical){const t=this._viewportToTiledImageRectangle(this.getClippedBounds(!0));e=e.intersection(t)}return e},getLoadArea:function(){let e=this._viewportToTiledImageRectangle(this.viewport.getBoundsWithMargins(!1));if(!this.wrapHorizontal&&!this.wrapVertical){const t=this._viewportToTiledImageRectangle(this.getClippedBounds(!1));e=e.intersection(t)}return e},getTilesToDraw:function(){const e=this._lastDrawn;let t=0;for(const i of this._tilesToDraw)if(Array.isArray(i))for(const n of i)e[t++]=n;else i&&(e[t++]=i);e.length=t,this._updateTilesInViewport(e),t=0;for(const i of this._tilesToDraw)if(Array.isArray(i))for(const n of i)n.tile.loaded&&(n.tile.beingDrawn=!0,e[t++]=n);else i&&i.tile.loaded&&(i.tile.beingDrawn=!0,e[t++]=i);return e.length=t,e},_getRotationPoint:function(e){return this.getBoundsNoRotate(e).getCenter()},get compositeOperation(){return this._compositeOperation},set compositeOperation(e){e!==this._compositeOperation&&(this._compositeOperation=e,this._needsDraw=!0,this.raiseEvent("composite-operation-change",{compositeOperation:this._compositeOperation}))},getCompositeOperation:function(){return this._compositeOperation},setCompositeOperation:function(e){this.compositeOperation=e},setAjaxHeaders:function(t,i){null===t&&(t={}),e.isPlainObject(t)?(this._ownAjaxHeaders=t,this._updateAjaxHeaders(i)):e.console.error("[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object")},_updateAjaxHeaders:function(t){if(void 0===t&&(t=!0),e.isPlainObject(this.viewer.ajaxHeaders)?this.ajaxHeaders=e.extend({},this.viewer.ajaxHeaders,this._ownAjaxHeaders):this.ajaxHeaders=this._ownAjaxHeaders,t){let t,i,n,o;for(const s in this.tilesMatrix){t=this.source.getNumTiles(s);const r=this.tilesMatrix[s];for(const a in r){i=(t.x+a%t.x)%t.x;for(const l in r[a])if(n=(t.y+l%t.y)%t.y,o=r[a][l],o.loadWithAjax=this.loadTilesWithAjax,o.loadWithAjax){const t=this.source.getTileAjaxHeaders(s,i,n);o.ajaxHeaders=e.extend({},this.ajaxHeaders,t)}else o.ajaxHeaders=null}}for(let e=0;e=i;t--,e++)l[e]=t;for(let e=n+1;e<=this.source.maxLevel;e++){const t=this.tilesMatrix[e]&&this.tilesMatrix[e][0]&&this.tilesMatrix[e][0][0];if(t&&t.isBottomMost&&t.isRightMost&&t.loaded){l.push(e);break}}let h=!1;for(let e=0;e=this.minPixelRatio)h=!0;else if(!h)continue;const n=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(t),!1).x*this._scaleSpring.current.value,c=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(Math.max(this.source.getClosestLevel(),0)),!1).x*this._scaleSpring.current.value,u=this.immediateRender?1:c,d=Math.min(1,(i-.5)/.5),p=u/Math.abs(u-n),g=this._updateLevel(t,d,p,o,s,a,r);if(this.viewer.world.ensureTilesUpToDate(g.tilesToDraw),r=g.bestLoadTileCandidates,this._tilesToDraw[t]=g.tilesToDraw,this._providesCoverage(this.coverage,t))break}if(r&&r.length>0){for(const e of r)e&&this._loadTile(e,a);return this._needsDraw=!0,!1}return 0===this._tilesLoading},_updateTilesInViewport:function(t){const i=e.now(),n=this;this._tilesLoading=0,this._wasBlending=this._isBlending,this._isBlending=!1,this.loadingCoverage={};const o=t.length?t[0].level:0;if(!this.getDrawArea())return;let s=0;for(const e of t){const t=e.tile;if(t&&t.loaded){const s=n._blendTile(t,t.x,t.y,e.level,e.levelOpacity,i,o);n._isBlending=n._isBlending||s,n._needsDraw=n._needsDraw||s||n._wasBlending}this._providesCoverage(this.coverage,e.level)&&(s=Math.max(s,e.level))}if(s>0)for(const e in this._tilesToDraw)e{const p=this._getTile(n,a,e,s,h);if(u||(u=this._getCachedArray(e,l)),this.viewer&&this.viewer.raiseEvent("update-tile",{tiledImage:this,tile:p}),this._setCoverage(this.coverage,e,n,a,!1),p.exists&&(p.loaded&&(1===p.opacity&&this._setCoverage(this.coverage,e,n,a,!0),u[d++]={tile:p,level:e,levelOpacity:t,currentTime:s},this._setCoverage(this.loadingCoverage,e,n,a,!0)),this._positionTile(p,this.source.tileOverlap,this.viewport,c,i)),o&&!p.loaded){let t=p.loading||this._isCovered(this.loadingCoverage,e,n,a);if(this._setCoverage(this.loadingCoverage,e,n,a,t),!p.exists)return;!p.loading&&this._tryFindTileCacheRecord(p)&&(t=!0),p.loading?this._tilesLoading++:t||(r=this._compareTiles(r,p,this._currentMaxTilesPerFrame))}}),this._currentMaxTilesPerFrame>this.maxTilesPerFrame&&(this._currentMaxTilesPerFrame=Math.max(Math.ceil(this._currentMaxTilesPerFrame/2),this.maxTilesPerFrame)),u&&(u.length=d),{bestLoadTileCandidates:r,tilesToDraw:u||[]}},_visitTiles:function(e,t,i){const n=t.getBoundingBox(),o=this._getCornerTiles(e,n.getTopLeft(),n.getBottomRight()),s=o.topLeft,r=o.bottomRight,a=this.source.getNumTiles(e);this.getFlip()&&(r.x+=1,this.wrapHorizontal||(r.x=Math.min(r.x,a.x-1)));const l=Math.max(0,(r.x-s.x)*(r.y-s.y));for(let n=s.x;n<=r.x;n++)for(let o=s.y;o<=r.y;o++){let s;if(this.getFlip()){const e=(a.x+n%a.x)%a.x;s=n+a.x-e-e-1}else s=n;null!==t.intersection(this.getTileBounds(e,s,o))&&i(s,o,l)}},_positionTile:function(t,i,n,o,s){const r=t.bounds.getTopLeft();r.x*=this._scaleSpring.current.value,r.y*=this._scaleSpring.current.value,r.x+=this._xSpring.current.value,r.y+=this._ySpring.current.value;const a=t.bounds.getSize();a.x*=this._scaleSpring.current.value,a.y*=this._scaleSpring.current.value,t.positionedBounds.x=r.x,t.positionedBounds.y=r.y,t.positionedBounds.width=a.x,t.positionedBounds.height=a.y;const l=n.pixelFromPointNoRotate(r,!0),h=n.pixelFromPointNoRotate(r,!1);let c=n.deltaPixelsFromPointsNoRotate(a,!0);const u=n.deltaPixelsFromPointsNoRotate(a,!1),d=h.plus(u.divide(2)),p=o.squaredDistanceTo(d);this.getDrawer().minimumOverlapRequired(this)&&(i||(c=c.plus(new e.Point(1,1))),t.isRightMost&&this.wrapHorizontal&&(c.x+=.75),t.isBottomMost&&this.wrapVertical&&(c.y+=.75)),t.position=l,t.size=c,t.squaredDistance=p,t.visibility=s},_getCornerTiles:function(t,i,n){let o,s,r,a;this.wrapHorizontal?(o=e.positiveModulo(i.x,1),s=e.positiveModulo(n.x,1)):(o=Math.max(0,i.x),s=Math.min(1,n.x));const l=1/this.source.aspectRatio;this.wrapVertical?(r=e.positiveModulo(i.y,l),a=e.positiveModulo(n.y,l)):(r=Math.max(0,i.y),a=Math.min(l,n.y));const h=this.source.getTileAtPoint(t,new e.Point(o,r)),c=this.source.getTileAtPoint(t,new e.Point(s,a)),u=this.source.getNumTiles(t);return this.wrapHorizontal&&(h.x+=u.x*Math.floor(i.x),c.x+=u.x*Math.floor(n.x)),this.wrapVertical&&(h.y+=u.y*Math.floor(i.y/l),c.y+=u.y*Math.floor(n.y/l)),{topLeft:h,bottomRight:c}},_tryFindTileCacheRecord:function(e){const t=this._tileCache.getCacheRecord(e.originalCacheKey);return!!t&&(e.loading=!0,this._setTileLoaded(e,t.data,null,null,t.type),!0)},_getTile:function(t,i,n,o,s){let r,a,l,h,c,u,d,p,g,m=this.tilesMatrix,f=this.source,v=m[n];return v||(m[n]=v={}),v[t]||(v[t]={}),v[t][i]&&!v[t][i].flipped==!this.flipped?g=v[t][i]:(r=(s.x+t%s.x)%s.x,a=(s.y+i%s.y)%s.y,l=this.getTileBounds(n,t,i),h=f.getTileBounds(n,r,a,!0),c=f.tileExists(n,r,a),u=f.getTileUrl(n,r,a),d=f.getTilePostData(n,r,a),this.loadTilesWithAjax?(p=f.getTileAjaxHeaders(n,r,a),e.isPlainObject(this.ajaxHeaders)&&(p=e.extend({},this.ajaxHeaders,p))):p=null,g=new e.Tile(n,t,i,l,c,u,void 0,this.loadTilesWithAjax,p,h,d,f.getTileHashKey(n,r,a,u,p,d)),this.getFlip()?0===r&&(g.isRightMost=!0):r===s.x-1&&(g.isRightMost=!0),a===s.y-1&&(g.isBottomMost=!0),g.flipped=this.flipped,v[t][i]=g),g.lastTouchTime=o,g},_loadTile:function(e,t){const i=this;e.loading=!0,e.tiledImage=this,this._imageLoader.addJob({src:e.getUrl(),tile:e,source:this.source,postData:e.postData,loadWithAjax:e.loadWithAjax,ajaxHeaders:e.ajaxHeaders,crossOriginPolicy:this.crossOriginPolicy,ajaxWithCredentials:this.ajaxWithCredentials,callback:function(n,o,s,r,a){i._onTileLoad(e,t,n,o,s,r,a)},abort:function(){e.loading=!1}})||this.viewer.raiseEvent("job-queue-full",{tile:e,tiledImage:this,time:t})},_onTileLoad:function(t,i,n,o,s,r,a){if(null==n)return e.console.error("Tile %s failed to load: %s - error: %s",t,t.getUrl(),o),this.viewer.raiseEvent("tile-load-failed",{tile:t,tiledImage:this,time:i,message:o,tileRequest:s,tries:a,maxReached:0===this.viewer.tileRetryMax||a>=this.viewer.tileRetryMax}),t.loading=!1,void(t.exists=!1);if(t.exists=!0,i{this._setTileLoaded(t,e,null,s,o)}).catch(i=>{e.console.warn("Failed to satisfy original type [%s] %s from %s: %s",o,t,r,i),this._setTileLoaded(t,n,null,s,r)})}else e.console.warn("Ignoring default base tile data type %s: no conversion possible from %s",this.originalDataType,r),this._setTileLoaded(t,n,null,s,r)}else this._setTileLoaded(t,n,null,s,r)},_setTileLoaded:function(t,i,n,o,s){t.tiledImage=this,e.console.assert(void 0!==s,"TileSource::downloadTileStart must return a dataType.");let r=!1;t.addCache(t.cacheKey,()=>(r=!0,i),s,!1,!1);let a=null,l=0,h=!1;const c=this;function u(){l--,l>0||(h=!0,t.hasTransparency=t.hasTransparency||c.source.hasTransparency(void 0,t.getUrl(),t.ajaxHeaders,t.postData),t.loading=!1,t.loaded=!0,c.redraw(),a(t))}function d(){return h&&e.console.error("Event 'tile-loaded' argument getCompletionCallback must be called synchronously. Its return value should be called asynchronously."),l++,u}function p(){const n=d();c.viewer.raiseEventAwaiting("tile-loaded",{tile:t,tiledImage:c,tileRequest:o,promise:new e.Promise(e=>{a=e}),get image(){return e.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile-invalidated' event to modify data instead."),i},get data(){return e.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile-invalidated' event to modify data instead."),i},getCompletionCallback:function(){return e.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: use async event handlers instead, execution order is deducted by addHandler(...) priority argument."),d()}}).catch(()=>{e.console.error("[tile-loaded] event finished with failure: there might be a problem with a plugin you are using.")}).then(n)}if(r)this.viewer.world.requestTileInvalidateEvent([t],void 0,!1,!0,!0).then(p).catch(p);else{const i=t.getCache(t.originalCacheKey),n=t=>{if(this.viewer.isDestroyed())return e.Promise.resolve();const i=this.getDrawer();return t.isUsableForDrawer(i)?e.Promise.resolve():t.prepareForRendering(i)};for(const e of i._tiles){if(e.cacheKey!==t.cacheKey){const i=e.getCache();return void n(i).then(()=>t.setCache(e.cacheKey,i,!0,!1)).then(p)}if(e.processing)return void e.processingPromise.then(e=>{const i=e.getCache();n(i).then(()=>(t.setCache(e.cacheKey,i,!0,!1),i.loaded?null:i.await())).then(p)})}n(i).then(p)}},_compareTiles:function(e,t,i){if(!e)return[t];let n=!1;for(let i=0;i0){e.splice(i,0,t),n=!0;break}}return n||e.push(t),e.length>i&&e.pop(),e},_sortTilesComparator:function(e,t){return null===e?1:null===t?-1:e.visibility===t.visibility?e.squaredDistance-t.squaredDistance:t.visibility-e.visibility},_getCachedArray:function(e,t=void 0){let i=this._arrayCacheMap[e];return i?void 0!==t&&(i.length=t):i=this._arrayCacheMap[e]=void 0!==t?new Array(t):[],i},_providesCoverage:function(e,t,i,n){let o,s,r,a;if(!e[t])return!1;if(void 0===i||void 0===n){for(r in o=e[t],o)if(Object.prototype.hasOwnProperty.call(o,r))for(a in s=o[r],s)if(Object.prototype.hasOwnProperty.call(s,a)&&!s[a])return!1;return!0}return void 0===e[t][i]||void 0===e[t][i][n]||!0===e[t][i][n]},_isCovered:function(e,t,i,n){return void 0===i||void 0===n?this._providesCoverage(e,t+1):this._providesCoverage(e,t+1,2*i,2*n)&&this._providesCoverage(e,t+1,2*i,2*n+1)&&this._providesCoverage(e,t+1,2*i+1,2*n)&&this._providesCoverage(e,t+1,2*i+1,2*n+1)},_setCoverage:function(t,i,n,o,s){t[i]?(t[i][n]||(t[i][n]={}),t[i][n][o]=s):e.console.warn("Setting coverage for a tile before its level's coverage has been reset: %s",i)},_resetCoverage:function(e,t){e[t]={}}})}(OpenSeadragon),function(e){const t=e,i=Symbol("DRAWER_INTERNAL_CACHE");t.CacheRecord=class{constructor(){this.revive()}get data(){return this._data}get type(){return this._type}await(){return this._promise?this._promise:e.Promise.resolve(this._data)}getImage(){return e.console.error("[CacheRecord.getImage] options.image is deprecated. Moreover, it might not work correctly as the cache system performs conversion asynchronously in case the type needs to be converted."),this.transformTo("image"),this.data}getRenderedContext(){return e.console.error("[CacheRecord.getRenderedContext] options.getRenderedContext is deprecated. Moreover, it might not work correctly as the cache system performs conversion asynchronously in case the type needs to be converted."),this.transformTo("context2d"),this.data}setDataAs(t,i){if(e.console.assert(null!=t,"[CacheRecord.setDataAs] needs valid data to set!"),this._conversionJobQueue){let n=null;const o=new e.Promise((e,t)=>{n=e});return this._conversionJobQueue.push(()=>n(this._overwriteData(t,i))),o}return this._overwriteData(t,i)}getDataAs(t=void 0,i=!0){return this.loaded?t===this._type?i?e.converter.copy(this._tRef,this._data,t||this._type):this._promise:this._transformDataIfNeeded(this._tRef,this._data,t||this._type,i)||this._promise:this._promise.then(e=>this._transformDataIfNeeded(this._tRef,e,t||this._type,i)||e)}_transformDataIfNeeded(t,i,n,o){if(this._destroyed)return e.Promise.resolve();let s;return n!==this._type?s=e.converter.convert(t,i,this._type,n):o&&(s=e.converter.copy(t,i,n)),!!s&&s.then(t=>{if(!this._destroyed)return t;e.converter.destroy(t,n)}).catch(e=>{this._handleConversionError(e)})}getDataForRendering(t,i){if(this._destroyed)return e.console.error(`Attempt to draw tile with destroyed main cache ${this}!`),void i._unload();if(!this.loaded){if(this._promise)return;return void e.console.error(`Attempt to draw cache ${this} when not loaded!`)}if(this._destroyed)return e.console.error(`Attempt to draw tile with destroyed main cache ${this}!`),void i._unload();if(!t.getSupportedDataFormats().includes(this.type))return e.console.error(`Attempt to draw tile cache ${this} with unsupported type '${this.type}' for the target drawer!`),void this.prepareForRendering(t);if(t.options.usePrivateCache){if(!t.options.preloadCache)return this.prepareInternalCacheSync(t);const i=this._getInternalCacheRef(t);return i&&i.loaded?i:void e.console.error(`Attempt to draw tile cache ${this} with internal cache non-ready state!`)}return this}isUsableForDrawer(e){if(!e.getSupportedDataFormats().includes(this.type))return!1;if(e.options.usePrivateCache){if(!this._getInternalCacheRef(e))return!1}return!0}prepareForRendering(e){const t=e.getRequiredDataFormats();if(!this.loaded)return this.await().then(t=>this.prepareForRendering(e));let i;i=t.includes(this.type)?this.await():this.transformTo(t);const n=e=>e.catch(e=>(this._handleConversionError(e),null));return e.options.usePrivateCache&&e.options.preloadCache?n(i.then(t=>this.prepareInternalCacheAsync(e))):n(i)}prepareInternalCacheAsync(t){let n=this._getInternalCacheRef(t);if(this._checkInternalCacheUpToDate(n,t))return n.await();n&&!n.loaded&&n.await().then(()=>n.destroy()),e.console.assert(this._tRef,"Data Create called from invalidation routine needs tile reference!");const o=t.internalCacheCreate(this,this._tRef);e.console.assert(void 0!==o,"[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");const s=t.getId();return n=this[i][s]=new e.InternalCacheRecord(o,s,e=>t.internalCacheFree(e)),n.await()}prepareInternalCacheSync(t){let n=this._getInternalCacheRef(t);if(this._checkInternalCacheUpToDate(n,t))return n;n&&n.destroy(),e.console.assert(this._tRef,"Data Create called from drawing loop needs tile reference!");const o=t.internalCacheCreate(this,this._tRef);e.console.assert(void 0!==o,"[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");const s=t.getId();return n=this[i][s]=new e.InternalCacheRecord(o,s,e=>t.internalCacheFree(e)),n}_getInternalCacheRef(t){if(!t.options.usePrivateCache)return void e.console.error("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false.");let n=this[i];return n||(n=this[i]={}),n[t.getId()]}_checkInternalCacheUpToDate(e,t){return e&&e.tstamp>=t._dataNeedsRefresh}transformTo(t=this._type){if(!this.loaded){this._conversionJobQueue=this._conversionJobQueue||[];let i=null;const n=new e.Promise((e,t)=>{i=e});return this._conversionJobQueue.push(()=>{this._destroyed||("string"==typeof t&&t!==this._type||Array.isArray(t)&&!t.includes(this._type)?(this._convert(this._type,t),this._promise.then(e=>i(e))):this._promise.then(e=>(this._checkAwaitsConvert(),i(e))))}),n}return("string"==typeof t&&t!==this._type||Array.isArray(t)&&!t.includes(this._type))&&this._convert(this._type,t),this._promise}destroyInternalCache(e=void 0){const t=this[i];if(t)if(e){const i=t[e];i&&(i.destroy(),delete t[e])}else{for(const e in t)t[e].destroy();delete this[i]}}withTileReference(e){return this._tRef=e,this}toString(){const e=this._tRef||this._tiles.length&&this._tiles[0];return e?`Cache ${this.type} [used e.g. by ${e.toString()}]`:"Orphan cache!"}revive(){e.console.assert(!this.loaded&&!this._type,"[CacheRecord::revive] must not be called when loaded!"),this._tiles=[],this._data=null,this._type=null,this.loaded=!1,this._promise=null,this._destroyed=!1,this._ownerTileCache=null,this.cacheKey=null}destroy(){if(!this._destroyed)if(delete this._conversionJobQueue,this._destroyed=!0,this.loaded)this._destroySelfUnsafe(this._data,this._type);else if(this._promise){const t=this._type;this._promise.then(e=>this._destroySelfUnsafe(e,t)).catch(e.console.error)}}_destroySelfUnsafe(t,i){e.converter.destroy(t,i),this.destroyInternalCache(),this._destroyed&&(this.loaded=!1,this._tiles=null,this._data=null,this._type=null,this._tRef=null,this._promise=null)}addTile(t,i,n){if(!this._destroyed)if(e.console.assert(t,"[CacheRecord.addTile] tile is required"),null!=i&&this._tiles.length<1)"function"==typeof i&&(i=i()),this.type&&this._promise?i instanceof e.Promise?this._promise=i.then(e=>{this._overwriteData(e,n)}):this._overwriteData(i,n):(i instanceof e.Promise?(this._promise=i.then(t=>{if(!this._destroyed)return this.loaded=!0,this._data=t,t;try{e.converter.destroy(t,this._type)}catch(e){}}).catch(e=>{this._handleConversionError(e)}),this._data=null):(this._promise=e.Promise.resolve(i),this._data=i,this.loaded=!0),this._type=n),this._tiles.push(t);else{const i=this._tiles.includes(t);!i&&this.type&&this._promise?this._tiles.push(t):i||e.console.warn("Tile %s caching attempt without data argument on uninitialized cache entry!",t)}}removeTile(t){if(this._destroyed)return!1;for(let e=0;e{if(!this._conversionJobQueue||this._destroyed)return;const e=this._conversionJobQueue[0];this._conversionJobQueue.splice(0,1),0===this._conversionJobQueue.length&&delete this._conversionJobQueue,e()})}_triggerNeedsDraw(){this._tiles.length>0&&this._tiles[0].tiledImage.viewer.forceRedraw()}_overwriteData(t,n){if(this._destroyed)return e.converter.destroy(t,n),e.Promise.resolve();if(this.loaded){if(this._data===t&&this._type===n)return this._promise;e.converter.destroy(this._data,this._type),this._type=n,this._data=t,this._promise=e.Promise.resolve(t);const o=this[i];if(o)for(const e in o)o[e].setDataAs(t,n);return this._triggerNeedsDraw(),this._promise}return this._promise.then(()=>{if(this._data===t&&this._type===n)return this._data;e.converter.destroy(this._data,this._type),this._type=n,this._data=t,this._promise=e.Promise.resolve(t);const o=this[i];if(o)for(const e in o)o[e].setDataAs(t,n);return this._triggerNeedsDraw(),this._data})}_convert(t,i){const n=e.converter,o=n.getConversionPath(t,i);if(!o)return void e.console.error(`[CacheRecord._convert] Conversion ${t} ---\x3e ${i} cannot be done!`);const s=this._data,r=o.length,a=this,l=(t,i)=>{if(i>=r)return a._data=t,a.loaded=!0,a._checkAwaitsConvert(),e.Promise.resolve(t);const s=o[i];let h;try{h=s.transform(a._tRef,t)}catch(i){return n.destroy(t,s.origin.value),e.Promise.reject(`[CacheRecord._convert] sync failure (while converting using ${s.target.value}, ${s.origin.value})`)}if(void 0===h)return a.loaded=!1,n.destroy(t,s.origin.value),e.Promise.reject(`[CacheRecord._convert] data mid result undefined value (while converting using ${s.target.value}, ${s.origin.value})`);n.destroy(t,s.origin.value);return("promise"===e.type(h)?h:e.Promise.resolve(h)).then(e=>l(e,i+1))};this.loaded=!1,this._data=void 0,this._type=o[r-1].target.value,this._promise=l(s,0).catch(e=>{this._handleConversionError(e)})}_handleConversionError(t){if(e.console.error("[CacheRecord] Conversion/preparation error:",t),this._destroyed=!0,this.loaded=!1,this._data=null,!this.cacheKey||!this._ownerTileCache)return this._promise=e.Promise.resolve(void 0),this._tiles=[],void(this._tRef=null);this._ownerTileCache._handleBrokenCacheRecord(this)}},t.InternalCacheRecord=class{constructor(t,i,n){this.tstamp=e.now(),this._ondestroy=n,this._type=i,t instanceof e.Promise?(this._promise=t,t.then(e=>{this.loaded=!0,this._data=e})):(this._promise=null,this.loaded=!0,this._data=t)}get data(){return this._data}get type(){return this._type}await(){return this._promise?this._promise:e.Promise.resolve(this._data)}withTileReference(e){return this._temporaryTileRef=e,this}destroy(){this.loaded&&(this._ondestroy&&this._ondestroy(this._data),this._data=null,this.loaded=!1)}},t.TileCache=class{constructor(t){t=t||{},this._maxCacheItemCount=t.maxImageCacheCount||e.DEFAULT_SETTINGS.maxImageCacheCount,this._tilesLoaded=[],this._zombiesLoaded=[],this._zombiesLoadedCount=0,this._cachesLoaded=[],this._cachesLoadedCount=0}numTilesLoaded(){return this._tilesLoaded.length}numCachesLoaded(){return this._zombiesLoadedCount+this._cachesLoadedCount}cacheTile(t){e.console.assert(t,"[TileCache.cacheTile] options is required");const i=t.tile;e.console.assert(i,"[TileCache.cacheTile] options.tile is required"),e.console.assert(i.cacheKey,"[TileCache.cacheTile] options.tile.cacheKey is required"),t.image instanceof Image&&(e.console.warn("[TileCache.cacheTile] options.image is deprecated!"),t.data=t.image,t.dataType="image");const n=t.cacheKey||i.cacheKey;let o=this._cachesLoaded[n];if(!o)if(void 0===t.data&&(e.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute has been deprecated and will be removed in the future."),t.data=t.image),o=this._zombiesLoaded[n],o)o._destroyed?o.revive():("function"==typeof t.data&&t.data(),delete t.data),delete this._zombiesLoaded[n],this._zombiesLoadedCount--,this._cachesLoaded[n]=o,this._cachesLoadedCount++;else{const i=void 0!==t.data&&null!==t.data&&!1!==t.data;e.console.assert(i,"[TileCache.cacheTile] options.data is required to create an CacheRecord"),o=this._cachesLoaded[n]=new e.CacheRecord,this._cachesLoadedCount++}return t.dataType||(e.console.error("[TileCache.cacheTile] options.dataType is newly required. For easier use of the cache system, use the tile instance API."),"function"==typeof t.data&&e.console.error("[TileCache.cacheTile] options.dataType is mandatory when data item is a callback!"),t.dataType=e.converter.guessType(t.data)),o._ownerTileCache=this,o.cacheKey=n,o.addTile(i,t.data,t.dataType),this._freeOldRecordRoutine(i,t.cutoff||0),o}renameCache(t){const i=t.newCacheKey,n=t.oldCacheKey;let o=this._cachesLoaded[n];if(o){if(this._cachesLoaded[i])return e.console.error("Cannot rename cache %s to %s: the target cache is occupied!",n,i),null;this._cachesLoaded[i]=o,delete this._cachesLoaded[n]}else{if(o=this._zombiesLoaded[n],e.console.assert(o,"[TileCache.renameCache] oldCacheKey must reference existing cache!"),this._zombiesLoaded[i])return e.console.error("Cannot rename zombie cache %s to %s: the target cache is occupied!",n,i),null;this._zombiesLoaded[i]=o,delete this._zombiesLoaded[n]}o._ownerTileCache=this,o.cacheKey=i;for(const e of o._tiles)e.reflectCacheRenamed(n,i);return o}cloneCache(t){const i=t.tile,n=t.copyTargetKey,o=this._cachesLoaded[n]||this._zombiesLoaded[n];e.console.assert(o,"[TileCache.cloneCache] attempt to clone non-existent cache %s!",n),e.console.assert(!this._cachesLoaded[t.newCacheKey],"[TileCache.cloneCache] attempt to copy clone to existing cache %s!",t.newCacheKey);const s=t.desiredType||void 0;return o.getDataAs(s,!0).then(n=>{const s=this._cachesLoaded[t.newCacheKey]=new e.CacheRecord;return s.addTile(i,n,o.type),this._cachesLoadedCount++,this._freeOldRecordRoutine(i,t.cutoff||0),s})}injectCache(t){const i=t.targetKey,n=t.tile;if(!t.tileAllowNotLoaded&&!n.loaded&&!n.loading)return void e.console.warn("Attempt to inject cache on tile in invalid state: this is probably a bug!");const o=this._cachesLoaded[i];if(o){const e=[...o._tiles];for(const t of e)this.unloadCacheForTile(t,i,!0,!1)}this._cachesLoaded[i]&&e.console.error("The inject routine should've freed cache!");const s=t.cache;this._cachesLoaded[i]=s,s._ownerTileCache=this,s.cacheKey=i;for(const e of n.getCache(n.originalCacheKey)._tiles)e.setCache(i,s,t.setAsMainCache,!1)}replaceCache(t){const i=t.victimKey,n=t.consumerKey,o=this._cachesLoaded[i],s=t.tile;if(!o||!t.tileAllowNotLoaded&&!s.loaded&&!s.loading)return void e.console.warn("Attempt to consume cache on tile in invalid state: this is probably a bug!");const r=this._cachesLoaded[n];if(r){const e=[...r._tiles];for(const t of e)this.unloadCacheForTile(t,n,!0,!1)}this._cachesLoaded[n]&&e.console.error("The consume routine should've freed cache!");const a=this.renameCache({oldCacheKey:i,newCacheKey:n});if(a)for(const e of s.getCache(s.originalCacheKey)._tiles)e.setCache(n,a,t.setAsMainCache,!1)}restoreTilesThatShareOriginalCache(e,t,i){for(const e of t._tiles)e.cacheKey!==e.originalCacheKey&&(this.unloadCacheForTile(e,e.cacheKey,i,!0),delete e._caches[e.cacheKey],e.cacheKey=e.originalCacheKey)}_freeOldRecordRoutine(e,t){let i=this._tilesLoaded.length,n=-1;if(this._cachesLoadedCount+this._zombiesLoadedCount>this._maxCacheItemCount)if(this._zombiesLoadedCount>0)for(const e in this._zombiesLoaded){this._zombiesLoaded[e].destroy(),delete this._zombiesLoaded[e],this._zombiesLoadedCount--;break}else{let e,o,s,r,a,l=null;for(let i=this._tilesLoaded.length-1;i>=0;i--)e=this._tilesLoaded[i],e.level<=t||e.beingDrawn||e.loading||e.processing||(l?(r=e.lastTouchTime,o=l.lastTouchTime,a=e.level,s=l.level,(rs)&&(l=e,n=i)):(l=e,n=i));l&&n>=0&&(this._unloadTile(l,!0),i=n)}0===e.getCacheSize()?this._tilesLoaded[i]=e:n>=0&&this._tilesLoaded.splice(i,1)}_handleBrokenCacheRecord(t){if(!t)return;const i=t.cacheKey;i&&this._cachesLoaded[i]===t&&(delete this._cachesLoaded[i],this._cachesLoadedCount--),i&&this._zombiesLoaded[i]===t&&(delete this._zombiesLoaded[i],this._zombiesLoadedCount--);const n=t._tiles?[...t._tiles]:[];for(const e of n){const n=e.getCache&&e.getCache()===t,o=i&&e.originalCacheKey===i;n||o?(e.exists=!1,e.unload(!0)):(e.removeCache&&i&&e.removeCache(i),t.removeTile(e))}t._promise=e.Promise.resolve(void 0),t._tiles=[],t._tRef=null,t._ownerTileCache=null}clearTilesFor(t){let i;e.console.assert(t,"[TileCache.clearTilesFor] tiledImage is required");let n=this._cachesLoadedCount+this._zombiesLoadedCount>this._maxCacheItemCount;if(t._zombieCache&&n&&this._zombiesLoadedCount>0){for(const e in this._zombiesLoaded)this._zombiesLoaded[e].destroy(),delete this._zombiesLoaded[e];this._zombiesLoadedCount=0,n=this._cachesLoadedCount>this._maxCacheItemCount}for(let e=this._tilesLoaded.length-1;e>=0;e--)i=this._tilesLoaded[e],i.tiledImage===t&&(i.loaded?i.tiledImage===t&&this._unloadTile(i,!t._zombieCache||n,e):this._tilesLoaded.splice(e,1))}clear(e=!0){for(const e in this._zombiesLoaded)this._zombiesLoaded[e].destroy();for(const e in this._tilesLoaded)this._unloadTile(e,!0);this._tilesLoaded=[],this._zombiesLoaded=[],this._zombiesLoadedCount=0,this._cachesLoaded=[],this._cachesLoadedCount=0}clearDrawerInternalCache(e){const t=e.getId();for(const e of this._zombiesLoaded)e&&e.destroyInternalCache(t);for(const e of this._cachesLoaded)e&&e.destroyInternalCache(t)}getLoadedTilesFor(e){return e?this._tilesLoaded.filter(t=>t.tiledImage===e):[...this._tilesLoaded]}getCacheRecord(t){return e.console.assert(t,"[TileCache.getCacheRecord] cacheKey is required"),this._cachesLoaded[t]||this._zombiesLoaded[t]}safeUnloadCache(t){if(t&&!t._destroyed&&t.getTileCount()<1){for(const e in this._zombiesLoaded){const i=this._zombiesLoaded[e];if(i===t)return delete this._zombiesLoaded[e],void i.destroy()}e.console.error("Attempt to delete an orphan cache that is not in zombie list: this could be a bug!",t),t.destroy()}}unloadCacheForTile(t,i,n,o){const s=this._cachesLoaded[i];return s?s.removeTile(t)?(s.getTileCount()||(n?s.destroy():(this._zombiesLoaded[i]=s,this._zombiesLoadedCount++),delete this._cachesLoaded[i],this._cachesLoadedCount--),!0):(e.console.error("[TileCache.unloadCacheForTile] System tried to delete tile from cache it does not belong to! This could mean a bug in the cache system."),!1):(o||e.console.warn("[TileCache.unloadCacheForTile] Attempting to delete missing cache!"),!1)}unloadTile(t,i=!1){if(!t.loaded)return void e.console.warn("Attempt to unload already unloaded tile.");const n=this._tilesLoaded.findIndex(e=>e===t);this._unloadTile(t,i,n)}_unloadTile(t,i,n=void 0){e.console.assert(t,"[TileCache._unloadTile] tile is required");for(const e in t._caches)this.unloadCacheForTile(t,e,i,!1);if(void 0!==n&&this._tilesLoaded.splice(n,1),!t.loaded)return;const o=t.tiledImage;t._unload(),o.viewer.raiseEvent("tile-unloaded",{tile:t,tiledImage:o,destroyed:i})}}}(OpenSeadragon),function(e){e.World=function(t){const i=this;e.console.assert(t.viewer,"[World] options.viewer is required"),e.EventSource.call(this),this.viewer=t.viewer,this._items=[],this._needsDraw=!1,this.__invalidatedAt=1,this._autoRefigureSizes=!0,this._needsSizesFigured=!1,this._delegatedFigureSizes=function(e){i._autoRefigureSizes?i._figureSizes():i._needsSizesFigured=!0},this._figureSizes()},e.extend(e.World.prototype,e.EventSource.prototype,{addItem:function(t,i){if(e.console.assert(t,"[World.addItem] item is required"),e.console.assert(t instanceof e.TiledImage,"[World.addItem] only TiledImages supported at this time"),void 0!==(i=i||{}).index){const e=Math.max(0,Math.min(this._items.length,i.index));this._items.splice(e,0,t)}else this._items.push(t);this._autoRefigureSizes?this._figureSizes():this._needsSizesFigured=!0,this._needsDraw=!0,t.addHandler("bounds-change",this._delegatedFigureSizes),t.addHandler("clip-change",this._delegatedFigureSizes),this.raiseEvent("add-item",{item:t})},getItemAt:function(t){return e.console.assert(void 0!==t,"[World.getItemAt] index is required"),this._items[t]},getIndexOfItem:function(t){return e.console.assert(t,"[World.getIndexOfItem] item is required"),e.indexOf(this._items,t)},getItemCount:function(){return this._items.length},setItemIndex:function(t,i){e.console.assert(t,"[World.setItemIndex] item is required"),e.console.assert(void 0!==i,"[World.setItemIndex] index is required");const n=this.getIndexOfItem(t);if(i>=this._items.length)throw new Error("Index bigger than number of layers.");this._items.splice(n,1),this._items.splice(i,0,t),this._needsDraw=!0,this.raiseEvent("item-index-change",{item:t,previousIndex:n,newIndex:i})},removeItem:function(t){e.console.assert(t,"[World.removeItem] item is required");const i=e.indexOf(this._items,t);-1!==i&&(t.removeHandler("bounds-change",this._delegatedFigureSizes),t.removeHandler("clip-change",this._delegatedFigureSizes),t.destroy(),this._items.splice(i,1),this._figureSizes(),this._needsDraw=!0,this._raiseRemoveItem(t))},removeAll:function(){let e;this.viewer._cancelPendingImages();for(let t=0;t=n,h=t.level<=(t.tiledImage.source.getClosestLevel()||0);i||h?s[r++]=t:(l._unloadTile(t,!1,e-a),a++)}return s.length=r,this.requestTileInvalidateEvent(s,i,t)},requestTileInvalidateEvent:function(t,i,n=!0,o=!1,s=!1){if(!this.viewer.isOpen())return e.Promise.resolve();void 0===i&&(i=this.__invalidatedAt);const r=[],a=t.map(t=>{if(!t||!o&&!t.loaded&&!t.processing)return Promise.resolve();const a=t.tiledImage,l=a.getDrawer(),h=l._parentViewer||this.viewer,c=t.getCache(t.originalCacheKey),u=t.getCache(t.originalCacheKey);if(u.__invStamp&&u.__invStamp>=i)return Promise.resolve();let d,p=!1;c.__finishProcessing&&c.__finishProcessing(!0),c.__resolve||(d=new e.Promise(e=>{c.__resolve=e})),c.__finishProcessing=e=>{p=p||e,t.processing=!1,c.__finishProcessing=null,e||(c.__resolve(t),c.__resolve=null)};for(const e of c._tiles)e.processing=i,d&&(e.processingPromise=d);c.__invStamp=i,c.__wasRestored=n;let g=null;const m=()=>{if(g){const e=t.buildDistinctMainCacheKey();a._tileCache.injectCache({tile:t,cache:g,targetKey:e,setAsMainCache:!0,tileAllowNotLoaded:t.loading})}else n&&a._tileCache.restoreTilesThatShareOriginalCache(t,t.getCache(t.originalCacheKey),!0)},f=()=>p||"number"==typeof c.__invStamp&&c.__invStamp{if(g)return g.getDataAs(i,!1);const o=n?t.originalCacheKey:t.cacheKey,s=t.getCache(o);return s?(i=i||s.type,g=(new e.CacheRecord).withTileReference(t),s.getDataAs(i,!0).then(n=>null==n?e.Promise.reject(new Error("[World.getData] Working cache source data unavailable")):(g.addTile(t,n,i),g.data))):(e.console.error("[Tile::getData] There is no cache available for tile with key %s",o),e.Promise.reject())},setData:(i,n)=>g?g.setDataAs(i,n):(g=(new e.CacheRecord).withTileReference(t),g.addTile(t,i,n),e.Promise.resolve()),resetData:()=>{g&&(g.destroy(),g=null)},stopPropagation:()=>f()}).catch(t=>{if(e.console.error("Update routine error:",t),g){try{g.destroy()}catch(e){}g=null}return p=!0,c.__finishProcessing&&c.__finishProcessing(!0),null}).then(o=>{if(this.viewer.isDestroyed())return c.__finishProcessing&&c.__finishProcessing(!0),null;if(p)return null;if(c.__finishProcessing){if(!p&&(t.loaded||t.loading)){if(c.__invStamp{p?(g.destroy(),g=null):(!f()&&e?m():(g.destroy(),g=null),c.__finishProcessing())});if(n){const e=t.getCache(),i=t.getCache(t.originalCacheKey);return e!==i?i.prepareForRendering(l).then(e=>{p||(!f()&&e&&m(),c.__finishProcessing())}):null}}else e.console.error(`Invalidation flow error: tile processing state is invalid. Tile: ${t?t.toString():"null"}, loaded: ${t?t.loaded:"n/a"}, loading: ${t?t.loading:"n/a"}, originalCache.__invStamp: ${c.__invStamp}, this.__invalidatedAt: ${this.__invalidatedAt}, tStamp: ${i}, wasOutdatedRun: ${p}`);if(s){return t.getCache().prepareForRendering(l).then(()=>{!p&&c.__finishProcessing&&c.__finishProcessing()})}return c.__finishProcessing(),null}p||c.__finishProcessing(!0)}if(s){return t.getCache().prepareForRendering(l).then(()=>{!p&&c.__finishProcessing&&c.__finishProcessing()})}return g&&(g.destroy(),g=null),null}).catch(t=>{e.console.error("Update routine error:",t),g&&(g.destroy(),g=null),c.__finishProcessing()})});return e.Promise.all(a).then(()=>{r.length&&this.requestTileInvalidateEvent(r,void 0,n,!0),o||this.viewer.isDestroyed()||this.draw()})},ensureTilesUpToDate:function(t){let i,n;for(let e of t){if(e=e.tile||e,!e.loaded||e.processing)continue;const t=e.getCache(e.originalCacheKey);n=t.__wasRestored,t.__invStampc.height?r:r*(c.width/c.height),d=u*(c.height/c.width),p=new e.Point(g+(r-u)/2,m+(r-d)/2),h.setPosition(p,i),h.setWidth(u,i),"horizontal"===n?g+=a:m+=a;this.setAutoRefigureSizes(!0)},_figureSizes:function(){const t=this._homeBounds?this._homeBounds.clone():null,i=this._contentSize?this._contentSize.clone():null,n=this._contentFactor||0;if(this._items.length){let t=this._items[0],i=t.getBounds();this._contentFactor=t.getContentSize().x/i.width;let n=t.getClippedBounds().getBoundingBox(),o=n.x,s=n.y,r=n.x+n.width,a=n.y+n.height;for(let e=1;e Date: Sat, 25 Apr 2026 09:48:40 +0200 Subject: [PATCH 7/9] Improved registration visuals. --- exact/exact/base/static/css/styles_v2.css | 15 ++++++++++++--- .../images/templates/images/imageset_v2.html | 16 ++++++++-------- exact/exact/images/views.py | 6 ++++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/exact/exact/base/static/css/styles_v2.css b/exact/exact/base/static/css/styles_v2.css index bd70c83b..553c476f 100644 --- a/exact/exact/base/static/css/styles_v2.css +++ b/exact/exact/base/static/css/styles_v2.css @@ -270,10 +270,19 @@ body { color: #EEE; } +.img-wrapper { + height: 148px; + width: 200px; + display: flex; + justify-content: center; + align-items: center; + background-color: #ffffff; /* Optional: adds a background if image doesn't fill the box */ +} + .card-img-top { - width: 128px; - padding-top: 20px; - height: 148px; + height: 100%; /* Forces image to fit height */ + width: 100%; /* Forces image to fit width */ + object-fit: contain; /* Keeps the whole image visible, no cropping */ } .card-body-extend { diff --git a/exact/exact/images/templates/images/imageset_v2.html b/exact/exact/images/templates/images/imageset_v2.html index b44bd0d9..15274513 100644 --- a/exact/exact/images/templates/images/imageset_v2.html +++ b/exact/exact/images/templates/images/imageset_v2.html @@ -193,11 +193,11 @@

{{ imageset.name }}

{% for image in imageset.images.all|dictsort:"name" %} {% if image.image_type != 1 %}
-
- Thumbnail {{image.name}} not found! +
+
Thumbnail {{image.name}} not found!
-
+
@@ -363,8 +363,8 @@

{{imageset.name}} : Registration

@@ -386,7 +386,7 @@

{{imageset.name}} : Registration

@@ -524,7 +524,7 @@

{{imageset.name}} : Registration {% endfor %} - {% for reg in image_registration_dst %} + {% for reg in image_registration_trg %} {{reg.id}} {{reg.source_image.name}} diff --git a/exact/exact/images/views.py b/exact/exact/images/views.py index 6116351f..ec913e4c 100644 --- a/exact/exact/images/views.py +++ b/exact/exact/images/views.py @@ -880,8 +880,10 @@ def view_imageset(request, image_set_id): scale=scale) # use default parameters for now showRegTab=True - image_registration_src = ImageRegistration.objects.filter(source_image_id__in=imageset.images.all()) - image_registration_trg = ImageRegistration.objects.filter(target_image_id__in=imageset.images.all()) + image_registration_src = ImageRegistration.objects.filter(source_image_id__in=imageset.images.all()) + src_ids = image_registration_src.values_list('pk', flat=True) + + image_registration_trg = ImageRegistration.objects.filter(target_image_id__in=imageset.images.all()).exclude(pk__in=src_ids) if ('target_imageset_id' in request.GET): From 693fd12cf25cf090dcc93c3a59e426419bf20bfe Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Sat, 25 Apr 2026 12:00:53 +0200 Subject: [PATCH 8/9] Fix for index view --- .../images/templates/images/index_v2.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/exact/exact/images/templates/images/index_v2.html b/exact/exact/images/templates/images/index_v2.html index 8a27b5cf..4e37f9b7 100644 --- a/exact/exact/images/templates/images/index_v2.html +++ b/exact/exact/images/templates/images/index_v2.html @@ -59,17 +59,17 @@
{% for imageset in team.image_sets.all|dictsort:"name" %} -
+
{% for image in imageset.images.all|slice:":1" %} - Thumbnail {{image.name}} not found! +
Thumbnail {{image.name}} not found!
{% endfor %} {% if imageset.images.count == 0 %} - - Create new imageset - +
+
Create new imageset
+
{% endif %}
@@ -80,10 +80,10 @@
{% endfor %} -
- +
+
Create new imageset - +
New imageset From 4274e2267c40a8a62d3f7a67f67aad461fce23e5 Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Mon, 27 Apr 2026 20:44:52 +0200 Subject: [PATCH 9/9] Introduced hard linking background and foreground for registered slides. --- .../annotations/static/annotations/js/exact-quad-tree.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 e940ecf2..4f58c927 100644 --- a/exact/exact/annotations/static/annotations/js/exact-quad-tree.js +++ b/exact/exact/annotations/static/annotations/js/exact-quad-tree.js @@ -105,14 +105,8 @@ class EXACTRegistrationHandler { $("#registration_selector").val(this.registration_pair.source_image.name); var self = this; this.viewer.addHandler('animation', function() { - if (self.background_viewer.canvas) { - self.background_viewer.canvas.hidden=true; - } + self.syncViewBackgroundForeground(); }); - this.viewer.addHandler('animation-finish', function() { - if (self.background_viewer.canvas) { - self.background_viewer.canvas.hidden=false; - }}); this.background_viewer.addHandler("open", function (event) {