diff --git a/.gitignore b/.gitignore
index e43b0f9..0328ccf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
.DS_Store
+.idea
+*.iml
+
diff --git a/siteTheme-Bahn-ng/site/assets/css/bahn-ng-theme.css b/siteTheme-Bahn-ng/site/assets/css/bahn-ng-theme.css
index 41d8e33..563f627 100644
--- a/siteTheme-Bahn-ng/site/assets/css/bahn-ng-theme.css
+++ b/siteTheme-Bahn-ng/site/assets/css/bahn-ng-theme.css
@@ -71,8 +71,8 @@
position: relative;
margin-bottom: 55px;
}
-.bahn.ng h2::after,
-.bahn.ng h1::after {
+.bahn.ng h1:not(.card-title)::after,
+.bahn.ng h2:not(.card-title)::after {
position: absolute;
bottom: -30px;
width: 80px;
@@ -322,6 +322,8 @@ div.toc-menu {
}
.bahn.ng span.label {
margin-left: 1em;
+ padding-bottom: 0.5rem;
+ padding-top: 0.5rem;
}
.bahn.ng ul > li.with-child a.td-sidebar-link.td-sidebar-link__section {
padding-left: 1em !important;
@@ -334,11 +336,13 @@ div.toc-menu {
overflow-wrap: break-word;
}
#td-section-nav {
- max-height: calc(100vh - 64px);
position: sticky;
top: 64px;
}
@media (min-width: 768px) {
+ #td-section-nav {
+ max-height: calc(100vh - 64px);
+ }
.td-sidebar-nav {
max-height: 100%;
overflow-y: auto;
@@ -474,8 +478,8 @@ div.toc-menu {
.bahn.ng h3 {
font-weight: 900;
}
-.bahn.ng h1,
-.bahn.ng h2 {
+.bahn.ng h1:not(.card-title),
+.bahn.ng h2:not(.card-title) {
margin-bottom: 3.5rem;
}
.bahn.ng h1 {
@@ -578,6 +582,10 @@ div.toc-menu {
.bahn.ng main code {
font-family: Consolas, Monaco, 'Andale Mono', monospace;
}
+/* be sure color contrast is high enough */
+.bahn.ng main code .str {
+ color: #0b810b;
+}
.bahn.ng .td-content .highlight {
margin-top: 0;
}
@@ -642,7 +650,7 @@ div.toc-menu {
.bahn.ng main .admonitionblock.warning > table .title {
font-weight: bold;
text-transform: uppercase;
- color: var(--color-yellow-700);
+ color: var(--color-orange-800);
}
.bahn.ng main .admonitionblock.important > table .title {
font-weight: bold;
@@ -657,7 +665,7 @@ div.toc-menu {
.bahn.ng main .admonitionblock.note > table .title {
font-weight: bold;
text-transform: uppercase;
- color: var(--color-cyan-600);
+ color: var(--color-cyan-700);
}
/* Text anchor */
@@ -707,9 +715,9 @@ div.toc-menu {
/* fix footer base style */
.bahn.ng #footnotes .footnote {
- padding: 0;
- margin-left: 0;
- text-indent: 0;
+ padding: 0;
+ margin-left: 0;
+ text-indent: 0;
}
@@ -843,9 +851,12 @@ div.toc-menu {
}
}
-h2[id]:before, h3[id]:before, h4[id]:before, h5[id]:before {
+h2[id]:before,
+h3[id]:before,
+h4[id]:before,
+h5[id]:before {
display: block;
- content: " ";
+ content: ' ';
visibility: hidden;
}
a[href^="//"]:after,
@@ -854,3 +865,52 @@ a[href^="https://"]:after {
content: url(../images/ic-db_link-external_20.svg);
margin: 0 0 0 0;
}
+div.listingblock,
+table.tableblock {
+ position: relative;
+}
+div.listingblock > copybutton,
+table.tableblock > copybutton {
+ visibility: hidden;
+ position: absolute;
+ right: 0px;
+ top: 2em;
+ top: 0;
+ z-index: 2;
+ padding: 12px 12px;
+
+ background: url('../db-ui-icons/db_ic_copy_32.svg') left no-repeat;
+ background-size: 22px 22px;
+}
+
+/* Show the button on hover of the parent div */
+div.listingblock:hover > copybutton,
+table.tableblock:hover > copybutton {
+ visibility: visible;
+ cursor: pointer;
+}
+
+.imageblock.lb-active {
+ width: 100vw;
+ position: fixed;
+ z-index: 100000;
+ top: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.8);
+ width: 100vw;
+ height: 100vh;
+ text-align: center;
+}
+.imageblock.lb-active img {
+ display: block;
+ height: 90vh;
+ width: auto;
+ margin-top: 65px;
+ margin-left: auto;
+ margin-right: auto;
+ background-color: white;
+}
+
+.openseadragon-container {
+ border: 1px solid black !important;
+}
diff --git a/siteTheme-Bahn-ng/site/assets/db-ui-icons/db_ic_copy_32.svg b/siteTheme-Bahn-ng/site/assets/db-ui-icons/db_ic_copy_32.svg
new file mode 100644
index 0000000..10a0ee3
--- /dev/null
+++ b/siteTheme-Bahn-ng/site/assets/db-ui-icons/db_ic_copy_32.svg
@@ -0,0 +1,3 @@
+
diff --git a/siteTheme-Bahn-ng/site/assets/js/copy-n-paste.js b/siteTheme-Bahn-ng/site/assets/js/copy-n-paste.js
new file mode 100644
index 0000000..0acc474
--- /dev/null
+++ b/siteTheme-Bahn-ng/site/assets/js/copy-n-paste.js
@@ -0,0 +1,61 @@
+document.addEventListener('DOMContentLoaded', function() {
+ // Get all divs with the 'copy' class
+ const copyDivs = document.querySelectorAll('div.listingblock');
+
+ copyDivs.forEach(listingblock => {
+ // Create a button
+ const btn = document.createElement('copybutton');
+ btn.innerText = '';
+
+ // Add click event to the button
+ btn.addEventListener('click', async function() {
+ try {
+ await copyCodeToClipboard(listingblock);
+ alert('Code copied to clipboard!');
+ } catch (err) {
+ console.error('Failed to copy Code: ', err);
+ }
+ });
+
+ // Append the button to the div
+ listingblock.appendChild(btn);
+ });
+});
+
+async function copyCodeToClipboard(listingblock) {
+ const codeHtml = listingblock.querySelector('div.content').innerText;
+ const blob = new Blob([codeHtml], { type: 'text/plain' });
+ const data = [new ClipboardItem({ 'text/plain': blob })];
+ await navigator.clipboard.write(data);
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ // Get all divs with the 'copy' class
+ const copyDivs = document.querySelectorAll('table.copy');
+
+ copyDivs.forEach(table => {
+ // Create a button
+ const btn = document.createElement('copybutton');
+ btn.innerText = '';
+
+ // Add click event to the button
+ btn.addEventListener('click', async function() {
+ try {
+ await copyTableToClipboard(table);
+ alert('Table copied to clipboard!');
+ } catch (err) {
+ console.error('Failed to copy table: ', err);
+ }
+ });
+
+ // Append the button to the div
+ table.appendChild(btn);
+ });
+});
+
+async function copyTableToClipboard(table) {
+ const tableHtml = table.outerHTML;
+ const blob = new Blob([tableHtml], { type: 'text/html' });
+ const data = [new ClipboardItem({ 'text/html': blob })];
+ await navigator.clipboard.write(data);
+}
\ No newline at end of file
diff --git a/siteTheme-Bahn-ng/site/assets/js/lightbox.js b/siteTheme-Bahn-ng/site/assets/js/lightbox.js
index ca5f05f..016561c 100644
--- a/siteTheme-Bahn-ng/site/assets/js/lightbox.js
+++ b/siteTheme-Bahn-ng/site/assets/js/lightbox.js
@@ -1,8 +1,5 @@
-const elements = document.querySelectorAll(".imageblock")
-console.log (elements);
-console.log(elements.length)
+const elements = document.querySelectorAll(".imageblock:not(.zoomable)")
for (let i = 0; i < elements.length; i++){
- console.log(i);
elements[i].addEventListener("click",function() {
if (this.className.includes("lb-active")) {
this.className = this.className.replace("lb-active","");
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/LICENSE.txt b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/LICENSE.txt
new file mode 100644
index 0000000..e69de29
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/changelog.txt b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/changelog.txt
new file mode 100644
index 0000000..e69de29
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_grouphover.png
new file mode 100644
index 0000000..9db590e
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_hover.png
new file mode 100644
index 0000000..645c241
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_pressed.png
new file mode 100644
index 0000000..d5b1d7d
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_rest.png
new file mode 100644
index 0000000..e232387
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/button_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_grouphover.png
new file mode 100644
index 0000000..24c110c
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_hover.png
new file mode 100644
index 0000000..af97e71
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_pressed.png
new file mode 100644
index 0000000..026d55e
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_rest.png
new file mode 100644
index 0000000..9714582
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/flip_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_grouphover.png
new file mode 100644
index 0000000..da9002b
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_hover.png
new file mode 100644
index 0000000..705b3d3
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_pressed.png
new file mode 100644
index 0000000..6fa182a
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_rest.png
new file mode 100644
index 0000000..bfab643
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/fullpage_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_grouphover.png
new file mode 100644
index 0000000..cb412ba
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_hover.png
new file mode 100644
index 0000000..c8f860b
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_pressed.png
new file mode 100644
index 0000000..00c349b
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_rest.png
new file mode 100644
index 0000000..6ac397d
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/home_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_grouphover.png
new file mode 100644
index 0000000..18c1a92
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_hover.png
new file mode 100644
index 0000000..fdce582
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_pressed.png
new file mode 100644
index 0000000..5297c52
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_rest.png
new file mode 100644
index 0000000..e3c5a3c
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/next_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_grouphover.png
new file mode 100644
index 0000000..5e0fda1
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_hover.png
new file mode 100644
index 0000000..9f9efe6
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_pressed.png
new file mode 100644
index 0000000..75c7e7d
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_rest.png
new file mode 100644
index 0000000..902a7b4
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/previous_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_grouphover.png
new file mode 100644
index 0000000..302ac62
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_hover.png
new file mode 100644
index 0000000..e757d87
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_pressed.png
new file mode 100644
index 0000000..1480b1a
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_rest.png
new file mode 100644
index 0000000..f0b8654
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateleft_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_grouphover.png
new file mode 100644
index 0000000..9e71371
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_hover.png
new file mode 100644
index 0000000..08f1416
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_pressed.png
new file mode 100644
index 0000000..351f824
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_rest.png
new file mode 100644
index 0000000..d70468a
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/rotateright_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_grouphover.png
new file mode 100644
index 0000000..98ecd29
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_hover.png
new file mode 100644
index 0000000..c25bda4
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_pressed.png
new file mode 100644
index 0000000..e617e03
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_rest.png
new file mode 100644
index 0000000..4380589
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomin_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_grouphover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_grouphover.png
new file mode 100644
index 0000000..b588ecf
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_grouphover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_hover.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_hover.png
new file mode 100644
index 0000000..a132cb4
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_hover.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_pressed.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_pressed.png
new file mode 100644
index 0000000..679c5cd
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_pressed.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_rest.png b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_rest.png
new file mode 100644
index 0000000..e3ac4ab
Binary files /dev/null and b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/images/zoomout_rest.png differ
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.js b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.js
new file mode 100644
index 0000000..cd41170
--- /dev/null
+++ b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.js
@@ -0,0 +1,24826 @@
+//! openseadragon 4.1.0
+//! Built on 2023-05-25
+//! Git commit: v4.1.0-0-8849681
+//! http://openseadragon.github.io
+//! License: http://openseadragon.github.io/license/
+
+/*
+ * OpenSeadragon
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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-2022 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 4.1.0
+ * @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 {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 canvas rendering (only if canvas is used). Note: Ignored
+ * by some (especially older) browsers which do not support this canvas property.
+ * This property can be changed in {@link Viewer.Drawer.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 {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} [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 {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} [navigatorAutoResize=true]
+ * Set to false to prevent polling for navigator size changes. Useful for providing custom resize behavior.
+ * Setting to false can also improve performance when the navigator is configured to a fixed size.
+ *
+ * @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]
+ * Set to false to not use an HTML canvas element for image rendering even if canvas is supported.
+ *
+ * @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} [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)
+ */
+
+ /**
+ * 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.
+ *
+ */
+
+/**
+ * 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: '4.1.0',
+ major: parseInt('4', 10),
+ minor: parseInt('1', 10),
+ revision: parseInt('0', 10)
+ };
+
+
+ /**
+ * Taken from jquery 1.6.1
+ * [[Class]] -> type pairs
+ * @private
+ */
+ var class2type = {
+ '[object Boolean]': 'boolean',
+ '[object Number]': 'number',
+ '[object String]': 'string',
+ '[object Function]': 'function',
+ '[object AsyncFunction]': 'function',
+ '[object Promise]': 'promise',
+ '[object Array]': 'array',
+ '[object Date]': 'date',
+ '[object RegExp]': 'regexp',
+ '[object Object]': 'object'
+ },
+ // Save a reference to some core methods
+ toString = Object.prototype.toString,
+ hasOwn = Object.prototype.hasOwnProperty;
+
+ /**
+ * 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.
+
+ var lastKey;
+ for (var 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 ( var 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 () {
+ var 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) {
+ var 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 () {
+ var supported = 0;
+
+ if ( $.supportsAddEventListener ) {
+ try {
+ var 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;
+ }());
+
+ /**
+ * 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
+ */
+ $.getCurrentPixelDensityRatio = function() {
+ if ( $.supportsCanvas ) {
+ var context = document.createElement('canvas').getContext('2d');
+ var devicePixelRatio = window.devicePixelRatio || 1;
+ var backingStoreRatio = context.webkitBackingStorePixelRatio ||
+ context.mozBackingStorePixelRatio ||
+ context.msBackingStorePixelRatio ||
+ context.oBackingStorePixelRatio ||
+ context.backingStorePixelRatio || 1;
+ return Math.max(devicePixelRatio, 1) / backingStoreRatio;
+ } else {
+ return 1;
+ }
+ };
+
+ /**
+ * @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() {
+ var options,
+ name,
+ src,
+ copy,
+ copyIsArray,
+ clone,
+ target = arguments[ 0 ] || {},
+ length = arguments.length,
+ deep = false,
+ i = 1;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[ 1 ] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) {
+ target = {};
+ }
+
+ // extend jQuery itself if only one argument is passed
+ if ( length === i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ options = arguments[ i ];
+ if ( options !== null || options !== undefined ) {
+ // Extend the base object
+ for ( name in options ) {
+ var 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;
+ };
+
+ var isIOSDevice = function () {
+ if (typeof navigator !== 'object') {
+ return false;
+ }
+ var 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,
+
+ //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,
+ 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,
+
+ //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
+
+ //VIEWPORT NAVIGATOR SETTINGS
+ showNavigator: false,
+ navigatorElement: null,
+ navigatorId: null,
+ navigatorPosition: null,
+ navigatorSizeRatio: 0.2,
+ navigatorMaintainSizeRatio: false,
+ navigatorTop: null,
+ navigatorLeft: null,
+ navigatorHeight: null,
+ navigatorWidth: null,
+ navigatorAutoResize: true,
+ navigatorAutoFade: true,
+ navigatorRotate: true,
+ navigatorBackground: '#000',
+ navigatorOpacity: 0.8,
+ navigatorBorderColor: '#555',
+ navigatorDisplayRegionColor: '#900',
+
+ // INITIAL ROTATION
+ degrees: 0,
+
+ // INITIAL FLIP STATE
+ flipped: false,
+
+ // APPEARANCE
+ opacity: 1,
+ preload: false,
+ compositeOperation: null,
+ imageSmoothingEnabled: true,
+ placeholderFillStyle: null,
+ subPixelRoundingForTransparency: null,
+
+ //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,
+ useCanvas: true, // Use canvas element for drawing if available
+ 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
+
+ },
+
+
+ /**
+ * TODO: get rid of this. I can't see how it's required at all. Looks
+ * like an early legacy code artifact.
+ * @static
+ * @ignore
+ */
+ SIGNAL: "----seadragon----",
+
+
+ /**
+ * 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(){
+ var 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 ) {
+ var result = new $.Point(),
+ isFixed,
+ offsetParent;
+
+ element = $.getElement( element );
+ isFixed = $.getElementStyle( element ).position === "fixed";
+ offsetParent = getOffsetParent( element, isFixed );
+
+ while ( offsetParent ) {
+
+ result.x += element.offsetLeft;
+ result.y += element.offsetTop;
+
+ if ( isFixed ) {
+ result = result.plus( $.getPageScroll() );
+ }
+
+ element = offsetParent;
+ isFixed = $.getElementStyle( element ).position === "fixed";
+ offsetParent = getOffsetParent( element, isFixed );
+ }
+
+ return result;
+ },
+
+
+ /**
+ * Determines the 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 );
+
+ var doc = element && element.ownerDocument,
+ docElement,
+ win,
+ boundingRect = { top: 0, left: 0 };
+
+ if ( !doc ) {
+ return new $.Point();
+ }
+
+ docElement = doc.documentElement;
+
+ if ( typeof element.getBoundingClientRect !== typeof undefined ) {
+ boundingRect = element.getBoundingClientRect();
+ }
+
+ 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) {
+ var memo = {};
+
+ $.getCssPropertyWithVendorPrefix = function(property) {
+ if (memo[property] !== undefined) {
+ return memo[property];
+ }
+ var style = document.createElement('div').style;
+ var result = null;
+ if (style[property] !== undefined) {
+ result = property;
+ } else {
+ var prefixes = ['Webkit', 'Moz', 'MS', 'O',
+ 'webkit', 'moz', 'ms', 'o'];
+ var suffix = $.capitalizeFirstLetter(property);
+ for (var i = 0; i < prefixes.length; i++) {
+ var 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) {
+ var 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 );
+ var offset = $.getElementOffset( element ),
+ 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 ){
+ var result = new $.Point();
+
+ result.x = event.pageX;
+ result.y = event.pageY;
+
+ return result;
+ };
+ } else if ( typeof ( event.clientX ) === "number" ) {
+ $.getMousePosition = function( event ){
+ var 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() {
+ var docElement = document.documentElement || {},
+ 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 {
+ var 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;
+ var 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() {
+ var docElement = document.documentElement || {},
+ 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:
+ */
+
+ var 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];
+ },
+
+
+ /**
+ * 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 ) {
+ var element = document.createElement( tagName ),
+ style = element.style;
+
+ style.background = "transparent none";
+ style.border = "none";
+ style.margin = "0px";
+ style.padding = "0px";
+ style.position = "static";
+
+ return element;
+ },
+
+
+ /**
+ * 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 ) {
+ var 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 ) {
+
+ var ieOpacity,
+ 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 ) {
+ var i,
+ pivot = ( fromIndex ) ? fromIndex : 0,
+ length;
+ if ( !array ) {
+ throw new TypeError( );
+ }
+
+ length = array.length;
+ if ( length === 0 || pivot >= length ) {
+ return -1;
+ }
+
+ if ( pivot < 0 ) {
+ pivot = length - Math.abs( pivot );
+ }
+
+ for ( 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 ) {
+ var oldClasses,
+ newClasses = [],
+ i;
+
+ element = $.getElement( element );
+ oldClasses = element.className.split( /\s+/ );
+ for ( 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) {
+ var 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();
+ },
+
+
+ /**
+ * Similar to OpenSeadragon.delegate, but it does not immediately call
+ * the method on the object, returning a function which can be called
+ * repeatedly to delegate the method. It also allows additional arguments
+ * to be passed during construction which will be added during each
+ * invocation, and each invocation can add additional arguments as well.
+ *
+ * @function
+ * @param {Object} object
+ * @param {Function} method
+ * @param [args] any additional arguments are passed as arguments to the
+ * created callback
+ * @returns {Function}
+ */
+ createCallback: function( object, method ) {
+ //TODO: This pattern is painful to use and debug. It's much cleaner
+ // to use pinning plus anonymous functions. Get rid of this
+ // pattern!
+ var initialArgs = [],
+ i;
+ for ( i = 2; i < arguments.length; i++ ) {
+ initialArgs.push( arguments[ i ] );
+ }
+
+ return function() {
+ var args = initialArgs.concat( [] ),
+ i;
+ for ( i = 0; i < arguments.length; i++ ) {
+ args.push( arguments[ i ] );
+ }
+
+ return method.apply( object, args );
+ };
+ },
+
+
+ /**
+ * 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
+ var 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 ) {
+ var 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] If set to true, the XHR will be file: protocol
+ * compatible if possible (but may raise a warning in the browser).
+ * @returns {XMLHttpRequest}
+ */
+ createAjaxRequest: function( local ) {
+ // IE11 does not support window.ActiveXObject so we just try to
+ // create one to see if it is supported.
+ // See: http://msdn.microsoft.com/en-us/library/ie/dn423948%28v=vs.85%29.aspx
+ var supportActiveX;
+ try {
+ /* global ActiveXObject:true */
+ supportActiveX = !!new ActiveXObject( "Microsoft.XMLHTTP" );
+ } catch( e ) {
+ supportActiveX = false;
+ }
+
+ if ( supportActiveX ) {
+ if ( window.XMLHttpRequest ) {
+ $.createAjaxRequest = function( local ) {
+ if ( local ) {
+ return new ActiveXObject( "Microsoft.XMLHTTP" );
+ }
+ return new XMLHttpRequest();
+ };
+ } else {
+ $.createAjaxRequest = function() {
+ return new ActiveXObject( "Microsoft.XMLHTTP" );
+ };
+ }
+ } else if ( window.XMLHttpRequest ) {
+ $.createAjaxRequest = function() {
+ return new XMLHttpRequest();
+ };
+ } else {
+ throw new Error( "Browser doesn't support XMLHttpRequest." );
+ }
+ return $.createAjaxRequest( local );
+ },
+
+ /**
+ * 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::getPostData), 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 ) {
+ var withCredentials;
+ var headers;
+ var responseType;
+ var 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;
+ }
+
+ var protocol = $.getUrlProtocol( url );
+ var request = $.createAjaxRequest( protocol === "file:" );
+
+ 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 );
+ }
+ }
+ }
+ };
+
+ var method = postData ? "POST" : "GET";
+ try {
+ request.open( method, url, true );
+
+ if (responseType) {
+ request.responseType = responseType;
+ }
+
+ if (headers) {
+ for (var 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 ){
+ var script,
+ url = options.url,
+ head = document.head ||
+ document.getElementsByTagName( "head" )[ 0 ] ||
+ document.documentElement,
+ jsonpCallback = options.callbackName || 'openseadragon' + $.now(),
+ previous = window[ jsonpCallback ],
+ replace = "$1" + jsonpCallback + "$2",
+ callbackParam = options.param || 'callback',
+ callback = options.callback;
+
+ url = url.replace( /(=)\?(&|$)|\?\?/i, replace );
+ // Add callback manually
+ url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback;
+
+ // Install callback
+ window[ jsonpCallback ] = function( response ) {
+ if ( !previous ){
+ 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 ) {
+ var xmlDoc = null,
+ parser;
+
+ parser = new DOMParser();
+ xmlDoc = parser.parseFromString( string, "text/xml" );
+ return xmlDoc;
+ };
+
+ } else if ( window.ActiveXObject ) {
+
+ $.parseXml = function( string ) {
+ var xmlDoc = null;
+
+ xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" );
+ xmlDoc.async = false;
+ xmlDoc.loadXML( string );
+ 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
+ *
{
+ * bmp: false,
+ * jpeg: true,
+ * jpg: true,
+ * png: true,
+ * tif: false,
+ * wdp: false
+ * }
+ *
+ * @function
+ * @example
+ * // sets webp as supported and png as unsupported
+ * setImageFormatsSupported({webp: true, png: false});
+ * @param {Object} formats An object containing format extensions as
+ * keys and booleans as values.
+ */
+ setImageFormatsSupported: function(formats) {
+ // 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
+ */
+ var 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
+ };
+
+
+ var FILEFORMATS = {
+ bmp: false,
+ jpeg: true,
+ jpg: true,
+ png: true,
+ tif: false,
+ wdp: false
+ },
+ URLPARAMS = {};
+
+ (function() {
+ //A small auto-executing routine to determine the browser vendor,
+ //version and supporting feature sets.
+ var ver = navigator.appVersion,
+ ua = navigator.userAgent,
+ regex;
+
+ //console.error( 'appName: ' + navigator.appName );
+ //console.error( 'appVersion: ' + navigator.appVersion );
+ //console.error( 'userAgent: ' + navigator.userAgent );
+
+ switch( navigator.appName ){
+ case "Microsoft Internet Explorer":
+ if( !!window.attachEvent &&
+ !!window.ActiveXObject ) {
+
+ $.Browser.vendor = $.BROWSERS.IE;
+ $.Browser.version = parseFloat(
+ ua.substring(
+ ua.indexOf( "MSIE" ) + 5,
+ ua.indexOf( ";", ua.indexOf( "MSIE" ) ) )
+ );
+ }
+ break;
+ case "Netscape":
+ if (window.addEventListener) {
+ if ( ua.indexOf( "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
+ var query = window.location.search.substring( 1 ),
+ parts = query.split('&'),
+ part,
+ sep,
+ i;
+
+ for ( i = 0; i < parts.length; i++ ) {
+ part = parts[ i ];
+ sep = part.indexOf( '=' );
+
+ if ( sep > 0 ) {
+ var key = part.substring( 0, sep ),
+ 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 && $.Browser.version < 11 ) {
+ $.console.error('Internet Explorer versions < 11 are 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
+ var requestAnimationFrame = w.requestAnimationFrame ||
+ w.mozRequestAnimationFrame ||
+ w.webkitRequestAnimationFrame ||
+ w.msRequestAnimationFrame;
+
+ var 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 {
+ var aAnimQueue = [],
+ processing = [],
+ iRequestId = 0,
+ iIntervalId;
+
+ // create a mock requestAnimationFrame function
+ $.requestAnimationFrame = function( callback ) {
+ aAnimQueue.push( [ ++iRequestId, callback ] );
+
+ if ( !iIntervalId ) {
+ iIntervalId = setInterval( function() {
+ if ( aAnimQueue.length ) {
+ var 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.
+ var 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
+ var 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;
+ }
+ }
+
+}(OpenSeadragon));
+
+
+// Universal Module Definition, supports CommonJS, AMD and simple script tag
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // expose as amd module
+ define([], factory);
+ } else if (typeof module === 'object' && module.exports) {
+ // expose as commonjs module
+ module.exports = factory();
+ } else {
+ // expose as window.OpenSeadragon
+ root.OpenSeadragon = factory();
+ }
+}(this, function () {
+ return OpenSeadragon;
+}));
+
+/*
+ * OpenSeadragon - full-screen support functions
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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.
+ */
+ var 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();
+ };
+ fullScreenApi.exitFullScreen = function() {
+ document.exitFullscreen();
+ };
+ 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-2022 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($){
+
+/**
+ * Event handler method signature used by all OpenSeadragon events.
+ *
+ * @callback EventHandler
+ * @memberof OpenSeadragon
+ * @param {Object} event - See individual events for event-specific properties.
+ */
+
+
+/**
+ * @class EventSource
+ * @classdesc For use by classes which want to support custom, non-browser events.
+ *
+ * @memberof OpenSeadragon
+ */
+$.EventSource = function() {
+ this.events = {};
+};
+
+/** @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} 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.
+ */
+ addOnceHandler: function(eventName, handler, userData, times, priority) {
+ var self = this;
+ times = times || 1;
+ var count = 0;
+ var onceHandler = function(event) {
+ count++;
+ if (count === times) {
+ self.removeHandler(eventName, onceHandler);
+ }
+ return handler(event);
+ };
+ 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} 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.
+ */
+ addHandler: function ( eventName, handler, userData, priority ) {
+ var events = this.events[ eventName ];
+ if ( !events ) {
+ this.events[ eventName ] = events = [];
+ }
+ if ( handler && $.isFunction( handler ) ) {
+ var 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--;
+ }
+ }
+ },
+
+ /**
+ * 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} handler - Function to be removed.
+ */
+ removeHandler: function ( eventName, handler ) {
+ var events = this.events[ eventName ],
+ handlers = [],
+ i;
+ if ( !events ) {
+ return;
+ }
+ if ( $.isArray( events ) ) {
+ for ( 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) {
+ var 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 ( var 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) {
+ var events = this.events[ eventName ];
+ if ( !events || !events.length ) {
+ return null;
+ }
+ events = events.length === 1 ?
+ [ events[ 0 ] ] :
+ Array.apply( null, events );
+ return function ( source, args ) {
+ var i,
+ length = events.length;
+ for ( i = 0; i < length; i++ ) {
+ if ( events[ i ] ) {
+ args.eventSource = source;
+ args.userData = events[ i ].userData;
+ events[ i ].handler( args );
+ }
+ }
+ };
+ },
+
+ /**
+ * Trigger an event, optionally passing additional information.
+ * @function
+ * @param {String} eventName - Name of event to register.
+ * @param {Object} eventArgs - Event-specific data.
+ */
+ raiseEvent: function( eventName, eventArgs ) {
+ //uncomment if you want to get a log of all events
+ //$.console.log( eventName );
+
+ var handler = this.getHandler( eventName );
+ if ( handler ) {
+ return handler( this, eventArgs || {} );
+ }
+ return undefined;
+ }
+};
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - MouseTracker
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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
+ var MOUSETRACKERS = [];
+
+ // dictionary from hash to private properties
+ var 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
+ * The number of milliseconds within which a pointer down-up event combination
+ * will be treated as a click gesture.
+ * @param {Number} options.clickDistThreshold
+ * 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
+ * The number of milliseconds within which two pointer down-up event combinations
+ * will be treated as a double-click gesture.
+ * @param {Number} options.dblClickDistThreshold
+ * 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 );
+
+ var args = arguments;
+
+ if ( !$.isPlainObject( options ) ) {
+ options = {
+ element: args[ 0 ],
+ clickTimeThreshold: args[ 1 ],
+ clickDistThreshold: args[ 2 ]
+ };
+ }
+
+ this.hash = Math.random(); // 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
+ var _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
+ };
+
+ this.hasGestureHandlers = !!( this.pressHandler || this.nonPrimaryPressHandler ||
+ this.releaseHandler || this.nonPrimaryReleaseHandler ||
+ this.clickHandler || this.dblClickHandler ||
+ this.dragHandler || this.dragEndHandler ||
+ this.pinchHandler );
+ this.hasScrollHandler = !!this.scrollHandler;
+
+ 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 () {
+ var i;
+
+ stopTracking( this );
+ this.element = null;
+
+ for ( 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 ) {
+ var delegate = THIS[ this.hash ],
+ i,
+ len = delegate.activePointersLists.length,
+ list;
+
+ for ( i = 0; i < len; i++ ) {
+ if ( delegate.activePointersLists[ i ].type === type ) {
+ return delegate.activePointersLists[ i ];
+ }
+ }
+
+ list = new $.MouseTracker.GesturePointList( type );
+ delegate.activePointersLists.push( list );
+ return list;
+ },
+
+ /**
+ * Returns the total number of pointers currently active on the tracked element.
+ * @function
+ * @returns {Number}
+ */
+ getActivePointerCount: function () {
+ var delegate = THIS[ this.hash ],
+ i,
+ len = delegate.activePointersLists.length,
+ count = 0;
+
+ for ( i = 0; i < len; i++ ) {
+ count += delegate.activePointersLists[ i ].getLength();
+ }
+
+ return count;
+ },
+
+ /**
+ * 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.var viewer = new OpenSeadragon.Viewer(options);
var viewer = OpenSeadragon(options);
rendered
is the context with the pre-drawn image.
+ * @param {Number} [scale=1] - Apply a scale to position and size
+ * @param {OpenSeadragon.Point} [translate] - A translation vector
+ * @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.
+ */
+ drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
+
+ var position = this.position.times($.pixelDensityRatio),
+ size = this.size.times($.pixelDensityRatio),
+ rendered;
+
+ if (!this.context2D && !this.cacheImageRecord) {
+ $.console.warn(
+ '[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached',
+ this.toString());
+ return;
+ }
+
+ rendered = this.getCanvasContext();
+
+ if ( !this.loaded || !rendered ){
+ $.console.warn(
+ "Attempting to draw tile %s when it's not yet loaded.",
+ this.toString()
+ );
+
+ return;
+ }
+
+ context.save();
+ context.globalAlpha = this.opacity;
+
+ 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 && this.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 gives the application a chance to make image manipulation
+ // changes as we are rendering the image
+ drawingHandler({context: context, tile: this, rendered: rendered});
+
+ var sourceWidth, sourceHeight;
+ if (this.sourceBounds) {
+ sourceWidth = Math.min(this.sourceBounds.width, rendered.canvas.width);
+ sourceHeight = Math.min(this.sourceBounds.height, rendered.canvas.height);
+ } else {
+ sourceWidth = rendered.canvas.width;
+ sourceHeight = rendered.canvas.height;
+ }
+
+ context.translate(position.x + size.x / 2, 0);
+ if (this.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 ratio between current and original size.
+ * @function
+ * @returns {Float}
+ */
+ getScaleForEdgeSmoothing: function() {
+ var context;
+ if (this.cacheImageRecord) {
+ context = this.cacheImageRecord.getRenderedContext();
+ } else if (this.context2D) {
+ context = this.context2D;
+ } else {
+ $.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.
+ var x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2));
+ var 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;
+ })
+ );
+ },
+
+ /**
+ * Removes tile from its container.
+ * @function
+ */
+ unload: function() {
+ if ( this.imgElement && this.imgElement.parentNode ) {
+ this.imgElement.parentNode.removeChild( this.imgElement );
+ }
+ if ( this.element && this.element.parentNode ) {
+ this.element.parentNode.removeChild( this.element );
+ }
+
+ this.element = null;
+ this.imgElement = null;
+ this.loaded = false;
+ this.loading = false;
+ }
+};
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - Overlay
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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
+ */
+
+ var options;
+ if ($.isPlainObject(element)) {
+ options = element;
+ } else {
+ options = {
+ element: element,
+ location: location,
+ placement: placement
+ };
+ }
+
+ this.element = options.element;
+ this.style = options.element.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) {
+ var 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() {
+ var element = this.element;
+ var 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 = "";
+ }
+ var transformOriginProp = $.getCssPropertyWithVendorPrefix(
+ 'transformOrigin');
+ var transformProp = $.getCssPropertyWithVendorPrefix(
+ 'transform');
+ if (transformOriginProp && transformProp) {
+ style[transformOriginProp] = "";
+ style[transformProp] = "";
+ }
+ },
+
+ /**
+ * @function
+ * @param {Element} container
+ */
+ drawHTML: function(container, viewport) {
+ var element = this.element;
+ 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(element);
+ }
+
+ var positionAndSize = this._getOverlayPositionAndSize(viewport);
+
+ var position = positionAndSize.position;
+ var size = this.size = positionAndSize.size;
+ var rotate = positionAndSize.rotate;
+
+ // 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 {
+ var style = this.style;
+ style.left = position.x + "px";
+ style.top = position.y + "px";
+ if (this.width !== null) {
+ style.width = size.x + "px";
+ }
+ if (this.height !== null) {
+ style.height = size.y + "px";
+ }
+ var transformOriginProp = $.getCssPropertyWithVendorPrefix(
+ 'transformOrigin');
+ var transformProp = $.getCssPropertyWithVendorPrefix(
+ 'transform');
+ if (transformOriginProp && transformProp) {
+ if (rotate) {
+ style[transformOriginProp] = this._getTransformOrigin();
+ style[transformProp] = "rotate(" + rotate + "deg)";
+ } else {
+ style[transformOriginProp] = "";
+ style[transformProp] = "";
+ }
+ }
+ style.display = 'block';
+ }
+ },
+
+ // private
+ _getOverlayPositionAndSize: function(viewport) {
+ var position = viewport.pixelFromPoint(this.location, true);
+ var size = this._getSizeInPixels(viewport);
+ this.adjust(position, size);
+
+ var 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) {
+ var rect = new $.Rect(position.x, position.y, size.x, size.y);
+ var boundingBox = this._getBoundingBox(rect, viewport.getRotation(true));
+ position = boundingBox.getTopLeft();
+ size = boundingBox.getSize();
+ } else {
+ rotate = viewport.getRotation(true);
+ }
+ }
+
+ return {
+ position: position,
+ size: size,
+ rotate: rotate
+ };
+ },
+
+ // private
+ _getSizeInPixels: function(viewport) {
+ var width = this.size.x;
+ var height = this.size.y;
+ if (this.width !== null || this.height !== null) {
+ var 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)) {
+ var eltSize = this.size = $.getElementSize(this.element);
+ if (this.width === null) {
+ width = eltSize.x;
+ }
+ if (this.height === null) {
+ height = eltSize.y;
+ }
+ }
+ return new $.Point(width, height);
+ },
+
+ // private
+ _getBoundingBox: function(rect, degrees) {
+ var refPoint = this._getPlacementPoint(rect);
+ return rect.rotate(degrees, refPoint).getBoundingBox();
+ },
+
+ // private
+ _getPlacementPoint: function(rect) {
+ var result = new $.Point(rect.x, rect.y);
+ var 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() {
+ var result = "";
+ var 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) {
+ var 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.');
+ var width = this.width;
+ var height = this.height;
+ if (width === null || height === null) {
+ var size = viewport.deltaPointsFromPixelsNoRotate(this.size, true);
+ if (width === null) {
+ width = size.x;
+ }
+ if (height === null) {
+ height = size.y;
+ }
+ }
+ var 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.
+ var 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 - Drawer
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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 Drawer
+ * @memberof OpenSeadragon
+ * @classdesc Handles rendering of tiles 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.
+ */
+$.Drawer = function( options ) {
+
+ $.console.assert( options.viewer, "[Drawer] options.viewer is required" );
+
+ //backward compatibility for positional args while preferring more
+ //idiomatic javascript options object as the only argument
+ var args = arguments;
+
+ if( !$.isPlainObject( options ) ){
+ options = {
+ source: args[ 0 ], // Reference to Viewer tile source.
+ viewport: args[ 1 ], // Reference to Viewer viewport.
+ element: args[ 2 ] // Parent element.
+ };
+ }
+
+ $.console.assert( options.viewport, "[Drawer] options.viewport is required" );
+ $.console.assert( options.element, "[Drawer] options.element is required" );
+
+ if ( options.source ) {
+ $.console.error( "[Drawer] options.source is no longer accepted; use TiledImage instead" );
+ }
+
+ this.viewer = options.viewer;
+ this.viewport = options.viewport;
+ this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor;
+ if (options.opacity) {
+ $.console.error( "[Drawer] options.opacity is no longer accepted; set the opacity on the TiledImage instead" );
+ }
+
+ this.useCanvas = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true );
+ /**
+ * The parent element of this Drawer instance, passed in when the Drawer was created.
+ * The parent of {@link OpenSeadragon.Drawer#canvas}.
+ * @member {Element} container
+ * @memberof OpenSeadragon.Drawer#
+ */
+ this.container = $.getElement( options.element );
+ /**
+ * A <canvas> element if the browser supports them, otherwise a <div> element.
+ * Child element of {@link OpenSeadragon.Drawer#container}.
+ * @member {Element} canvas
+ * @memberof OpenSeadragon.Drawer#
+ */
+ this.canvas = $.makeNeutralElement( this.useCanvas ? "canvas" : "div" );
+ /**
+ * 2d drawing context for {@link OpenSeadragon.Drawer#canvas} if it's a <canvas> element, otherwise null.
+ * @member {Object} context
+ * @memberof OpenSeadragon.Drawer#
+ */
+ this.context = this.useCanvas ? this.canvas.getContext( "2d" ) : null;
+
+ /**
+ * 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;
+
+ /**
+ * @member {Element} element
+ * @memberof OpenSeadragon.Drawer#
+ * @deprecated Alias for {@link OpenSeadragon.Drawer#container}.
+ */
+ this.element = this.container;
+
+ // We force our container to ltr because our drawing math doesn't work in rtl.
+ // This issue only affects our canvas renderer, but we do it always for consistency.
+ // Note that this means overlays you want to be rtl need to be explicitly set to rtl.
+ this.container.dir = 'ltr';
+
+ // check canvas available width and height, set canvas width and height such that the canvas backing store is set to the proper pixel density
+ if (this.useCanvas) {
+ var viewportSize = this._calculateCanvasSize();
+ this.canvas.width = viewportSize.x;
+ this.canvas.height = viewportSize.y;
+ }
+
+ this.canvas.style.width = "100%";
+ this.canvas.style.height = "100%";
+ this.canvas.style.position = "absolute";
+ $.setElementOpacity( this.canvas, this.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 );
+
+ // Image smoothing for canvas rendering (only if canvas is used).
+ // Canvas default is "true", so this will only be changed if user specified "false".
+ this._imageSmoothingEnabled = true;
+};
+
+/** @lends OpenSeadragon.Drawer.prototype */
+$.Drawer.prototype = {
+ // deprecated
+ addOverlay: function( element, location, placement, onDraw ) {
+ $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead.");
+ this.viewer.addOverlay( element, location, placement, onDraw );
+ return this;
+ },
+
+ // deprecated
+ updateOverlay: function( element, location, placement ) {
+ $.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead.");
+ this.viewer.updateOverlay( element, location, placement );
+ return this;
+ },
+
+ // deprecated
+ removeOverlay: function( element ) {
+ $.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead.");
+ this.viewer.removeOverlay( element );
+ return this;
+ },
+
+ // deprecated
+ clearOverlays: function() {
+ $.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead.");
+ this.viewer.clearOverlays();
+ return this;
+ },
+
+ /**
+ * 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: function(point) {
+ var vpPoint = this.viewport.pixelFromPointNoRotate(point, true);
+ return new $.Point(
+ vpPoint.x * $.pixelDensityRatio,
+ vpPoint.y * $.pixelDensityRatio
+ );
+ },
+
+ /**
+ * This function will create multiple polygon paths on the drawing context by provided polygons,
+ * then clip the context to the paths.
+ * @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: function (polygons, useSketch) {
+ if (!this.useCanvas) {
+ return;
+ }
+ var context = this._getContext(useSketch);
+ context.beginPath();
+ polygons.forEach(function (polygon) {
+ polygon.forEach(function (coord, i) {
+ context[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
+ });
+ });
+ context.clip();
+ },
+
+ /**
+ * Set the opacity of the drawer.
+ * @param {Number} opacity
+ * @returns {OpenSeadragon.Drawer} Chainable.
+ */
+ setOpacity: function( opacity ) {
+ $.console.error("drawer.setOpacity is deprecated. Use tiledImage.setOpacity instead.");
+ var world = this.viewer.world;
+ for (var i = 0; i < world.getItemCount(); i++) {
+ world.getItemAt( i ).setOpacity( opacity );
+ }
+ return this;
+ },
+
+ /**
+ * Get the opacity of the drawer.
+ * @returns {Number}
+ */
+ getOpacity: function() {
+ $.console.error("drawer.getOpacity is deprecated. Use tiledImage.getOpacity instead.");
+ var world = this.viewer.world;
+ var maxOpacity = 0;
+ for (var i = 0; i < world.getItemCount(); i++) {
+ var opacity = world.getItemAt( i ).getOpacity();
+ if ( opacity > maxOpacity ) {
+ maxOpacity = opacity;
+ }
+ }
+ return maxOpacity;
+ },
+
+ // deprecated
+ needsUpdate: function() {
+ $.console.error( "[Drawer.needsUpdate] this function is deprecated. Use World.needsDraw instead." );
+ return this.viewer.world.needsDraw();
+ },
+
+ // deprecated
+ numTilesLoaded: function() {
+ $.console.error( "[Drawer.numTilesLoaded] this function is deprecated. Use TileCache.numTilesLoaded instead." );
+ return this.viewer.tileCache.numTilesLoaded();
+ },
+
+ // deprecated
+ reset: function() {
+ $.console.error( "[Drawer.reset] this function is deprecated. Use World.resetItems instead." );
+ this.viewer.world.resetItems();
+ return this;
+ },
+
+ // deprecated
+ update: function() {
+ $.console.error( "[Drawer.update] this function is deprecated. Use Drawer.clear and World.draw instead." );
+ this.clear();
+ this.viewer.world.draw();
+ return this;
+ },
+
+ /**
+ * @returns {Boolean} True if rotation is supported.
+ */
+ canRotate: function() {
+ return this.useCanvas;
+ },
+
+ /**
+ * Destroy the drawer (unload current loaded tiles)
+ */
+ destroy: function() {
+ //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;
+ },
+
+ /**
+ * Clears the Drawer so it's ready to draw another frame.
+ */
+ clear: function() {
+ this.canvas.innerHTML = "";
+ if ( this.useCanvas ) {
+ var 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 ) {
+ var sketchCanvasSize = this._calculateSketchCanvasSize();
+ this.sketchCanvas.width = sketchCanvasSize.x;
+ this.sketchCanvas.height = sketchCanvasSize.y;
+ this._updateImageSmoothingEnabled(this.sketchContext);
+ }
+ }
+ this._clear();
+ }
+ },
+
+ _clear: function (useSketch, bounds) {
+ if (!this.useCanvas) {
+ return;
+ }
+ var context = this._getContext(useSketch);
+ if (bounds) {
+ context.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);
+ } else {
+ var canvas = context.canvas;
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ }
+ },
+
+ /**
+ * 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: function(rectangle) {
+ var topLeft = this.viewport.pixelFromPointNoRotate(rectangle.getTopLeft(), true);
+ var size = this.viewport.deltaPixelsFromPointsNoRotate(rectangle.getSize(), true);
+
+ return new $.Rect(
+ topLeft.x * $.pixelDensityRatio,
+ topLeft.y * $.pixelDensityRatio,
+ size.x * $.pixelDensityRatio,
+ size.y * $.pixelDensityRatio
+ );
+ },
+
+ /**
+ * Draws the given tile.
+ * @param {OpenSeadragon.Tile} tile - The tile to draw.
+ * @param {Function} drawingHandler - Method for firing the drawing event if using canvas.
+ * drawingHandler({context, tile, rendered})
+ * @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: function( tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize, source) {
+ $.console.assert(tile, '[Drawer.drawTile] tile is required');
+ $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required');
+
+ if (this.useCanvas) {
+ var context = this._getContext(useSketch);
+ scale = scale || 1;
+ tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source);
+ } else {
+ tile.drawHTML( this.canvas );
+ }
+ },
+
+ _getContext: function( useSketch ) {
+ var context = this.context;
+ if ( useSketch ) {
+ if (this.sketchCanvas === null) {
+ this.sketchCanvas = document.createElement( "canvas" );
+ var 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) {
+ var self = this;
+ this.viewer.addHandler('rotate', function resizeSketchCanvas() {
+ if (self.viewport.getRotation() === 0) {
+ return;
+ }
+ self.viewer.removeHandler('rotate', resizeSketchCanvas);
+ var sketchCanvasSize = self._calculateSketchCanvasSize();
+ self.sketchCanvas.width = sketchCanvasSize.x;
+ self.sketchCanvas.height = sketchCanvasSize.y;
+ });
+ }
+ this._updateImageSmoothingEnabled(this.sketchContext);
+ }
+ context = this.sketchContext;
+ }
+ return context;
+ },
+
+ // private
+ saveContext: function( useSketch ) {
+ if (!this.useCanvas) {
+ return;
+ }
+
+ this._getContext( useSketch ).save();
+ },
+
+ // private
+ restoreContext: function( useSketch ) {
+ if (!this.useCanvas) {
+ return;
+ }
+
+ this._getContext( useSketch ).restore();
+ },
+
+ // private
+ setClip: function(rect, useSketch) {
+ if (!this.useCanvas) {
+ return;
+ }
+
+ var context = this._getContext( useSketch );
+ context.beginPath();
+ context.rect(rect.x, rect.y, rect.width, rect.height);
+ context.clip();
+ },
+
+ // private
+ drawRectangle: function(rect, fillStyle, useSketch) {
+ if (!this.useCanvas) {
+ return;
+ }
+
+ var 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: function(opacity, scale, translate, compositeOperation) {
+ var options = opacity;
+ if (!$.isPlainObject(options)) {
+ options = {
+ opacity: opacity,
+ scale: scale,
+ translate: translate,
+ compositeOperation: compositeOperation
+ };
+ }
+ if (!this.useCanvas || !this.sketchCanvas) {
+ return;
+ }
+ opacity = options.opacity;
+ compositeOperation = options.compositeOperation;
+ var 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;
+ var position = translate instanceof $.Point ?
+ translate : new $.Point(0, 0);
+
+ var widthExt = 0;
+ var heightExt = 0;
+ if (translate) {
+ var widthDiff = this.sketchCanvas.width - this.canvas.width;
+ var 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
+ drawDebugInfo: function(tile, count, i, tiledImage) {
+ if ( !this.useCanvas ) {
+ return;
+ }
+
+ var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length;
+ var 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];
+
+ if (this.viewport.getRotation(true) % 360 !== 0 ) {
+ this._offsetForRotation({degrees: this.viewport.getRotation(true)});
+ }
+ if (tiledImage.getRotation(true) % 360 !== 0) {
+ this._offsetForRotation({
+ degrees: tiledImage.getRotation(true),
+ point: tiledImage.viewport.pixelFromPointNoRotate(
+ tiledImage._getRotationPoint(true), true)
+ });
+ }
+ if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
+ tiledImage.getRotation(true) % 360 === 0) {
+ if(tiledImage._drawer.viewer.viewport.getFlip()) {
+ tiledImage._drawer._flip();
+ }
+ }
+
+ context.strokeRect(
+ tile.position.x * $.pixelDensityRatio,
+ tile.position.y * $.pixelDensityRatio,
+ tile.size.x * $.pixelDensityRatio,
+ tile.size.y * $.pixelDensityRatio
+ );
+
+ var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio;
+ var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio;
+
+ // Rotate the text the right way around.
+ context.translate( tileCenterX, tileCenterY );
+ context.rotate( Math.PI / 180 * -this.viewport.getRotation(true) );
+ 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();
+ }
+
+ if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
+ tiledImage.getRotation(true) % 360 === 0) {
+ if(tiledImage._drawer.viewer.viewport.getFlip()) {
+ tiledImage._drawer._flip();
+ }
+ }
+
+ context.restore();
+ },
+
+ // private
+ debugRect: function(rect) {
+ if ( this.useCanvas ) {
+ var 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();
+ }
+ },
+
+ /**
+ * 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: function(imageSmoothingEnabled){
+ if ( this.useCanvas ) {
+ this._imageSmoothingEnabled = imageSmoothingEnabled;
+ this._updateImageSmoothingEnabled(this.context);
+ this.viewer.forceRedraw();
+ }
+ },
+
+ // private
+ _updateImageSmoothingEnabled: function(context){
+ context.msImageSmoothingEnabled = this._imageSmoothingEnabled;
+ context.imageSmoothingEnabled = this._imageSmoothingEnabled;
+ },
+
+ /**
+ * Get the canvas size
+ * @param {Boolean} sketch If set to true return the size of the sketch canvas
+ * @returns {OpenSeadragon.Point} The size of the canvas
+ */
+ getCanvasSize: function(sketch) {
+ var canvas = this._getContext(sketch).canvas;
+ return new $.Point(canvas.width, canvas.height);
+ },
+
+ getCanvasCenter: function() {
+ return new $.Point(this.canvas.width / 2, this.canvas.height / 2);
+ },
+
+ // private
+ _offsetForRotation: function(options) {
+ var point = options.point ?
+ options.point.times($.pixelDensityRatio) :
+ this.getCanvasCenter();
+
+ var context = this._getContext(options.useSketch);
+ context.save();
+
+ context.translate(point.x, point.y);
+ if(this.viewer.viewport.flipped){
+ context.rotate(Math.PI / 180 * -options.degrees);
+ context.scale(-1, 1);
+ } else{
+ context.rotate(Math.PI / 180 * options.degrees);
+ }
+ context.translate(-point.x, -point.y);
+ },
+
+ // private
+ _flip: function(options) {
+ options = options || {};
+ var point = options.point ?
+ options.point.times($.pixelDensityRatio) :
+ this.getCanvasCenter();
+ var context = this._getContext(options.useSketch);
+
+ context.translate(point.x, 0);
+ context.scale(-1, 1);
+ context.translate(-point.x, 0);
+ },
+
+ // private
+ _restoreRotationChanges: function(useSketch) {
+ var context = this._getContext(useSketch);
+ context.restore();
+ },
+
+ // private
+ _calculateCanvasSize: function() {
+ var pixelDensityRatio = $.pixelDensityRatio;
+ var 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: function() {
+ var 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.
+ var sketchCanvasSize = Math.ceil(Math.sqrt(
+ canvasSize.x * canvasSize.x +
+ canvasSize.y * canvasSize.y));
+ return {
+ x: sketchCanvasSize,
+ y: sketchCanvasSize
+ };
+ }
+};
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - Viewport
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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
+ var args = arguments;
+ if (args.length && args[0] instanceof $.Point) {
+ options = {
+ containerSize: args[0],
+ contentSize: args[1],
+ config: args[2]
+ };
+ }
+
+ //options.config and the general config argument are deprecated
+ //in favor of the more direct specification of optional settings
+ //being 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._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;
+ }
+
+ var aspectFactor = this._contentAspectRatio / this.getAspectRatio();
+ var 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() {
+ var center = this._contentBounds.getCenter();
+ var width = 1.0 / this.getHomeZoom();
+ var 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() {
+ var homeZoom = this.getHomeZoom(),
+ zoom = this.minZoomLevel ?
+ this.minZoomLevel :
+ this.minZoomImageRatio * homeZoom;
+
+ return zoom;
+ },
+
+ /**
+ * @function
+ */
+ getMaxZoom: function() {
+ var 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) {
+ var center = this.getCenter(current);
+ var width = 1.0 / this.getZoom(current);
+ var 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) {
+ var bounds = this.getBoundsNoRotate(current);
+ var 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 ) {
+ var centerCurrent = new $.Point(
+ this.centerSpringX.current.value,
+ this.centerSpringY.current.value
+ ),
+ centerTarget = new $.Point(
+ this.centerSpringX.target.value,
+ this.centerSpringY.target.value
+ ),
+ oldZoomPixel,
+ zoom,
+ width,
+ height,
+ bounds,
+ newZoomPixel,
+ deltaZoomPixels,
+ deltaZoomPoints;
+
+ if ( current ) {
+ return centerCurrent;
+ } else if ( !this.zoomPoint ) {
+ return centerTarget;
+ }
+
+ oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
+
+ zoom = this.getZoom();
+ width = 1.0 / zoom;
+ height = width / this.getAspectRatio();
+ bounds = new $.Rect(
+ centerCurrent.x - width / 2.0,
+ centerCurrent.y - height / 2.0,
+ width,
+ height
+ );
+
+ newZoomPixel = this._pixelFromPoint(this.zoomPoint, bounds);
+ deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ).rotate(-this.getRotation(true));
+ 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) {
+ var newBounds = this.viewportToViewerElementRectangle(bounds).getBoundingBox();
+ var cb = this.viewportToViewerElementRectangle(this._contentBoundsNoRotate).getBoundingBox();
+
+ var xConstrained = false;
+ var yConstrained = false;
+
+ if (this.wrapHorizontal) {
+ //do nothing
+ } else {
+ var boundsRight = newBounds.x + newBounds.width;
+ var contentRight = cb.x + cb.width;
+
+ var 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 {
+ var boundsBottom = newBounds.y + newBounds.height;
+ var contentBottom = cb.y + cb.height;
+
+ var 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;
+ }
+
+ }
+
+ var constraintApplied = xConstrained || yConstrained;
+ var 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) {
+ var actualZoom = this.getZoom();
+ var constrainedZoom = this._applyZoomConstraints(actualZoom);
+
+ if (actualZoom !== constrainedZoom) {
+ this.zoomTo(constrainedZoom, this.zoomPoint, immediately);
+ }
+
+ var 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 || {};
+ var immediately = options.immediately || false;
+ var constraints = options.constraints || false;
+
+ var aspect = this.getAspectRatio();
+ var center = bounds.getCenter();
+
+ // Compute width and height of bounding box.
+ var 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;
+ var newZoom = 1.0 / newBounds.width;
+
+ if (immediately) {
+ this.panTo(center, true);
+ this.zoomTo(newZoom, null, true);
+ if(constraints){
+ this.applyConstraints(true);
+ }
+ return this;
+ }
+
+ var currentCenter = this.getCenter(true);
+ var currentZoom = this.getZoom(true);
+ this.panTo(currentCenter, true);
+ this.zoomTo(currentZoom, null, true);
+
+ var oldBounds = this.getBounds();
+ var 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);
+
+ var constrainedBounds = this.getConstrainedBounds();
+
+ this.panTo(currentCenter, true);
+ this.zoomTo(currentZoom, null, true);
+
+ this.fitBounds(constrainedBounds);
+ } else {
+ var rotatedNewBounds = newBounds.rotate(-this.getRotation());
+ var 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) {
+ var 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) {
+ var 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) {
+ var bounds,
+ constrainedBounds;
+
+ bounds = this.getBounds(current);
+
+ 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 ) {
+ var center = new $.Point(
+ this.centerSpringX.target.value,
+ 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) {
+ var _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){
+ var changeInDegrees = degrees - this._oldDegrees;
+ if(!changeInDegrees){
+ this.rotationPivot = null;
+ return this;
+ }
+ this._rotateAboutPivot(degrees);
+ } else{
+ this.degreesSpring.resetTo(degrees);
+ }
+ } else {
+ var normalizedFrom = $.positiveModulo(this.degreesSpring.current.value, 360);
+ var normalizedTo = $.positiveModulo(degrees, 360);
+ var diff = normalizedTo - normalizedFrom;
+ if (diff > 180) {
+ normalizedTo -= 360;
+ } else if (diff < -180) {
+ normalizedTo += 360;
+ }
+
+ var 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 ) {
+ var oldBounds = this.getBoundsNoRotate(),
+ newBounds = oldBounds,
+ widthDeltaFactor;
+
+ 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
+ });
+ }
+
+ var 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 any change has been made, false otherwise.
+ */
+ update: function() {
+ var _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();
+ }
+
+
+ var 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._oldCenterX = this.centerSpringX.current.value;
+ this._oldCenterY = this.centerSpringY.current.value;
+ this._oldZoom = this.zoomSpring.current.value;
+ this._oldDegrees = this.degreesSpring.current.value;
+
+ return changed;
+ },
+
+ // private - pass true to use spring, or a number for degrees for immediate rotation
+ _rotateAboutPivot: function(degreesOrUseSpring){
+ var useSpring = degreesOrUseSpring === true;
+
+ var 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);
+ }
+
+ var changeInDegrees = this.degreesSpring.current.value - this._oldDegrees;
+ var rdelta = delta.rotate(changeInDegrees * -1).times(-1);
+ this.centerSpringX.shiftBy(rdelta.x);
+ this.centerSpringY.shiftBy(rdelta.y);
+ },
+
+ // private
+ _adjustCenterSpringsForZoomPoint: function(zoomSpringHandler) {
+ if (this.zoomPoint) {
+ var oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
+ zoomSpringHandler();
+ var newZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
+
+ var deltaZoomPixels = newZoomPixel.minus(oldZoomPixel);
+ var 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) {
+ var 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 ) {
+ var 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) {
+ var 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.
+ var 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 ) {
+ var 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) {
+ var 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.
+ var item = this.viewer.world.getItemAt(0);
+ return item.imageToViewportCoordinates(imageX, imageY, true);
+ }
+ }
+
+ var 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) {
+ var rect = imageX;
+ if (!(rect instanceof $.Rect)) {
+ //they passed individual components instead of a rectangle
+ rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight);
+ }
+
+ if (this.viewer) {
+ var 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.
+ var item = this.viewer.world.getItemAt(0);
+ return item.imageToViewportRectangle(
+ imageX, imageY, pixelWidth, pixelHeight, true);
+ }
+ }
+
+ var coordA = this.imageToViewportCoordinates(rect.x, rect.y);
+ var 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) {
+ var rect = viewerX;
+ if (!(rect instanceof $.Rect)) {
+ //they passed individual components instead of a rectangle
+ rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight);
+ }
+
+ if (this.viewer) {
+ var 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.
+ var item = this.viewer.world.getItemAt(0);
+ return item.viewportToImageRectangle(
+ viewerX, viewerY, pointWidth, pointHeight, true);
+ }
+ }
+
+ var coordA = this.viewportToImageCoordinates(rect.x, rect.y);
+ var 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 ) {
+ var 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 ) {
+ var 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.");
+ var viewerCoordinates = pixel.minus(
+ $.getElementPosition(this.viewer.element));
+ 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.");
+ var viewerCoordinates = this.imageToViewerElementCoordinates(pixel);
+ return viewerCoordinates.plus(
+ $.getElementPosition(this.viewer.element));
+ },
+
+ /**
+ * 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.");
+ var viewerCoordinates = pixel.minus(
+ $.getElementPosition(this.viewer.element));
+ 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.");
+ var viewerCoordinates = this.viewportToViewerElementCoordinates(point);
+ return viewerCoordinates.plus(
+ $.getElementPosition(this.viewer.element));
+ },
+
+ /**
+ * 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) {
+ var 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.
+ var item = this.viewer.world.getItemAt(0);
+ return item.viewportToImageZoom(viewportZoom);
+ }
+ }
+
+ var imageWidth = this._contentSizeNoRotate.x;
+ var containerWidth = this._containerInnerSize.x;
+ var scale = this._contentBoundsNoRotate.width;
+ var 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.
+ * @function
+ * @param {Number} imageZoom The image zoom
+ * target zoom.
+ * @returns {Number} viewportZoom The viewport zoom
+ */
+ imageToViewportZoom: function(imageZoom) {
+ if (this.viewer) {
+ var count = this.viewer.world.getItemCount();
+ if (count > 1) {
+ if (!this.silenceMultiImageWarnings) {
+ $.console.error('[Viewport.imageToViewportZoom] is not accurate ' +
+ 'with multi-image.');
+ }
+ } else if (count === 1) {
+ // It is better to use TiledImage.imageToViewportZoom
+ // because this._contentBoundsNoRotate can not be relied on
+ // with clipping.
+ var item = this.viewer.world.getItemAt(0);
+ return item.imageToViewportZoom(imageZoom);
+ }
+ }
+
+ var imageWidth = this._contentSizeNoRotate.x;
+ var containerWidth = this._containerInnerSize.x;
+ var scale = this._contentBoundsNoRotate.width;
+ var 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;
+ }
+
+};
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - TiledImage
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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( $ ){
+
+/**
+ * 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.
+ */
+$.TiledImage = function( options ) {
+ var _this = this;
+ /**
+ * 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 );
+
+ 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;
+
+ var x = options.x || 0;
+ delete options.x;
+ var 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;
+
+ var 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;
+ }
+
+ var fitBounds = options.fitBounds;
+ delete options.fitBounds;
+ var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER;
+ delete options.fitBoundsPlacement;
+
+ var degrees = options.degrees || 0;
+ delete options.degrees;
+
+ var ajaxHeaders = options.ajaxHeaders;
+ delete options.ajaxHeaders;
+
+ $.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.
+ lastDrawn: [], // An unordered list of Tiles drawn last frame.
+ lastResetTime: 0, // Last time for which the tiledImage was reset.
+ _midDraw: false, // Is the tiledImage currently updating the viewport?
+ _needsDraw: 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.
+ //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,
+ blendTime: $.DEFAULT_SETTINGS.blendTime,
+ alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend,
+ minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio,
+ smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom,
+ iOSDevice: $.DEFAULT_SETTINGS.iOSDevice,
+ debugMode: $.DEFAULT_SETTINGS.debugMode,
+ crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy,
+ ajaxWithCredentials: $.DEFAULT_SETTINGS.ajaxWithCredentials,
+ placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle,
+ opacity: $.DEFAULT_SETTINGS.opacity,
+ preload: $.DEFAULT_SETTINGS.preload,
+ compositeOperation: $.DEFAULT_SETTINGS.compositeOperation,
+ subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency
+ }, 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);
+ }
+
+ // We need a callback to give image manipulation a chance to happen
+ this._drawingHandler = function(args) {
+ /**
+ * 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 drawer is using a <canvas>.
+ *
+ * @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 {OpenSeadragon.Tile} context - The HTML canvas context being drawn into.
+ * @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ _this.viewer.raiseEvent('tile-drawing', $.extend({
+ tiledImage: _this
+ }, args));
+ };
+
+ this._ownAjaxHeaders = {};
+ this.setAjaxHeaders(ajaxHeaders, false);
+};
+
+$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{
+ /**
+ * @returns {Boolean} Whether the TiledImage needs to be drawn.
+ */
+ needsDraw: function() {
+ return this._needsDraw;
+ },
+
+ /**
+ * @returns {Boolean} Whether all tiles necessary for this TiledImage to draw at the current view have been loaded.
+ */
+ getFullyLoaded: function() {
+ return this._fullyLoaded;
+ },
+
+ // 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
+ });
+ },
+
+ /**
+ * Clears all tiles and triggers an update on the next call to
+ * {@link OpenSeadragon.TiledImage#update}.
+ */
+ reset: function() {
+ this._tileCache.clearTilesFor(this);
+ this.lastResetTime = $.now();
+ this._needsDraw = true;
+ },
+
+ /**
+ * Updates the TiledImage's bounds, animating if needed.
+ * @returns {Boolean} Whether the TiledImage animated.
+ */
+ update: function() {
+ var xUpdated = this._xSpring.update();
+ var yUpdated = this._ySpring.update();
+ var scaleUpdated = this._scaleSpring.update();
+ var degreesUpdated = this._degreesSpring.update();
+
+ if (xUpdated || yUpdated || scaleUpdated || degreesUpdated) {
+ this._updateForScale();
+ this._needsDraw = true;
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Draws the TiledImage to its Drawer.
+ */
+ draw: function() {
+ if (this.opacity !== 0 || this._preload) {
+ this._midDraw = true;
+ this._updateViewport();
+ this._midDraw = false;
+ }
+ // Images with opacity 0 should not need to be drawn in future. this._needsDraw = false is set in this._updateViewport() for other images.
+ else {
+ this._needsDraw = false;
+ }
+ },
+
+ /**
+ * Destroy the TiledImage (unload current loaded tiles).
+ */
+ destroy: function() {
+ this.reset();
+
+ if (this.source.destroy) {
+ this.source.destroy();
+ }
+ },
+
+ /**
+ * 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) {
+ var bounds = this.getBoundsNoRotate(current);
+ if (this._clip) {
+ var worldWidth = current ?
+ this._worldWidthCurrent : this._worldWidthTarget;
+ var ratio = worldWidth / this.source.dimensions.x;
+ var 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 ) {
+ var numTiles = this.source.getNumTiles(level);
+ var xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
+ var yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
+ var bounds = this.source.getTileBounds(level, xMod, yMod);
+ if (this.getFlip()) {
+ bounds.x = 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() {
+ var topLeft = this.imageToWindowCoordinates(new $.Point(0, 0));
+ var bottomRight = this.imageToWindowCoordinates(this.getContentSize());
+ return new $.Point(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
+ },
+
+ // private
+ _viewportToImageDelta: function( viewerX, viewerY, current ) {
+ var 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) {
+ var 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 ) {
+ var 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;
+ }
+
+ var point = this._imageToViewportDelta(imageX, imageY);
+ 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) {
+ var rect = imageX;
+ if (rect instanceof $.Rect) {
+ //they passed a rect instead of individual components
+ current = imageY;
+ } else {
+ rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight);
+ }
+
+ var coordA = this.imageToViewportCoordinates(rect.getTopLeft(), current);
+ var 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 ) {
+ var rect = viewerX;
+ if (viewerX instanceof $.Rect) {
+ //they passed a rect instead of individual components
+ current = viewerY;
+ } else {
+ rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight);
+ }
+
+ var coordA = this.viewportToImageCoordinates(rect.getTopLeft(), current);
+ var 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 ) {
+ var 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 ) {
+ var 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 ) {
+ var 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 ) {
+ var 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) {
+ var 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 ) {
+ var 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...
+ * Note: not accurate with multi-image.
+ * @function
+ * @param {Number} imageZoom The image zoom
+ * @returns {Number} viewportZoom The viewport zoom
+ */
+ imageToViewportZoom: function( imageZoom ) {
+ var 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) {
+ var 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;
+ } else {
+ if (sameTarget) {
+ return;
+ }
+
+ this._xSpring.springTo(position.x);
+ this._ySpring.springTo(position.y);
+ this._needsDraw = 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 ) {
+
+ var isXYObject = function(obj) {
+ return obj instanceof $.Point || (typeof obj.x === 'number' && typeof obj.y === 'number');
+ };
+
+ var 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);
+ });
+ } catch (e) {
+ $.console.error('[TiledImage.setCroppingPolygons] Cropping polygon format not supported');
+ $.console.error(e);
+ this._croppingPolygons = null;
+ }
+ },
+
+ /**
+ * Resets the cropping polygons, thus next render will remove all cropping
+ * polygon effects.
+ */
+ resetCroppingPolygons: function() {
+ this._croppingPolygons = null;
+ },
+
+ /**
+ * 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;
+ var anchorProperties = $.Placement.properties[anchor];
+ var aspectRatio = this.contentAspectX;
+ var xOffset = 0;
+ var yOffset = 0;
+ var displayedWidthRatio = 1;
+ var 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
+ var height = bounds.height / displayedHeightRatio;
+ var 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
+ var width = bounds.width / displayedWidthRatio;
+ var 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._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;
+ this._needsDraw = true;
+ this._raiseBoundsChange();
+ },
+
+ /**
+ * @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) {
+ if (opacity === this.opacity) {
+ return;
+ }
+
+ this.opacity = opacity;
+ this._needsDraw = 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._raiseBoundsChange();
+ },
+
+ /**
+ * 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();
+ },
+
+ /**
+ * @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) {
+ 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
+ });
+ },
+
+ /**
+ * 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) {
+ var numTiles, xMod, yMod, tile;
+
+ for (var level in this.tilesMatrix) {
+ numTiles = this.source.getNumTiles(level);
+
+ for (var x in this.tilesMatrix[level]) {
+ xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
+
+ for (var y in this.tilesMatrix[level][x]) {
+ yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
+ tile = this.tilesMatrix[level][x][y];
+
+ tile.loadWithAjax = this.loadTilesWithAjax;
+ if (tile.loadWithAjax) {
+ var tileAjaxHeaders = this.source.getTileAjaxHeaders( level, xMod, yMod );
+ tile.ajaxHeaders = $.extend({}, this.ajaxHeaders, tileAjaxHeaders);
+ } else {
+ tile.ajaxHeaders = null;
+ }
+ }
+ }
+ }
+
+ for (var i = 0; i < this._imageLoader.jobQueue.length; i++) {
+ var job = this._imageLoader.jobQueue[i];
+ job.loadWithAjax = job.tile.loadWithAjax;
+ job.ajaxHeaders = job.tile.loadWithAjax ? job.tile.ajaxHeaders : null;
+ }
+ }
+ },
+
+ // private
+ _setScale: function(scale, immediately) {
+ var 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;
+ } else {
+ if (sameTarget) {
+ return;
+ }
+
+ this._scaleSpring.springTo(scale);
+ this._updateForScale();
+ this._needsDraw = 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() {
+ var lowestLevel = Math.max(
+ this.source.minLevel,
+ Math.floor(Math.log(this.minZoomImageRatio) / Math.log(2))
+ );
+ var currentZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate(
+ this.source.getPixelRatio(0), true).x *
+ this._scaleSpring.current.value;
+ var 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
+ };
+ },
+
+ /**
+ * @private
+ * @inner
+ * Pretty much every other line in this needs to be documented so it's clear
+ * how each piece of this routine contributes to the drawing process. That's
+ * why there are so many TODO's inside this function.
+ */
+ _updateViewport: function() {
+ this._needsDraw = false;
+ this._tilesLoading = 0;
+ this.loadingCoverage = {};
+
+ // Reset tile's internal drawn state
+ while (this.lastDrawn.length > 0) {
+ var tile = this.lastDrawn.pop();
+ tile.beingDrawn = false;
+ }
+
+ var viewport = this.viewport;
+ var drawArea = this._viewportToTiledImageRectangle(
+ viewport.getBoundsWithMargins(true));
+
+ if (!this.wrapHorizontal && !this.wrapVertical) {
+ var tiledImageBounds = this._viewportToTiledImageRectangle(
+ this.getClippedBounds(true));
+ drawArea = drawArea.intersection(tiledImageBounds);
+ if (drawArea === null) {
+ return;
+ }
+ }
+
+ var levelsInterval = this._getLevelsInterval();
+ var lowestLevel = levelsInterval.lowestLevel;
+ var highestLevel = levelsInterval.highestLevel;
+ var bestTile = null;
+ var haveDrawn = false;
+ var currentTime = $.now();
+
+ // Update any level that will be drawn
+ for (var level = highestLevel; level >= lowestLevel; level--) {
+ var drawLevel = false;
+
+ //Avoid calculations for draw if we have already drawn this
+ var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
+ this.source.getPixelRatio(level),
+ true
+ ).x * this._scaleSpring.current.value;
+
+ if (level === lowestLevel ||
+ (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) {
+ drawLevel = true;
+ haveDrawn = true;
+ } else if (!haveDrawn) {
+ continue;
+ }
+
+ //Perform calculations for draw if we haven't drawn this
+ var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
+ this.source.getPixelRatio(level),
+ false
+ ).x * this._scaleSpring.current.value;
+
+ var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate(
+ this.source.getPixelRatio(
+ Math.max(
+ this.source.getClosestLevel(),
+ 0
+ )
+ ),
+ false
+ ).x * this._scaleSpring.current.value;
+
+ var optimalRatio = this.immediateRender ? 1 : targetZeroRatio;
+ var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5);
+ var levelVisibility = optimalRatio / Math.abs(
+ optimalRatio - targetRenderPixelRatio
+ );
+
+ // Update the level and keep track of 'best' tile to load
+ bestTile = this._updateLevel(
+ haveDrawn,
+ drawLevel,
+ level,
+ levelOpacity,
+ levelVisibility,
+ drawArea,
+ currentTime,
+ bestTile
+ );
+
+ // Stop the loop if lower-res tiles would all be covered by
+ // already drawn tiles
+ if (this._providesCoverage(this.coverage, level)) {
+ break;
+ }
+ }
+
+ // Perform the actual drawing
+ this._drawTiles(this.lastDrawn);
+
+ // Load the new 'best' tile
+ if (bestTile && !bestTile.context2D) {
+ this._loadTile(bestTile, currentTime);
+ this._needsDraw = true;
+ this._setFullyLoaded(false);
+ } else {
+ this._setFullyLoaded(this._tilesLoading === 0);
+ }
+ },
+
+ // private
+ _getCornerTiles: function(level, topLeftBound, bottomRightBound) {
+ var leftX;
+ var 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);
+ }
+ var topY;
+ var bottomY;
+ var 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);
+ }
+
+ var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY));
+ var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY));
+ var 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,
+ };
+ },
+
+ /**
+ * Updates all tiles at a given resolution level.
+ * @private
+ * @param {Boolean} haveDrawn
+ * @param {Boolean} drawLevel
+ * @param {Number} level
+ * @param {Number} levelOpacity
+ * @param {Number} levelVisibility
+ * @param {OpenSeadragon.Rect} drawArea
+ * @param {Number} currentTime
+ * @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
+ */
+ _updateLevel: function(haveDrawn, drawLevel, level, levelOpacity,
+ levelVisibility, drawArea, currentTime, best) {
+
+ var topLeftBound = drawArea.getBoundingBox().getTopLeft();
+ var bottomRightBound = 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
+ * @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: haveDrawn,
+ level: level,
+ opacity: levelOpacity,
+ visibility: levelVisibility,
+ drawArea: drawArea,
+ topleft: topLeftBound,
+ bottomright: bottomRightBound,
+ currenttime: currentTime,
+ best: best
+ });
+ }
+
+ this._resetCoverage(this.coverage, level);
+ this._resetCoverage(this.loadingCoverage, level);
+
+ //OK, a new drawing so do your calculations
+ var cornerTiles = this._getCornerTiles(level, topLeftBound, bottomRightBound);
+ var topLeftTile = cornerTiles.topLeft;
+ var bottomRightTile = cornerTiles.bottomRight;
+ var numberOfTiles = this.source.getNumTiles(level);
+
+ var viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter());
+
+ 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.
+ bottomRightTile.x += 1;
+ if (!this.wrapHorizontal) {
+ bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1);
+ }
+ }
+
+ for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) {
+ for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) {
+
+ var flippedX;
+ if (this.getFlip()) {
+ var xMod = ( numberOfTiles.x + ( x % numberOfTiles.x ) ) % numberOfTiles.x;
+ flippedX = x + numberOfTiles.x - xMod - xMod - 1;
+ } else {
+ flippedX = x;
+ }
+
+ if (drawArea.intersection(this.getTileBounds(level, flippedX, y)) === null) {
+ // This tile is outside of the viewport, no need to draw it
+ continue;
+ }
+
+ best = this._updateTile(
+ drawLevel,
+ haveDrawn,
+ flippedX, y,
+ level,
+ levelOpacity,
+ levelVisibility,
+ viewportCenter,
+ numberOfTiles,
+ currentTime,
+ best
+ );
+ }
+ }
+
+ return best;
+ },
+
+ /**
+ * @private
+ * @inner
+ * Update a single tile at a particular resolution level.
+ * @param {Boolean} haveDrawn
+ * @param {Boolean} drawLevel
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} level
+ * @param {Number} levelOpacity
+ * @param {Number} levelVisibility
+ * @param {OpenSeadragon.Point} viewportCenter
+ * @param {Number} numberOfTiles
+ * @param {Number} currentTime
+ * @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
+ */
+ _updateTile: function( haveDrawn, drawLevel, x, y, level, levelOpacity,
+ levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
+
+ var tile = this._getTile(
+ x, y,
+ level,
+ currentTime,
+ numberOfTiles,
+ this._worldWidthCurrent,
+ this._worldHeightCurrent
+ ),
+ drawTile = drawLevel;
+
+ if( this.viewer ){
+ /**
+ * - Needs documentation -
+ *
+ * @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 );
+
+ var loadingCoverage = tile.loaded || tile.loading || this._isCovered(this.loadingCoverage, level, x, y);
+ this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage);
+
+ if ( !tile.exists ) {
+ return best;
+ }
+
+ if ( haveDrawn && !drawTile ) {
+ if ( this._isCovered( this.coverage, level, x, y ) ) {
+ this._setCoverage( this.coverage, level, x, y, true );
+ } else {
+ drawTile = true;
+ }
+ }
+
+ if ( !drawTile ) {
+ return best;
+ }
+
+ this._positionTile(
+ tile,
+ this.source.tileOverlap,
+ this.viewport,
+ viewportCenter,
+ levelVisibility
+ );
+
+ if (!tile.loaded) {
+ if (tile.context2D) {
+ this._setTileLoaded(tile);
+ } else {
+ var imageRecord = this._tileCache.getImageRecord(tile.cacheKey);
+ if (imageRecord) {
+ this._setTileLoaded(tile, imageRecord.getData());
+ }
+ }
+ }
+
+ if ( tile.loaded ) {
+ var needsDraw = this._blendTile(
+ tile,
+ x, y,
+ level,
+ levelOpacity,
+ currentTime
+ );
+
+ if ( needsDraw ) {
+ this._needsDraw = true;
+ }
+ } else if ( tile.loading ) {
+ // the tile is already in the download queue
+ this._tilesLoading++;
+ } else if (!loadingCoverage) {
+ best = this._compareTiles( best, tile );
+ }
+
+ return best;
+ },
+
+ /**
+ * @private
+ * @inner
+ * Obtains a tile at the given location.
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} level
+ * @param {Number} time
+ * @param {Number} numTiles
+ * @param {Number} worldWidth
+ * @param {Number} worldHeight
+ * @returns {OpenSeadragon.Tile}
+ */
+ _getTile: function(
+ x, y,
+ level,
+ time,
+ numTiles,
+ worldWidth,
+ worldHeight
+ ) {
+ var xMod,
+ yMod,
+ bounds,
+ sourceBounds,
+ exists,
+ urlOrGetter,
+ post,
+ ajaxHeaders,
+ context2D,
+ tile,
+ tilesMatrix = this.tilesMatrix,
+ tileSource = this.source;
+
+ if ( !tilesMatrix[ level ] ) {
+ tilesMatrix[ level ] = {};
+ }
+ if ( !tilesMatrix[ level ][ x ] ) {
+ tilesMatrix[ level ][ x ] = {};
+ }
+
+ if ( !tilesMatrix[ level ][ x ][ y ] || !tilesMatrix[ level ][ 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;
+ }
+
+ context2D = tileSource.getContext2D ?
+ tileSource.getContext2D(level, xMod, yMod) : undefined;
+
+ tile = new $.Tile(
+ level,
+ x,
+ y,
+ bounds,
+ exists,
+ urlOrGetter,
+ context2D,
+ 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;
+
+ tilesMatrix[ level ][ x ][ y ] = tile;
+ }
+
+ tile = tilesMatrix[ level ][ x ][ y ];
+ tile.lastTouchTime = time;
+
+ return tile;
+ },
+
+ /**
+ * @private
+ * @inner
+ * Dispatch a job to the ImageLoader to load the Image for a Tile.
+ * @param {OpenSeadragon.Tile} tile
+ * @param {Number} time
+ */
+ _loadTile: function(tile, time ) {
+ var _this = this;
+ tile.loading = true;
+ 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 ){
+ _this._onTileLoad( tile, time, data, errorMsg, tileRequest );
+ },
+ abort: function() {
+ tile.loading = false;
+ }
+ });
+ },
+
+ /**
+ * @private
+ * @inner
+ * Callback fired when a Tile's Image finished downloading.
+ * @param {OpenSeadragon.Tile} tile
+ * @param {Number} time
+ * @param {*} data image data
+ * @param {String} errorMsg
+ * @param {XMLHttpRequest} tileRequest
+ */
+ _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) {
+ if ( !data ) {
+ $.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 {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
+ });
+ 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;
+ }
+
+ var _this = this,
+ finish = function() {
+ var ccc = _this.source;
+ var cutoff = ccc.getClosestLevel();
+ _this._setTileLoaded(tile, data, cutoff, tileRequest);
+ };
+
+ // Check if we're mid-update; this can happen on IE8 because image load events for
+ // cached images happen immediately there
+ if ( !this._midDraw ) {
+ finish();
+ } else {
+ // Wait until after the update, in case caching unloads any tiles
+ window.setTimeout( finish, 1);
+ }
+ },
+
+ /**
+ * @private
+ * @inner
+ * @param {OpenSeadragon.Tile} tile
+ * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
+ * @param {Number|undefined} cutoff
+ * @param {XMLHttpRequest|undefined} tileRequest
+ */
+ _setTileLoaded: function(tile, data, cutoff, tileRequest) {
+ var increment = 0,
+ eventFinished = false,
+ _this = this;
+
+ 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 completionCallback() {
+ increment--;
+ if (increment === 0) {
+ tile.loading = false;
+ tile.loaded = true;
+ tile.hasTransparency = _this.source.hasTransparency(
+ tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData
+ );
+ if (!tile.context2D) {
+ _this._tileCache.cacheTile({
+ data: data,
+ tile: tile,
+ cutoff: cutoff,
+ tiledImage: _this
+ });
+ }
+ _this._needsDraw = true;
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @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
+ * @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 {function} getCompletionCallback - A function giving a callback to call
+ * when the asynchronous processing of the image is done. The image will be
+ * marked as entirely loaded when the callback has been called once for each
+ * call to getCompletionCallback.
+ */
+
+ var fallbackCompletion = getCompletionCallback();
+ this.viewer.raiseEvent("tile-loaded", {
+ tile: tile,
+ tiledImage: this,
+ tileRequest: tileRequest,
+ get image() {
+ $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead.");
+ return data;
+ },
+ data: data,
+ getCompletionCallback: getCompletionCallback
+ });
+ eventFinished = true;
+ // In case the completion callback is never called, we at least force it once.
+ fallbackCompletion();
+ },
+
+ /**
+ * @private
+ * @inner
+ * @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 ){
+ var 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;
+
+ var boundsSize = tile.bounds.getSize();
+
+ boundsSize.x *= this._scaleSpring.current.value;
+ boundsSize.y *= this._scaleSpring.current.value;
+
+ var positionC = viewport.pixelFromPointNoRotate(boundsTL, true),
+ positionT = viewport.pixelFromPointNoRotate(boundsTL, false),
+ sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true),
+ sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false),
+ tileCenter = positionT.plus( sizeT.divide( 2 ) ),
+ tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter );
+
+ 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
+ * @inner
+ * 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.
+ *
+ * @param {OpenSeadragon.Tile} tile
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} level
+ * @param {Number} levelOpacity
+ * @param {Number} currentTime
+ * @returns {Boolean}
+ */
+ _blendTile: function( tile, x, y, level, levelOpacity, currentTime ){
+ var 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.alwaysBlend ) {
+ opacity *= levelOpacity;
+ }
+
+ tile.opacity = opacity;
+
+ this.lastDrawn.push( tile );
+
+ if ( opacity === 1 ) {
+ this._setCoverage( this.coverage, level, x, y, true );
+ this._hasOpaqueTile = true;
+ } else if ( deltaTime < blendTimeMillis ) {
+ return true;
+ }
+
+ return false;
+ },
+
+
+ /**
+ * @private
+ * @inner
+ * Determines whether the 'last best' tile for the area is better than the
+ * tile in question.
+ *
+ * @param {OpenSeadragon.Tile} previousBest
+ * @param {OpenSeadragon.Tile} tile
+ * @returns {OpenSeadragon.Tile} The new best tile.
+ */
+ _compareTiles: function( previousBest, tile ) {
+ if ( !previousBest ) {
+ return tile;
+ }
+
+ if ( tile.visibility > previousBest.visibility ) {
+ return tile;
+ } else if ( tile.visibility === previousBest.visibility ) {
+ if ( tile.squaredDistance < previousBest.squaredDistance ) {
+ return tile;
+ }
+ }
+ return previousBest;
+ },
+
+ /**
+ * @private
+ * @inner
+ * Draws a TiledImage.
+ * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
+ */
+ _drawTiles: function( lastDrawn ) {
+ if (this.opacity === 0 || (lastDrawn.length === 0 && !this.placeholderFillStyle)) {
+ return;
+ }
+
+ var tile = lastDrawn[0];
+ var useSketch;
+
+ if (tile) {
+ useSketch = this.opacity < 1 ||
+ (this.compositeOperation && this.compositeOperation !== 'source-over') ||
+ (!this._isBottomItem() &&
+ this.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
+ }
+
+ var sketchScale;
+ var sketchTranslate;
+
+ var zoom = this.viewport.getZoom(true);
+ var imageZoom = this.viewportToImageZoom(zoom);
+
+ if (lastDrawn.length > 1 &&
+ imageZoom > this.smoothTileEdgesMinZoom &&
+ !this.iOSDevice &&
+ this.getRotation(true) % 360 === 0 && // TODO: support tile edge smoothing with tiled image rotation.
+ $.supportsCanvas && this.viewer.useCanvas) {
+ // 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;
+ sketchScale = tile.getScaleForEdgeSmoothing();
+ sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
+ this._drawer.getCanvasSize(false),
+ this._drawer.getCanvasSize(true));
+ }
+
+ var 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(
+ this.getClippedBounds(true))
+ .getIntegerBoundingBox();
+
+ if(this._drawer.viewer.viewport.getFlip()) {
+ if (this.viewport.getRotation(true) % 360 !== 0 ||
+ this.getRotation(true) % 360 !== 0) {
+ bounds.x = this._drawer.viewer.container.clientWidth - (bounds.x + bounds.width);
+ }
+ }
+
+ bounds = bounds.times($.pixelDensityRatio);
+ }
+ this._drawer._clear(true, bounds);
+ }
+
+ // When scaling, we must rotate only when blending the sketch canvas to
+ // avoid interpolation
+ if (!sketchScale) {
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._drawer._offsetForRotation({
+ degrees: this.viewport.getRotation(true),
+ useSketch: useSketch
+ });
+ }
+ if (this.getRotation(true) % 360 !== 0) {
+ this._drawer._offsetForRotation({
+ degrees: this.getRotation(true),
+ point: this.viewport.pixelFromPointNoRotate(
+ this._getRotationPoint(true), true),
+ useSketch: useSketch
+ });
+ }
+
+ if (this.viewport.getRotation(true) % 360 === 0 &&
+ this.getRotation(true) % 360 === 0) {
+ if(this._drawer.viewer.viewport.getFlip()) {
+ this._drawer._flip();
+ }
+ }
+ }
+
+ var usedClip = false;
+ if ( this._clip ) {
+ this._drawer.saveContext(useSketch);
+
+ var box = this.imageToViewportRectangle(this._clip, true);
+ box = box.rotate(-this.getRotation(true), this._getRotationPoint(true));
+ var clipRect = this._drawer.viewportToDrawerRectangle(box);
+ if (sketchScale) {
+ clipRect = clipRect.times(sketchScale);
+ }
+ if (sketchTranslate) {
+ clipRect = clipRect.translate(sketchTranslate);
+ }
+ this._drawer.setClip(clipRect, useSketch);
+
+ usedClip = true;
+ }
+
+ if (this._croppingPolygons) {
+ var self = this;
+ this._drawer.saveContext(useSketch);
+ try {
+ var polygons = this._croppingPolygons.map(function (polygon) {
+ return polygon.map(function (coord) {
+ var point = self
+ .imageToViewportCoordinates(coord.x, coord.y, true)
+ .rotate(-self.getRotation(true), self._getRotationPoint(true));
+ var clipPoint = self._drawer.viewportCoordToDrawerCoord(point);
+ if (sketchScale) {
+ clipPoint = clipPoint.times(sketchScale);
+ }
+ if (sketchTranslate) {
+ clipPoint = clipPoint.plus(sketchTranslate);
+ }
+ return clipPoint;
+ });
+ });
+ this._drawer.clipWithPolygons(polygons, useSketch);
+ } catch (e) {
+ $.console.error(e);
+ }
+ usedClip = true;
+ }
+
+ if ( this.placeholderFillStyle && this._hasOpaqueTile === false ) {
+ var placeholderRect = this._drawer.viewportToDrawerRectangle(this.getBounds(true));
+ if (sketchScale) {
+ placeholderRect = placeholderRect.times(sketchScale);
+ }
+ if (sketchTranslate) {
+ placeholderRect = placeholderRect.translate(sketchTranslate);
+ }
+
+ var fillStyle = null;
+ if ( typeof this.placeholderFillStyle === "function" ) {
+ fillStyle = this.placeholderFillStyle(this, this._drawer.context);
+ }
+ else {
+ fillStyle = this.placeholderFillStyle;
+ }
+
+ this._drawer.drawRectangle(placeholderRect, fillStyle, useSketch);
+ }
+
+ var subPixelRoundingRule = determineSubPixelRoundingRule(this.subPixelRoundingForTransparency);
+
+ var shouldRoundPositionAndSize = false;
+
+ if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) {
+ shouldRoundPositionAndSize = true;
+ } else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) {
+ var isAnimating = this.viewer && this.viewer.isAnimating();
+ shouldRoundPositionAndSize = !isAnimating;
+ }
+
+ for (var i = lastDrawn.length - 1; i >= 0; i--) {
+ tile = lastDrawn[ i ];
+ this._drawer.drawTile( tile, this._drawingHandler, useSketch, sketchScale,
+ sketchTranslate, shouldRoundPositionAndSize, this.source );
+ tile.beingDrawn = true;
+
+ if( this.viewer ){
+ /**
+ * - Needs documentation -
+ *
+ * @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: this,
+ tile: tile
+ });
+ }
+ }
+
+ if ( usedClip ) {
+ this._drawer.restoreContext( useSketch );
+ }
+
+ if (!sketchScale) {
+ if (this.getRotation(true) % 360 !== 0) {
+ this._drawer._restoreRotationChanges(useSketch);
+ }
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._drawer._restoreRotationChanges(useSketch);
+ }
+ }
+
+ if (useSketch) {
+ if (sketchScale) {
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._drawer._offsetForRotation({
+ degrees: this.viewport.getRotation(true),
+ useSketch: false
+ });
+ }
+ if (this.getRotation(true) % 360 !== 0) {
+ this._drawer._offsetForRotation({
+ degrees: this.getRotation(true),
+ point: this.viewport.pixelFromPointNoRotate(
+ this._getRotationPoint(true), true),
+ useSketch: false
+ });
+ }
+ }
+ this._drawer.blendSketch({
+ opacity: this.opacity,
+ scale: sketchScale,
+ translate: sketchTranslate,
+ compositeOperation: this.compositeOperation,
+ bounds: bounds
+ });
+ if (sketchScale) {
+ if (this.getRotation(true) % 360 !== 0) {
+ this._drawer._restoreRotationChanges(false);
+ }
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._drawer._restoreRotationChanges(false);
+ }
+ }
+ }
+
+ if (!sketchScale) {
+ if (this.viewport.getRotation(true) % 360 === 0 &&
+ this.getRotation(true) % 360 === 0) {
+ if(this._drawer.viewer.viewport.getFlip()) {
+ this._drawer._flip();
+ }
+ }
+ }
+
+ this._drawDebugInfo( lastDrawn );
+ },
+
+ /**
+ * @private
+ * @inner
+ * Draws special debug information for a TiledImage if in debug mode.
+ * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
+ */
+ _drawDebugInfo: function( lastDrawn ) {
+ if( this.debugMode ) {
+ for ( var i = lastDrawn.length - 1; i >= 0; i-- ) {
+ var tile = lastDrawn[ i ];
+ try {
+ this._drawer.drawDebugInfo(tile, lastDrawn.length, i, this);
+ } catch(e) {
+ $.console.error(e);
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ * @inner
+ * Returns true if the given tile provides coverage to lower-level tiles of
+ * lower resolution representing the same content. If neither x nor y is
+ * given, returns true if the entire visible level provides coverage.
+ *
+ * Note that out-of-bounds tiles provide coverage in this sense, since
+ * there's no content that they would need to cover. Tiles at non-existent
+ * levels that are within the image bounds, however, do not.
+ *
+ * @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 ) {
+ var rows,
+ cols,
+ 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
+ );
+ },
+
+ /**
+ * @private
+ * @inner
+ * Returns true if the given tile is completely covered by higher-level
+ * tiles of higher resolution representing the same content. If neither x
+ * nor y is given, returns true if the entire visible level is covered.
+ *
+ * @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 )
+ );
+ }
+ },
+
+ /**
+ * @private
+ * @inner
+ * Sets whether the given tile provides coverage or not.
+ *
+ * @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;
+ },
+
+ /**
+ * @private
+ * @inner
+ * Resets coverage information for the given level. This should be called
+ * after every draw routine. Note that at the beginning of the next draw
+ * routine, coverage for every visible tile should be explicitly set.
+ *
+ * @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 ] = {};
+ }
+});
+
+
+/**
+ * @private
+ * @inner
+ * Defines the value for subpixel rounding to fallback to in case of missing or
+ * invalid value.
+ */
+var DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
+
+/**
+ * @private
+ * @inner
+ * Checks whether the input value is an invalid subpixel rounding enum value.
+ *
+ * @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;
+}
+
+/**
+ * @private
+ * @inner
+ * 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.
+ *
+ * @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;
+}
+
+/**
+ * @private
+ * @inner
+ * Ensures the returned value is always a valid subpixel rounding enum value,
+ * defaulting to 'NEVER' if input is missing or invalid.
+ *
+ * @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;
+ }
+
+ var subPixelRoundingRule = subPixelRoundingRules[$.Browser.vendor];
+
+ if (isSubPixelRoundingRuleUnknown(subPixelRoundingRule)) {
+ subPixelRoundingRule = subPixelRoundingRules['*'];
+ }
+
+ return normalizeSubPixelRoundingRule(subPixelRoundingRule);
+}
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - TileCache
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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( $ ){
+
+// private class
+var TileRecord = function( options ) {
+ $.console.assert( options, "[TileCache.cacheTile] options is required" );
+ $.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" );
+ $.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" );
+ this.tile = options.tile;
+ this.tiledImage = options.tiledImage;
+};
+
+// private class
+var ImageRecord = function(options) {
+ $.console.assert( options, "[ImageRecord] options is required" );
+ $.console.assert( options.data, "[ImageRecord] options.data is required" );
+ this._tiles = [];
+
+ options.create.apply(null, [this, options.data, options.ownerTile]);
+ this._destroyImplementation = options.destroy.bind(null, this);
+ this.getImage = options.getImage.bind(null, this);
+ this.getData = options.getData.bind(null, this);
+ this.getRenderedContext = options.getRenderedContext.bind(null, this);
+};
+
+ImageRecord.prototype = {
+ destroy: function() {
+ this._destroyImplementation();
+ this._tiles = null;
+ },
+
+ addTile: function(tile) {
+ $.console.assert(tile, '[ImageRecord.addTile] tile is required');
+ this._tiles.push(tile);
+ },
+
+ removeTile: function(tile) {
+ for (var i = 0; i < this._tiles.length; i++) {
+ if (this._tiles[i] === tile) {
+ this._tiles.splice(i, 1);
+ return;
+ }
+ }
+
+ $.console.warn('[ImageRecord.removeTile] trying to remove unknown tile', tile);
+ },
+
+ getTileCount: function() {
+ return this._tiles.length;
+ }
+};
+
+/**
+ * @class 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.
+ */
+$.TileCache = function( options ) {
+ options = options || {};
+
+ this._maxImageCacheCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount;
+ this._tilesLoaded = [];
+ this._imagesLoaded = [];
+ this._imagesLoadedCount = 0;
+};
+
+/** @lends OpenSeadragon.TileCache.prototype */
+$.TileCache.prototype = {
+ /**
+ * @returns {Number} The total number of tiles that have been loaded by
+ * this TileCache.
+ */
+ numTilesLoaded: function() {
+ return this._tilesLoaded.length;
+ },
+
+ /**
+ * 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.
+ * @param {Object} options - Tile info.
+ * @param {OpenSeadragon.Tile} options.tile - The tile to cache.
+ * @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache.
+ * @param {Image} options.image - The image of the tile to cache.
+ * @param {OpenSeadragon.TiledImage} options.tiledImage - The TiledImage that owns that tile.
+ * @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.
+ */
+ cacheTile: function( options ) {
+ $.console.assert( options, "[TileCache.cacheTile] options is required" );
+ $.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" );
+ $.console.assert( options.tile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required" );
+ $.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" );
+
+ var cutoff = options.cutoff || 0;
+ var insertionIndex = this._tilesLoaded.length;
+
+ var imageRecord = this._imagesLoaded[options.tile.cacheKey];
+ if (!imageRecord) {
+
+ if (!options.data) {
+ $.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;
+ }
+
+ $.console.assert( options.data, "[TileCache.cacheTile] options.data is required to create an ImageRecord" );
+ imageRecord = this._imagesLoaded[options.tile.cacheKey] = new ImageRecord({
+ data: options.data,
+ ownerTile: options.tile,
+ create: options.tiledImage.source.createTileCache,
+ destroy: options.tiledImage.source.destroyTileCache,
+ getImage: options.tiledImage.source.getTileCacheDataAsImage,
+ getData: options.tiledImage.source.getTileCacheData,
+ getRenderedContext: options.tiledImage.source.getTileCacheDataAsContext2D,
+ });
+
+ this._imagesLoadedCount++;
+ }
+
+ imageRecord.addTile(options.tile);
+ options.tile.cacheImageRecord = imageRecord;
+
+ // Note that just because we're unloading a tile doesn't necessarily mean
+ // we're unloading an image. With repeated calls it should sort itself out, though.
+ if ( this._imagesLoadedCount > this._maxImageCacheCount ) {
+ var worstTile = null;
+ var worstTileIndex = -1;
+ var worstTileRecord = null;
+ var prevTile, worstTime, worstLevel, prevTime, prevLevel, prevTileRecord;
+
+ for ( var i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
+ prevTileRecord = this._tilesLoaded[ i ];
+ prevTile = prevTileRecord.tile;
+
+ if ( prevTile.level <= cutoff || prevTile.beingDrawn ) {
+ continue;
+ } else if ( !worstTile ) {
+ worstTile = prevTile;
+ worstTileIndex = i;
+ worstTileRecord = prevTileRecord;
+ continue;
+ }
+
+ prevTime = prevTile.lastTouchTime;
+ worstTime = worstTile.lastTouchTime;
+ prevLevel = prevTile.level;
+ worstLevel = worstTile.level;
+
+ if ( prevTime < worstTime ||
+ ( prevTime === worstTime && prevLevel > worstLevel ) ) {
+ worstTile = prevTile;
+ worstTileIndex = i;
+ worstTileRecord = prevTileRecord;
+ }
+ }
+
+ if ( worstTile && worstTileIndex >= 0 ) {
+ this._unloadTile(worstTileRecord);
+ insertionIndex = worstTileIndex;
+ }
+ }
+
+ this._tilesLoaded[ insertionIndex ] = new TileRecord({
+ tile: options.tile,
+ tiledImage: options.tiledImage
+ });
+ },
+
+ /**
+ * Clears all tiles associated with the specified tiledImage.
+ * @param {OpenSeadragon.TiledImage} tiledImage
+ */
+ clearTilesFor: function( tiledImage ) {
+ $.console.assert(tiledImage, '[TileCache.clearTilesFor] tiledImage is required');
+ var tileRecord;
+ for ( var i = 0; i < this._tilesLoaded.length; ++i ) {
+ tileRecord = this._tilesLoaded[ i ];
+ if ( tileRecord.tiledImage === tiledImage ) {
+ this._unloadTile(tileRecord);
+ this._tilesLoaded.splice( i, 1 );
+ i--;
+ }
+ }
+ },
+
+ // private
+ getImageRecord: function(cacheKey) {
+ $.console.assert(cacheKey, '[TileCache.getImageRecord] cacheKey is required');
+ return this._imagesLoaded[cacheKey];
+ },
+
+ // private
+ _unloadTile: function(tileRecord) {
+ $.console.assert(tileRecord, '[TileCache._unloadTile] tileRecord is required');
+ var tile = tileRecord.tile;
+ var tiledImage = tileRecord.tiledImage;
+
+ tile.unload();
+ tile.cacheImageRecord = null;
+
+ var imageRecord = this._imagesLoaded[tile.cacheKey];
+ imageRecord.removeTile(tile);
+ if (!imageRecord.getTileCount()) {
+ imageRecord.destroy();
+ delete this._imagesLoaded[tile.cacheKey];
+ this._imagesLoadedCount--;
+ }
+
+ /**
+ * Triggered when a tile has just been unloaded from memory.
+ *
+ * @event tile-unloaded
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the unloaded tile.
+ * @property {OpenSeadragon.Tile} tile - The tile which has been unloaded.
+ */
+ tiledImage.viewer.raiseEvent("tile-unloaded", {
+ tile: tile,
+ tiledImage: tiledImage
+ });
+ }
+};
+
+}( OpenSeadragon ));
+
+/*
+ * OpenSeadragon - World
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2022 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 ) {
+ var _this = this;
+
+ $.console.assert( options.viewer, "[World] options.viewer is required" );
+
+ $.EventSource.call( this );
+
+ this.viewer = options.viewer;
+ this._items = [];
+ this._needsDraw = false;
+ 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 {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) {
+ var 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");
+
+ var oldIndex = this.getIndexOfItem( item );
+
+ if ( index >= this._items.length ) {
+ throw new Error( "Index bigger than number of layers." );
+ }
+
+ if ( index === oldIndex || oldIndex === -1 ) {
+ return;
+ }
+
+ 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");
+
+ var 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();
+ var item;
+ var i;
+ for (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();
+ }
+
+ var removedItems = this._items;
+ this._items = [];
+ this._figureSizes();
+ this._needsDraw = true;
+
+ for (i = 0; i < removedItems.length; i++) {
+ item = removedItems[i];
+ this._raiseRemoveItem(item);
+ }
+ },
+
+ /**
+ * Clears all tiles and triggers updates for all items.
+ */
+ resetItems: function() {
+ for ( var i = 0; i < this._items.length; i++ ) {
+ this._items[i].reset();
+ }
+ },
+
+ /**
+ * Updates (i.e. animates bounds of) all items.
+ */
+ update: function() {
+ var animated = false;
+ for ( var i = 0; i < this._items.length; i++ ) {
+ animated = this._items[i].update() || animated;
+ }
+
+ return animated;
+ },
+
+ /**
+ * Draws all items.
+ */
+ draw: function() {
+ for ( var i = 0; i < this._items.length; i++ ) {
+ this._items[i].draw();
+ }
+
+ this._needsDraw = false;
+ },
+
+ /**
+ * @returns {Boolean} true if any items need updating.
+ */
+ needsDraw: function() {
+ for ( var 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 || {};
+ var immediately = options.immediately || false;
+ var layout = options.layout || $.DEFAULT_SETTINGS.collectionLayout;
+ var rows = options.rows || $.DEFAULT_SETTINGS.collectionRows;
+ var columns = options.columns || $.DEFAULT_SETTINGS.collectionColumns;
+ var tileSize = options.tileSize || $.DEFAULT_SETTINGS.collectionTileSize;
+ var tileMargin = options.tileMargin || $.DEFAULT_SETTINGS.collectionTileMargin;
+ var increment = tileSize + tileMargin;
+ var wrap;
+ if (!options.rows && columns) {
+ wrap = columns;
+ } else {
+ wrap = Math.ceil(this._items.length / rows);
+ }
+ var x = 0;
+ var y = 0;
+ var item, box, width, height, position;
+
+ this.setAutoRefigureSizes(false);
+ for (var 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.getBounds();
+ 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() {
+ var oldHomeBounds = this._homeBounds ? this._homeBounds.clone() : null;
+ var oldContentSize = this._contentSize ? this._contentSize.clone() : null;
+ var 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 {
+ var item = this._items[0];
+ var bounds = item.getBounds();
+ this._contentFactor = item.getContentSize().x / bounds.width;
+ var clippedBounds = item.getClippedBounds().getBoundingBox();
+ var left = clippedBounds.x;
+ var top = clippedBounds.y;
+ var right = clippedBounds.x + clippedBounds.width;
+ var bottom = clippedBounds.y + clippedBounds.height;
+ for (var 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/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.js.map b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.js.map
new file mode 100644
index 0000000..e69de29
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.min.js b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.min.js
new file mode 100644
index 0000000..e69de29
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.min.js.map b/siteTheme-Bahn-ng/site/assets/js/openseadragon-4.1.0/openseadragon.min.js.map
new file mode 100644
index 0000000..e69de29
diff --git a/siteTheme-Bahn-ng/site/assets/js/openseadragon.js b/siteTheme-Bahn-ng/site/assets/js/openseadragon.js
new file mode 100644
index 0000000..b51a07f
--- /dev/null
+++ b/siteTheme-Bahn-ng/site/assets/js/openseadragon.js
@@ -0,0 +1,20 @@
+document.addEventListener('DOMContentLoaded', function() {
+ const imageDivs = document.querySelectorAll('.imageblock.zoomable');
+ var i = 1;
+ imageDivs.forEach(imageDiv => {
+
+ const imageUrl = imageDiv.querySelector('div img').getAttribute("src");
+ imageDiv.innerHTML = '';
+
+ OpenSeadragon({
+ id: "openseadragon"+i,
+ prefixUrl: toRoot+'js/openseadragon-4.1.0/images/',
+ tileSources: {
+ type: 'image',
+ url: imageUrl
+ }
+ });
+ console.log(toRoot);
+ i = i+1;
+ });
+});
diff --git a/siteTheme-Bahn-ng/site/groovy/menu.groovy b/siteTheme-Bahn-ng/site/groovy/menu.groovy
index 419f9e8..2cd04dd 100644
--- a/siteTheme-Bahn-ng/site/groovy/menu.groovy
+++ b/siteTheme-Bahn-ng/site/groovy/menu.groovy
@@ -13,7 +13,8 @@ try {
order: page['jbake-order'] as Integer,
filename: page['filename'],
uri: page['uri'],
- children: []
+ children: [],
+ date: page['date']
]
}
}
diff --git a/siteTheme-Bahn-ng/site/templates/footer.gsp b/siteTheme-Bahn-ng/site/templates/footer.gsp
index 472be9d..bb208ee 100644
--- a/siteTheme-Bahn-ng/site/templates/footer.gsp
+++ b/siteTheme-Bahn-ng/site/templates/footer.gsp
@@ -13,27 +13,27 @@