From c24252001c94f0b6997e5341dbd021d0dbc571e3 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Wed, 21 May 2014 15:47:45 -0400 Subject: [PATCH 1/5] Annotation Tools: Add helper functions and fixed pep8/pylint errors Small plugin fixes --- common/lib/xmodule/xmodule/annotator_mixin.py | 45 ++++++++++++++++ common/lib/xmodule/xmodule/annotator_token.py | 4 +- .../xmodule/tests/test_annotator_mixin.py | 52 +++++++++++++++++++ .../xmodule/tests/test_annotator_token.py | 4 +- .../xmodule/tests/test_videoannotation.py | 2 +- .../xmodule/xmodule/textannotation_module.py | 8 +-- .../xmodule/xmodule/videoannotation_module.py | 18 ++----- .../css/vendor/ova/richText-annotator.css | 16 +++++- .../js/vendor/ova/flagging-annotator.js | 2 +- .../static/js/vendor/ova/reply-annotator.js | 1 + common/static/js/vendor/ova/tags-annotator.js | 35 +++++++++++-- 11 files changed, 154 insertions(+), 33 deletions(-) create mode 100644 common/lib/xmodule/xmodule/annotator_mixin.py create mode 100644 common/lib/xmodule/xmodule/tests/test_annotator_mixin.py diff --git a/common/lib/xmodule/xmodule/annotator_mixin.py b/common/lib/xmodule/xmodule/annotator_mixin.py new file mode 100644 index 0000000000..8b12263580 --- /dev/null +++ b/common/lib/xmodule/xmodule/annotator_mixin.py @@ -0,0 +1,45 @@ +""" +Annotations Tool Mixin +This file contains global variables and functions used in the various Annotation Tools. +""" +from pkg_resources import resource_string +from lxml import etree +from urlparse import urlparse +from os.path import splitext, basename +from HTMLParser import HTMLParser + +def get_instructions(xmltree): + """ Removes from the xmltree and returns them as a string, otherwise None. """ + instructions = xmltree.find('instructions') + if instructions is not None: + instructions.tag = 'div' + xmltree.remove(instructions) + return etree.tostring(instructions, encoding='unicode') + return None + +def get_extension(srcurl): + ''' get the extension of a given url ''' + if 'youtu' in srcurl: + return 'video/youtube' + else: + disassembled = urlparse(srcurl) + file_ext = splitext(basename(disassembled.path))[1] + return 'video/' + file_ext.replace('.', '') + +class MLStripper(HTMLParser): + "helper function for html_to_text below" + def __init__(self): + self.reset() + self.fed = [] + def handle_data(self, d): + self.fed.append(d) + def handle_entityref(self, name): + self.fed.append('&%s;' % name) + def get_data(self): + return ''.join(self.fed) + +def html_to_text(html): + "strips the html tags off of the text to return plaintext" + s = MLStripper() + s.feed(html) + return s.get_data() \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/annotator_token.py b/common/lib/xmodule/xmodule/annotator_token.py index 6fa5695978..129315739c 100644 --- a/common/lib/xmodule/xmodule/annotator_token.py +++ b/common/lib/xmodule/xmodule/annotator_token.py @@ -23,8 +23,8 @@ def retrieve_token(userid, secret): dtnow = datetime.datetime.now() dtutcnow = datetime.datetime.utcnow() delta = dtnow - dtutcnow - newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60) - newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin) + newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60) # pylint: disable=E1103 + newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin) # pylint: disable=E1103 # uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a # federated system in the annotation backend server custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400} diff --git a/common/lib/xmodule/xmodule/tests/test_annotator_mixin.py b/common/lib/xmodule/xmodule/tests/test_annotator_mixin.py new file mode 100644 index 0000000000..09d216b709 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_annotator_mixin.py @@ -0,0 +1,52 @@ +""" +This test will run for annotator_mixin.py +""" + +import unittest +from lxml import etree + +from xmodule.annotator_mixin import get_instructions, get_extension, html_to_text + +class HelperFunctionTest(unittest.TestCase): + """ + Tests to ensure that the following helper functions work for the annotation tool + """ + sample_xml = ''' + +

Helper Test Instructions.

+
+ ''' + sample_sourceurl = "http://video-js.zencoder.com/oceans-clip.mp4" + sample_youtubeurl = "http://www.youtube.com/watch?v=yxLIu-scR9Y" + sample_html = '

Testing here and not bolded here

' + + def test_get_instructions(self): + """ + Function takes in an input of a specific xml string with surrounding instructions tags and returns a valid html string. + """ + xmltree = etree.fromstring(self.sample_xml) + + expected_xml = u"

Helper Test Instructions.

" + actual_xml = get_instructions(xmltree) # pylint: disable=W0212 + self.assertIsNotNone(actual_xml) + self.assertEqual(expected_xml.strip(), actual_xml.strip()) + + xmltree = etree.fromstring('foo') + actual = get_instructions(xmltree) # pylint: disable=W0212 + self.assertIsNone(actual) + + def test_get_extension(self): + """ + Tests whether given a url if the video will return a youtube source or extension + """ + expectedyoutube = 'video/youtube' + expectednotyoutube = 'video/mp4' + result1 = get_extension(self.sample_sourceurl) # pylint: disable=W0212 + result2 = get_extension(self.sample_youtubeurl) # pylint: disable=W0212 + self.assertEqual(expectedyoutube, result2) + self.assertEqual(expectednotyoutube, result1) + + def test_html_to_text(self): + expectedText = "Testing here and not bolded here" + result = html_to_text(self.sample_html) + self.assertEqual(expectedText, result) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/tests/test_annotator_token.py b/common/lib/xmodule/xmodule/tests/test_annotator_token.py index ae06808bba..49f376436b 100644 --- a/common/lib/xmodule/xmodule/tests/test_annotator_token.py +++ b/common/lib/xmodule/xmodule/tests/test_annotator_token.py @@ -12,9 +12,9 @@ class TokenRetriever(unittest.TestCase): """ def test_token(self): """ - Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text. + Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text. """ expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ" response = retrieve_token("username", "fake_secret") self.assertEqual(expected.split('.')[0], response.split('.')[0]) - self.assertNotEqual(expected.split('.')[2], response.split('.')[2]) \ No newline at end of file + self.assertNotEqual(expected.split('.')[2], response.split('.')[2]) diff --git a/common/lib/xmodule/xmodule/tests/test_videoannotation.py b/common/lib/xmodule/xmodule/tests/test_videoannotation.py index 4a081803aa..533ea80203 100644 --- a/common/lib/xmodule/xmodule/tests/test_videoannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_videoannotation.py @@ -66,6 +66,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): """ Tests to make sure variables passed in truly exist within the html once it is all rendered. """ - context = self.mod.get_html() # pylint: disable=W0212 + context = self.mod.get_html() # pylint: disable=W0212 for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']: self.assertIn(key, context) diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index 2f5f3250a9..258f270787 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -6,6 +6,7 @@ from pkg_resources import resource_string from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String +from xmodule.annotator_mixin import get_instructions from xmodule.annotator_token import retrieve_token import textwrap @@ -70,12 +71,7 @@ class TextAnnotationModule(AnnotatableFields, XModule): def _extract_instructions(self, xmltree): """ Removes from the xmltree and returns them as a string, otherwise None. """ - instructions = xmltree.find('instructions') - if instructions is not None: - instructions.tag = 'div' - xmltree.remove(instructions) - return etree.tostring(instructions, encoding='unicode') - return None + return get_instructions(xmltree) def get_html(self): """ Renders parameters to template. """ diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py index df6236d006..1431b03b25 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -7,6 +7,7 @@ from pkg_resources import resource_string from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String +from xmodule.annotator_mixin import get_instructions, get_extension from xmodule.annotator_token import retrieve_token import textwrap @@ -65,24 +66,11 @@ class VideoAnnotationModule(AnnotatableFields, XModule): def _extract_instructions(self, xmltree): """ Removes from the xmltree and returns them as a string, otherwise None. """ - instructions = xmltree.find('instructions') - if instructions is not None: - instructions.tag = 'div' - xmltree.remove(instructions) - return etree.tostring(instructions, encoding='unicode') - return None + return get_instructions(xmltree) def _get_extension(self, srcurl): ''' get the extension of a given url ''' - if 'youtu' in srcurl: - return 'video/youtube' - else: - spliturl = srcurl.split(".") - extensionplus1 = spliturl[len(spliturl) - 1] - spliturl = extensionplus1.split("?") - extensionplus2 = spliturl[0] - spliturl = extensionplus2.split("#") - return 'video/' + spliturl[0] + return get_extension(srcurl) def get_html(self): """ Renders parameters to template. """ diff --git a/common/static/css/vendor/ova/richText-annotator.css b/common/static/css/vendor/ova/richText-annotator.css index 395cfc5f17..4cadb8e055 100644 --- a/common/static/css/vendor/ova/richText-annotator.css +++ b/common/static/css/vendor/ova/richText-annotator.css @@ -17,9 +17,21 @@ } .annotator-wrapper .mce-container { - z-index:3000000000!important; /*To fix full-screen problems*/ + z-index: 3000000000!important; /*To fix full-screen problems*/ } +.mce-container-body { + min-width: 400px; +} + +.iframe[id="annotator-field"] { + width: inherit; + min-width: 400px; +} + +div.mce-tinymce.mce-container.mce-panel { + min-width:400px; +} /* Some change in the design of Annotator */ .annotator-editor .annotator-widget{ @@ -30,4 +42,4 @@ .mce-ico.mce-i-rubric{ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QkBBB07nraNoQAAAhZJREFUKM+NkstrE1EUxr+5c08ykztpJtVoazHBF8FgQQzonyBKEZS6FrQKLl0EXBRT0ZULJSs3oii4TyHgu90IlTaL6qouWlv7Ck1N0BSnmZk714WbPHz07M4534+Pw3eAHdTY8A9+Nd9/bshU1DpnO4HXjh2ZY2J9/OSTxHTrnP8PvJYf+BDQ6qEDaQBB43jrTusUFy4oPjsYWYzF+VS91nxLYfdhKgONaQT3W/KMxr1XY5e+qj86f8zsKYYsZ6AvjWFzA8ORHkAnwN8So7evzL/8pzMAXL/Hq8mMv1up371T7Z+/c3n9cKeuDS6Xy6dN07zLuZ56Onk2Ed2/ANJsnE/PQMpgyffle+kYzwazB1+3waVS6X48Hr9BRPB9H57nYXplFKeSt8D1Hriug9XKF0x+Lmw+ys8m2m42DOOn4zhQSsGyLOi6jqONm9isbmFVFlDbaGKx8QaB1rvdlbNhGLAsC0IIGIYBIQSy2ROQ0oOp7wOPraHXEugRvDtnzjmi0SiICEIIEBGklAB9B6cmbG0AUnrY5m73h+m6DsYYTNMEYwxEBMY0hGNVhHkcZigBO9qHlDHS7cwYg23bAIBQKAQigud7IH0XwtxDoHwEIQ9SLKx0wa7rPiaivYyxESklXNeFBg0mjyNQTQSuATMSm6ipuYt//eVcLhdeXl5+UKlUlur1upqamVAv3j3/VCyOD3VqfwF6uLp3q+vMcgAAAABJRU5ErkJggg=='); background-repeat: no-repeat; -} +} \ No newline at end of file diff --git a/common/static/js/vendor/ova/flagging-annotator.js b/common/static/js/vendor/ova/flagging-annotator.js index ac24ffff24..16b658d6de 100644 --- a/common/static/js/vendor/ova/flagging-annotator.js +++ b/common/static/js/vendor/ova/flagging-annotator.js @@ -101,4 +101,4 @@ Annotator.Plugin.Flagging = (function(_super) { return Flagging; -})(Annotator.Plugin); \ No newline at end of file +})(Annotator.Plugin); diff --git a/common/static/js/vendor/ova/reply-annotator.js b/common/static/js/vendor/ova/reply-annotator.js index f88efae2da..6ced09eb15 100644 --- a/common/static/js/vendor/ova/reply-annotator.js +++ b/common/static/js/vendor/ova/reply-annotator.js @@ -82,6 +82,7 @@ Annotator.Plugin.Reply = (function(_super) { var string; return self; }); + field.remove(); this.annotation = annotation; //Create the actions for the buttons return ret; diff --git a/common/static/js/vendor/ova/tags-annotator.js b/common/static/js/vendor/ova/tags-annotator.js index 36a28fa9de..dde5529e6f 100644 --- a/common/static/js/vendor/ova/tags-annotator.js +++ b/common/static/js/vendor/ova/tags-annotator.js @@ -1,3 +1,27 @@ +/* + HighlightTags Annotator Plugin v1.0 (https://github.com/lduarte1991/tags-annotator) + Copyright (C) 2014 Luis F Duarte + License: https://github.com/lduarte1991/tags-annotator/blob/master/LICENSE.rst + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +/*=============================================================================== + =============================================================================== + =============================================================================== + =============================================================================== + ==============================================================================*/ /* * jQuery Plugin: Tokenizing Autocomplete Text Entry * Version 1.6.0 @@ -22,7 +46,7 @@ var DEFAULT_SETTINGS = { // Display settings hintText: "Type in a search term", - noResultsText: "No results", + noResultsText: "Not Found. Hit ENTER to add a personal tag.", searchingText: "Searching...", deleteText: "×", animateDropdown: true, @@ -39,7 +63,7 @@ var DEFAULT_SETTINGS = { prePopulate: null, processPrePopulate: false, - // Manipulation settings + // Manipulation settings idPrefix: "token-input-", // Formatters @@ -271,7 +295,10 @@ $.TokenList = function (input, url_or_data, settings) { add_token($(selected_dropdown_item).data("tokeninput")); hidden_input.change(); return false; - } + } else{ + add_token({id:$(this).val(), name:$(this).val()}); + hidden_input.change(); + } break; case KEY.ESCAPE: @@ -886,7 +913,7 @@ Annotator.Plugin.HighlightTags = (function(_super) { HighlightTags.prototype.field = null; HighlightTags.prototype.input = null; HighlightTags.prototype.colors = null; - HighlightTags.prototype.isFirstTime = true; + HighlightTags.prototype.isFirstTime = true; //this function will initialize the plug in. Create your fields here in the editor and viewer. HighlightTags.prototype.pluginInit = function() { From 32e96681f69270d60ab8d604fa64c89a7f6d2bc5 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Wed, 21 May 2014 16:12:31 -0400 Subject: [PATCH 2/5] Image Annotation Tool: create xmodule and install js files --- .../contentstore/tests/test_contentstore.py | 2 +- common/lib/xmodule/setup.py | 1 + .../xmodule/xmodule/imageannotation_module.py | 123 + .../xmodule/tests/test_imageannotation.py | 78 + .../js/vendor/ova/OpenSeaDragonAnnotation.js | 792 + common/static/js/vendor/ova/catch/js/catch.js | 138 +- common/static/js/vendor/ova/openseadragon.js | 12946 ++++++++++++++++ lms/envs/common.py | 2 + lms/templates/imageannotation.html | 209 + 9 files changed, 14206 insertions(+), 85 deletions(-) create mode 100644 common/lib/xmodule/xmodule/imageannotation_module.py create mode 100644 common/lib/xmodule/xmodule/tests/test_imageannotation.py create mode 100644 common/static/js/vendor/ova/OpenSeaDragonAnnotation.js create mode 100644 common/static/js/vendor/ova/openseadragon.js create mode 100644 lms/templates/imageannotation.html diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 5d1ca5ffaf..3d95fd1c8e 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -139,7 +139,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # response HTML self.check_components_on_page( ADVANCED_COMPONENT_TYPES, - ['Word cloud', 'Annotation', 'Text Annotation', 'Video Annotation', + ['Word cloud', 'Annotation', 'Text Annotation', 'Video Annotation', 'Image Annotation', 'Open Response Assessment', 'Peer Grading Interface', 'openassessment'], ) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 04de2696df..7cd306b026 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -36,6 +36,7 @@ XMODULES = [ "annotatable = xmodule.annotatable_module:AnnotatableDescriptor", "textannotation = xmodule.textannotation_module:TextAnnotationDescriptor", "videoannotation = xmodule.videoannotation_module:VideoAnnotationDescriptor", + "imageannotation = xmodule.imageannotation_module:ImageAnnotationDescriptor", "foldit = xmodule.foldit_module:FolditDescriptor", "word_cloud = xmodule.word_cloud_module:WordCloudDescriptor", "hidden = xmodule.hidden_module:HiddenDescriptor", diff --git a/common/lib/xmodule/xmodule/imageannotation_module.py b/common/lib/xmodule/xmodule/imageannotation_module.py new file mode 100644 index 0000000000..1875b3d668 --- /dev/null +++ b/common/lib/xmodule/xmodule/imageannotation_module.py @@ -0,0 +1,123 @@ +""" +Module for Image annotations using annotator. +""" +from lxml import etree +from pkg_resources import resource_string + +from xmodule.x_module import XModule +from xmodule.raw_module import RawDescriptor +from xblock.core import Scope, String +from xmodule.annotator_mixin import get_instructions, html_to_text +from xmodule.annotator_token import retrieve_token + +import textwrap + + +class AnnotatableFields(object): + """ Fields for `ImageModule` and `ImageDescriptor`. """ + data = String(help="XML data for the annotation", scope=Scope.content, default=textwrap.dedent("""\ + + +

+ Add the instructions to the assignment here. +

+
+

+ Lorem ipsum dolor sit amet, at amet animal petentium nec. Id augue nemore postulant mea. Ex eam dicant noluisse expetenda, alia admodum abhorreant qui et. An ceteros expetenda mea, tale natum ipsum quo no, ut pro paulo alienum noluisse. +

+ + navigatorSizeRatio: 0.25, + wrapHorizontal: false, + showNavigator: true, + navigatorPosition: "BOTTOM_LEFT", + showNavigationControl: true, + tileSources: [{ + Image: { + xmlns: "http://schemas.microsoft.com/deepzoom/2009", + Url: "http://static.seadragon.com/content/misc/milwaukee_files/", + TileSize: "254", + Overlap: "1", + Format: "jpg", + ServerFormat: "Default", + Size: { + Width: "15497", + Height: "5378" + } + } + },], + +
+ """)) + display_name = String( + display_name="Display Name", + help="Display name for this module", + scope=Scope.settings, + default='Image Annotation', + ) + instructor_tags = String( + display_name="Tags for Assignments", + help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue", + scope=Scope.settings, + default='professor:green,teachingAssistant:blue', + ) + annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage") + annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation") + + +class ImageAnnotationModule(AnnotatableFields, XModule): + '''Image Annotation Module''' + js = { + 'coffee': [ + resource_string(__name__, 'js/src/javascript_loader.coffee'), + resource_string(__name__, 'js/src/html/display.coffee'), + resource_string(__name__, 'js/src/annotatable/display.coffee'), + ], + 'js': [ + resource_string(__name__, 'js/src/collapsible.js'), + ] + } + css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} + icon_class = 'imageannotation' + + def __init__(self, *args, **kwargs): + super(ImageAnnotationModule, self).__init__(*args, **kwargs) + + xmltree = etree.fromstring(self.data) + + self.instructions = self._extract_instructions(xmltree) + self.openseadragonjson = html_to_text(etree.tostring(xmltree.find('json'), encoding='unicode')) + self.user = "" + if self.runtime.get_real_user is not None: + self.user = self.runtime.get_real_user(self.runtime.anonymous_student_id).email + + def _extract_instructions(self, xmltree): + """ Removes from the xmltree and returns them as a string, otherwise None. """ + return get_instructions(xmltree) + + def get_html(self): + """ Renders parameters to template. """ + context = { + 'display_name': self.display_name_with_default, + 'instructions_html': self.instructions, + 'annotation_storage': self.annotation_storage_url, + 'token':retrieve_token(self.user, self.annotation_token_secret), + 'tag': self.instructor_tags, + 'openseadragonjson': self.openseadragonjson, + } + + return self.system.render_template('imageannotation.html', context) + + +class ImageAnnotationDescriptor(AnnotatableFields, RawDescriptor): + ''' Image annotation descriptor ''' + module_class = ImageAnnotationModule + mako_template = "widgets/raw-edit.html" + + @property + def non_editable_metadata_fields(self): + non_editable_fields = super(ImageAnnotationDescriptor, self).non_editable_metadata_fields + non_editable_fields.extend([ + ImageAnnotationDescriptor.annotation_storage_url, + ImageAnnotationDescriptor.annotation_token_secret, + ]) + return non_editable_fields \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/tests/test_imageannotation.py b/common/lib/xmodule/xmodule/tests/test_imageannotation.py new file mode 100644 index 0000000000..5a6710b7a0 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_imageannotation.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"Test for Image Annotation Xmodule functional logic." + +import unittest +from mock import Mock +from lxml import etree + +from xblock.field_data import DictFieldData +from xblock.fields import ScopeIds + +from xmodule.imageannotation_module import ImageAnnotationModule + +from . import get_test_system + + +class ImageAnnotationModuleTestCase(unittest.TestCase): + ''' Image Annotation Module Test Case ''' + sample_xml = ''' + +

Image Test Instructions.

+ + navigatorSizeRatio: 0.25, + wrapHorizontal: false, + showNavigator: true, + navigatorPosition: "BOTTOM_LEFT", + showNavigationControl: true, + tileSources: [{ + Image: { + xmlns: "http://schemas.microsoft.com/deepzoom/2009", + Url: "http://static.seadragon.com/content/misc/milwaukee_files/", + TileSize: "254", + Overlap: "1", + Format: "jpg", + ServerFormat: "Default", + Size: { + Width: "15497", + Height: "5378" + } + } + },], + +
+ ''' + + def setUp(self): + """ + Makes sure that the Module is declared and mocked with the sample xml above. + """ + self.mod = ImageAnnotationModule( + Mock(), + get_test_system(), + DictFieldData({'data': self.sample_xml}), + ScopeIds(None, None, None, None) + ) + + def test_extract_instructions(self): + """ + Tests to make sure that the instructions are correctly pulled from the sample xml above. + It also makes sure that if no instructions exist, that it does in fact return nothing. + """ + xmltree = etree.fromstring(self.sample_xml) + + expected_xml = u"

Image Test Instructions.

" + actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=W0212 + self.assertIsNotNone(actual_xml) + self.assertEqual(expected_xml.strip(), actual_xml.strip()) + + xmltree = etree.fromstring('foo') + actual = self.mod._extract_instructions(xmltree) # pylint: disable=W0212 + self.assertIsNone(actual) + + def test_get_html(self): + """ + Tests the function that passes in all the information in the context that will be used in templates/textannotation.html + """ + context = self.mod.get_html() + for key in ['display_name', 'instructions_html', 'annotation_storage', 'token', 'tag', 'openseadragonjson']: + self.assertIn(key, context) \ No newline at end of file diff --git a/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js b/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js new file mode 100644 index 0000000000..8a1b939340 --- /dev/null +++ b/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js @@ -0,0 +1,792 @@ +/* +OpenSeaDragonAnnotation v1.0 (http://) +Copyright (C) 2014 CHS (Harvard University), Daniel Cebrián Robles and Phil Desenne +License: https://github.com/CtrHellenicStudies/OpenSeaDragonAnnotation/blob/master/License.rst + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +(function($) { + $.Viewer.prototype.annotation = function(options) { + //-- wait for plugins --// + var wrapper = jQuery('.annotator-wrapper').parent()[0], + annotator = jQuery.data(wrapper, 'annotator'), + self = this, + isOpenViewer = false; + + this.addHandler("open", function() { + isOpenViewer = true; + if (typeof self.annotationInstance!='undefined') + self.annotationInstance.refreshDisplay(); + }); + + + annotator + //-- Finished the Annotator DOM + .subscribe("annotationsLoaded", function (annotations){ + if (!self.annotationInstance) { + self.annotationInstance = new $._annotation({ + viewer: self, + annotator: annotator, + }); + annotator.osda = self.annotationInstance; + //Wait until viewer is opened + function refreshDisplay(){ + if(!isOpenViewer){ + setTimeout(refreshDisplay,200); + }else{ + self.annotationInstance.refreshDisplay(); + } + } + refreshDisplay(); + } else { + self.annotationInstance.refreshDisplay(); + } + }); + }; + // INIT annotation + $._annotation = function(options) { + //options + options = options || {}; + if (!options.viewer) { + throw new Error("A viewer must be specified."); + } + + //variables + this.viewer = options.viewer; + this.annotator = options.annotator; + this.options = options; + this.isAnnotating = false; //If the user is annotating + this.isDrawing = false; //if the user is drawing something + + //Init + this.init(); + }; + + //-- Methods + $._annotation.prototype = { + init: function(){ + var viewer = this.viewer; + + //create Buttons + this._createNewButton(); + + /* canvas Events */ + //- Bind canvas functions + var onCanvasMouseDown = this.__bind(this._onCanvasMouseDown,this), + onCanvasMouseMove = this.__bind(this._onCanvasMouseMove,this), + onDocumentMouseUp = this.__bind(this._onDocumentMouseUp,this); + + //- Add canvas events + $.addEvent(viewer.canvas, "mousedown", onCanvasMouseDown, true); + $.addEvent(viewer.canvas, "mousemove", onCanvasMouseMove, true); + $.addEvent(document, "mouseup", onDocumentMouseUp, true); + + //Viewer events + var self = this; + }, + newAnnotation:function(){ + var annotator = this.annotator; + + //This variable is to say the editor that we want create an image annotation + annotator.editor.OpenSeaDragon = this.viewer.id; + + annotator.adder.show(); + + this._setOverShape(annotator.adder); + + //Open a new annotator dialog + annotator.onAdderClick(); + }, + editAnnotation: function(annotation,editor){ + //This will be usefull when we are going to edit an annotation. + if (this._isOpenSeaDragon(annotation)){ + //this.hideDisplay(); + var editor = editor || this.annotator.editor; + + //set the editor over the range slider + this._setOverShape(editor.element); + editor.checkOrientation(); + + //This variable is to say the editor that we want create an image annotation + editor.OpenSeaDragon = this.viewer.id; + } + }, + refreshDisplay: function(){ + var allannotations = this.annotator.plugins['Store'].annotations; + var annotator = this.annotator; + + //Sort by date the Array + this._sortByDate(allannotations); + + //remove all the overlays + this.viewer.drawer.clearOverlays(); + + for (var item in allannotations) { + var an = allannotations[item]; + + //check if the annotation is an OpenSeaDragon annotation + if (this._isOpenSeaDragon(an)) + this.drawRect(an); + annotator.publish('colorizeHighlight', [an]); + }; + }, + modeAnnotation:function(e){ + this._reset(); + var viewer = this.viewer; + if (!this.isAnnotating){ + jQuery('.openseadragon1').css('cursor', 'crosshair'); + jQuery('.openseadragon1').css('border', '2px solid rgb(51,204,102)'); + e.eventSource.imgGroup.src = this.resolveUrl( viewer.prefixUrl,"newan_hover.png"); + e.eventSource.imgRest.src = this.resolveUrl( viewer.prefixUrl,"newan_hover.png"); + e.eventSource.imgHover.src = this.resolveUrl( viewer.prefixUrl,"newan_grouphover.png"); + }else{ + jQuery('.openseadragon1').css('cursor', 'all-scroll'); + jQuery('.openseadragon1').css('border', 'inherit'); + e.eventSource.imgGroup.src = this.resolveUrl( viewer.prefixUrl,"newan_grouphover.png"); + e.eventSource.imgRest.src = this.resolveUrl( viewer.prefixUrl,"newan_rest.png"); + e.eventSource.imgHover.src = this.resolveUrl( viewer.prefixUrl,"newan_hover.png"); + } + this.isAnnotating = !this.isAnnotating?true:false; + }, + drawRect:function(an){ + if (typeof an.rangePosition!='undefined'){ + var span = document.createElement('span'), + rectPosition = an.rangePosition; + //Span + span.className = "annotator-hl"; + span.style.border = '1px solid rgba(0,0,0,0.5)'; + var onAnnotationMouseMove = this.__bind(this._onAnnotationMouseMove,this), + onAnnotationClick = this.__bind(this._onAnnotationClick,this); + $.addEvent(span, "mousemove", onAnnotationMouseMove, true); + $.addEvent(span, "click", onAnnotationClick, true); + + //Set the object in the div + jQuery.data(span, 'annotation', an); + //Add the highlights to the annotation + an.highlights = jQuery(span); + + var olRect = new OpenSeadragon.Rect(rectPosition.left, rectPosition.top, rectPosition.width, rectPosition.height); + return this.viewer.drawer.addOverlay({ + element: span, + location: olRect, + placement: OpenSeadragon.OverlayPlacement.TOP_LEFT + }); + } + return false; + }, + //Change object(this.rectPosition)the rectangle Position using div element(this.rect) + setRectPosition:function(){ + var left = parseInt(this.rect.style.left), + top = parseInt(this.rect.style.top), + width = parseInt(this.rect.style.left)+parseInt(this.rect.style.width), + height = parseInt(this.rect.style.top)+parseInt(this.rect.style.height), + startPoint = new $.Point(left,top), + endPoint = new $.Point(width,height); + this.rectPosition = {left:this._physicalToLogicalXY(startPoint).x, + top:this._physicalToLogicalXY(startPoint).y, + width:this._physicalToLogicalXY(endPoint).x-this._physicalToLogicalXY(startPoint).x, + height:this._physicalToLogicalXY(endPoint).y-this._physicalToLogicalXY(startPoint).y + }; + }, + /* Handlers */ + _onCanvasMouseDown: function(event,seft) { + if (this.isAnnotating){ + var viewer = this.viewer; + event.preventDefault(); + + //reset the display + this._reset(); + + //set mode drawing + this.isDrawing = true; + + //Create rect element + var mouse = $.getMousePosition( event ), + elementPosition = $.getElementPosition(viewer.canvas), + position = mouse.minus( elementPosition ); + viewer.innerTracker.setTracking(false); + this.rect = document.createElement('div'); + this.rect.style.background = 'rgba(0,0,0,0.25)'; + this.rect.style.border = '1px solid rgba(0,0,0,0.5)'; + this.rect.style.position = 'absolute'; + this.rect.className = 'DrawingRect'; + //set the initial position + this.rect.style.top = position.y+"px"; + this.rect.style.left = position.x+"px"; + this.rect.style.width = "1px"; + this.rect.style.height = "1px"; + + //save the start Position + this.startPosition = position; + //save rectPosition as initial rectangle parameter to Draw in the canvas + this.setRectPosition(); + + //append Child to the canvas + viewer.canvas.appendChild(this.rect); + } + }, + _onCanvasMouseMove: function(event) { + if (this.isAnnotating && this.isDrawing){ + var viewer = this.viewer; + + //Calculate the new end position + var mouse = $.getMousePosition( event ), + elementPosition = $.getElementPosition(viewer.canvas), + endPosition = mouse.minus( elementPosition ); + //retrieve start position + var startPosition = this.startPosition; + + var newWidth= endPosition.x-startPosition.x, + newHeight =endPosition.y-startPosition.y; + + //Set new position + this.rect.style.width = (newWidth<0) ? (-1*newWidth) +'px' : newWidth +'px'; + this.rect.style.left = (newWidth<0) ? (startPosition.x + newWidth) +'px' : startPosition.x +'px'; + this.rect.style.height = (newHeight<0) ? (-1*newHeight) +'px' : newHeight +'px'; + this.rect.style.top = (newHeight<0) ? (startPosition.y + newHeight) +'px' : startPosition.y +'px'; + + //Modify the rectPosition with the new this.rect values + this.setRectPosition(); + + //Show adder and hide editor + this.annotator.editor.element[0].style.display = 'none'; + this._setOverShape(this.annotator.adder); + } + }, + _onDocumentMouseUp: function() { + if (this.isAnnotating && this.isDrawing){ + var viewer = this.viewer; + + viewer.innerTracker.setTracking(true); + this.isDrawing = false; + + //Set the new position for the rectangle + this.setRectPosition(); + + //Open Annotator editor + this.newAnnotation(); + + //Hide adder and show editor + this.annotator.editor.element[0].style.display = 'block'; + this._setOverShape(this.annotator.editor.element); + this.annotator.editor.checkOrientation(); + } + }, + _onAnnotationMouseMove: function(event){ + var annotator = this.annotator; + var elem = jQuery(event.target).parents('.annotator-hl').andSelf(); + //if there is a opened annotation then show the new annotation mouse over + if (typeof annotator!='undefined' && elem.hasClass("annotator-hl") && !this.isDrawing){ + //hide the last open viewer + annotator.viewer.hide(); + //get the annotation over the mouse + var annotations = jQuery(event.target.parentNode).find('.annotator-hl').map(function() { + var self = jQuery(this), + offset = self.offset(), + l = offset.left, + t = offset.top, + h = self.height(), + w = self.width(), + x = $.getMousePosition(event).x, + y = $.getMousePosition(event).y; + + var maxx = l + w, + maxy = t + h; + this.style.background = (y <= maxy && y >= t) && (x <= maxx && x >= l)? + 'rgba(12, 150, 0, 0.3)':'rgba(255, 255, 10, 0.3)'; + return (y <= maxy && y >= t) && (x <= maxx && x >= l)? jQuery(this).data("annotation") : null; + }); + //show the annotation in the viewer + var mousePosition = { + top:$.getMousePosition(event).y, + left:$.getMousePosition(event).x, + }; + if (annotations.length>0) annotator.showViewer(jQuery.makeArray(annotations), mousePosition); + } + }, + _onAnnotationClick: function(event){ + var an = jQuery.data(event.target, 'annotation'), + bounds = typeof an.bounds!='undefined'?an.bounds:{}, + currentBounds = this.viewer.drawer.viewport.getBounds(); + if (typeof bounds.x!='undefined') currentBounds.x = bounds.x; + if (typeof bounds.y!='undefined') currentBounds.y = bounds.y; + if (typeof bounds.width!='undefined') currentBounds.width = bounds.width; + if (typeof bounds.height!='undefined') currentBounds.height = bounds.height; + //change the zoom to the saved + this.viewer.drawer.viewport.fitBounds(currentBounds); + }, + _onAnnotationMouseOut: function(event){ + var annotator = this.annotator; + var elem = jQuery(event.target).parents('.annotator-hl').andSelf(); + //if there is a opened annotation then show the new annotation mouse over + if (typeof annotator!='undefined' && elem.hasClass("annotator-hl") && !this.isDrawing){ + /*jQuery(event.target.parentNode).find('.annotator-hl').map(function() { + return this.style.background = 'rgba(255, 255, 10, 0.3)'; + });*/ + } + }, + /* Utilities */ + _sortByDate: function (annotations,type){ + var type = type || 'asc'; //asc => The value [0] will be the most recent date + annotations.sort(function(a,b){ + a = new Date(typeof a.updated!='undefined'?createDateFromISO8601(a.updated):''); + b = new Date(typeof b.updated!='undefined'?createDateFromISO8601(b.updated):''); + if (type == 'asc') + return ba?1:0; + else + return ab?1:0; + }); + }, + _createNewButton:function(){ + var viewer = this.viewer, + onFocusHandler = $.delegate( this, onFocus ), + onBlurHandler = $.delegate( this, onBlur ), + onModeAnnotationHandler = $.delegate( this, this.modeAnnotation ); + /* Buttons */ + var viewer = this.viewer; + var self = this; + viewer.modeAnnotation = new $.Button({ + element: viewer.modeAnnotation ? $.getElement( viewer.modeAnnotation ) : null, + clickTimeThreshold: viewer.clickTimeThreshold, + clickDistThreshold: viewer.clickDistThreshold, + tooltip: "New Annotation", + srcRest: self.resolveUrl( viewer.prefixUrl,"newan_rest.png"), + srcGroup: self.resolveUrl( viewer.prefixUrl,"newan_grouphover.png"), + srcHover: self.resolveUrl( viewer.prefixUrl,"newan_hover.png"), + srcDown: self.resolveUrl( viewer.prefixUrl,"newan_pressed.png"), + onRelease: onModeAnnotationHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + }); + + //- Wrapper Annotation Menu + viewer.wrapperAnnotation = new $.ButtonGroup({ + buttons: [ + viewer.modeAnnotation, + ], + clickTimeThreshold: viewer.clickTimeThreshold, + clickDistThreshold: viewer.clickDistThreshold + }); + + /* Set elements to the control menu */ + viewer.annotatorControl = viewer.wrapperAnnotation.element; + if( viewer.toolbar ){ + viewer.toolbar.addControl( + viewer.annotatorControl, + {anchor: $.ControlAnchor.BOTTOM_RIGHT} + ); + }else{ + viewer.addControl( + viewer.annotatorControl, + {anchor: $.ControlAnchor.TOP_LEFT} + ); + } + }, + _reset: function(){ + //Find and remove DrawingRect. This is the previous rectangle + this._removeElemsByClass('DrawingRect',this.viewer.canvas); + //Show adder and hide editor + this.annotator.editor.element[0].style.display = 'none'; + }, + __bind: function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + // Remove all the elements with a given name inside "inElement" + _removeElemsByClass: function(className,inElement){ + var className = className || '', + inElement = inElement || {}; + divs = inElement.getElementsByClassName(className); + for(var i = 0; i < divs.length; i++) { + divs[i].remove(); + } + }, + //Detect if the annotation is an image annotation + _isOpenSeaDragon: function (an){ + var annotator = this.annotator, + rp = an.rangePosition, + isOpenSeaDragon = (typeof annotator.osda != 'undefined'), + isContainer = (typeof an.target!='undefined' && an.target.container==this.viewer.id ), + isImage = (typeof an.media!='undefined' && an.media=='image'), + isRP = (typeof rp!='undefined'), + isSource = false; + //Save source url + var source = this.viewer.source, + tilesUrl = typeof source.tilesUrl!='undefined'?source.tilesUrl:''; + functionUrl = typeof source.getTileUrl!='undefined'?source.getTileUrl:'', + compareUrl = tilesUrl!=''?tilesUrl:(''+functionUrl).replace(/\s+/g, ' '); + if(isContainer) isSource = (an.target.src == compareUrl); + return (isOpenSeaDragon && isContainer && isImage && isRP && isSource); + }, + /* Annotator Utilities */ + _setOverShape: function(elem){ + //Calculate Point absolute positions + var rectPosition = this.rectPosition || {}, + startPoint = this._logicalToPhysicalXY(new $.Point(rectPosition.left,rectPosition.top)), + endPoint = this._logicalToPhysicalXY(new $.Point(rectPosition.left+rectPosition.width,rectPosition.top+rectPosition.height)); + + //Calculate Point absolute positions + var wrapper = jQuery('.annotator-wrapper')[0], + positionAnnotator = $.getElementPosition(wrapper), + positionCanvas = $.getElementPosition(this.viewer.canvas), + positionAdder = {}; + + //Fix with positionCanvas + startPoint = startPoint.plus(positionCanvas); + endPoint = endPoint.plus(positionCanvas); + + elem[0].style.display = 'block'; //Show the adder + + positionAdder.left = (startPoint.x - positionAnnotator.x) + (endPoint.x - startPoint.x) / 2; + positionAdder.top = (startPoint.y - positionAnnotator.y) + (endPoint.y - startPoint.y) / 2; //It is not necessary fix with - positionAnnotator.y + elem.css(positionAdder); + }, + resolveUrl: function( prefix, url ) { + return prefix ? prefix + url : url; + }, + /* Canvas Utilities */ + // return a point with the values in percentage related to the Image + // point is an object $.Point with the value of the canvas relative coordenates + _physicalToLogicalXY: function(point){ + var point = typeof point!='undefined'?point:{}, + boundX = this.viewer.viewport.getBounds(true).x, + boundY = this.viewer.viewport.getBounds(true).y, + boundWidth = this.viewer.viewport.getBounds(true).width, + boundHeight = this.viewer.viewport.getBounds(true).height, + containerSizeX = this.viewer.viewport.getContainerSize().x, + containerSizeY = this.viewer.viewport.getContainerSize().y, + x = typeof point.x!='undefined'?point.x:0, + y = typeof point.y!='undefined'?point.y:0; + x = boundX + ((x / containerSizeX) * boundWidth); + y = boundY + ((y / containerSizeY) * boundHeight); + return new $.Point(x,y); + }, + // return a point with the values in pixels related to the canvas element + // point is an object $.Point with the value of the Image relative percentage + _logicalToPhysicalXY: function(point){ + var point = typeof point!='undefined'?point:{}, + boundX = this.viewer.viewport.getBounds(true).x, + boundY = this.viewer.viewport.getBounds(true).y, + boundWidth = this.viewer.viewport.getBounds(true).width, + boundHeight = this.viewer.viewport.getBounds(true).height, + containerSizeX = this.viewer.viewport.getContainerSize().x, + containerSizeY = this.viewer.viewport.getContainerSize().y, + x = typeof point.x!='undefined'?point.x:0, + y = typeof point.y!='undefined'?point.y:0; + x = (x - boundX) * containerSizeX / boundWidth; + y = (y - boundY) * containerSizeY / boundHeight; + return new $.Point(x,y); + }, + } + + /* General functions */ + //initiates an animation to hide the controls + function beginControlsAutoHide( viewer ) { + if ( !viewer.autoHideControls ) { + return; + } + viewer.controlsShouldFade = true; + viewer.controlsFadeBeginTime = + $.now() + + viewer.controlsFadeDelay; + + window.setTimeout( function(){ + scheduleControlsFade( viewer ); + }, viewer.controlsFadeDelay ); + } + //stop the fade animation on the controls and show them + function abortControlsAutoHide( viewer ) { + var i; + viewer.controlsShouldFade = false; + for ( i = viewer.controls.length - 1; i >= 0; i-- ) { + viewer.controls[ i ].setOpacity( 1.0 ); + } + } + function onFocus(){ + abortControlsAutoHide( this.viewer ); + } + + function onBlur(){ + beginControlsAutoHide( this.viewer ); + } + + +})(OpenSeadragon); + + + +//----------------Plugin for Annotator to setup OpenSeaDragon----------------// + +Annotator.Plugin.OpenSeaDragon = (function(_super) { + __extends(OpenSeaDragon, _super); + + //constructor + function OpenSeaDragon() { + this.pluginSubmit = __bind(this.pluginSubmit, this); + _ref = OpenSeaDragon.__super__.constructor.apply(this, arguments); + this.__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; } + return _ref; + } + + OpenSeaDragon.prototype.field = null; + OpenSeaDragon.prototype.input = null; + + OpenSeaDragon.prototype.pluginInit = function() { + //Check that annotator is working + if (!Annotator.supported()) { + return; + } + + //-- Editor + this.field = this.annotator.editor.addField({ + id: 'osd-input-rangePosition-annotations', + type: 'input', //options (textarea,input,select,checkbox) + submit: this.pluginSubmit, + EditOpenSeaDragonAn: this.EditOpenSeaDragonAn + }); + + //Modify the element created with annotator to be an invisible span + var select = '
  • ', + newfield = Annotator.$(select); + Annotator.$(this.field).replaceWith(newfield); + this.field=newfield[0]; + + //-- Listener for OpenSeaDragon Plugin + this.initListeners(); + + return this.input = $(this.field).find(':input'); + } + + + + // New JSON for the database + OpenSeaDragon.prototype.pluginSubmit = function(field, annotation) { + //Select the new JSON for the Object to save + if (this.EditOpenSeaDragonAn()){ + var annotator = this.annotator, + osda = annotator.osda, + position = osda.rectPosition, + isNew = typeof annotation.media=='undefined'; + if (typeof annotation.media == 'undefined') annotation.media = "image"; // - media + annotation.target = annotation.target || {}; // - target + annotation.target.container = osda.viewer.id || ""; // - target.container + //Save source url + var source = osda.viewer.source, + tilesUrl = typeof source.tilesUrl!='undefined'?source.tilesUrl:'', + functionUrl = typeof source.getTileUrl!='undefined'?source.getTileUrl:''; + annotation.target.src = tilesUrl!=''?tilesUrl:(''+functionUrl).replace(/\s+/g, ' '); // - target.src (media source) + annotation.target.ext = source.fileFormat || ""; // - target.ext (extension) + annotation.bounds = osda.viewer.drawer.viewport.getBounds() || {}; // - bounds + if(isNew) annotation.rangePosition = position || {}; // - rangePosition + annotation.updated = new Date().toISOString(); // - updated + if (typeof annotation.created == 'undefined') + annotation.created = annotation.updated; // - created + }else{ + if (typeof annotation.media == 'undefined') + annotation.media = "text"; // - media + annotation.updated = new Date().toISOString(); // - updated + if (typeof annotation.created == 'undefined') + annotation.created = annotation.updated; // - created + } + return annotation.media; + }; + + + + //------ Methods ------// + //Detect if we are creating or editing an OpenSeaDragon annotation + OpenSeaDragon.prototype.EditOpenSeaDragonAn = function (){ + var wrapper = $('.annotator-wrapper').parent()[0], + annotator = window.annotator = $.data(wrapper, 'annotator'), + isOpenSeaDragon = (typeof annotator.osda != 'undefined'), + OpenSeaDragon = annotator.editor.OpenSeaDragon; + return (isOpenSeaDragon && typeof OpenSeaDragon!='undefined' && OpenSeaDragon!==-1); + }; + + //Detect if the annotation is an OpenSeaDragon annotation + OpenSeaDragon.prototype.isOpenSeaDragon = function (an){ + var wrapper = $('.annotator-wrapper').parent()[0], + annotator = window.annotator = $.data(wrapper, 'annotator'), + rp = an.rangePosition, + isOpenSeaDragon = (typeof annotator.osda != 'undefined'), + isContainer = (typeof an.target!='undefined' && an.target.container==annotator.osda.viewer.id ), + isImage = (typeof an.media!='undefined' && an.media=='image'), + isRP = (typeof rp!='undefined'), + isSource = false; + //Save source url + var source = annotator.osda.viewer.source, + tilesUrl = typeof source.tilesUrl!='undefined'?source.tilesUrl:''; + functionUrl = typeof source.getTileUrl!='undefined'?source.getTileUrl:'', + compareUrl = tilesUrl!=''?tilesUrl:(''+functionUrl).replace(/\s+/g, ' '); + if(isContainer) isSource = (an.target.src == compareUrl); + return (isOpenSeaDragon && isContainer && isImage && isRP && isSource); + }; + + //Delete OpenSeaDragon Annotation + OpenSeaDragon.prototype._deleteAnnotation = function(an){ + //Remove the annotation of the plugin Store + var annotations = this.annotator.plugins['Store'].annotations; + if (annotations.indexOf(an)>-1) + annotations.splice(annotations.indexOf(an), 1); + //Refresh the annotations in the display + this.annotator.osda.refreshDisplay(); + }; + + + //--Listeners + OpenSeaDragon.prototype.initListeners = function (){ + var wrapper = $('.annotator-wrapper').parent()[0], + annotator = $.data(wrapper, 'annotator'); + var EditOpenSeaDragonAn = this.EditOpenSeaDragonAn, + isOpenSeaDragon = this.isOpenSeaDragon, + self = this; + + //local functions + //-- Editor + function annotationEditorHidden(editor) { + if (EditOpenSeaDragonAn()){ + annotator.osda._reset(); + annotator.osda.refreshDisplay(); //Reload the display of annotations + } + annotator.editor.OpenSeaDragon=-1; + annotator.unsubscribe("annotationEditorHidden", annotationEditorHidden); + }; + function annotationEditorShown(editor,annotation) { + annotator.osda.editAnnotation(annotation,editor); + annotator.subscribe("annotationEditorHidden", annotationEditorHidden); + }; + //-- Annotations + function annotationDeleted(annotation) { + if (isOpenSeaDragon(annotation)) + self._deleteAnnotation(annotation); + }; + //-- Viewer + function hideViewer(){ + jQuery(annotator.osda.viewer.canvas.parentNode).find('.annotator-hl').map(function() { + return this.style.background = 'rgba(255, 255, 10, 0.3)'; + }); + annotator.viewer.unsubscribe("hide", hideViewer); + }; + function annotationViewerShown(viewer,annotations) { + var wrapper = jQuery('.annotator-wrapper').offset(); + + //Fix with positionCanvas + var startPoint = {x: parseFloat(viewer.element[0].style.left), + y: parseFloat(viewer.element[0].style.top)}; + + var separation = viewer.element.hasClass(viewer.classes.invert.y)?5:-5, + newpos = { + top: (startPoint.y - wrapper.top)+separation, + left: (startPoint.x - wrapper.left) + }; + viewer.element.css(newpos); + + //Remove the time to wait until disapear, to be more faster that annotator by default + viewer.element.find('.annotator-controls').removeClass(viewer.classes.showControls); + + annotator.viewer.subscribe("hide", hideViewer); + }; + //subscribe to Annotator + annotator.subscribe("annotationEditorShown", annotationEditorShown) + .subscribe("annotationDeleted", annotationDeleted) + .subscribe("annotationViewerShown", annotationViewerShown); + } + + return OpenSeaDragon; + +})(Annotator.Plugin); + + + +//----------------PUBLIC OBJECT TO CONTROL THE ANNOTATIONS----------------// + +//The name of the plugin that the user will write in the html +OpenSeadragonAnnotation = ("OpenSeadragonAnnotation" in window) ? OpenSeadragonAnnotation : {}; + +OpenSeadragonAnnotation = function (element, options) { + //local variables + var $ = jQuery, + options = options || {}; + options.optionsOpenSeadragon = options.optionsOpenSeadragon || {}; + options.optionsOSDA = options.optionsOSDA || {}; + options.optionsAnnotator = options.optionsAnnotator || {}; + + //if there isn't store optinos it will create a uri and limit variables for the Back-end of Annotations + if (typeof options.optionsAnnotator.store=='undefined') + options.optionsAnnotator.store = {}; + var store = options.optionsAnnotator.store; + if (typeof store.annotationData=='undefined') + store.annotationData = {}; + if (typeof store.annotationData.uri=='undefined'){ + var uri = location.protocol + '//' + location.host + location.pathname; + store.annotationData.store = {uri:uri}; + } + if (typeof store.loadFromSearch=='undefined') + store.loadFromSearch={}; + if (typeof store.loadFromSearch.uri=='undefined') + store.loadFromSearch.uri = uri; + if (typeof store.loadFromSearch.limit=='undefined') + store.loadFromSearch.limit = 10000; + + //global variables + this.currentUser = null; + + //-- Init all the classes --/ + //Annotator + this.annotator = $(element).annotator(options.optionsAnnotator.annotator).data('annotator'); + + //-- Activate all the Annotator plugins --// + if (typeof options.optionsAnnotator.auth!='undefined') + this.annotator.addPlugin('Auth', options.optionsAnnotator.auth); + + if (typeof options.optionsAnnotator.permissions!='undefined') + this.annotator.addPlugin("Permissions", options.optionsAnnotator.permissions); + + if (typeof options.optionsAnnotator.store!='undefined') + this.annotator.addPlugin("Store", options.optionsAnnotator.store); + + if (typeof Annotator.Plugin["Geolocation"] === 'function') + this.annotator.addPlugin("Geolocation",options.optionsAnnotator.geolocation); + + if (typeof Annotator.Plugin["Share"] === 'function') + this.annotator.addPlugin("Share",options.optionsAnnotator.share); + + if (typeof Annotator.Plugin["RichText"] === 'function') + this.annotator.addPlugin("RichText",options.optionsAnnotator.richText); + + if (typeof Annotator.Plugin["Reply"] === 'function') + this.annotator.addPlugin("Reply"); + + if (typeof Annotator.Plugin["OpenSeaDragon"] === 'function') + this.annotator.addPlugin("OpenSeaDragon"); + + if (typeof Annotator.Plugin["Flagging"] === 'function') + this.annotator.addPlugin("Flagging"); + + if (typeof Annotator.Plugin["HighlightTags"] === 'function') + this.annotator.addPlugin("HighlightTags", options.optionsAnnotator.highlightTags); + + //- OpenSeaDragon + this.viewer = OpenSeadragon(options.optionsOpenSeadragon); + //- OpenSeaDragon Plugins + this.viewer.annotation(options.optionsOSDA); + + //Set annotator.editor.OpenSeaDragon by default + this.annotator.editor.OpenSeaDragon=-1; + + this.options = options; + + return this; +} + + + \ No newline at end of file diff --git a/common/static/js/vendor/ova/catch/js/catch.js b/common/static/js/vendor/ova/catch/js/catch.js index 113d7d95c8..30e16ac362 100644 --- a/common/static/js/vendor/ova/catch/js/catch.js +++ b/common/static/js/vendor/ova/catch/js/catch.js @@ -1,3 +1,22 @@ +/* +Grid Annotation Plugin v1.0 +Copyright (C) 2014 Daniel Cebrian Robles and Luis Duarte +License: https://github.com/danielcebrian/share-annotator/blob/master/License.rst + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ //The name of the plugin that the user will write in the html window.CatchAnnotation = ("CatchAnnotation" in window) ? CatchAnnotation : {}; window.CatchSources = ("CatchSources" in window) ? CatchSources : {}; @@ -76,13 +95,10 @@ annotationMediaSelector: '
  • '+ 'Video'+ '
  • '+ + 'li class="ui-state-default" media="image">'+ + 'Image'+ + ''+ '', -// '
    Texttext
    '+ -// '
    Videovideo
    ', -// '
    Imagesimage
    '+ -// '
    Audioaudio
    '+ -// '
    Mapsmap
    '+ -// '
    3D studio3d
    ', //Main->ContainerRow annotationItem: @@ -162,7 +178,15 @@ annotationRow: //Main->ContainerRow->DetailRow annotationDetail: - '
    '+ + '{{#if mediatypeforgrid.text}}'+ + '
    '+ + '{{/if}}'+ + '{{#if mediatypeforgrid.video}}'+ + '
    '+ + '{{/if}}'+ + '{{#if mediatypeforgrid.image}}'+ + '
    '+ + '{{/if}}'+ '
    '+ ''+ 'Hide Details'+ @@ -181,77 +205,25 @@ annotationDetail: '{{/if}}'+ '
    '+ + '{{#if mediatypeforgrid.text}}'+ '
    '+ '
    '+ '
    {{{ quote }}}
    '+ ''+ ''+ '
    '+ - - '
    '+ - '{{{ text }}}'+ - '
    '+ - - '
    '+ - '
    Reply
     '+ - '
    Show Replies
     '+ - '{{#if authToEditButton}}'+ - '
    Edit
    '+ - '{{/if}}'+ - '{{#if authToDeleteButton}}'+ - '
    Delete
    '+ - '{{/if}}'+ - - '
    '+ - - '
    '+ - - '{{#if tags}}'+ - '
    '+ - '

    Tags:

    '+ - '{{#each tags}}'+ - '
    '+ - '{{this}}'+ - '
    '+ - '{{/each}}'+ - '
    '+ '{{/if}}'+ - - '
    '+ - //'Privacy Settings'+ -// 'Groups Access'+ - //''+ - '
    '+ - '
    ', - -//Main->ContainerRow->DetailRow (Video) -videoAnnotationDetail: - '
    '+ - '
    '+ - ''+ - 'Hide Details'+ - ''+ - 'On {{ updated }} {{{ user.name }}}{{#if geolocation}}, wrote from {{/if}}'+ - '{{#if geolocation}}'+ - ''+ - 'Location Map'+ - ''+ - ''+ - ''+ - ''+ - '
    '+ - '
    '+ - '
    '+ - '{{/if}}'+ - '
    '+ - + '{{#if mediatypeforgrid.video}}'+ '
    '+ 'Play segment {{{ rangeTime.start }}} - {{{ rangeTime.end }}}'+ ''+ ''+ ''+ '
    '+ - + '{{/if}}'+ + '{{#if mediatypeforgrid.image}}'+ + ''+ + '{{/if}}'+ '
    '+ '{{{ text }}}'+ '
    '+ @@ -324,8 +296,8 @@ CatchAnnotation = function (element, options) { $( document ).ready(function() { self.init(); self.refreshCatch(true); - var moreBut = self.element.find('.annotationListButtons .moreButtonCatch'); - moreBut.hide(); + var moreBut = self.element.find('.annotationListButtons .moreButtonCatch'); + moreBut.hide(); }); return this; @@ -343,7 +315,6 @@ CatchAnnotation.prototype = { "annotationReply",//Main->ContainerRow->Reply "annotationRow", //Main->ContainerRow->Row "annotationDetail",//Main->ContainerRow->DetailRow - "videoAnnotationDetail"//Main->ContainerRow->DetailRow (Video) ]; //annotator var wrapper = $('.annotator-wrapper').parent()[0], @@ -400,7 +371,7 @@ CatchAnnotation.prototype = { evenOrOdd: index % 2 ? "odd" : "even", openOrClosed: "closed", annotationRow: self.TEMPLATES.annotationRow(item), - annotationDetail: (mediaType === "video") ? self.TEMPLATES.videoAnnotationDetail(item):self.TEMPLATES.annotationDetail(item), + annotationDetail: self.TEMPLATES.annotationDetail(item), }); index++; annotationItems.push(html); @@ -442,7 +413,7 @@ CatchAnnotation.prototype = { //Bind functions var openAnnotationItem = this.__bind(this._openAnnotationItem,this), - closeAnnotationItem = this.__bind(this._closeAnnotationItem,this), + closeAnnotationItem = this.__bind(this._closeAnnotationItem,this), onGeolocationClick = this.__bind(this._onGeolocationClick,this), onPlaySelectionClick = this.__bind(this._onPlaySelectionClick,this), onShareControlsClick = this.__bind(this._onShareControlsClick,this), @@ -493,16 +464,16 @@ CatchAnnotation.prototype = { changeMedia: function(media) { var media = media || 'text'; this.options.media = media; - this._refresh(); + this._refresh(); this.refreshCatch(true); - this.checkTotAnnotations(); + this.checkTotAnnotations(); }, changeUserId: function(userId) { var userId = userId || ''; this.options.userId = userId; this._refresh(); this.refreshCatch(true); - this.checkTotAnnotations(); + this.checkTotAnnotations(); }, loadAnnotations: function() { var annotator = this.annotator, @@ -520,9 +491,9 @@ CatchAnnotation.prototype = { //annotator.plugins['Store'].loadAnnotationsFromSearch(loadFromSearch); //Make sure to be openned all annotations for this pagination - loadFromSearch.limit = this.options.pagination+loadedAn; - loadFromSearch.offset = 0; - annotator.plugins['Store'].loadAnnotationsFromSearch(loadFromSearch); + loadFromSearch.limit = this.options.pagination+loadedAn; + loadFromSearch.offset = 0; + annotator.plugins['Store'].loadAnnotationsFromSearch(loadFromSearch); //text loading annotations var moreBut = this.element.find('.annotationListButtons .moreButtonCatch'); @@ -568,7 +539,6 @@ CatchAnnotation.prototype = { var moreBut = this.element.find('.annotationListButtons .moreButtonCatch'); moreBut.html('More'); - setTimeout(); }, // @@ -598,7 +568,7 @@ CatchAnnotation.prototype = { setTimeout(function(){ if (new_tot != tot){ self.refreshCatch(true); - self.checkTotAnnotations(); + self.checkTotAnnotations(); }else{ attempts++; ischanged(); @@ -617,7 +587,7 @@ CatchAnnotation.prototype = { self.refreshCatch(); if (typeof annotation.parent != 'undefined' && annotation.parent != '0'){ var replies = $("[annotationid="+annotation.parent+"]").find(".controlReplies .hideReplies"); - replies.show(); + replies.show(); replies.click(); replies.click(); } @@ -632,7 +602,7 @@ CatchAnnotation.prototype = { }, __bind: function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, _compileTemplates: function() { - var self = this; + var self = this; //Change the html tags to functions this.TEMPLATENAMES.forEach(function(templateName) { self.TEMPLATES[templateName] = Handlebars.compile(self.HTMLTEMPLATES[templateName]); @@ -678,6 +648,9 @@ CatchAnnotation.prototype = { });//Change to < and > tags item.plainText = item.plainText.replace(/<\/?[^>]+(>|$)/g, "").replace(' ',''); //remove all the html tags + item.mediatypeforgrid = {}; + item.mediatypeforgrid[item.media] = true; + //Flags if(!this.options.flags && typeof item.tags != 'undefined' && item.tags.length > 0){ for(var len=item.tags.length, index = len-1; index >= 0; --index){ @@ -1095,13 +1068,10 @@ CatchAnnotation.prototype = { annotation = item.data('annotation'); var authorized = permissions.options.userAuthorize('delete', annotation,permissions.user); if(authorized){ - //annotator.deleteAnnotation(annotation); if(confirm('Would you like to delete this reply?')){ annotator.plugins['Store']._apiRequest('destroy', annotation, function(){}); item.remove(); - } + } } } } - - diff --git a/common/static/js/vendor/ova/openseadragon.js b/common/static/js/vendor/ova/openseadragon.js new file mode 100644 index 0000000000..789c0ce403 --- /dev/null +++ b/common/static/js/vendor/ova/openseadragon.js @@ -0,0 +1,12946 @@ +//! OpenSeadragon 0.9.131 +//! Built on 2013-11-05 +//! Git commit: v0.9.131-144-gdbb7cee +//! http://openseadragon.github.io +//! License: http://openseadragon.github.io/license/ + +/* + * OpenSeadragon + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Portions of this source file taken from jQuery: + * + * Copyright 2011 John Resig + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * Portions of this source file taken from mattsnider.com: + * + * Copyright (c) 2006-2013 Matt Snider + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT + * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + + + /** + * @version OpenSeadragon 0.9.131 + * + * @fileOverview + *

    + * + * OpenSeadragon - Javascript Deep Zooming + * + *

    + *

    + * OpenSeadragon is provides an html interface for creating + * deep zoom user interfaces. The simplest examples include deep + * zoom for large resolution images, and complex examples include + * zoomable map interfaces driven by SVG files. + *

    + */ + + /** + * The root namespace for OpenSeadragon, this function also serves as a single + * point of instantiation for an {@link OpenSeadragon.Viewer}, including all + * combinations of out-of-the-box configurable features. All utility methods + * and classes are defined on or below this namespace. + * + * @namespace + * @function + * @name OpenSeadragon + * @exports $ as OpenSeadragon + * + * @param {Object} options All required and optional settings for instantiating + * a new instance of an OpenSeadragon image viewer. + * + * @param {String} options.xmlPath + * DEPRECATED. A relative path to load a DZI file from the server. + * Prefer the newer options.tileSources. + * + * @param {Array|String|Function|Object[]|Array[]|String[]|Function[]} options.tileSources + * As an Array, the tileSource can hold either be all Objects or mixed + * types of Arrays of Objects, String, Function. When a value is a String, + * the tileSource is used to create a {@link OpenSeadragon.DziTileSource}. + * When a value is a Function, the function is used to create a new + * {@link OpenSeadragon.TileSource} whose abstract method + * getUrl( level, x, y ) is implemented by the function. Finally, when it + * is an Array of objects, it is used to create a + * {@link OpenSeadragon.LegacyTileSource}. + * + * @param {Boolean} [options.debugMode=true] + * Currently does nothing. TODO: provide an in-screen panel providing event + * detail feedback. + * + * @param {Number} [options.animationTime=1.5] + * Specifies the animation duration per each {@link OpenSeadragon.Spring} + * which occur when the image is dragged or zoomed. + * + * @param {Number} [options.blendTime=0.5] + * Specifies the duration of animation as higher or lower level tiles are + * replacing the existing tile. + * + * @param {Boolean} [options.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. + * + * @param {Boolean} [options.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. + * + * @param {Boolean} [options.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. + * + * @param {Boolean} [options.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. + * + * @param {Boolean} [options.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. + * + * @param {Number} [options.minZoomImageRatio=0.8] + * 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 infinitly. + * + * @param {Number} [options.maxZoomPixelRatio=2] + * 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 + * availble on the viewing device. + * + * @param {Number} [options.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 ration is + * achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to + * true will provide the effect of an infinitely scrolling viewport. + * + * @param {Number} [options.springStiffness=5.0] + * + * @param {Number} [options.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. + * + * @param {Number} [options.clickTimeThreshold=200] + * If multiple mouse clicks occurs within less than this number of + * milliseconds, treat them as a single click. + * + * @param {Number} [options.clickDistThreshold=5] + * If a mouse or touch drag occurs and the distance to the starting drag + * point is less than this many pixels, ignore the drag event. + * + * @param {Number} [options.zoomPerClick=2.0] + * The "zoom distance" per mouse click or touch tap. + * + * @param {Number} [options.zoomPerScroll=1.2] + * The "zoom distance" per mouse scroll or touch pinch. + * + * @param {Number} [options.zoomPerSecond=2.0] + * The number of seconds to animate a single zoom event over. + * + * @param {Boolean} [options.showNavigationControl=true] + * Set to false to prevent the appearance of the default navigation controls. + * + * @param {Boolean} [options.showNavigator=false] + * Set to true to make the navigator minimap appear. + * + * @param {Boolean} [options.navigatorId=navigator-GENERATED DATE] + * Set the ID of a div to hold the navigator minimap. If one is not specified, + * one will be generated and placed on top of the main image + * + * @param {Number} [options.controlsFadeDelay=2000] + * The number of milliseconds to wait once the user has stopped interacting + * with the interface before begining to fade the controls. Assumes + * showNavigationControl and autoHideControls are both true. + * + * @param {Number} [options.controlsFadeLength=1500] + * The number of milliseconds to animate the controls fading out. + * + * @param {Number} [options.maxImageCacheCount=100] + * The max number of images we should keep in memory (per drawer). + * + * @param {Number} [options.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 bandwith you may + * try setting this to 1.5 or higher. + * + * @param {Boolean} [options.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. + * + * @param {Boolean} [options.preserveViewport=false] + * If the viewer has been configured with a sequence of tile sources, then + * normally navigating to 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. + * + * @param {String} [options.prefixUrl='/images/'] + * Prepends the prefixUrl to navImages paths, which is very useful + * since the default paths are rarely useful for production + * environments. + * + * @param {Object} [options.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 botton + * 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. + * + * @param {Boolean} [options.navPrevNextWrap=false] + * If the 'previous' button will wrap to the last image when viewing the first + * image and if the 'next' button will wrap to the first image when viewing + * the last image. + * + * @returns {OpenSeadragon.Viewer} + */ +window.OpenSeadragon = window.OpenSeadragon || function( options ){ + + return new OpenSeadragon.Viewer( options ); + +}; + +(function( $ ){ + + + /** + * 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 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 + * @name $.isFunction + * @function + * @see jQuery + */ + $.isFunction = function( obj ) { + return $.type(obj) === "function"; + }; + + + /** + * Taken from jQuery 1.6.1 + * @name $.isArray + * @function + * @see 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 + * @name $.isWindow + * @function + * @see jQuery + */ + $.isWindow = function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }; + + + /** + * Taken from jQuery 1.6.1 + * @name $.type + * @function + * @see jQuery + */ + $.type = function( obj ) { + return ( obj === null ) || ( obj === undefined ) ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }; + + + /** + * Taken from jQuery 1.6.1 + * @name $.isPlainObject + * @function + * @see 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 key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }; + + + /** + * Taken from jQuery 1.6.1 + * @name $.isEmptyObject + * @function + * @see jQuery + */ + $.isEmptyObject = function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }; + + + /** + * Detect event model and create appropriate _addEvent/_removeEvent methods + */ + if ( window.addEventListener ) { + $._addEvent = function ( element, eventName, handler, useCapture ) { + element = $.getElement( element ); + element.addEventListener( eventName, handler, useCapture ); + }; + } else if ( window.attachEvent ) { + $._addEvent = function ( element, eventName, handler, useCapture ) { + element = $.getElement( element ); + element.attachEvent( 'on' + eventName, handler ); + if ( useCapture && element.setCapture ) { + element.setCapture(); + } + }; + } else { + throw new Error( "No known event model." ); + } + + if ( window.removeEventListener ) { + $._removeEvent = function ( element, eventName, handler, useCapture ) { + element = $.getElement( element ); + element.removeEventListener( eventName, handler, useCapture ); + }; + } else if ( window.detachEvent ) { + $._removeEvent = function( element, eventName, handler, useCapture ) { + element = $.getElement( element ); + element.detachEvent( 'on' + eventName, handler ); + if ( useCapture && element.releaseCapture ) { + element.releaseCapture(); + } + }; + } else { + throw new Error( "No known event model." ); + } + + +}( OpenSeadragon )); + +/** + * This closure defines all static methods available to the OpenSeadragon + * namespace. Many, if not most, are taked 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 + * @see 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 ) { + src = target[ name ]; + copy = options[ name ]; + + // 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 ) ) ) ) { + 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; + }; + + + $.extend( $, { + /** + * These are the default values for the optional settings documented + * in the {@link OpenSeadragon} constructor detail. + * @name $.DEFAULT_SETTINGS + * @static + */ + DEFAULT_SETTINGS: { + //DATA SOURCE DETAILS + xmlPath: null, + tileSources: null, + tileHost: null, + initialPage: 0, + + //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, + + //UI RESPONSIVENESS AND FEEL + springStiffness: 7.0, + clickTimeThreshold: 300, + clickDistThreshold: 5, + zoomPerClick: 2, + zoomPerScroll: 1.2, + zoomPerSecond: 1.0, + animationTime: 1.2, + 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 + pixelsPerWheelLine: 40, + + //DEFAULT CONTROL SETTINGS + showSequenceControl: true, //SEQUENCE + preserveViewport: false, //SEQUENCE + showNavigationControl: true, //ZOOM/HOME/FULL/SEQUENCE + controlsFadeDelay: 2000, //ZOOM/HOME/FULL/SEQUENCE + controlsFadeLength: 1500, //ZOOM/HOME/FULL/SEQUENCE + mouseNavEnabled: true, //GENERAL MOUSE INTERACTIVITY + + //VIEWPORT NAVIGATOR SETTINGS + showNavigator: false, + navigatorId: null, + navigatorHeight: null, + navigatorWidth: null, + navigatorPosition: null, + navigatorSizeRatio: 0.2, + + // INITIAL ROTATION + degrees: 0, + + //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 + collectionLayout: 'horizontal', //vertical + collectionMode: false, + collectionTileSize: 800, + + //EVENT RELATED CALLBACKS + onPageChange: null, + + //PERFORMANCE SETTINGS + imageLoaderLimit: 0, + maxImageCacheCount: 200, + timeout: 30000, + + //INTERFACE RESOURCE SETTINGS + prefixUrl: "${settings.STATIC_URL}" + "js/vendor/ova/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' + }, + 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' + } + }, + navPrevNextWrap: false, + + //DEVELOPER SETTINGS + debugMode: false, + debugGridColor: '#437AB2' + }, + + + /** + * 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----", + + + /** + * Invokes the the method as if it where a method belonging to the object. + * @name $.delegate + * @function + * @param {Object} object + * @param {Function} method + */ + delegate: function( object, method ) { + return function(){ + var args = arguments; + if ( args === undefined ){ + args = []; + } + return method.apply( object, args ); + }; + }, + + + /** + * An enumeration of Browser vendors including UNKNOWN, IE, FIREFOX, + * SAFARI, CHROME, and OPERA. + * @name $.BROWSERS + * @static + */ + BROWSERS: { + UNKNOWN: 0, + IE: 1, + FIREFOX: 2, + SAFARI: 3, + CHROME: 4, + OPERA: 5 + }, + + + /** + * Returns a DOM Element for the given id or element. + * @function + * @name OpenSeadragon.getElement + * @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 + * @name OpenSeadragon.getElementPosition + * @param {Element|String} element - the elemenet we want the position for. + * @returns {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 + * @name OpenSeadragon.getElementOffset + * @param {Element|String} element - the element we want the position for. + * @returns {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 + * @name OpenSeadragon.getElementSize + * @param {Element|String} element + * @returns {Point} + */ + getElementSize: function( element ) { + element = $.getElement( element ); + + return new $.Point( + element.clientWidth, + element.clientHeight + ); + }, + + + /** + * Returns the CSSStyle object for the given element. + * @function + * @name OpenSeadragon.getElementStyle + * @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, "" ); + }, + + + /** + * Gets the latest event, really only useful internally since its + * specific to IE behavior. TODO: Deprecate this from the api and + * use it internally. + * @function + * @name OpenSeadragon.getEvent + * @param {Event} [event] + * @returns {Event} + */ + getEvent: function( event ) { + if( event ){ + $.getEvent = function( event ) { + return event; + }; + } else { + $.getEvent = function() { + return window.event; + }; + } + return $.getEvent( event ); + }, + + + /** + * Gets the position of the mouse on the screen for a given event. + * @function + * @name OpenSeadragon.getMousePosition + * @param {Event} [event] + * @returns {Point} + */ + getMousePosition: function( event ) { + + if ( typeof( event.pageX ) == "number" ) { + $.getMousePosition = function( event ){ + var result = new $.Point(); + + event = $.getEvent( event ); + result.x = event.pageX; + result.y = event.pageY; + + return result; + }; + } else if ( typeof( event.clientX ) == "number" ) { + $.getMousePosition = function( event ){ + var result = new $.Point(); + + event = $.getEvent( event ); + 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 pages current scroll position. + * @function + * @name OpenSeadragon.getPageScroll + * @returns {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 { + $.getPageScroll = function(){ + return new $.Point(0,0); + }; + } + + return $.getPageScroll(); + }, + + + /** + * Determines the size of the browsers window. + * @function + * @name OpenSeadragon.getWindowSize + * @returns {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 + * @name OpenSeadragon.makeCenteredNode + * @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 + * @name OpenSeadragon.makeNeutralElement + * @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 + * @name $.now + * @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. + * Generally only IE has issues doing this correctly for formats like + * png. + * @function + * @name OpenSeadragon.makeTransparentImage + * @param {String} src + * @returns {Element} + */ + makeTransparentImage: function( src ) { + + $.makeTransparentImage = function( src ){ + var img = $.makeNeutralElement( "img" ); + + img.src = src; + + return img; + }; + + if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 7 ) { + + $.makeTransparentImage = function( src ){ + var img = $.makeNeutralElement( "img" ), + element = null; + + element = $.makeNeutralElement("span"); + element.style.display = "inline-block"; + + img.onload = function() { + element.style.width = element.style.width || img.width + "px"; + element.style.height = element.style.height || img.height + "px"; + + img.onload = null; + img = null; // to prevent memory leaks in IE + }; + + img.src = src; + element.style.filter = + "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + + src + + "', sizingMethod='scale')"; + + return element; + }; + + } + + return $.makeTransparentImage( src ); + }, + + + /** + * Sets the opacity of the specified element. + * @function + * @name OpenSeadragon.setElementOpacity + * @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 = ""; + } + } + }, + + + /** + * Add the specified CSS class to the element if not present. + * @name $.addClass + * @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; + } + }, + + + /** + * Remove the specified CSS class from the element. + * @name $.removeClass + * @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(' '); + }, + + + /** + * Adds an event listener for the given element, eventName and handler. + * @function + * @name OpenSeadragon.addEvent + * @param {Element|String} element + * @param {String} eventName + * @param {Function} handler + * @param {Boolean} [useCapture] + */ + addEvent: function( element, eventName, handler, useCapture ) { + return $._addEvent( element, eventName, handler, useCapture ); + }, + + + /** + * Remove a given event listener for the given element, event type and + * handler. + * @function + * @name OpenSeadragon.removeEvent + * @param {Element|String} element + * @param {String} eventName + * @param {Function} handler + * @param {Boolean} [useCapture] + */ + removeEvent: function( element, eventName, handler, useCapture ) { + return $._removeEvent( element, eventName, handler, useCapture ); + }, + + + /** + * Cancels the default browser behavior had the event propagated all + * the way up the DOM to the window object. + * @function + * @name OpenSeadragon.cancelEvent + * @param {Event} [event] + */ + cancelEvent: function( event ) { + event = $.getEvent( event ); + + if ( event.preventDefault ) { + $.cancelEvent = function( event ){ + // W3C for preventing default + event.preventDefault(); + }; + } else { + $.cancelEvent = function( event ){ + event = $.getEvent( event ); + // legacy for preventing default + event.cancel = true; + // IE for preventing default + event.returnValue = false; + }; + } + $.cancelEvent( event ); + }, + + + /** + * Stops the propagation of the event up the DOM. + * @function + * @name OpenSeadragon.stopEvent + * @param {Event} [event] + */ + stopEvent: function( event ) { + event = $.getEvent( event ); + + if ( event.stopPropagation ) { + // W3C for stopping propagation + $.stopEvent = function( event ){ + event.stopPropagation(); + }; + } else { + // IE for stopping propagation + $.stopEvent = function( event ){ + event = $.getEvent( event ); + event.cancelBubble = true; + }; + + } + + $.stopEvent( event ); + }, + + + /** + * 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 additonal arguments + * to be passed during construction which will be added during each + * invocation, and each invocation can add additional arguments as well. + * + * @function + * @name OpenSeadragon.createCallback + * @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 ); + }; + }, + + + /** + * Retreives the value of a url parameter from the window.location string. + * @function + * @name OpenSeadragon.getUrlParameter + * @param {String} key + * @returns {String} The value of the url parameter or null if no param matches. + */ + getUrlParameter: function( key ) { + var value = URLPARAMS[ key ]; + return value ? value : null; + }, + + + createAjaxRequest: function(){ + var request; + + if ( window.XMLHttpRequest ) { + $.createAjaxRequest = function( ){ + return new XMLHttpRequest(); + }; + request = new XMLHttpRequest(); + } else if ( window.ActiveXObject ) { + /*jshint loopfunc:true*/ + /* global ActiveXObject:true */ + for ( var i = 0; i < ACTIVEX.length; i++ ) { + try { + request = new ActiveXObject( ACTIVEX[ i ] ); + $.createAjaxRequest = function( ){ + return new ActiveXObject( ACTIVEX[ i ] ); + }; + break; + } catch (e) { + continue; + } + } + } + + if ( !request ) { + throw new Error( "Browser doesn't support XMLHttpRequest." ); + } + + return request; + }, + + + /** + * Makes an AJAX request. + * @function + * @name OpenSeadragon.makeAjaxRequest + * @param {String} url - the url to request + * @param {Function} onSuccess - a function to call on a successful response + * @param {Function} onError - a function to call on when an error occurs + * @throws {Error} + */ + makeAjaxRequest: function( url, onSuccess, onError ) { + var request = $.createAjaxRequest(); + + if ( !$.isFunction( onSuccess ) ) { + throw new Error( "makeAjaxRequest requires a success callback" ); + } + + request.onreadystatechange = function() { + // 4 = DONE (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#Properties) + if ( request.readyState == 4 ) { + request.onreadystatechange = function(){}; + + if ( request.status == 200 ) { + onSuccess( request ); + } else { + $.console.log( "AJAX request returned %s: %s", request.status, url ); + + if ( $.isFunction( onError ) ) { + onError( request ); + } + } + } + }; + + try { + request.open( "GET", url, true ); + request.send( null ); + } catch (e) { + var msg = e.message; + + /* + IE < 10 does not support CORS and an XHR request to a different origin will fail as soon + as send() is called. This is particularly easy to miss during development and appear in + production if you use a CDN or domain sharding and the security policy is likely to break + exception handlers since any attempt to access a property of the request object will + raise an access denied TypeError inside the catch block. + + To be friendlier, we'll check for this specific error and add a documentation pointer + to point developers in the right direction. We test the exception number because IE's + error messages are localized. + */ + var oldIE = $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 10; + if ( oldIE && typeof( e.number ) != "undefined" && e.number == -2147024891 ) { + msg += "\nSee http://msdn.microsoft.com/en-us/library/ms537505(v=vs.85).aspx#xdomain"; + } + + $.console.log( "%s while making AJAX request: %s", e.name, msg ); + + request.onreadystatechange = function(){}; + + if ( $.isFunction( onError ) ) { + onError( request, e ); + } + } + }, + + /** + * Taken from jQuery 1.6.1 + * @function + * @name OpenSeadragon.jsonp + * @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 + * @name OpenSeadragon.createFromDZI + * @deprecated - use OpenSeadragon.Viewer.prototype.open + */ + createFromDZI: function() { + throw "OpenSeadragon.createFromDZI is deprecated, use Viewer.open."; + }, + + /** + * Parses an XML string into a DOM Document. + * @function + * @name OpenSeadragon.parseXml + * @param {String} string + * @returns {Document} + */ + parseXml: function( string ) { + //TODO: yet another example where we can determine the correct + // implementation once at start-up instead of everytime we use + // the function. DONE. + if ( window.ActiveXObject ) { + + $.parseXml = function( string ){ + var xmlDoc = null; + + xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); + xmlDoc.async = false; + xmlDoc.loadXML( string ); + return xmlDoc; + }; + + } else if ( window.DOMParser ) { + + $.parseXml = function( string ){ + var xmlDoc = null, + parser; + + parser = new DOMParser(); + xmlDoc = parser.parseFromString( string, "text/xml" ); + return xmlDoc; + }; + + } else { + throw new Error( "Browser doesn't support XML DOM." ); + } + + return $.parseXml( string ); + }, + + + /** + * Reports whether the image format is supported for tiling in this + * version. + * @function + * @name OpenSeadragon.imageFormatSupported + * @param {String} [extension] + * @returns {Boolean} + */ + imageFormatSupported: function( extension ) { + extension = extension ? extension : ""; + return !!FILEFORMATS[ extension.toLowerCase() ]; + } + + }); + + + /** + * The current browser vendor, version, and related information regarding + * detected features. Features include
    + * 'alpha' - Does the browser support image alpha + * transparency.
    + * @name $.Browser + * @static + */ + $.Browser = { + vendor: $.BROWSERS.UNKNOWN, + version: 0, + alpha: true + }; + + + var ACTIVEX = [ + "Msxml2.XMLHTTP", + "Msxml3.XMLHTTP", + "Microsoft.XMLHTTP" + ], + 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 app = navigator.appName, + ver = navigator.appVersion, + ua = navigator.userAgent; + + //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( "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" ) + ) + ); + } + } + 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 ) { + URLPARAMS[ part.substring( 0, sep ) ] = + decodeURIComponent( part.substring( sep + 1 ) ); + } + } + + //determine if this browser supports image alpha transparency + $.Browser.alpha = !( + ( + $.Browser.vendor == $.BROWSERS.IE && + $.Browser.version < 9 + ) || ( + $.Browser.vendor == $.BROWSERS.CHROME && + $.Browser.version < 2 + ) + ); + + //determine if this browser supports element.style.opacity + $.Browser.opacity = !( + $.Browser.vendor == $.BROWSERS.IE && + $.Browser.version < 9 + ); + + })(); + + + //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 propogate 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 + }; + + + // 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; + } + } + + /** + * @private + * @inner + * @function + * @param {XMLHttpRequest} xhr + * @param {String} tilesUrl + * @deprecated + */ + function processDZIResponse( xhr, tilesUrl ) { + var status, + statusText, + doc = null; + + if ( !xhr ) { + throw new Error( $.getString( "Errors.Security" ) ); + } else if ( xhr.status !== 200 && xhr.status !== 0 ) { + status = xhr.status; + statusText = ( status == 404 ) ? + "Not Found" : + xhr.statusText; + throw new Error( $.getString( "Errors.Status", status, statusText ) ); + } + + if ( xhr.responseXML && xhr.responseXML.documentElement ) { + doc = xhr.responseXML; + } else if ( xhr.responseText ) { + doc = $.parseXml( xhr.responseText ); + } + + return processDZIXml( doc, tilesUrl ); + } + + /** + * @private + * @inner + * @function + * @param {Document} xmlDoc + * @param {String} tilesUrl + * @deprecated + */ + function processDZIXml( xmlDoc, tilesUrl ) { + + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + var root = xmlDoc.documentElement, + rootName = root.tagName; + + if ( rootName == "Image" ) { + try { + return processDZI( root, tilesUrl ); + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( $.getString("Errors.Dzi") ); + } + } else if ( rootName == "Collection" ) { + throw new Error( $.getString( "Errors.Dzc" ) ); + } else if ( rootName == "Error" ) { + return $._processDZIError( root ); + } + + throw new Error( $.getString( "Errors.Dzi" ) ); + } + + /** + * @private + * @inner + * @function + * @param {Element} imageNode + * @param {String} tilesUrl + * @deprecated + */ + function processDZI( imageNode, tilesUrl ) { + var fileFormat = imageNode.getAttribute( "Format" ), + sizeNode = imageNode.getElementsByTagName( "Size" )[ 0 ], + dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ), + width = parseInt( sizeNode.getAttribute( "Width" ), 10 ), + height = parseInt( sizeNode.getAttribute( "Height" ), 10 ), + tileSize = parseInt( imageNode.getAttribute( "TileSize" ), 10 ), + tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ), 10 ), + dispRects = [], + dispRectNode, + rectNode, + i; + + if ( !$.imageFormatSupported( fileFormat ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) + ); + } + + for ( i = 0; i < dispRectNodes.length; i++ ) { + dispRectNode = dispRectNodes[ i ]; + rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ]; + + dispRects.push( new $.DisplayRect( + parseInt( rectNode.getAttribute( "X" ), 10 ), + parseInt( rectNode.getAttribute( "Y" ), 10 ), + parseInt( rectNode.getAttribute( "Width" ), 10 ), + parseInt( rectNode.getAttribute( "Height" ), 10 ), + 0, // ignore MinLevel attribute, bug in Deep Zoom Composer + parseInt( dispRectNode.getAttribute( "MaxLevel" ), 10 ) + )); + } + return new $.DziTileSource( + width, + height, + tileSize, + tileOverlap, + tilesUrl, + fileFormat, + dispRects + ); + } + + /** + * @private + * @inner + * @function + * @param {Element} imageNode + * @param {String} tilesUrl + * @deprecated + */ + function processDZIJSON( imageData, tilesUrl ) { + var fileFormat = imageData.Format, + sizeData = imageData.Size, + dispRectData = imageData.DisplayRect || [], + width = parseInt( sizeData.Width, 10 ), + height = parseInt( sizeData.Height, 10 ), + tileSize = parseInt( imageData.TileSize, 10 ), + tileOverlap = parseInt( imageData.Overlap, 10 ), + dispRects = [], + rectData, + i; + + if ( !$.imageFormatSupported( fileFormat ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) + ); + } + + for ( i = 0; i < dispRectData.length; i++ ) { + rectData = dispRectData[ i ].Rect; + + dispRects.push( new $.DisplayRect( + parseInt( rectData.X, 10 ), + parseInt( rectData.Y, 10 ), + parseInt( rectData.Width, 10 ), + parseInt( rectData.Height, 10 ), + 0, // ignore MinLevel attribute, bug in Deep Zoom Composer + parseInt( rectData.MaxLevel, 10 ) + )); + } + return new $.DziTileSource( + width, + height, + tileSize, + tileOverlap, + tilesUrl, + fileFormat, + dispRects + ); + } + + /** + * @private + * @inner + * @function + * @param {Document} errorNode + * @throws {Error} + * @deprecated + */ + $._processDZIError = function ( errorNode ) { + var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ], + message = messageNode.firstChild.nodeValue; + + throw new Error(message); + }; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - full-screen support functions + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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. + */ + +/* + * Implementation and research by John Dyer in: + * http://johndyer.name/native-fullscreen-javascript-api-plus-jquery-plugin/ + * John Dyer has released this fullscreen code under the MIT license; see + * . + * + * Copyright (C) 2011 John Dyer + * + * 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. + */ + + +/** + * Determines the appropriate level of native full screen support we can get + * from the browser. + * @name $.supportsFullScreen + */ +(function( $ ) { + var fullScreenApi = { + supportsFullScreen: false, + isFullScreen: function() { return false; }, + requestFullScreen: function() {}, + cancelFullScreen: function() {}, + fullScreenEventName: '', + prefix: '' + }, + browserPrefixes = 'webkit moz o ms khtml'.split(' '); + + // check for native support + if (typeof document.cancelFullScreen != 'undefined') { + fullScreenApi.supportsFullScreen = true; + } else { + // check for fullscreen support by vendor prefix + for (var i = 0, il = browserPrefixes.length; i < il; i++ ) { + fullScreenApi.prefix = browserPrefixes[i]; + + if (typeof document[fullScreenApi.prefix + 'CancelFullScreen' ] != 'undefined' ) { + fullScreenApi.supportsFullScreen = true; + + break; + } + } + } + + // update methods to do something useful + if (fullScreenApi.supportsFullScreen) { + fullScreenApi.fullScreenEventName = fullScreenApi.prefix + 'fullscreenchange'; + + fullScreenApi.isFullScreen = function() { + switch (this.prefix) { + case '': + return document.fullScreen; + case 'webkit': + return document.webkitIsFullScreen; + default: + return document[this.prefix + 'FullScreen']; + } + }; + fullScreenApi.requestFullScreen = function( element ) { + return (this.prefix === '') ? + element.requestFullScreen() : + element[this.prefix + 'RequestFullScreen'](); + + }; + fullScreenApi.cancelFullScreen = function() { + return (this.prefix === '') ? + document.cancelFullScreen() : + document[this.prefix + 'CancelFullScreen'](); + }; + } else if ( typeof window.ActiveXObject !== "undefined" ){ + // Older IE. Support based on: + // http://stackoverflow.com/questions/1125084/how-to-make-in-javascript-full-screen-windows-stretching-all-over-the-screen/7525760 + fullScreenApi.requestFullScreen = function(){ + /* global ActiveXObject:true */ + var wscript = new ActiveXObject("WScript.Shell"); + if ( wscript !== null ) { + wscript.SendKeys("{F11}"); + } + return false; + }; + fullScreenApi.cancelFullScreen = fullScreenApi.requestFullScreen; + } + + // export api + $.extend( $, fullScreenApi ); + +})( OpenSeadragon ); + +/* + * OpenSeadragon - EventSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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($){ + +/** + * For use by classes which want to support custom, non-browser events. + * TODO: Add a method 'one' which automatically unbinds a listener after + * the first triggered event that matches. + * @class + */ +$.EventSource = function() { + this.events = {}; +}; + +$.EventSource.prototype = { + + /** + * Add an event handler for a given event. + * @function + * @param {String} eventName - Name of event to register. + * @param {Function} handler - Function to call when event is triggered. + * @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler. + */ + addHandler: function ( eventName, handler, userData ) { + var events = this.events[ eventName ]; + if ( !events ) { + this.events[ eventName ] = events = []; + } + if ( handler && $.isFunction( handler ) ) { + events[ events.length ] = { handler: handler, userData: userData || null }; + } + }, + + /** + * 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 {Function} 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; + } + }, + + + /** + * 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 ] = []; + } + } + }, + + /** + * Retrive the list of all handlers registered for a given event. + * @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 {Function} handler - Function to call when event is triggered. + */ + 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 ) { + if ( !eventArgs ) { + eventArgs = {}; + } + + handler( this, eventArgs ); + } + } +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - MouseTracker + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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 ( $ ) { + + // is any button currently being pressed while mouse events occur + var IS_BUTTON_DOWN = false, + // is any tracker currently capturing? + IS_CAPTURING = false, + // dictionary from hash to MouseTracker + ACTIVE = {}, + // list of trackers interested in capture + CAPTURING = [], + // dictionary from hash to private properties + THIS = {}; + + /** + * The MouseTracker allows other classes to set handlers for common mouse + * events on a specific element like, 'enter', 'exit', 'press', 'release', + * 'scroll', 'click', and 'drag'. + * @class + * @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 'elements', 'clickTimeThreshold', + * and 'clickDistThreshold' in that order. + * @param {Element|String} options.element + * A reference to an element or an element id for which the mouse + * events will be monitored. + * @param {Number} options.clickTimeThreshold + * The number of milliseconds within which multiple mouse clicks + * will be treated as a single event. + * @param {Number} options.clickDistThreshold + * The distance between mouse click within multiple mouse clicks + * will be treated as a single event. + * @param {Number} options.stopDelay + * The number of milliseconds without mouse move before the mouse stop + * event is fired. + * @param {Function} options.enterHandler + * An optional handler for mouse enter. + * @param {Function} options.exitHandler + * An optional handler for mouse exit. + * @param {Function} options.pressHandler + * An optional handler for mouse press. + * @param {Function} options.releaseHandler + * An optional handler for mouse release. + * @param {Function} options.moveHandler + * An optional handler for mouse move. + * @param {Function} options.scrollHandler + * An optional handler for mouse scroll. + * @param {Function} options.clickHandler + * An optional handler for mouse click. + * @param {Function} options.dragHandler + * An optional handler for mouse drag. + * @param {Function} options.keyHandler + * An optional handler for keypress. + * @param {Function} options.focusHandler + * An optional handler for focus. + * @param {Function} options.blurHandler + * An optional handler for blur. + * @param {Object} [options.userData=null] + * Arbitrary object to be passed unchanged to any attached handler methods. + * @property {Number} hash + * An unique hash for this tracker. + * @property {Element} element + * The element for which mouse event are being monitored. + * @property {Number} clickTimeThreshold + * The number of milliseconds within which mutliple mouse clicks + * will be treated as a single event. + * @property {Number} clickDistThreshold + * The distance between mouse click within multiple mouse clicks + * will be treated as a single event. + */ + $.MouseTracker = function ( options ) { + + var args = arguments; + + if ( !$.isPlainObject( options ) ) { + options = { + element: args[ 0 ], + clickTimeThreshold: args[ 1 ], + clickDistThreshold: args[ 2 ] + }; + } + + this.hash = Math.random(); + this.element = $.getElement( options.element ); + this.clickTimeThreshold = options.clickTimeThreshold; + this.clickDistThreshold = options.clickDistThreshold; + this.userData = options.userData || null; + this.stopDelay = options.stopDelay || 50; + + this.enterHandler = options.enterHandler || null; + this.exitHandler = options.exitHandler || null; + this.pressHandler = options.pressHandler || null; + this.releaseHandler = options.releaseHandler || null; + this.moveHandler = options.moveHandler || null; + this.scrollHandler = options.scrollHandler || null; + this.clickHandler = options.clickHandler || null; + this.dragHandler = options.dragHandler || null; + this.stopHandler = options.stopHandler || null; + this.keyHandler = options.keyHandler || null; + this.focusHandler = options.focusHandler || null; + this.blurHandler = options.blurHandler || null; + + //Store private properties in a scope sealed hash map + var _this = this; + + /** + * @private + * @property {Boolean} tracking + * Are we currently tracking mouse events. + * @property {Boolean} capturing + * Are we curruently capturing mouse events. + * @property {Boolean} insideElementPressed + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @property {Boolean} insideElement + * Are we currently inside the screen area of the tracked element. + * @property {OpenSeadragon.Point} lastPoint + * Position of last mouse down/move + * @property {Number} lastMouseDownTime + * Time of last mouse down. + * @property {OpenSeadragon.Point} lastMouseDownPoint + * Position of last mouse down + */ + THIS[ this.hash ] = { + mouseover: function ( event ) { onMouseOver( _this, event, false ); }, + mouseout: function ( event ) { onMouseOut( _this, event, false ); }, + mousedown: function ( event ) { onMouseDown( _this, event ); }, + mouseup: function ( event ) { onMouseUp( _this, event, false ); }, + mousemove: function ( event ) { onMouseMove( _this, event ); }, + click: function ( event ) { onMouseClick( _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 ); }, + mouseupie: function ( event ) { onMouseUpIE( _this, event ); }, + mousemovecapturedie: function ( event ) { onMouseMoveCapturedIE( _this, event ); }, + mouseupcaptured: function ( event ) { onMouseUpCaptured( _this, event ); }, + mousemovecaptured: function ( event ) { onMouseMoveCaptured( _this, event, false ); }, + touchstart: function ( event ) { onTouchStart( _this, event ); }, + touchmove: function ( event ) { onTouchMove( _this, event ); }, + touchend: function ( event ) { onTouchEnd( _this, event ); }, + keypress: function ( event ) { onKeyPress( _this, event ); }, + focus: function ( event ) { onFocus( _this, event ); }, + blur: function ( event ) { onBlur( _this, event ); }, + tracking: false, + capturing: false, + insideElementPressed: false, + insideElement: false, + lastPoint: null, + lastMouseDownTime: null, + lastMouseDownPoint: null, + lastPinchDelta: 0 + }; + + }; + + $.MouseTracker.prototype = { + + /** + * Clean up any events or objects created by the mouse tracker. + * @function + */ + destroy: function () { + stopTracking( this ); + this.element = null; + }, + + /** + * 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; + }, + + /** + * 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 {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. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. + * @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 + * @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 {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. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. + * @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 + * @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 {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. + * @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 {OpenSeadragon.Point} event.position + * The position of the event relative to 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.insideElementReleased + * True if the cursor still inside the tracked element when the button was released. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. + * @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 {OpenSeadragon.Point} event.position + * The position of the event relative to the tracked element. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. + * @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 {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. + * @param {Object} event.originalEvent + * The original event object. + * @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 {OpenSeadragon.Point} event.position + * The position of the event relative to the tracked element. + * @param {Number} event.quick + * True only if the clickDistThreshold and clickDeltaThreshold are both pased. Useful for ignoring 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. + * @param {Object} event.originalEvent + * The original event object. + * @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 {OpenSeadragon.Point} event.position + * The position of the event relative to the tracked element. + * @param {OpenSeadragon.Point} event.delta + * The x,y components of the difference between start drag and end drag. Usefule for ignoring or weighting the 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. + * @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 {OpenSeadragon.Point} event.position + * The position of the event relative to the tracked element. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. + * @param {Object} event.originalEvent + * The original event object. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + stopHandler: function () { }, + + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @param {Object} event + * @param {OpenSeadragon.MouseTracker} event.eventSource + * A reference to the tracker instance. + * @param {Number} event.keyCode + * The key code that was pressed. + * @param {Boolean} event.shift + * True if the shift key was pressed during this event. + * @param {Object} event.originalEvent + * The original event object. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + keyHandler: function () { }, + + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @param {Object} event + * @param {OpenSeadragon.MouseTracker} event.eventSource + * A reference to the tracker instance. + * @param {Object} event.originalEvent + * The original event object. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + focusHandler: function () { }, + + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @param {Object} event + * @param {OpenSeadragon.MouseTracker} event.eventSource + * A reference to the tracker instance. + * @param {Object} event.originalEvent + * The original event object. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + blurHandler: function () { } + }; + + /** + * Detect available mouse wheel event. + */ + $.MouseTracker.wheelEventName = ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version > 8 ) || + ( 'onwheel' in document.createElement( 'div' ) ) ? 'wheel' : // Modern browsers support 'wheel' + document.onmousewheel !== undefined ? 'mousewheel' : // Webkit and IE support at least 'mousewheel' + 'DOMMouseScroll'; // Assume old Firefox + + /** + * Starts tracking mouse events on this element. + * @private + * @inner + */ + function startTracking( tracker ) { + var events = [ + "mouseover", "mouseout", "mousedown", "mouseup", "mousemove", + "click", + $.MouseTracker.wheelEventName, + "touchstart", "touchmove", "touchend", + "keypress", + "focus", "blur" + ], + delegate = THIS[ tracker.hash ], + event, + i; + + // Add 'MozMousePixelScroll' event handler for older Firefox + if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { + events.push( "MozMousePixelScroll" ); + } + + if ( !delegate.tracking ) { + for ( i = 0; i < events.length; i++ ) { + event = events[ i ]; + $.addEvent( + tracker.element, + event, + delegate[ event ], + false + ); + } + delegate.tracking = true; + ACTIVE[ tracker.hash ] = tracker; + } + } + + /** + * Stops tracking mouse events on this element. + * @private + * @inner + */ + function stopTracking( tracker ) { + var events = [ + "mouseover", "mouseout", "mousedown", "mouseup", "mousemove", + "click", + $.MouseTracker.wheelEventName, + "touchstart", "touchmove", "touchend", + "keypress", + "focus", "blur" + ], + delegate = THIS[ tracker.hash ], + event, + i; + + // Remove 'MozMousePixelScroll' event handler for older Firefox + if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { + events.push( "MozMousePixelScroll" ); + } + + if ( delegate.tracking ) { + for ( i = 0; i < events.length; i++ ) { + event = events[ i ]; + $.removeEvent( + tracker.element, + event, + delegate[ event ], + false + ); + } + + releaseMouse( tracker ); + delegate.tracking = false; + delete ACTIVE[ tracker.hash ]; + } + } + + /** + * @private + * @inner + */ + function hasMouse( tracker ) { + return THIS[ tracker.hash ].insideElement; + } + + /** + * Begin capturing mouse events on this element. + * @private + * @inner + */ + function captureMouse( tracker ) { + var delegate = THIS[ tracker.hash ]; + if ( !delegate.capturing ) { + + if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { + $.removeEvent( + tracker.element, + "mouseup", + delegate.mouseup, + false + ); + $.addEvent( + tracker.element, + "mouseup", + delegate.mouseupie, + true + ); + $.addEvent( + tracker.element, + "mousemove", + delegate.mousemovecapturedie, + true + ); + } else { + $.addEvent( + window, + "mouseup", + delegate.mouseupcaptured, + true + ); + $.addEvent( + window, + "mousemove", + delegate.mousemovecaptured, + true + ); + } + delegate.capturing = true; + } + } + + + /** + * Stop capturing mouse events on this element. + * @private + * @inner + */ + function releaseMouse( tracker ) { + var delegate = THIS[ tracker.hash ]; + if ( delegate.capturing ) { + + if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { + $.removeEvent( + tracker.element, + "mousemove", + delegate.mousemovecapturedie, + true + ); + $.removeEvent( + tracker.element, + "mouseup", + delegate.mouseupie, + true + ); + $.addEvent( + tracker.element, + "mouseup", + delegate.mouseup, + false + ); + } else { + $.removeEvent( + window, + "mousemove", + delegate.mousemovecaptured, + true + ); + $.removeEvent( + window, + "mouseup", + delegate.mouseupcaptured, + true + ); + } + delegate.capturing = false; + } + } + + + /** + * @private + * @inner + */ + function triggerOthers( tracker, handler, event, isTouch ) { + var otherHash; + for ( otherHash in ACTIVE ) { + if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { + handler( ACTIVE[ otherHash ], event, isTouch ); + } + } + } + + + /** + * @private + * @inner + */ + function onFocus( tracker, event ) { + //console.log( "focus %s", event ); + var propagate; + if ( tracker.focusHandler ) { + propagate = tracker.focusHandler( + { + eventSource: tracker, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onBlur( tracker, event ) { + //console.log( "blur %s", event ); + var propagate; + if ( tracker.blurHandler ) { + propagate = tracker.blurHandler( + { + eventSource: tracker, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onKeyPress( tracker, event ) { + //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); + var propagate; + if ( tracker.keyHandler ) { + propagate = tracker.keyHandler( + { + eventSource: tracker, + position: getMouseRelative( event, tracker.element ), + keyCode: event.keyCode ? event.keyCode : event.charCode, + shift: event.shiftKey, + originalEvent: event, + userData: tracker.userData + } + ); + if ( !propagate ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onMouseOver( tracker, event, isTouch ) { + + var delegate = THIS[ tracker.hash ], + propagate; + + isTouch = isTouch || false; + + event = $.getEvent( event ); + + if ( !isTouch ) { + if ( $.Browser.vendor == $.BROWSERS.IE && + $.Browser.version < 9 && + delegate.capturing && + !isChild( event.srcElement, tracker.element ) ) { + + triggerOthers( tracker, onMouseOver, event, isTouch ); + } + + var to = event.target ? + event.target : + event.srcElement, + from = event.relatedTarget ? + event.relatedTarget : + event.fromElement; + + if ( !isChild( tracker.element, to ) || + isChild( tracker.element, from ) ) { + return; + } + } + + delegate.insideElement = true; + + if ( tracker.enterHandler ) { + propagate = tracker.enterHandler( + { + eventSource: tracker, + position: getMouseRelative( isTouch ? event.changedTouches[ 0 ] : event, tracker.element ), + insideElementPressed: delegate.insideElementPressed, + buttonDownAny: IS_BUTTON_DOWN, + isTouchEvent: isTouch, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onMouseOut( tracker, event, isTouch ) { + var delegate = THIS[ tracker.hash ], + propagate; + + isTouch = isTouch || false; + + event = $.getEvent( event ); + + if ( !isTouch ) { + if ( $.Browser.vendor == $.BROWSERS.IE && + $.Browser.version < 9 && + delegate.capturing && + !isChild( event.srcElement, tracker.element ) ) { + + triggerOthers( tracker, onMouseOut, event, isTouch ); + + } + + var from = event.target ? + event.target : + event.srcElement, + to = event.relatedTarget ? + event.relatedTarget : + event.toElement; + + if ( !isChild( tracker.element, from ) || + isChild( tracker.element, to ) ) { + return; + } + } + + delegate.insideElement = false; + + if ( tracker.exitHandler ) { + propagate = tracker.exitHandler( + { + eventSource: tracker, + position: getMouseRelative( isTouch ? event.changedTouches[ 0 ] : event, tracker.element ), + insideElementPressed: delegate.insideElementPressed, + buttonDownAny: IS_BUTTON_DOWN, + isTouchEvent: isTouch, + originalEvent: event, + userData: tracker.userData + } + ); + + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onMouseDown( tracker, event, noCapture, isTouch ) { + var delegate = THIS[ tracker.hash ], + propagate; + + isTouch = isTouch || false; + + event = $.getEvent(event); + + var eventOrTouchPoint = isTouch ? event.touches[ 0 ] : event; + + if ( event.button == 2 ) { + return; + } + + delegate.insideElementPressed = true; + + delegate.lastPoint = getMouseAbsolute( eventOrTouchPoint ); + delegate.lastMouseDownPoint = delegate.lastPoint; + delegate.lastMouseDownTime = $.now(); + + if ( tracker.pressHandler ) { + propagate = tracker.pressHandler( + { + eventSource: tracker, + position: getMouseRelative( eventOrTouchPoint, tracker.element ), + isTouchEvent: isTouch, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + + if ( tracker.pressHandler || tracker.dragHandler ) { + $.cancelEvent( event ); + } + + if ( noCapture ) { + return; + } + + if ( isTouch || + !( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) || + !IS_CAPTURING ) { + captureMouse( tracker ); + IS_CAPTURING = true; + // reset to empty & add us + CAPTURING = [ tracker ]; + } else if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { + // add us to the list + CAPTURING.push( tracker ); + } + } + + /** + * @private + * @inner + */ + function onTouchStart( tracker, event ) { + var touchA, + touchB; + + if ( event.touches.length == 1 && + event.targetTouches.length == 1 && + event.changedTouches.length == 1 ) { + + THIS[ tracker.hash ].lastTouch = event.touches[ 0 ]; + onMouseOver( tracker, event, true ); + // call with no capture as the onMouseMoveCaptured will + // be triggered by onTouchMove + onMouseDown( tracker, event, true, true ); + } + + if ( event.touches.length == 2 ) { + + touchA = getMouseAbsolute( event.touches[ 0 ] ); + touchB = getMouseAbsolute( event.touches[ 1 ] ); + THIS[ tracker.hash ].lastPinchDelta = + Math.abs( touchA.x - touchB.x ) + + Math.abs( touchA.y - touchB.y ); + THIS[ tracker.hash ].pinchMidpoint = new $.Point( + ( touchA.x + touchB.x ) / 2, + ( touchA.y + touchB.y ) / 2 + ); + //$.console.debug("pinch start : "+THIS[ tracker.hash ].lastPinchDelta); + } + + event.preventDefault(); + } + + + /** + * @private + * @inner + */ + function onMouseUp( tracker, event, isTouch ) { + var delegate = THIS[ tracker.hash ], + //were we inside the tracked element when we were pressed + insideElementPressed = delegate.insideElementPressed, + //are we still inside the tracked element when we released + insideElementReleased = delegate.insideElement, + propagate; + + isTouch = isTouch || false; + + event = $.getEvent(event); + + if ( event.button == 2 ) { + return; + } + + delegate.insideElementPressed = false; + + if ( tracker.releaseHandler ) { + propagate = tracker.releaseHandler( + { + eventSource: tracker, + position: getMouseRelative( isTouch ? event.changedTouches[ 0 ] : event, tracker.element ), + insideElementPressed: insideElementPressed, + insideElementReleased: insideElementReleased, + isTouchEvent: isTouch, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + + if ( insideElementPressed && insideElementReleased ) { + handleMouseClick( tracker, event, isTouch ); + } + } + + + /** + * @private + * @inner + */ + function onTouchEnd( tracker, event ) { + + if ( event.touches.length === 0 && + event.targetTouches.length === 0 && + event.changedTouches.length == 1 ) { + + THIS[ tracker.hash ].lastTouch = null; + + // call with no release, as the mouse events are + // not registered in onTouchStart + onMouseUpCaptured( tracker, event, true, true ); + onMouseOut( tracker, event, true ); + } + if ( event.touches.length + event.changedTouches.length == 2 ) { + THIS[ tracker.hash ].lastPinchDelta = null; + THIS[ tracker.hash ].pinchMidpoint = null; + //$.console.debug("pinch end"); + } + event.preventDefault(); + } + + + /** + * Only triggered once by the deepest element that initially received + * the mouse down event. We want to make sure THIS event doesn't bubble. + * Instead, we want to trigger the elements that initially received the + * mouse down event (including this one) only if the mouse is no longer + * inside them. Then, we want to release capture, and emulate a regular + * mouseup on the event that this event was meant for. + * @private + * @inner + */ + function onMouseUpIE( tracker, event ) { + var othertracker, + i; + + event = $.getEvent( event ); + + if ( event.button == 2 ) { + return; + } + + for ( i = 0; i < CAPTURING.length; i++ ) { + othertracker = CAPTURING[ i ]; + if ( !hasMouse( othertracker ) ) { + onMouseUp( othertracker, event, false ); + } + } + + releaseMouse( tracker ); + IS_CAPTURING = false; + event.srcElement.fireEvent( + "on" + event.type, + document.createEventObject( event ) + ); + + $.stopEvent( event ); + } + + + /** + * Only triggered in W3C browsers by elements within which the mouse was + * initially pressed, since they are now listening to the window for + * mouseup during the capture phase. We shouldn't handle the mouseup + * here if the mouse is still inside this element, since the regular + * mouseup handler will still fire. + * @private + * @inner + */ + function onMouseUpCaptured( tracker, event, noRelease, isTouch ) { + isTouch = isTouch || false; + + if ( !THIS[ tracker.hash ].insideElement || isTouch ) { + onMouseUp( tracker, event, isTouch ); + } + + if ( noRelease ) { + return; + } + + releaseMouse( tracker ); + } + + + /** + * @private + * @inner + */ + function onMouseMove( tracker, event ) { + if ( tracker.moveHandler ) { + event = $.getEvent( event ); + + var propagate = tracker.moveHandler( + { + eventSource: tracker, + position: getMouseRelative( event, tracker.element ), + isTouchEvent: false, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + if ( tracker.stopHandler ) { + clearTimeout( tracker.stopTimeOut ); + tracker.stopTimeOut = setTimeout( function() { + onMouseStop( tracker, event ); + }, tracker.stopDelay ); + } + } + + /** + * @private + * @inner + */ + function onMouseStop( tracker, originalMoveEvent ) { + if ( tracker.stopHandler ) { + tracker.stopHandler( { + eventSource: tracker, + position: getMouseRelative( originalMoveEvent, tracker.element ), + isTouchEvent: false, + originalEvent: originalMoveEvent, + userData: tracker.userData + } ); + } + } + + /** + * @private + * @inner + */ + function onMouseClick( tracker, event ) { + if ( tracker.clickHandler ) { + $.cancelEvent( event ); + } + } + + + /** + * Handler for 'wheel' events + * + * @private + * @inner + */ + function onWheel( tracker, event ) { + handleWheelEvent( tracker, event, event, false ); + } + + + /** + * Handler for 'mousewheel', 'DOMMouseScroll', and 'MozMousePixelScroll' events + * + * @private + * @inner + */ + function onMouseWheel( tracker, event ) { + // For legacy IE, access the global (window) event object + event = event || window.event; + + // Simulate a 'wheel' event + var simulatedEvent = { + target: event.target || event.srcElement, + type: "wheel", + shiftKey: event.shiftKey || false, + clientX: event.clientX, + clientY: event.clientY, + pageX: event.pageX ? event.pageX : event.clientX, + pageY: event.pageY ? event.pageY : event.clientY, + deltaMode: event.type == "MozMousePixelScroll" ? 0 : 1, // 0=pixel, 1=line, 2=page + deltaX: 0, + deltaZ: 0 + }; + + // Calculate deltaY + if ( $.MouseTracker.wheelEventName == "mousewheel" ) { + simulatedEvent.deltaY = - 1 / $.DEFAULT_SETTINGS.pixelsPerWheelLine * event.wheelDelta; + } else { + simulatedEvent.deltaY = event.detail; + } + + handleWheelEvent( tracker, simulatedEvent, event, false ); + } + + + /** + * Handles 'wheel' events. + * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()) or onTouchMove(). + * + * @private + * @inner + */ + function handleWheelEvent( tracker, event, originalEvent, isTouch ) { + var nDelta = 0, + propagate; + + isTouch = isTouch || false; + + // The nDelta variable is gated to provide smooth z-index scrolling + // since the mouse wheel allows for substantial deltas meant for rapid + // y-index scrolling. + // event.deltaMode: 0=pixel, 1=line, 2=page + // TODO: Deltas in pixel mode should be accumulated then a scroll value computed after $.DEFAULT_SETTINGS.pixelsPerWheelLine threshold reached + nDelta = event.deltaY < 0 ? 1 : -1; + + if ( tracker.scrollHandler ) { + propagate = tracker.scrollHandler( + { + eventSource: tracker, + position: getMouseRelative( event, tracker.element ), + scroll: nDelta, + shift: event.shiftKey, + isTouchEvent: isTouch, + originalEvent: originalEvent, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( originalEvent ); + } + } + } + + + /** + * @private + * @inner + */ + function handleMouseClick( tracker, event, isTouch ) { + var delegate = THIS[ tracker.hash ], + propagate; + + isTouch = isTouch || false; + + event = $.getEvent( event ); + + var eventOrTouchPoint = isTouch ? event.changedTouches[ 0 ] : event; + + if ( event.button == 2 ) { + return; + } + + var time = $.now() - delegate.lastMouseDownTime, + point = getMouseAbsolute( eventOrTouchPoint ), + distance = delegate.lastMouseDownPoint.distanceTo( point ), + quick = time <= tracker.clickTimeThreshold && + distance <= tracker.clickDistThreshold; + + if ( tracker.clickHandler ) { + propagate = tracker.clickHandler( + { + eventSource: tracker, + position: getMouseRelative( eventOrTouchPoint, tracker.element ), + quick: quick, + shift: event.shiftKey, + isTouchEvent: isTouch, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onMouseMoveCaptured( tracker, event, isTouch ) { + var delegate = THIS[ tracker.hash ], + delta, + propagate, + point; + + isTouch = isTouch || false; + + event = $.getEvent(event); + var eventOrTouchPoint = isTouch ? event.touches[ 0 ] : event; + point = getMouseAbsolute( eventOrTouchPoint ); + delta = point.minus( delegate.lastPoint ); + + delegate.lastPoint = point; + + if ( tracker.dragHandler ) { + propagate = tracker.dragHandler( + { + eventSource: tracker, + position: getMouseRelative( eventOrTouchPoint, tracker.element ), + delta: delta, + shift: event.shiftKey, + isTouchEvent: isTouch, + originalEvent: event, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onTouchMove( tracker, event ) { + var touchA, + touchB, + pinchDelta; + + if ( !THIS[ tracker.hash ].lastTouch ) { + return; + } + + if ( event.touches.length === 1 && + event.targetTouches.length === 1 && + event.changedTouches.length === 1 && + THIS[ tracker.hash ].lastTouch.identifier === event.touches[ 0 ].identifier ) { + + onMouseMoveCaptured( tracker, event, true ); + + } else if ( event.touches.length === 2 ) { + + touchA = getMouseAbsolute( event.touches[ 0 ] ); + touchB = getMouseAbsolute( event.touches[ 1 ] ); + pinchDelta = + Math.abs( touchA.x - touchB.x ) + + Math.abs( touchA.y - touchB.y ); + + //TODO: make the 75px pinch threshold configurable + if ( Math.abs( THIS[ tracker.hash ].lastPinchDelta - pinchDelta ) > 75 ) { + //$.console.debug( "pinch delta : " + pinchDelta + " | previous : " + THIS[ tracker.hash ].lastPinchDelta); + + // Simulate a 'wheel' event + var simulatedEvent = { + target: event.target || event.srcElement, + type: "wheel", + shiftKey: event.shiftKey || false, + clientX: THIS[ tracker.hash ].pinchMidpoint.x, + clientY: THIS[ tracker.hash ].pinchMidpoint.y, + pageX: THIS[ tracker.hash ].pinchMidpoint.x, + pageY: THIS[ tracker.hash ].pinchMidpoint.y, + deltaMode: 1, // 0=pixel, 1=line, 2=page + deltaX: 0, + deltaY: ( THIS[ tracker.hash ].lastPinchDelta > pinchDelta ) ? 1 : -1, + deltaZ: 0 + }; + + handleWheelEvent( tracker, simulatedEvent, event, true ); + + THIS[ tracker.hash ].lastPinchDelta = pinchDelta; + } + } + event.preventDefault(); + } + + /** + * Only triggered once by the deepest element that initially received + * the mouse down event. Since no other element has captured the mouse, + * we want to trigger the elements that initially received the mouse + * down event (including this one). The the param tracker isn't used + * but for consistency with the other event handlers we include it. + * @private + * @inner + */ + function onMouseMoveCapturedIE( tracker, event ) { + var i; + for ( i = 0; i < CAPTURING.length; i++ ) { + onMouseMoveCaptured( CAPTURING[ i ], event, false ); + } + + $.stopEvent( event ); + } + + /** + * @private + * @inner + */ + function getMouseAbsolute( event ) { + return $.getMousePosition( event ); + } + + /** + * @private + * @inner + */ + function getMouseRelative( event, element ) { + var mouse = $.getMousePosition( event ), + offset = $.getElementOffset( element ); + + return mouse.minus( offset ); + } + + /** + * @private + * @inner + * Returns true if elementB is a child node of elementA, or if they're equal. + */ + function isChild( elementA, elementB ) { + var body = document.body; + while ( elementB && elementA != elementB && body != elementB ) { + try { + elementB = elementB.parentNode; + } catch ( e ) { + return false; + } + } + return elementA == elementB; + } + + /** + * @private + * @inner + */ + function onGlobalMouseDown() { + IS_BUTTON_DOWN = true; + } + + /** + * @private + * @inner + */ + function onGlobalMouseUp() { + IS_BUTTON_DOWN = false; + } + + + (function () { + if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { + $.addEvent( document, "mousedown", onGlobalMouseDown, false ); + $.addEvent( document, "mouseup", onGlobalMouseUp, false ); + } else { + $.addEvent( window, "mousedown", onGlobalMouseDown, true ); + $.addEvent( window, "mouseup", onGlobalMouseUp, true ); + } + } )(); + +} ( OpenSeadragon ) ); + +/* + * OpenSeadragon - Control + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + +/** + * An enumeration of supported locations where controls can be anchored, + * including NONE, TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, and BOTTOM_LEFT. + * The anchoring is always relative to the container + * @static + */ +$.ControlAnchor = { + NONE: 0, + TOP_LEFT: 1, + TOP_RIGHT: 2, + BOTTOM_RIGHT: 3, + BOTTOM_LEFT: 4 +}; + +/** + * A Control represents any interface element which is meant to allow the user + * to interact with the zoomable interface. Any control can be anchored to any + * element. + * @class + * @param {Element} element - the control element to be anchored in the container. + * @param {Object } options - All required and optional settings for configuring a control element. + * @param {OpenSeadragon.ControlAnchor} [options.anchor=OpenSeadragon.ControlAnchor.NONE] - the position of the control + * relative to the container. + * @param {Boolean} [options.attachToViewer=true] - Whether the control should be added directly to the viewer, or + * directly to the container + * @param {Boolean} [options.autoFade=true] - Whether the control should have the autofade behavior + * @param {Element} container - the element to control will be anchored too. + * + * @property {Element} element - the element providing the user interface with + * some type of control. Eg a zoom-in button + * @property {OpenSeadragon.ControlAnchor} anchor - the position of the control + * relative to the container. + * @property {Boolean} autoFade - Whether the control should have the autofade behavior + * @property {Element} container - the element within with the control is + * positioned. + * @property {Element} wrapper - a neutral element surrounding the control + * element. + */ +$.Control = function ( element, options, container ) { + var parent = element.parentNode; + if (typeof options === 'number') + { + $.console.error("Passing an anchor directly into the OpenSeadragon.Control constructor is deprecated; " + + "please use an options object instead. " + + "Support for this deprecated variant is scheduled for removal in December 2013"); + options = {anchor: options}; + } + options.attachToViewer = (typeof options.attachToViewer === 'undefined') ? true : options.attachToViewer; + this.autoFade = (typeof options.autoFade === 'undefined') ? true : options.autoFade; + this.element = element; + this.anchor = options.anchor; + this.container = container; + this.wrapper = $.makeNeutralElement( "span" ); + this.wrapper.style.display = "inline-block"; + this.wrapper.appendChild( this.element ); + + if ( this.anchor == $.ControlAnchor.NONE ) { + // IE6 fix + this.wrapper.style.width = this.wrapper.style.height = "100%"; + } + + if (options.attachToViewer ) { + if ( this.anchor == $.ControlAnchor.TOP_RIGHT || + this.anchor == $.ControlAnchor.BOTTOM_RIGHT ) { + this.container.insertBefore( + this.wrapper, + this.container.firstChild + ); + } else { + this.container.appendChild( this.wrapper ); + } + } else { + parent.appendChild( this.wrapper ); + } +}; + +$.Control.prototype = { + + /** + * Removes the control from the container. + * @function + */ + destroy: function() { + this.wrapper.removeChild( this.element ); + this.container.removeChild( this.wrapper ); + }, + + /** + * Determines if the control is currently visible. + * @function + * @return {Boolean} true if currenly visible, false otherwise. + */ + isVisible: function() { + return this.wrapper.style.display != "none"; + }, + + /** + * Toggles the visibility of the control. + * @function + * @param {Boolean} visible - true to make visible, false to hide. + */ + setVisible: function( visible ) { + this.wrapper.style.display = visible ? + "inline-block" : + "none"; + }, + + /** + * Sets the opacity level for the control. + * @function + * @param {Number} opactiy - a value between 1 and 0 inclusively. + */ + setOpacity: function( opacity ) { + if ( this.element[ $.SIGNAL ] && $.Browser.vendor == $.BROWSERS.IE ) { + $.setElementOpacity( this.element, opacity, true ); + } else { + $.setElementOpacity( this.wrapper, opacity, true ); + } + } +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - ControlDock + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + /** + * @class + */ + $.ControlDock = function( options ){ + var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'], + layout, + i; + + $.extend( true, this, { + id: 'controldock-'+$.now()+'-'+Math.floor(Math.random()*1000000), + container: $.makeNeutralElement('form'), + controls: [] + }, options ); + + // Disable the form's submit; otherwise button clicks and return keys + // can trigger it. + this.container.onsubmit = function() { + return false; + }; + + if( this.element ){ + this.element = $.getElement( this.element ); + this.element.appendChild( this.container ); + this.element.style.position = 'relative'; + this.container.style.width = '100%'; + this.container.style.height = '100%'; + } + + for( i = 0; i < layouts.length; i++ ){ + layout = layouts[ i ]; + this.controls[ layout ] = $.makeNeutralElement( "div" ); + this.controls[ layout ].style.position = 'absolute'; + if ( layout.match( 'left' ) ){ + this.controls[ layout ].style.left = '0px'; + } + if ( layout.match( 'right' ) ){ + this.controls[ layout ].style.right = '0px'; + } + if ( layout.match( 'top' ) ){ + this.controls[ layout ].style.top = '0px'; + } + if ( layout.match( 'bottom' ) ){ + this.controls[ layout ].style.bottom = '0px'; + } + } + + this.container.appendChild( this.controls.topleft ); + this.container.appendChild( this.controls.topright ); + this.container.appendChild( this.controls.bottomright ); + this.container.appendChild( this.controls.bottomleft ); + }; + + $.ControlDock.prototype = { + + /** + * @function + */ + addControl: function ( element, controlOptions ) { + element = $.getElement( element ); + var div = null; + + if ( getControlIndex( this, element ) >= 0 ) { + return; // they're trying to add a duplicate control + } + + switch ( controlOptions.anchor ) { + case $.ControlAnchor.TOP_RIGHT: + div = this.controls.topright; + element.style.position = "relative"; + element.style.paddingRight = "0px"; + element.style.paddingTop = "0px"; + break; + case $.ControlAnchor.BOTTOM_RIGHT: + div = this.controls.bottomright; + element.style.position = "relative"; + element.style.paddingRight = "0px"; + element.style.paddingBottom = "0px"; + break; + case $.ControlAnchor.BOTTOM_LEFT: + div = this.controls.bottomleft; + element.style.position = "relative"; + element.style.paddingLeft = "0px"; + element.style.paddingBottom = "0px"; + break; + case $.ControlAnchor.TOP_LEFT: + div = this.controls.topleft; + element.style.position = "relative"; + element.style.paddingLeft = "0px"; + element.style.paddingTop = "0px"; + break; + default: + case $.ControlAnchor.NONE: + div = this.container; + element.style.margin = "0px"; + element.style.padding = "0px"; + break; + } + + this.controls.push( + new $.Control( element, controlOptions, div ) + ); + element.style.display = "inline-block"; + }, + + + /** + * @function + * @return {OpenSeadragon.ControlDock} Chainable. + */ + removeControl: function ( element ) { + element = $.getElement( element ); + var i = getControlIndex( this, element ); + + if ( i >= 0 ) { + this.controls[ i ].destroy(); + this.controls.splice( i, 1 ); + } + + return this; + }, + + /** + * @function + * @return {OpenSeadragon.ControlDock} Chainable. + */ + clearControls: function () { + while ( this.controls.length > 0 ) { + this.controls.pop().destroy(); + } + + return this; + }, + + + /** + * @function + * @return {Boolean} + */ + areControlsEnabled: function () { + var i; + + for ( i = this.controls.length - 1; i >= 0; i-- ) { + if ( this.controls[ i ].isVisible() ) { + return true; + } + } + + return false; + }, + + + /** + * @function + * @return {OpenSeadragon.ControlDock} Chainable. + */ + setControlsEnabled: function( enabled ) { + var i; + + for ( i = this.controls.length - 1; i >= 0; i-- ) { + this.controls[ i ].setVisible( enabled ); + } + + return this; + } + + }; + + + /////////////////////////////////////////////////////////////////////////////// + // Utility methods + /////////////////////////////////////////////////////////////////////////////// + function getControlIndex( dock, element ) { + var controls = dock.controls, + i; + + for ( i = controls.length - 1; i >= 0; i-- ) { + if ( controls[ i ].element == element ) { + return i; + } + } + + return -1; + } + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Viewer + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + +// dictionary from hash to private properties +var THIS = {}, +// We keep a list of viewers so we can 'wake-up' each viewer on +// a page after toggling between fullpage modes + VIEWERS = {}; + +/** + * + * The main point of entry into creating a zoomable image on the page. + * + * We have provided an idiomatic javascript constructor which takes + * a single object, but still support the legacy positional arguments. + * + * The options below are given in order that they appeared in the constructor + * as arguments and we translate a positional call into an idiomatic call. + * + * @class + * @extends OpenSeadragon.EventSource + * @extends OpenSeadragon.ControlDock + * @param {Object} options + * @param {String} options.element Id of Element to attach to, + * @param {String} options.xmlPath Xpath ( TODO: not sure! ), + * @param {String} options.prefixUrl Url used to prepend to paths, eg button + * images, etc. + * @param {OpenSeadragon.Control[]} options.controls Array of OpenSeadragon.Control, + * @param {OpenSeadragon.Overlay[]} options.overlays Array of OpenSeadragon.Overlay, + * @param {OpenSeadragon.Control[]} options.overlayControls An Array of ( TODO: + * not sure! ) + * @property {OpenSeadragon.Viewport} viewport The viewer's viewport, where you + * can access zoom, pan, etc. + * + **/ +$.Viewer = function( options ) { + + var args = arguments, + _this = this, + i; + + + //backward compatibility for positional args while prefering more + //idiomatic javascript options object as the only argument + if( !$.isPlainObject( options ) ){ + options = { + id: args[ 0 ], + xmlPath: args.length > 1 ? args[ 1 ] : undefined, + prefixUrl: args.length > 2 ? args[ 2 ] : undefined, + controls: args.length > 3 ? args[ 3 ] : undefined, + overlays: args.length > 4 ? args[ 4 ] : undefined, + overlayControls: args.length > 5 ? args[ 5 ] : undefined + }; + } + + //options.config and the general config argument are deprecated + //in favor of the more direct specification of optional settings + //being pass directly on the options object + if ( options.config ){ + $.extend( true, options, options.config ); + delete options.config; + } + + //Public properties + //Allow the options object to override global defaults + $.extend( true, this, { + + //internal state and dom identifiers + id: options.id, + hash: options.hash || options.id, + + //dom nodes + element: null, + canvas: null, + container: null, + + //TODO: not sure how to best describe these + overlays: [], + overlayControls:[], + + //private state properties + previousBody: [], + + //This was originally initialized in the constructor and so could never + //have anything in it. now it can because we allow it to be specified + //in the options and is only empty by default if not specified. Also + //this array was returned from get_controls which I find confusing + //since this object has a controls property which is treated in other + //functions like clearControls. I'm removing the accessors. + customControls: [], + + //These are originally not part options but declared as members + //in initialize. Its still considered idiomatic to put them here + source: null, + drawer: null, + drawers: [], + viewport: null, + navigator: null, + + //A collection viewport is a seperate viewport used to provide + //simultanious rendering of sets of tiless + collectionViewport: null, + collectionDrawer: null, + + //UI image resources + //TODO: rename navImages to uiImages + navImages: null, + + //interface button controls + buttons: null, + + //TODO: this is defunct so safely remove it + profiler: null + + }, $.DEFAULT_SETTINGS, options ); + + if ( typeof( this.hash) === "undefined" ) { + throw new Error("A hash must be defined, either by specifying options.id or options.hash."); + } + if ( typeof( THIS[ this.hash ] ) !== "undefined" ) { + // We don't want to throw an error here, as the user might have discarded + // the previous viewer with the same hash and now want to recreate it. + $.console.warn("Hash " + this.hash + " has already been used."); + } + + //Private state properties + THIS[ this.hash ] = { + "fsBoundsDelta": new $.Point( 1, 1 ), + "prevContainerSize": null, + "animating": false, + "forceRedraw": false, + "mouseInside": false, + "group": null, + // whether we should be continuously zooming + "zooming": false, + // how much we should be continuously zooming by + "zoomFactor": null, + "lastZoomTime": null, + // did we decide this viewer has a sequence of tile sources + "sequenced": false, + "sequence": 0, + "fullPage": false, + "onfullscreenchange": null + }; + + this._updateRequestId = null; + + //Inherit some behaviors and properties + $.EventSource.call( this ); + + this.addHandler( 'open-failed', function ( event ) { + var msg = $.getString( "Errors.OpenFailed", event.eventSource, event.message); + _this._showMessage( msg ); + }); + + $.ControlDock.call( this, options ); + + //Deal with tile sources + var initialTileSource; + + if ( this.xmlPath ){ + //Deprecated option. Now it is preferred to use the tileSources option + this.tileSources = [ this.xmlPath ]; + } + + if ( this.tileSources ){ + // tileSources is a complex option... + // + // It can be a string, object, or an array of any of strings and objects. + // At this point we only care about if it is an Array or not. + // + if( $.isArray( this.tileSources ) ){ + + //must be a sequence of tileSource since the first item + //is a legacy tile source + if( this.tileSources.length > 1 ){ + THIS[ this.hash ].sequenced = true; + } + + //Keeps the initial page within bounds + if ( this.initialPage > this.tileSources.length - 1 ){ + this.initialPage = this.tileSources.length - 1; + } + + initialTileSource = this.tileSources[ this.initialPage ]; + + //Update the sequence (aka currrent page) property + THIS[ this.hash ].sequence = this.initialPage; + } else { + initialTileSource = this.tileSources; + } + } + + this.element = this.element || document.getElementById( this.id ); + this.canvas = $.makeNeutralElement( "div" ); + this.keyboardCommandArea = $.makeNeutralElement( "textarea" ); + + this.canvas.className = "openseadragon-canvas"; + (function( style ){ + style.width = "100%"; + style.height = "100%"; + style.overflow = "hidden"; + style.position = "absolute"; + style.top = "0px"; + style.left = "0px"; + }( this.canvas.style )); + + //the container is created through applying the ControlDock constructor above + this.container.className = "openseadragon-container"; + (function( style ){ + style.width = "100%"; + style.height = "100%"; + style.position = "relative"; + style.overflow = "hidden"; + style.left = "0px"; + style.top = "0px"; + style.textAlign = "left"; // needed to protect against + }( this.container.style )); + + this.keyboardCommandArea.className = "keyboard-command-area"; + (function( style ){ + style.width = "100%"; + style.height = "100%"; + style.overflow = "hidden"; + style.position = "absolute"; + style.top = "0px"; + style.left = "0px"; + style.resize = "none"; + }( this.keyboardCommandArea.style )); + + this.container.insertBefore( this.canvas, this.container.firstChild ); + this.container.insertBefore( this.keyboardCommandArea, this.container.firstChild ); + this.element.appendChild( this.container ); + + //Used for toggling between fullscreen and default container size + //TODO: these can be closure private and shared across Viewer + // instances. + this.bodyWidth = document.body.style.width; + this.bodyHeight = document.body.style.height; + this.bodyOverflow = document.body.style.overflow; + this.docOverflow = document.documentElement.style.overflow; + + this.keyboardCommandArea.innerTracker = new $.MouseTracker({ + _this : this, + element: this.keyboardCommandArea, + focusHandler: function(){ + var point = $.getElementPosition( this.element ); + window.scrollTo( 0, point.y ); + }, + + keyHandler: function( event ){ + switch( event.keyCode ){ + case 61://=|+ + _this.viewport.zoomBy(1.1); + _this.viewport.applyConstraints(); + return false; + case 45://-|_ + _this.viewport.zoomBy(0.9); + _this.viewport.applyConstraints(); + return false; + case 48://0|) + _this.viewport.goHome(); + _this.viewport.applyConstraints(); + return false; + case 119://w + case 87://W + case 38://up arrow + if ( event.shift ) { + _this.viewport.zoomBy(1.1); + } else { + _this.viewport.panBy(new $.Point(0, -0.05)); + } + _this.viewport.applyConstraints(); + return false; + case 115://s + case 83://S + case 40://down arrow + if ( event.shift ) { + _this.viewport.zoomBy(0.9); + } else { + _this.viewport.panBy(new $.Point(0, 0.05)); + } + _this.viewport.applyConstraints(); + return false; + case 97://a + case 37://left arrow + _this.viewport.panBy(new $.Point(-0.05, 0)); + _this.viewport.applyConstraints(); + return false; + case 100://d + case 39://right arrow + _this.viewport.panBy(new $.Point(0.05, 0)); + _this.viewport.applyConstraints(); + return false; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + return true; + } + } + }).setTracking( true ); // default state + + + this.innerTracker = new $.MouseTracker({ + element: this.canvas, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + clickHandler: $.delegate( this, onCanvasClick ), + dragHandler: $.delegate( this, onCanvasDrag ), + releaseHandler: $.delegate( this, onCanvasRelease ), + scrollHandler: $.delegate( this, onCanvasScroll ) + }).setTracking( this.mouseNavEnabled ? true : false ); // default state + + this.outerTracker = new $.MouseTracker({ + element: this.container, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + enterHandler: $.delegate( this, onContainerEnter ), + exitHandler: $.delegate( this, onContainerExit ), + releaseHandler: $.delegate( this, onContainerRelease ) + }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking + + if( this.toolbar ){ + this.toolbar = new $.ControlDock({ element: this.toolbar }); + } + + this.bindStandardControls(); + this.bindSequenceControls(); + + if ( initialTileSource ) { + this.open( initialTileSource ); + + if ( this.tileSources.length > 1 ) { + this._updateSequenceButtons( this.initialPage ); + } + } + + for ( i = 0; i < this.customControls.length; i++ ) { + this.addControl( + this.customControls[ i ].id, + {anchor: this.customControls[ i ].anchor} + ); + } + + $.requestAnimationFrame( function(){ + beginControlsAutoHide( _this ); + } ); // initial fade out + +}; + +$.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, { + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isOpen + * @return {Boolean} + */ + isOpen: function () { + return !!this.source; + }, + + /** + * A deprecated function, renamed to 'open' to match event name and + * match current 'close' method. + * @function + * @name OpenSeadragon.Viewer.prototype.openDzi + * @param {String} dzi xml string or the url to a DZI xml document. + * @return {OpenSeadragon.Viewer} Chainable. + * + * @deprecated - use 'open' instead. + */ + openDzi: function ( dzi ) { + return this.open( dzi ); + }, + + /** + * A deprecated function, renamed to 'open' to match event name and + * match current 'close' method. + * @function + * @name OpenSeadragon.Viewer.prototype.openTileSource + * @param {String|Object|Function} See OpenSeadragon.Viewer.prototype.open + * @return {OpenSeadragon.Viewer} Chainable. + * + * @deprecated - use 'open' instead. + */ + openTileSource: function ( tileSource ) { + return this.open( tileSource ); + }, + + /** + * Open a TileSource object into the viewer. + * + * tileSources is a complex option... + * + * It can be a string, object, function, or an array of any of these: + * + * - A String implies a url used to determine the tileSource implementation + * based on the file extension of url. JSONP is implied by *.js, + * otherwise the url is retrieved as text and the resulting text is + * introspected to determine if its json, xml, or text and parsed. + * - An Object implies an inline configuration which has a single + * property sufficient for being able to determine tileSource + * implementation. If the object has a property which is a function + * named 'getTileUrl', it is treated as a custom TileSource. + * @function + * @name OpenSeadragon.Viewer.prototype.open + * @param {String|Object|Function} + * @return {OpenSeadragon.Viewer} Chainable. + */ + open: function ( tileSource ) { + var _this = this, + customTileSource, + readySource, + $TileSource, + options; + + _this._hideMessage(); + + //allow plain xml strings or json strings to be parsed here + if( $.type( tileSource ) == 'string' ){ + if( tileSource.match(/\s*<.*/) ){ + tileSource = $.parseXml( tileSource ); + }else if( tileSource.match(/\s*[\{\[].*/) ){ + /*jshint evil:true*/ + tileSource = eval( '('+tileSource+')' ); + } + } + + setTimeout(function(){ + if ( $.type( tileSource ) == 'string') { + //If its still a string it means it must be a url at this point + tileSource = new $.TileSource( tileSource, function( event ){ + openTileSource( _this, event.tileSource ); + }); + tileSource.addHandler( 'open-failed', function ( event ) { + _this.raiseEvent( 'open-failed', event ); + }); + + } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ){ + if( $.isFunction( tileSource.getTileUrl ) ){ + //Custom tile source + customTileSource = new $.TileSource(tileSource); + customTileSource.getTileUrl = tileSource.getTileUrl; + openTileSource( _this, customTileSource ); + } else { + //inline configuration + $TileSource = $.TileSource.determineType( _this, tileSource ); + if ( !$TileSource ) { + _this.raiseEvent( 'open-failed', { + message: "Unable to load TileSource", + source: tileSource + }); + return; + } + options = $TileSource.prototype.configure.apply( _this, [ tileSource ]); + readySource = new $TileSource( options ); + openTileSource( _this, readySource ); + } + } else { + //can assume it's already a tile source implementation + openTileSource( _this, tileSource ); + } + }, 1); + + return this; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.close + * @return {OpenSeadragon.Viewer} Chainable. + */ + close: function ( ) { + if ( this._updateRequestId !== null ) { + $.cancelAnimationFrame( this._updateRequestId ); + this._updateRequestId = null; + } + + if ( this.navigator ) { + this.navigator.close(); + } + + if ( this.drawer ) { + this.drawer.clearOverlays(); + } + + this.source = null; + this.drawer = null; + + this.viewport = this.preserveViewport ? this.viewport : null; + //this.profiler = null; + if (this.canvas){ + this.canvas.innerHTML = ""; + } + + VIEWERS[ this.hash ] = null; + delete VIEWERS[ this.hash ]; + + this.raiseEvent( 'close' ); + + return this; + }, + + + /** + * Function to destroy the viewer and clean up everything created by + * OpenSeadragon. + * @function + * @name OpenSeadragon.Viewer.prototype.destroy + */ + destroy: function( ) { + this.close(); + + this.removeAllHandlers(); + + // Go through top element (passed to us) and remove all children + // Use removeChild to make sure it handles SVG or any non-html + // also it performs better - http://jsperf.com/innerhtml-vs-removechild/15 + if (this.element){ + while (this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } + } + + // destroy the mouse trackers + if (this.keyboardCommandArea){ + this.keyboardCommandArea.innerTracker.destroy(); + } + if (this.innerTracker){ + this.innerTracker.destroy(); + } + if (this.outerTracker){ + this.outerTracker.destroy(); + } + + // clear all our references to dom objects + this.canvas = null; + this.keyboardCommandArea = null; + this.container = null; + + // clear our reference to the main element - they will need to pass it in again, creating a new viewer + this.element = null; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isMouseNavEnabled + * @return {Boolean} + */ + isMouseNavEnabled: function () { + return this.innerTracker.isTracking(); + }, + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setMouseNavEnabled + * @return {OpenSeadragon.Viewer} Chainable. + */ + setMouseNavEnabled: function( enabled ){ + this.innerTracker.setTracking( enabled ); + this.raiseEvent( 'mouse-enabled', { enabled: enabled } ); + return this; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.areControlsEnabled + * @return {Boolean} + */ + areControlsEnabled: function () { + var enabled = this.controls.length, + i; + for( i = 0; i < this.controls.length; i++ ){ + enabled = enabled && this.controls[ i ].isVisibile(); + } + return enabled; + }, + + + /** + * Shows or hides the controls (e.g. the default navigation buttons). + * + * @function + * @name OpenSeadragon.Viewer.prototype.setControlsEnabled + * @param {Boolean} true to show, false to hide. + * @return {OpenSeadragon.Viewer} Chainable. + */ + setControlsEnabled: function( enabled ) { + if( enabled ){ + abortControlsAutoHide( this ); + } else { + beginControlsAutoHide( this ); + } + this.raiseEvent( 'controls-enabled', { enabled: enabled } ); + return this; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isFullPage + * @return {Boolean} + */ + isFullPage: function () { + return THIS[ this.hash ].fullPage; + }, + + + /** + * Toggle full page mode. + * @function + * @name OpenSeadragon.Viewer.prototype.setFullPage + * @param {Boolean} fullPage + * If true, enter full page mode. If false, exit full page mode. + * @return {OpenSeadragon.Viewer} Chainable. + */ + setFullPage: function( fullPage ) { + + var body = document.body, + bodyStyle = body.style, + docStyle = document.documentElement.style, + canvasStyle = this.canvas.style, + _this = this, + oldBounds, + newBounds, + viewer, + hash, + nodes, + i; + + //dont bother modifying the DOM if we are already in full page mode. + if ( fullPage == this.isFullPage() ) { + return; + } + + + if ( fullPage ) { + + this.bodyOverflow = bodyStyle.overflow; + this.docOverflow = docStyle.overflow; + bodyStyle.overflow = "hidden"; + docStyle.overflow = "hidden"; + + this.bodyWidth = bodyStyle.width; + this.bodyHeight = bodyStyle.height; + bodyStyle.width = "100%"; + bodyStyle.height = "100%"; + + //when entering full screen on the ipad it wasnt sufficient to leave + //the body intact as only only the top half of the screen would + //respond to touch events on the canvas, while the bottom half treated + //them as touch events on the document body. Thus we remove and store + //the bodies elements and replace them when we leave full screen. + this.previousBody = []; + THIS[ this.hash ].prevElementParent = this.element.parentNode; + THIS[ this.hash ].prevNextSibling = this.element.nextSibling; + THIS[ this.hash ].prevElementWidth = this.element.style.width; + THIS[ this.hash ].prevElementHeight = this.element.style.height; + nodes = body.childNodes.length; + for ( i = 0; i < nodes; i ++ ){ + this.previousBody.push( body.childNodes[ 0 ] ); + body.removeChild( body.childNodes[ 0 ] ); + } + + //If we've got a toolbar, we need to enable the user to use css to + //preserve it in fullpage mode + if( this.toolbar && this.toolbar.element ){ + //save a reference to the parent so we can put it back + //in the long run we need a better strategy + this.toolbar.parentNode = this.toolbar.element.parentNode; + this.toolbar.nextSibling = this.toolbar.element.nextSibling; + body.appendChild( this.toolbar.element ); + + //Make sure the user has some ability to style the toolbar based + //on the mode + $.addClass( this.toolbar.element, 'fullpage' ); + } + + $.addClass( this.element, 'fullpage' ); + body.appendChild( this.element ); + + if( $.supportsFullScreen ){ + THIS[ this.hash ].onfullscreenchange = function() { + /* + fullscreenchange events don't include the new fullscreen status so we need to + retrieve the current status from the fullscreen API. See: + https://developer.mozilla.org/en-US/docs/Web/Reference/Events/fullscreenchange + */ + + if( $.isFullScreen() ){ + _this.setFullPage( true ); + } else { + _this.setFullPage( false ); + } + }; + + $.requestFullScreen( document.body ); + + // The target of the event is always the document, + // but it is possible to retrieve the fullscreen element through the API + // Note that the API is still vendor-prefixed in browsers implementing it + document.addEventListener( + $.fullScreenEventName, + THIS[ this.hash ].onfullscreenchange + ); + this.element.style.height = '100%'; + this.element.style.width = '100%'; + }else{ + this.element.style.height = $.getWindowSize().y + 'px'; + this.element.style.width = $.getWindowSize().x + 'px'; + } + + if( this.toolbar && this.toolbar.element ){ + this.element.style.height = ( + $.getElementSize( this.element ).y - $.getElementSize( this.toolbar.element ).y + ) + 'px'; + } + + THIS[ this.hash ].fullPage = true; + + // mouse will be inside container now + $.delegate( this, onContainerEnter )( {} ); + + + } else { + + if( $.supportsFullScreen ){ + document.removeEventListener( + $.fullScreenEventName, + THIS[ this.hash ].onfullscreenchange + ); + $.cancelFullScreen( document ); + } + + bodyStyle.overflow = this.bodyOverflow; + docStyle.overflow = this.docOverflow; + + bodyStyle.width = this.bodyWidth; + bodyStyle.height = this.bodyHeight; + + canvasStyle.backgroundColor = ""; + canvasStyle.color = ""; + + body.removeChild( this.element ); + nodes = this.previousBody.length; + for ( i = 0; i < nodes; i++ ){ + body.appendChild( this.previousBody.shift() ); + } + + $.removeClass( this.element, 'fullpage' ); + THIS[ this.hash ].prevElementParent.insertBefore( + this.element, + THIS[ this.hash ].prevNextSibling + ); + + //If we've got a toolbar, we need to enable the user to use css to + //reset it to its original state + if( this.toolbar && this.toolbar.element ){ + body.removeChild( this.toolbar.element ); + + //Make sure the user has some ability to style the toolbar based + //on the mode + $.removeClass( this.toolbar.element, 'fullpage' ); + //this.toolbar.element.style.position = 'relative'; + this.toolbar.parentNode.insertBefore( + this.toolbar.element, + this.toolbar.nextSibling + ); + delete this.toolbar.parentNode; + delete this.toolbar.nextSibling; + + //this.container.style.top = 'auto'; + } + + this.element.style.width = THIS[ this.hash ].prevElementWidth; + this.element.style.height = THIS[ this.hash ].prevElementHeight; + + THIS[ this.hash ].fullPage = false; + + // mouse will likely be outside now + $.delegate( this, onContainerExit )( {} ); + + + } + this.raiseEvent( 'fullpage', { fullpage: fullPage } ); + + if ( this.viewport ) { + oldBounds = this.viewport.getBounds(); + this.viewport.resize( THIS[ this.hash ].prevContainerSize ); + newBounds = this.viewport.getBounds(); + + if ( fullPage ) { + THIS[ this.hash ].fsBoundsDelta = new $.Point( + newBounds.width / oldBounds.width, + newBounds.height / oldBounds.height + ); + } else { + this.viewport.update(); + this.viewport.zoomBy( + Math.max( + THIS[ this.hash ].fsBoundsDelta.x, + THIS[ this.hash ].fsBoundsDelta.y + ), + null, + true + ); + //Ensures that if multiple viewers are on a page, the viewers that + //were hidden during fullpage are 'reopened' + for( hash in VIEWERS ){ + viewer = VIEWERS[ hash ]; + if( viewer !== this && viewer != this.navigator ){ + viewer.open( viewer.source ); + if( viewer.navigator ){ + viewer.navigator.open( viewer.source ); + } + } + } + } + + THIS[ this.hash ].forceRedraw = true; + updateOnce( this ); + + } + return this; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isVisible + * @return {Boolean} + */ + isVisible: function () { + return this.container.style.visibility != "hidden"; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setVisible + * @return {OpenSeadragon.Viewer} Chainable. + */ + setVisible: function( visible ){ + this.container.style.visibility = visible ? "" : "hidden"; + this.raiseEvent( 'visible', { visible: visible } ); + return this; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.bindSequenceControls + * @return {OpenSeadragon.Viewer} Chainable. + */ + bindSequenceControls: function(){ + + ////////////////////////////////////////////////////////////////////////// + // Image Sequence Controls + ////////////////////////////////////////////////////////////////////////// + var onFocusHandler = $.delegate( this, onFocus ), + onBlurHandler = $.delegate( this, onBlur ), + onNextHandler = $.delegate( this, onNext ), + onPreviousHandler = $.delegate( this, onPrevious ), + navImages = this.navImages, + useGroup = true ; + + if( this.showSequenceControl && THIS[ this.hash ].sequenced ){ + + if( this.previousButton || this.nextButton ){ + //if we are binding to custom buttons then layout and + //grouping is the responsibility of the page author + useGroup = false; + } + + this.previousButton = new $.Button({ + element: this.previousButton ? $.getElement( this.previousButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.PreviousPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ), + onRelease: onPreviousHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + }); + + this.nextButton = new $.Button({ + element: this.nextButton ? $.getElement( this.nextButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.NextPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ), + onRelease: onNextHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + }); + + if( !this.navPrevNextWrap ){ + this.previousButton.disable(); + } + + if( useGroup ){ + this.paging = new $.ButtonGroup({ + buttons: [ + this.previousButton, + this.nextButton + ], + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold + }); + + this.pagingControl = this.paging.element; + + if( this.toolbar ){ + this.toolbar.addControl( + this.pagingControl, + {anchor: $.ControlAnchor.BOTTOM_RIGHT} + ); + }else{ + this.addControl( + this.pagingControl, + {anchor: $.ControlAnchor.TOP_LEFT} + ); + } + } + } + return this; + }, + + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.bindStandardControls + * @return {OpenSeadragon.Viewer} Chainable. + */ + bindStandardControls: function(){ + ////////////////////////////////////////////////////////////////////////// + // Navigation Controls + ////////////////////////////////////////////////////////////////////////// + var beginZoomingInHandler = $.delegate( this, beginZoomingIn ), + endZoomingHandler = $.delegate( this, endZooming ), + doSingleZoomInHandler = $.delegate( this, doSingleZoomIn ), + beginZoomingOutHandler = $.delegate( this, beginZoomingOut ), + doSingleZoomOutHandler = $.delegate( this, doSingleZoomOut ), + onHomeHandler = $.delegate( this, onHome ), + onFullPageHandler = $.delegate( this, onFullPage ), + onFocusHandler = $.delegate( this, onFocus ), + onBlurHandler = $.delegate( this, onBlur ), + navImages = this.navImages, + buttons = [], + useGroup = true ; + + + if( this.showNavigationControl ){ + + if( this.zoomInButton || this.zoomOutButton || this.homeButton || this.fullPageButton ){ + //if we are binding to custom buttons then layout and + //grouping is the responsibility of the page author + useGroup = false; + } + + buttons.push( this.zoomInButton = new $.Button({ + element: this.zoomInButton ? $.getElement( this.zoomInButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.ZoomIn" ), + srcRest: resolveUrl( this.prefixUrl, navImages.zoomIn.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ), + onPress: beginZoomingInHandler, + onRelease: endZoomingHandler, + onClick: doSingleZoomInHandler, + onEnter: beginZoomingInHandler, + onExit: endZoomingHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + buttons.push( this.zoomOutButton = new $.Button({ + element: this.zoomOutButton ? $.getElement( this.zoomOutButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.ZoomOut" ), + srcRest: resolveUrl( this.prefixUrl, navImages.zoomOut.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ), + onPress: beginZoomingOutHandler, + onRelease: endZoomingHandler, + onClick: doSingleZoomOutHandler, + onEnter: beginZoomingOutHandler, + onExit: endZoomingHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + buttons.push( this.homeButton = new $.Button({ + element: this.homeButton ? $.getElement( this.homeButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.Home" ), + srcRest: resolveUrl( this.prefixUrl, navImages.home.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.home.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.home.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.home.DOWN ), + onRelease: onHomeHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + buttons.push( this.fullPageButton = new $.Button({ + element: this.fullPageButton ? $.getElement( this.fullPageButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.FullPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.fullpage.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ), + onRelease: onFullPageHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + if( useGroup ){ + this.buttons = new $.ButtonGroup({ + buttons: buttons, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold + }); + + this.navControl = this.buttons.element; + this.addHandler( 'open', $.delegate( this, lightUp ) ); + + if( this.toolbar ){ + this.toolbar.addControl( + this.navControl, + {anchor: $.ControlAnchor.TOP_LEFT} + ); + }else{ + this.addControl( + this.navControl, + {anchor: $.ControlAnchor.TOP_LEFT} + ); + } + } + + } + return this; + }, + + /** + * Gets the active page of a sequence + * @function + * @name OpenSeadragon.Viewer.prototype.currentPage + * @return {Number} + */ + currentPage: function () { + return THIS[ this.hash ].sequence; + }, + + /** + * @function + * @name OpenSeadragon.Viewer.prototype.goToPage + * @return {OpenSeadragon.Viewer} Chainable. + */ + goToPage: function( page ){ + //page is a 1 based index so normalize now + //page = page; + this.raiseEvent( 'page', { page: page } ); + + if( this.tileSources.length > page ){ + + THIS[ this.hash ].sequence = page; + + this._updateSequenceButtons( page ); + + this.open( this.tileSources[ page ] ); + } + + if( $.isFunction( this.onPageChange ) ){ + this.onPageChange({ + page: page, + viewer: this + }); + } + if( this.referenceStrip ){ + this.referenceStrip.setFocus( page ); + } + return this; + }, + + /** + * Updates the sequence buttons. + * @function + * @private + * @param {Number} Sequence Value + */ + _updateSequenceButtons: function (page) { + + if( this.nextButton ){ + if( ( this.tileSources.length - 1 ) === page ){ + //Disable next button + if(!this.navPrevNextWrap){ + this.nextButton.disable(); + } + } else { + this.nextButton.enable(); + } + } + if( this.previousButton ){ + if( page > 0 ){ + //Enable previous button + this.previousButton.enable(); + } else { + if(!this.navPrevNextWrap){ + this.previousButton.disable(); + } + } + } + }, + + /** + * Display a message in the viewport + * @function + * @private + * @param {String} text message + */ + _showMessage: function ( message ) { + this._hideMessage(); + + var div = $.makeNeutralElement( "div" ); + div.appendChild( document.createTextNode( message ) ); + + this.messageDiv = $.makeCenteredNode( div ); + + $.addClass(this.messageDiv, "openseadragon-message"); + + this.container.appendChild( this.messageDiv ); + }, + + /** + * Hide any currently displayed viewport message + * @function + * @private + */ + _hideMessage: function () { + var div = this.messageDiv; + if (div) { + div.parentNode.removeChild(div); + delete this.messageDiv; + } + } +}); + + +/** + * _getSafeElemSize is like getElementSize(), but refuses to return 0 for x or y, + * which was causing some calling operations in updateOnce and openTileSource to + * return NaN. + * @returns {Point} + * @private + */ +function _getSafeElemSize (oElement) { + oElement = $.getElement( oElement ); + + return new $.Point( + (oElement.clientWidth === 0 ? 1 : oElement.clientWidth), + (oElement.clientHeight === 0 ? 1 : oElement.clientHeight) + ); +} + +/** + * @function + * @private + */ +function openTileSource( viewer, source ) { + var _this = viewer, + overlay, + i; + + if ( _this.source ) { + _this.close( ); + } + + _this.canvas.innerHTML = ""; + THIS[ _this.hash ].prevContainerSize = _getSafeElemSize( _this.container ); + + + if( _this.collectionMode ){ + _this.source = new $.TileSourceCollection({ + rows: _this.collectionRows, + layout: _this.collectionLayout, + tileSize: _this.collectionTileSize, + tileSources: _this.tileSources, + tileMargin: _this.collectionTileMargin + }); + _this.viewport = _this.viewport ? _this.viewport : new $.Viewport({ + collectionMode: true, + collectionTileSource: _this.source, + containerSize: THIS[ _this.hash ].prevContainerSize, + contentSize: _this.source.dimensions, + springStiffness: _this.springStiffness, + animationTime: _this.animationTime, + showNavigator: false, + minZoomImageRatio: 1, + maxZoomPixelRatio: 1, + viewer: _this //, + //TODO: figure out how to support these in a way that makes sense + //minZoomLevel: this.minZoomLevel, + //maxZoomLevel: this.maxZoomLevel + }); + }else{ + if( source ){ + _this.source = source; + } + _this.viewport = _this.viewport ? _this.viewport : new $.Viewport({ + containerSize: THIS[ _this.hash ].prevContainerSize, + contentSize: _this.source.dimensions, + springStiffness: _this.springStiffness, + animationTime: _this.animationTime, + minZoomImageRatio: _this.minZoomImageRatio, + maxZoomPixelRatio: _this.maxZoomPixelRatio, + visibilityRatio: _this.visibilityRatio, + wrapHorizontal: _this.wrapHorizontal, + wrapVertical: _this.wrapVertical, + defaultZoomLevel: _this.defaultZoomLevel, + minZoomLevel: _this.minZoomLevel, + maxZoomLevel: _this.maxZoomLevel, + viewer: _this + }); + } + + if( _this.preserveViewport ){ + _this.viewport.resetContentSize( _this.source.dimensions ); + } + + _this.source.overlays = _this.source.overlays || []; + + _this.drawer = new $.Drawer({ + viewer: _this, + source: _this.source, + viewport: _this.viewport, + element: _this.canvas, + overlays: [].concat( _this.overlays ).concat( _this.source.overlays ), + maxImageCacheCount: _this.maxImageCacheCount, + imageLoaderLimit: _this.imageLoaderLimit, + minZoomImageRatio: _this.minZoomImageRatio, + wrapHorizontal: _this.wrapHorizontal, + wrapVertical: _this.wrapVertical, + immediateRender: _this.immediateRender, + blendTime: _this.blendTime, + alwaysBlend: _this.alwaysBlend, + minPixelRatio: _this.collectionMode ? 0 : _this.minPixelRatio, + timeout: _this.timeout, + debugMode: _this.debugMode, + debugGridColor: _this.debugGridColor + }); + + //Instantiate a navigator if configured + if ( _this.showNavigator && !_this.collectionMode ){ + // Note: By passing the fully parsed source, the navigator doesn't + // have to load it again. + if ( _this.navigator ) { + _this.navigator.open( source ); + } else { + _this.navigator = new $.Navigator({ + id: _this.navigatorId, + position: _this.navigatorPosition, + sizeRatio: _this.navigatorSizeRatio, + height: _this.navigatorHeight, + width: _this.navigatorWidth, + tileSources: source, + tileHost: _this.tileHost, + prefixUrl: _this.prefixUrl, + overlays: _this.overlays, + viewer: _this + }); + } + } + + //Instantiate a referencestrip if configured + if ( _this.showReferenceStrip && !_this.referenceStrip ){ + _this.referenceStrip = new $.ReferenceStrip({ + id: _this.referenceStripElement, + position: _this.referenceStripPosition, + sizeRatio: _this.referenceStripSizeRatio, + scroll: _this.referenceStripScroll, + height: _this.referenceStripHeight, + width: _this.referenceStripWidth, + tileSources: _this.tileSources, + tileHost: _this.tileHost, + prefixUrl: _this.prefixUrl, + overlays: _this.overlays, + viewer: _this + }); + } + + //this.profiler = new $.Profiler(); + + THIS[ _this.hash ].animating = false; + THIS[ _this.hash ].forceRedraw = true; + _this._updateRequestId = scheduleUpdate( _this, updateMulti ); + + //Assuming you had programatically created a bunch of overlays + //and added them via configuration + for ( i = 0; i < _this.overlayControls.length; i++ ) { + + overlay = _this.overlayControls[ i ]; + + if ( overlay.point ) { + + _this.drawer.addOverlay( + overlay.id, + new $.Point( + overlay.point.X, + overlay.point.Y + ), + $.OverlayPlacement.TOP_LEFT + ); + + } else { + + _this.drawer.addOverlay( + overlay.id, + new $.Rect( + overlay.rect.Point.X, + overlay.rect.Point.Y, + overlay.rect.Width, + overlay.rect.Height + ), + overlay.placement + ); + + } + } + VIEWERS[ _this.hash ] = _this; + + _this.raiseEvent( 'open', { source: source } ); + + return _this; +} + + + + +/////////////////////////////////////////////////////////////////////////////// +// Schedulers provide the general engine for animation +/////////////////////////////////////////////////////////////////////////////// +function scheduleUpdate( viewer, updateFunc ){ + return $.requestAnimationFrame( function(){ + updateFunc( viewer ); + } ); +} + + +//provides a sequence in the fade animation +function scheduleControlsFade( viewer ) { + $.requestAnimationFrame( function(){ + updateControlsFade( viewer ); + }); +} + + +//initiates an animation to hide the controls +function beginControlsAutoHide( viewer ) { + if ( !viewer.autoHideControls ) { + return; + } + viewer.controlsShouldFade = true; + viewer.controlsFadeBeginTime = + $.now() + + viewer.controlsFadeDelay; + + window.setTimeout( function(){ + scheduleControlsFade( viewer ); + }, viewer.controlsFadeDelay ); +} + + +//determines if fade animation is done or continues the animation +function updateControlsFade( viewer ) { + var currentTime, + deltaTime, + opacity, + i; + if ( viewer.controlsShouldFade ) { + currentTime = $.now(); + deltaTime = currentTime - viewer.controlsFadeBeginTime; + opacity = 1.0 - deltaTime / viewer.controlsFadeLength; + + opacity = Math.min( 1.0, opacity ); + opacity = Math.max( 0.0, opacity ); + + for ( i = viewer.controls.length - 1; i >= 0; i--) { + if (viewer.controls[ i ].autoFade) { + viewer.controls[ i ].setOpacity( opacity ); + } + } + + if ( opacity > 0 ) { + // fade again + scheduleControlsFade( viewer ); + } + } +} + + +//stop the fade animation on the controls and show them +function abortControlsAutoHide( viewer ) { + var i; + viewer.controlsShouldFade = false; + for ( i = viewer.controls.length - 1; i >= 0; i-- ) { + viewer.controls[ i ].setOpacity( 1.0 ); + } +} + + + +/////////////////////////////////////////////////////////////////////////////// +// Default view event handlers. +/////////////////////////////////////////////////////////////////////////////// +function onFocus(){ + abortControlsAutoHide( this ); +} + +function onBlur(){ + beginControlsAutoHide( this ); + +} + +function onCanvasClick( event ) { + var zoomPerClick, + factor; + if ( this.viewport && event.quick ) { // ignore clicks where mouse moved + zoomPerClick = this.zoomPerClick; + factor = event.shift ? 1.0 / zoomPerClick : zoomPerClick; + this.viewport.zoomBy( + factor, + this.viewport.pointFromPixel( event.position, true ) + ); + this.viewport.applyConstraints(); + } + this.raiseEvent( 'canvas-click', { + tracker: event.eventSource, + position: event.position, + quick: event.quick, + shift: event.shift, + originalEvent: event.originalEvent + }); +} + +function onCanvasDrag( event ) { + if ( this.viewport ) { + if( !this.panHorizontal ){ + event.delta.x = 0; + } + if( !this.panVertical ){ + event.delta.y = 0; + } + this.viewport.panBy( + this.viewport.deltaPointsFromPixels( + event.delta.negate() + ) + ); + if( this.constrainDuringPan ){ + this.viewport.applyConstraints(); + } + } + this.raiseEvent( 'canvas-drag', { + tracker: event.eventSource, + position: event.position, + delta: event.delta, + shift: event.shift, + originalEvent: event.originalEvent + }); +} + +function onCanvasRelease( event ) { + if ( event.insideElementPressed && this.viewport ) { + this.viewport.applyConstraints(); + } + this.raiseEvent( 'canvas-release', { + tracker: event.eventSource, + position: event.position, + insideElementPressed: event.insideElementPressed, + insideElementReleased: event.insideElementReleased, + originalEvent: event.originalEvent + }); +} + +function onCanvasScroll( event ) { + var factor; + if ( this.viewport ) { + factor = Math.pow( this.zoomPerScroll, event.scroll ); + this.viewport.zoomBy( + factor, + this.viewport.pointFromPixel( event.position, true ) + ); + this.viewport.applyConstraints(); + } + this.raiseEvent( 'canvas-scroll', { + tracker: event.eventSource, + position: event.position, + scroll: event.scroll, + shift: event.shift, + originalEvent: event.originalEvent + }); + //cancels event + return false; +} + +function onContainerExit( event ) { + if ( !event.insideElementPressed ) { + THIS[ this.hash ].mouseInside = false; + if ( !THIS[ this.hash ].animating ) { + beginControlsAutoHide( this ); + } + } + this.raiseEvent( 'container-exit', { + tracker: event.eventSource, + position: event.position, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + +function onContainerRelease( event ) { + if ( !event.insideElementReleased ) { + THIS[ this.hash ].mouseInside = false; + if ( !THIS[ this.hash ].animating ) { + beginControlsAutoHide( this ); + } + } + this.raiseEvent( 'container-release', { + tracker: event.eventSource, + position: event.position, + insideElementPressed: event.insideElementPressed, + insideElementReleased: event.insideElementReleased, + originalEvent: event.originalEvent + }); +} + +function onContainerEnter( event ) { + THIS[ this.hash ].mouseInside = true; + abortControlsAutoHide( this ); + this.raiseEvent( 'container-enter', { + tracker: event.eventSource, + position: event.position, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + + +/////////////////////////////////////////////////////////////////////////////// +// Page update routines ( aka Views - for future reference ) +/////////////////////////////////////////////////////////////////////////////// + +function updateMulti( viewer ) { + if ( !viewer.source ) { + viewer._updateRequestId = null; + return; + } + + updateOnce( viewer ); + + // Request the next frame, unless we've been closed during the updateOnce() + if ( viewer.source ) { + viewer._updateRequestId = scheduleUpdate( viewer, updateMulti ); + } +} + +function updateOnce( viewer ) { + + var containerSize, + animated; + + if ( !viewer.source ) { + return; + } + + //viewer.profiler.beginUpdate(); + + containerSize = _getSafeElemSize( viewer.container ); + if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) { + // maintain image position + viewer.viewport.resize( containerSize, true ); + THIS[ viewer.hash ].prevContainerSize = containerSize; + } + + animated = viewer.viewport.update(); + + if( viewer.referenceStrip ){ + animated = viewer.referenceStrip.update( viewer.viewport ) || animated; + } + + if ( !THIS[ viewer.hash ].animating && animated ) { + viewer.raiseEvent( "animation-start" ); + abortControlsAutoHide( viewer ); + } + + if ( animated ) { + viewer.drawer.update(); + if( viewer.navigator ){ + viewer.navigator.update( viewer.viewport ); + } + viewer.raiseEvent( "animation" ); + } else if ( THIS[ viewer.hash ].forceRedraw || viewer.drawer.needsUpdate() ) { + viewer.drawer.update(); + if( viewer.navigator ){ + viewer.navigator.update( viewer.viewport ); + } + THIS[ viewer.hash ].forceRedraw = false; + } + + if ( THIS[ viewer.hash ].animating && !animated ) { + viewer.raiseEvent( "animation-finish" ); + + if ( !THIS[ viewer.hash ].mouseInside ) { + beginControlsAutoHide( viewer ); + } + } + + THIS[ viewer.hash ].animating = animated; + + //viewer.profiler.endUpdate(); +} + + + +/////////////////////////////////////////////////////////////////////////////// +// Navigation Controls +/////////////////////////////////////////////////////////////////////////////// +function resolveUrl( prefix, url ) { + return prefix ? prefix + url : url; +} + + + +function beginZoomingIn() { + THIS[ this.hash ].lastZoomTime = $.now(); + THIS[ this.hash ].zoomFactor = this.zoomPerSecond; + THIS[ this.hash ].zooming = true; + scheduleZoom( this ); +} + + +function beginZoomingOut() { + THIS[ this.hash ].lastZoomTime = $.now(); + THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond; + THIS[ this.hash ].zooming = true; + scheduleZoom( this ); +} + + +function endZooming() { + THIS[ this.hash ].zooming = false; +} + + +function scheduleZoom( viewer ) { + $.requestAnimationFrame( $.delegate( viewer, doZoom ) ); +} + + +function doZoom() { + var currentTime, + deltaTime, + adjustedFactor; + + if ( THIS[ this.hash ].zooming && this.viewport) { + currentTime = $.now(); + deltaTime = currentTime - THIS[ this.hash ].lastZoomTime; + adjustedFactor = Math.pow( THIS[ this.hash ].zoomFactor, deltaTime / 1000 ); + + this.viewport.zoomBy( adjustedFactor ); + this.viewport.applyConstraints(); + THIS[ this.hash ].lastZoomTime = currentTime; + scheduleZoom( this ); + } +} + + +function doSingleZoomIn() { + if ( this.viewport ) { + THIS[ this.hash ].zooming = false; + this.viewport.zoomBy( + this.zoomPerClick / 1.0 + ); + this.viewport.applyConstraints(); + } +} + + +function doSingleZoomOut() { + if ( this.viewport ) { + THIS[ this.hash ].zooming = false; + this.viewport.zoomBy( + 1.0 / this.zoomPerClick + ); + this.viewport.applyConstraints(); + } +} + + +function lightUp() { + this.buttons.emulateEnter(); + this.buttons.emulateExit(); +} + + +function onHome() { + if ( this.viewport ) { + this.viewport.goHome(); + } +} + + +function onFullPage() { + this.setFullPage( !this.isFullPage() ); + // correct for no mouseout event on change + if( this.buttons ){ + this.buttons.emulateExit(); + } + this.fullPageButton.element.focus(); + if ( this.viewport ) { + this.viewport.applyConstraints(); + } +} + + +function onPrevious(){ + var previous = THIS[ this.hash ].sequence - 1; + if(this.navPrevNextWrap && previous < 0){ + previous += this.tileSources.length; + } + this.goToPage( previous ); +} + + +function onNext(){ + var next = THIS[ this.hash ].sequence + 1; + if(this.navPrevNextWrap && next >= this.tileSources.length){ + next = 0; + } + this.goToPage( next ); +} + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Navigator + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + +/** + * The Navigator provides a small view of the current image as fixed + * while representing the viewport as a moving box serving as a frame + * of reference in the larger viewport as to which portion of the image + * is currently being examined. The navigator's viewport can be interacted + * with using the keyboard or the mouse. + * @class + * @name OpenSeadragon.Navigator + * @extends OpenSeadragon.Viewer + * @extends OpenSeadragon.EventSource + * @param {Object} options + * @param {String} options.viewerId + */ +$.Navigator = function( options ){ + + var viewer = options.viewer, + viewerSize = $.getElementSize( viewer.element), + unneededElement; + + //We may need to create a new element and id if they did not + //provide the id for the existing element + if( !options.id ){ + options.id = 'navigator-' + $.now(); + this.element = $.makeNeutralElement( "div" ); + options.controlOptions = { + anchor: $.ControlAnchor.TOP_RIGHT, + attachToViewer: true, + autoFade: true + }; + + if( options.position ){ + if( 'BOTTOM_RIGHT' == options.position ){ + options.controlOptions.anchor = $.ControlAnchor.BOTTOM_RIGHT; + } else if( 'BOTTOM_LEFT' == options.position ){ + options.controlOptions.anchor = $.ControlAnchor.BOTTOM_LEFT; + } else if( 'TOP_RIGHT' == options.position ){ + options.controlOptions.anchor = $.ControlAnchor.TOP_RIGHT; + } else if( 'TOP_LEFT' == options.position ){ + options.controlOptions.anchor = $.ControlAnchor.TOP_LEFT; + } + } + + } else { + this.element = document.getElementById( options.id ); + options.controlOptions = { + anchor: $.ControlAnchor.NONE, + attachToViewer: false, + autoFade: false + }; + } + this.element.id = options.id; + this.element.className += ' navigator'; + + options = $.extend( true, { + sizeRatio: $.DEFAULT_SETTINGS.navigatorSizeRatio + }, options, { + element: this.element, + //These need to be overridden to prevent recursion since + //the navigator is a viewer and a viewer has a navigator + showNavigator: false, + mouseNavEnabled: false, + showNavigationControl: false, + showSequenceControl: false, + immediateRender: true, + blendTime: 0, + animationTime: 0 + }); + + options.minPixelRatio = this.minPixelRatio = viewer.minPixelRatio; + + this.viewerSizeInPoints = viewer.viewport.deltaPointsFromPixels(viewerSize); + this.borderWidth = 2; + //At some browser magnification levels the display regions lines up correctly, but at some there appears to + //be a one pixel gap. + this.fudge = new $.Point(1, 1); + this.totalBorderWidths = new $.Point(this.borderWidth*2, this.borderWidth*2).minus(this.fudge); + + + (function( style, borderWidth ){ + style.margin = '0px'; + style.border = borderWidth + 'px solid #555'; + style.padding = '0px'; + style.background = '#000'; + style.opacity = 0.8; + style.overflow = 'hidden'; + }( this.element.style, this.borderWidth)); + + this.displayRegion = $.makeNeutralElement( "div" ); + this.displayRegion.id = this.element.id + '-displayregion'; + this.displayRegion.className = 'displayregion'; + + (function( style, borderWidth ){ + style.position = 'relative'; + style.top = '0px'; + style.left = '0px'; + style.fontSize = '0px'; + style.overflow = 'hidden'; + style.border = borderWidth + 'px solid #900'; + style.margin = '0px'; + style.padding = '0px'; + //TODO: IE doesnt like this property being set + //try{ style.outline = '2px auto #909'; }catch(e){/*ignore*/} + + style.background = 'transparent'; + + // We use square bracket notation on the statement below, because float is a keyword. + // This is important for the Google Closure compiler, if nothing else. + /*jshint sub:true */ + style['float'] = 'left'; //Webkit + + style.cssFloat = 'left'; //Firefox + style.styleFloat = 'left'; //IE + style.zIndex = 999999999; + style.cursor = 'default'; + }( this.displayRegion.style, this.borderWidth )); + + + this.element.innerTracker = new $.MouseTracker({ + element: this.element, + dragHandler: $.delegate( this, onCanvasDrag ), + clickHandler: $.delegate( this, onCanvasClick ), + releaseHandler: $.delegate( this, onCanvasRelease ), + scrollHandler: function(){ + //dont scroll the page up and down if the user is scrolling + //in the navigator + return false; + } + }).setTracking( true ); + + /*this.displayRegion.outerTracker = new $.MouseTracker({ + element: this.container, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + enterHandler: $.delegate( this, onContainerEnter ), + exitHandler: $.delegate( this, onContainerExit ), + releaseHandler: $.delegate( this, onContainerRelease ) + }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking*/ + + + viewer.addControl( + this.element, + options.controlOptions + ); + + if( options.width && options.height ){ + this.element.style.width = options.width + 'px'; + this.element.style.height = options.height + 'px'; + } else { + this.element.style.width = ( viewerSize.x * options.sizeRatio ) + 'px'; + this.element.style.height = ( viewerSize.y * options.sizeRatio ) + 'px'; + } + + $.Viewer.apply( this, [ options ] ); + + this.element.getElementsByTagName('form')[0].appendChild( this.displayRegion ); + unneededElement = this.element.getElementsByTagName('textarea')[0]; + if (unneededElement) { + unneededElement.parentNode.removeChild(unneededElement); + } + +}; + +$.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, { + + /** + * @function + * @name OpenSeadragon.Navigator.prototype.update + */ + update: function( viewport ){ + + var bounds, + topleft, + bottomright; + + if( viewport && this.viewport ){ + bounds = viewport.getBounds( true ); + topleft = this.viewport.pixelFromPoint( bounds.getTopLeft()); + bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight()).minus(this.totalBorderWidths); + + //update style for navigator-box + (function(style) { + + style.top = topleft.y + 'px'; + style.left = topleft.x + 'px'; + + var width = Math.abs( topleft.x - bottomright.x ); + var height = Math.abs( topleft.y - bottomright.y ); + // make sure width and height are non-negative so IE doesn't throw + style.width = Math.max( width, 0 ) + 'px'; + style.height = Math.max( height, 0 ) + 'px'; + + }( this.displayRegion.style )); + } + + }, + + open: function( source ){ + var containerSize = this.viewer.viewport.containerSize.times( this.sizeRatio ); + if( source.tileSize > containerSize.x || + source.tileSize > containerSize.y ){ + this.minPixelRatio = Math.min( + containerSize.x, + containerSize.y + ) / source.tileSize; + } else { + this.minPixelRatio = this.viewer.minPixelRatio; + } + return $.Viewer.prototype.open.apply( this, [ source ] ); + } + +}); + +/** + * @private + * @inner + * @function + */ +function onCanvasClick( event ) { + var newBounds, + viewerPosition, + dimensions; + if (! this.drag) { + if ( this.viewer.viewport ) { + viewerPosition = this.viewport.deltaPointsFromPixels( event.position ); + dimensions = this.viewer.viewport.getBounds().getSize(); + newBounds = new $.Rect( + viewerPosition.x - dimensions.x/2, + viewerPosition.y - dimensions.y/2, + dimensions.x, + dimensions.y + ); + if (this.viewer.source.aspectRatio > this.viewer.viewport.getAspectRatio()) { + newBounds.y = newBounds.y - ((this.viewerSizeInPoints.y - (1/this.viewer.source.aspectRatio)) /2 ); + } + else { + newBounds.x = newBounds.x - ((this.viewerSizeInPoints.x -1) /2 ); + } + this.viewer.viewport.fitBounds(newBounds); + this.viewer.viewport.applyConstraints(); + } + } + else { + this.drag = false; + } +} + +/** + * @private + * @inner + * @function + */ +function onCanvasDrag( event ) { + if ( this.viewer.viewport ) { + this.drag = true; + if( !this.panHorizontal ){ + event.delta.x = 0; + } + if( !this.panVertical ){ + event.delta.y = 0; + } + this.viewer.viewport.panBy( + this.viewport.deltaPointsFromPixels( + event.delta + ) + ); + } +} + + +/** + * @private + * @inner + * @function + */ +function onCanvasRelease( event ) { + if ( event.insideElementPressed && this.viewer.viewport ) { + this.viewer.viewport.applyConstraints(); + } +} + + +/** + * @private + * @inner + * @function + */ +function onCanvasScroll( event ) { + var factor; + if ( this.viewer.viewport ) { + factor = Math.pow( this.zoomPerScroll, event.scroll ); + this.viewer.viewport.zoomBy( + factor, + this.viewport.getCenter() + ); + this.viewer.viewport.applyConstraints(); + } + //cancels event + return false; +} + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - getString/setString + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + +//TODO: I guess this is where the i18n needs to be reimplemented. I'll look +// into existing patterns for i18n in javascript but i think that mimicking +// pythons gettext might be a reasonable approach. +var I18N = { + Errors: { + Dzc: "Sorry, we don't support Deep Zoom Collections!", + Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", + Security: "It looks like a security restriction stopped us from " + + "loading this Deep Zoom Image.", + Status: "This space unintentionally left blank ({0} {1}).", + OpenFailed: "Unable to open {0}: {1}" + }, + + Tooltips: { + FullPage: "Toggle full page", + Home: "Go home", + ZoomIn: "Zoom in", + ZoomOut: "Zoom out", + NextPage: "Next page", + PreviousPage: "Previous page" + } +}; + +$.extend( $, { + + /** + * @function + * @name OpenSeadragon.getString + * @param {String} property + */ + getString: function( prop ) { + + var props = prop.split('.'), + string = null, + args = arguments, + container = I18N, + i; + + for ( i = 0; i < props.length-1; i++ ) { + // in case not a subproperty + container = container[ props[ i ] ] || {}; + } + string = container[ props[ i ] ]; + + if ( typeof( string ) != "string" ) { + $.console.debug( "Untranslated source string:", prop ); + string = ""; // FIXME: this breaks gettext()-style convention, which would return source + } + + return string.replace(/\{\d+\}/g, function(capture) { + var i = parseInt( capture.match( /\d+/ ), 10 ) + 1; + return i < args.length ? + args[ i ] : + ""; + }); + }, + + /** + * @function + * @name OpenSeadragon.setString + * @param {String} property + * @param {*} value + */ + setString: function( prop, value ) { + + var props = prop.split('.'), + container = I18N, + i; + + for ( i = 0; i < props.length - 1; i++ ) { + if ( !container[ props[ i ] ] ) { + container[ props[ i ] ] = {}; + } + container = container[ props[ i ] ]; + } + + container[ props[ i ] ] = value; + } + +}); + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Point + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + +/** + * A Point is really used as a 2-dimensional vector, equally useful for + * representing a point on a plane, or the height and width of a plane + * not requiring any other frame of reference. + * @class + * @param {Number} [x] The vector component 'x'. Defaults to the origin at 0. + * @param {Number} [y] The vector component 'y'. Defaults to the origin at 0. + * @property {Number} [x] The vector component 'x'. + * @property {Number} [y] The vector component 'y'. + */ +$.Point = function( x, y ) { + this.x = typeof ( x ) == "number" ? x : 0; + this.y = typeof ( y ) == "number" ? y : 0; +}; + +$.Point.prototype = { + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + plus: function( point ) { + return new $.Point( + this.x + point.x, + this.y + point.y + ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + minus: function( point ) { + return new $.Point( + this.x - point.x, + this.y - point.y + ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + times: function( factor ) { + return new $.Point( + this.x * factor, + this.y * factor + ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + divide: function( factor ) { + return new $.Point( + this.x / factor, + this.y / factor + ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + negate: function() { + return new $.Point( -this.x, -this.y ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + distanceTo: function( point ) { + return Math.sqrt( + Math.pow( this.x - point.x, 2 ) + + Math.pow( this.y - point.y, 2 ) + ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + apply: function( func ) { + return new $.Point( func( this.x ), func( this.y ) ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + equals: function( point ) { + return ( + point instanceof $.Point + ) && ( + this.x === point.x + ) && ( + this.y === point.y + ); + }, + + /** + * Rotates the point around the specified pivot + * From http://stackoverflow.com/questions/4465931/rotate-rectangle-around-a-point + * @function + * @param {Number} degress to rotate around the pivot. + * @param {OpenSeadragon.Point} pivot Point about which to rotate. + * @returns {OpenSeadragon.Point}. A new point representing the point rotated around the specified pivot + */ + rotate: function ( degrees, pivot ) { + var angle = degrees * Math.PI / 180.0, + x = Math.cos( angle ) * ( this.x - pivot.x ) - Math.sin( angle ) * ( this.y - pivot.y ) + pivot.x, + y = Math.sin( angle ) * ( this.x - pivot.x ) + Math.cos( angle ) * ( this.y - pivot.y ) + pivot.y; + return new $.Point( x, y ); + }, + + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ + toString: function() { + return "(" + Math.round(this.x) + "," + Math.round(this.y) + ")"; + } +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - TileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + + +/** + * The TileSource contains the most basic implementation required to create a + * smooth transition between layer in an image pyramid. It has only a single key + * interface that must be implemented to complete it key functionality: + * 'getTileUrl'. It also has several optional interfaces that can be + * implemented if a new TileSource wishes to support configuration via a simple + * object or array ('configure') and if the tile source supports or requires + * configuration via retreival of a document on the network ala AJAX or JSONP, + * ('getImageInfo'). + *
    + * By default the image pyramid is split into N layers where the images longest + * side in M (in pixels), where N is the smallest integer which satisfies + * 2^(N+1) >= M. + * @class + * @extends OpenSeadragon.EventSource + * @param {Number|Object|Array|String} width + * If more than a single argument is supplied, the traditional use of + * positional parameters is supplied and width is expected to be the width + * source image at it's max resolution in pixels. If a single argument is supplied and + * it is an Object or Array, the construction is assumed to occur through + * the extending classes implementation of 'configure'. Finally if only a + * single argument is supplied and it is a String, the extending class is + * expected to implement 'getImageInfo' and 'configure'. + * @param {Number} height + * Width of the source image at max resolution in pixels. + * @param {Number} tileSize + * The size of the tiles to assumed to make up each pyramid layer in pixels. + * Tile size determines the point at which the image pyramid must be + * divided into a matrix of smaller images. + * @param {Number} tileOverlap + * The number of pixels each tile is expected to overlap touching tiles. + * @param {Number} minLevel + * The minimum level to attempt to load. + * @param {Number} maxLevel + * The maximum level to attempt to load. + * @property {Number} aspectRatio + * Ratio of width to height + * @property {OpenSeadragon.Point} dimensions + * Vector storing x and y dimensions ( width and height respectively ). + * @property {Number} tileSize + * The size of the image tiles used to compose the image. + * @property {Number} tileOverlap + * The overlap in pixels each tile shares with it's adjacent neighbors. + * @property {Number} minLevel + * The minimum pyramid level this tile source supports or should attempt to load. + * @property {Number} maxLevel + * The maximum pyramid level this tile source supports or should attempt to load. + */ +$.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) { + var callback = null, + args = arguments, + options, + i; + + if( $.isPlainObject( width ) ){ + options = width; + }else{ + options = { + width: args[0], + height: args[1], + tileSize: args[2], + tileOverlap: args[3], + minLevel: args[4], + maxLevel: args[5] + }; + } + + //Tile sources supply some events, namely 'ready' when they must be configured + //by asyncronously fetching their configuration data. + $.EventSource.call( this ); + + //we allow options to override anything we dont treat as + //required via idiomatic options or which is functionally + //set depending on the state of the readiness of this tile + //source + $.extend( true, this, options ); + + //Any functions that are passed as arguments are bound to the ready callback + /*jshint loopfunc:true*/ + for ( i = 0; i < arguments.length; i++ ) { + if ( $.isFunction( arguments[ i ] ) ) { + callback = arguments[ i ]; + this.addHandler( 'ready', function ( event ) { + callback( event ); + } ); + //only one callback per constructor + break; + } + } + + if( 'string' == $.type( arguments[ 0 ] ) ){ + //in case the getImageInfo method is overriden and/or implies an + //async mechanism set some safe defaults first + this.aspectRatio = 1; + this.dimensions = new $.Point( 10, 10 ); + this.tileSize = 0; + this.tileOverlap = 0; + this.minLevel = 0; + this.maxLevel = 0; + this.ready = false; + //configuration via url implies the extending class + //implements and 'configure' + console.log(arguments[ 0 ]); + this.getImageInfo( arguments[ 0 ] ); + + } else { + + //explicit configuration via positional args in constructor + //or the more idiomatic 'options' object + this.ready = true; + this.aspectRatio = ( options.width && options.height ) ? + ( options.width / options.height ) : 1; + this.dimensions = new $.Point( options.width, options.height ); + this.tileSize = options.tileSize ? options.tileSize : 0; + this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0; + this.minLevel = options.minLevel ? options.minLevel : 0; + this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ? + options.maxLevel : ( + ( options.width && options.height ) ? Math.ceil( + Math.log( Math.max( options.width, options.height ) ) / + Math.log( 2 ) + ) : 0 + ); + if( callback && $.isFunction( callback ) ){ + callback( this ); + } + } + + +}; + + +$.TileSource.prototype = { + + /** + * @function + * @param {Number} level + */ + getLevelScale: function( level ) { + + // see https://github.com/openseadragon/openseadragon/issues/22 + // we use the tilesources implementation of getLevelScale to generate + // a memoized re-implementation + var levelScaleCache = {}, + i; + for( i = 0; i <= this.maxLevel; i++ ){ + levelScaleCache[ i ] = 1 / Math.pow(2, this.maxLevel - i); + } + this.getLevelScale = function( _level ){ + return levelScaleCache[ _level ]; + }; + return this.getLevelScale( level ); + }, + + /** + * @function + * @param {Number} level + */ + getNumTiles: function( level ) { + var scale = this.getLevelScale( level ), + x = Math.ceil( scale * this.dimensions.x / this.tileSize ), + y = Math.ceil( scale * this.dimensions.y / this.tileSize ); + + return new $.Point( x, y ); + }, + + /** + * @function + * @param {Number} level + */ + getPixelRatio: function( level ) { + var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ), + rx = 1.0 / imageSizeScaled.x, + ry = 1.0 / imageSizeScaled.y; + + return new $.Point(rx, ry); + }, + + + /** + * @function + * @param {Number} level + */ + getClosestLevel: function( rect ) { + var i, + tilesPerSide = Math.floor( Math.max( rect.x, rect.y ) / this.tileSize ), + tiles; + for( i = this.minLevel; i < this.maxLevel; i++ ){ + tiles = this.getNumTiles( i ); + if( Math.max( tiles.x, tiles.y ) + 1 >= tilesPerSide ){ + break; + } + } + return Math.max( 0, i - 1 ); + }, + + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function( level, point ) { + var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ), + tx = Math.floor( pixel.x / this.tileSize ), + ty = Math.floor( pixel.y / this.tileSize ); + + return new $.Point( tx, ty ); + }, + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileBounds: function( level, x, y ) { + var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), + px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap, + py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap, + sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap, + sy = this.tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap, + scale = 1.0 / dimensionsScaled.x; + + sx = Math.min( sx, dimensionsScaled.x - px ); + sy = Math.min( sy, dimensionsScaled.y - py ); + + return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); + }, + + + /** + * Responsible for retrieving, and caching the + * image metadata pertinent to this TileSources implementation. + * @function + * @param {String} url + * @throws {Error} + */ + getImageInfo: function( url ) { + var _this = this, + callbackName, + callback, + readySource, + options, + urlParts, + filename, + lastDot; + + + if( url ) { + urlParts = url.split( '/' ); + filename = urlParts[ urlParts.length - 1 ]; + lastDot = filename.lastIndexOf( '.' ); + if ( lastDot > -1 ) { + urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); + } + } + + callback = function( data ){ + if( typeof(data) === "string" ) { + data = $.parseXml( data ); + } + var $TileSource = $.TileSource.determineType( _this, data, url ); + if ( !$TileSource ) { + _this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } ); + return; + } + + options = $TileSource.prototype.configure.apply( _this, [ data, url ]); + readySource = new $TileSource( options ); + _this.ready = true; + _this.raiseEvent( 'ready', { tileSource: readySource } ); + }; + + if( url.match(/\.js$/) ){ + //TODO: Its not very flexible to require tile sources to end jsonp + // request for info with a url that ends with '.js' but for + // now it's the only way I see to distinguish uniformly. + callbackName = url.split( '/' ).pop().replace('.js',''); + $.jsonp({ + url: url, + async: false, + callbackName: callbackName, + callback: callback + }); + } else { + // request info via xhr asyncronously. + $.makeAjaxRequest( url, function( xhr ) { + var data = processResponse( xhr ); + callback( data ); + }, function ( xhr, exc ) { + var msg; + + /* + IE < 10 will block XHR requests to different origins. Any property access on the request + object will raise an exception which we'll attempt to handle by formatting the original + exception rather than the second one raised when we try to access xhr.status + */ + try { + msg = "HTTP " + xhr.status + " attempting to load TileSource"; + } catch ( e ) { + var formattedExc; + if ( typeof( exc ) == "undefined" || !exc.toString ) { + formattedExc = "Unknown error"; + } else { + formattedExc = exc.toString(); + } + + msg = formattedExc + " attempting to load TileSource"; + } + + _this.raiseEvent( 'open-failed', { + message: msg, + source: url + }); + }); + } + + }, + + /** + * Responsible determining if a the particular TileSource supports the + * data format ( and allowed to apply logic against the url the data was + * loaded from, if any ). Overriding implementations are expected to do + * something smart with data and / or url to determine support. Also + * understand that iteration order of TileSources is not guarunteed so + * please make sure your data or url is expressive enough to ensure a simple + * and sufficient mechanisim for clear determination. + * @function + * @param {String|Object|Array|Document} data + * @param {String} url - the url the data was loaded + * from if any. + * @return {Boolean} + */ + supports: function( data, url ) { + return false; + }, + + /** + * Responsible for parsing and configuring the + * image metadata pertinent to this TileSources implementation. + * This method is not implemented by this class other than to throw an Error + * announcing you have to implement it. Because of the variety of tile + * server technologies, and various specifications for building image + * pyramids, this method is here to allow easy integration. + * @function + * @param {String|Object|Array|Document} data + * @param {String} url - the url the data was loaded + * from if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + * @throws {Error} + */ + configure: function( data, url ) { + throw new Error( "Method not implemented." ); + }, + + /** + * Responsible for retriving the url which will return an image for the + * region speified by the given x, y, and level components. + * This method is not implemented by this class other than to throw an Error + * announcing you have to implement it. Because of the variety of tile + * server technologies, and various specifications for building image + * pyramids, this method is here to allow easy integration. + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @throws {Error} + */ + getTileUrl: function( level, x, y ) { + throw new Error( "Method not implemented." ); + }, + + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + tileExists: function( level, x, y ) { + var numTiles = this.getNumTiles( level ); + return level >= this.minLevel && + level <= this.maxLevel && + x >= 0 && + y >= 0 && + x < numTiles.x && + y < numTiles.y; + } +}; + + +$.extend( true, $.TileSource.prototype, $.EventSource.prototype ); + + +/** + * Decides whether to try to process the response as xml, json, or hand back + * the text + * @eprivate + * @inner + * @function + * @param {XMLHttpRequest} xhr - the completed network request + */ +function processResponse( xhr ){ + var responseText = xhr.responseText, + status = xhr.status, + statusText, + data; + + if ( !xhr ) { + throw new Error( $.getString( "Errors.Security" ) ); + } else if ( xhr.status !== 200 && xhr.status !== 0 ) { + status = xhr.status; + statusText = ( status == 404 ) ? + "Not Found" : + xhr.statusText; + throw new Error( $.getString( "Errors.Status", status, statusText ) ); + } + + if( responseText.match(/\s*<.*/) ){ + try{ + data = ( xhr.responseXML && xhr.responseXML.documentElement ) ? + xhr.responseXML : + $.parseXml( responseText ); + } catch (e){ + data = xhr.responseText; + } + }else if( responseText.match(/\s*[\{\[].*/) ){ + /*jshint evil:true*/ + data = eval( '('+responseText+')' ); + }else{ + data = responseText; + } + return data; +} + + +/** + * Determines the TileSource Implementation by introspection of OpenSeadragon + * namespace, calling each TileSource implementation of 'isType' + * @eprivate + * @inner + * @function + * @param {Object|Array|Document} data - the tile source configuration object + * @param {String} url - the url where the tile source configuration object was + * loaded from, if any. + */ +$.TileSource.determineType = function( tileSource, data, url ){ + var property; + for( property in OpenSeadragon ){ + if( property.match(/.+TileSource$/) && + $.isFunction( OpenSeadragon[ property ] ) && + $.isFunction( OpenSeadragon[ property ].prototype.supports ) && + OpenSeadragon[ property ].prototype.supports.call( tileSource, data, url ) + ){ + return OpenSeadragon[ property ]; + } + } + + $.console.error( "No TileSource was able to open %s %s", url, data ); +}; + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - DziTileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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 + * @extends OpenSeadragon.TileSource + * @param {Number|Object} width - the pixel width of the image or the idiomatic + * options object which is used instead of positional arguments. + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + * @param {String} fileFormat + * @param {OpenSeadragon.DisplayRect[]} displayRects + * @property {String} tilesUrl + * @property {String} fileFormat + * @property {OpenSeadragon.DisplayRect[]} displayRects + */ +$.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects, minLevel, maxLevel ) { + var i, + rect, + level, + options; + + if( $.isPlainObject( width ) ){ + options = width; + }else{ + options = { + width: arguments[ 0 ], + height: arguments[ 1 ], + tileSize: arguments[ 2 ], + tileOverlap: arguments[ 3 ], + tilesUrl: arguments[ 4 ], + fileFormat: arguments[ 5 ], + displayRects: arguments[ 6 ], + minLevel: arguments[ 7 ], + maxLevel: arguments[ 8 ] + }; + } + + this._levelRects = {}; + this.tilesUrl = options.tilesUrl; + this.fileFormat = options.fileFormat; + this.displayRects = options.displayRects; + + if ( this.displayRects ) { + for ( i = this.displayRects.length - 1; i >= 0; i-- ) { + rect = this.displayRects[ i ]; + for ( level = rect.minLevel; level <= rect.maxLevel; level++ ) { + if ( !this._levelRects[ level ] ) { + this._levelRects[ level ] = []; + } + this._levelRects[ level ].push( rect ); + } + } + } + + $.TileSource.apply( this, [ options ] ); + +}; + +$.extend( $.DziTileSource.prototype, $.TileSource.prototype, { + + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @name OpenSeadragon.DziTileSource.prototype.supports + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function( data, url ){ + var ns; + if ( data.Image ) { + ns = data.Image.xmlns; + } else if ( data.documentElement && "Image" == data.documentElement.tagName ) { + ns = data.documentElement.namespaceURI; + } + + return ( "http://schemas.microsoft.com/deepzoom/2008" == ns || + "http://schemas.microsoft.com/deepzoom/2009" == ns ); + }, + + /** + * + * @function + * @name OpenSeadragon.DziTileSource.prototype.configure + * @param {Object|XMLDocument} data - the raw configuration + * @param {String} url - the url the data was retreived from if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( data, url ){ + + var options; + + if( !$.isPlainObject(data) ){ + + options = configureFromXML( this, data ); + + }else{ + + options = configureFromObject( this, data ); + } + + if (url && !options.tilesUrl) { + options.tilesUrl = url.replace(/([^\/]+)\.(dzi|xml|js)$/, '$1_files/'); + } + + return options; + }, + + + /** + * @function + * @name OpenSeadragon.DziTileSource.prototype.getTileUrl + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function( level, x, y ) { + return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat ].join( '' ); + }, + + + /** + * @function + * @name OpenSeadragon.DziTileSource.prototype.tileExists + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + tileExists: function( level, x, y ) { + var rects = this._levelRects[ level ], + rect, + scale, + xMin, + yMin, + xMax, + yMax, + i; + + if ( !rects || !rects.length ) { + return true; + } + + for ( i = rects.length - 1; i >= 0; i-- ) { + rect = rects[ i ]; + + if ( level < rect.minLevel || level > rect.maxLevel ) { + continue; + } + + scale = this.getLevelScale( level ); + xMin = rect.x * scale; + yMin = rect.y * scale; + xMax = xMin + rect.width * scale; + yMax = yMin + rect.height * scale; + + xMin = Math.floor( xMin / this.tileSize ); + yMin = Math.floor( yMin / this.tileSize ); + xMax = Math.ceil( xMax / this.tileSize ); + yMax = Math.ceil( yMax / this.tileSize ); + + if ( xMin <= x && x < xMax && yMin <= y && y < yMax ) { + return true; + } + } + + return false; + } +}); + + +/** + * @private + * @inner + * @function + */ +function configureFromXML( tileSource, xmlDoc ){ + + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + var root = xmlDoc.documentElement, + rootName = root.tagName, + configuration = null, + displayRects = [], + dispRectNodes, + dispRectNode, + rectNode, + sizeNode, + i; + + if ( rootName == "Image" ) { + + try { + sizeNode = root.getElementsByTagName( "Size" )[ 0 ]; + configuration = { + Image: { + xmlns: "http://schemas.microsoft.com/deepzoom/2008", + Url: root.getAttribute( "Url" ), + Format: root.getAttribute( "Format" ), + DisplayRect: null, + Overlap: parseInt( root.getAttribute( "Overlap" ), 10 ), + TileSize: parseInt( root.getAttribute( "TileSize" ), 10 ), + Size: { + Height: parseInt( sizeNode.getAttribute( "Height" ), 10 ), + Width: parseInt( sizeNode.getAttribute( "Width" ), 10 ) + } + } + }; + + if ( !$.imageFormatSupported( configuration.Image.Format ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", configuration.Image.Format.toUpperCase() ) + ); + } + + dispRectNodes = root.getElementsByTagName( "DisplayRect" ); + for ( i = 0; i < dispRectNodes.length; i++ ) { + dispRectNode = dispRectNodes[ i ]; + rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ]; + + displayRects.push({ + Rect: { + X: parseInt( rectNode.getAttribute( "X" ), 10 ), + Y: parseInt( rectNode.getAttribute( "Y" ), 10 ), + Width: parseInt( rectNode.getAttribute( "Width" ), 10 ), + Height: parseInt( rectNode.getAttribute( "Height" ), 10 ), + MinLevel: parseInt( dispRectNode.getAttribute( "MinLevel" ), 10 ), + MaxLevel: parseInt( dispRectNode.getAttribute( "MaxLevel" ), 10 ) + } + }); + } + + if( displayRects.length ){ + configuration.Image.DisplayRect = displayRects; + } + + return configureFromObject( tileSource, configuration ); + + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( $.getString("Errors.Dzi") ); + } + } else if ( rootName == "Collection" ) { + throw new Error( $.getString( "Errors.Dzc" ) ); + } else if ( rootName == "Error" ) { + return $._processDZIError( root ); + } + + throw new Error( $.getString( "Errors.Dzi" ) ); +} + +/** + * @private + * @inner + * @function + */ +function configureFromObject( tileSource, configuration ){ + var imageData = configuration.Image, + tilesUrl = imageData.Url, + fileFormat = imageData.Format, + sizeData = imageData.Size, + dispRectData = imageData.DisplayRect || [], + width = parseInt( sizeData.Width, 10 ), + height = parseInt( sizeData.Height, 10 ), + tileSize = parseInt( imageData.TileSize, 10 ), + tileOverlap = parseInt( imageData.Overlap, 10 ), + displayRects = [], + rectData, + i; + + //TODO: need to figure out out to better handle image format compatibility + // which actually includes additional file formats like xml and pdf + // and plain text for various tilesource implementations to avoid low + // level errors. + // + // For now, just don't perform the check. + // + /*if ( !imageFormatSupported( fileFormat ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) + ); + }*/ + + for ( i = 0; i < dispRectData.length; i++ ) { + rectData = dispRectData[ i ].Rect; + + displayRects.push( new $.DisplayRect( + parseInt( rectData.X, 10 ), + parseInt( rectData.Y, 10 ), + parseInt( rectData.Width, 10 ), + parseInt( rectData.Height, 10 ), + parseInt( rectData.MinLevel, 10 ), + parseInt( rectData.MaxLevel, 10 ) + )); + } + + return $.extend(true, { + width: width, /* width *required */ + height: height, /* height *required */ + tileSize: tileSize, /* tileSize *required */ + tileOverlap: tileOverlap, /* tileOverlap *required */ + minLevel: null, /* minLevel */ + maxLevel: null, /* maxLevel */ + tilesUrl: tilesUrl, /* tilesUrl */ + fileFormat: fileFormat, /* fileFormat */ + displayRects: displayRects /* displayRects */ + }, configuration ); + +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - IIIFTileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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. + */ + +/* + * The getTileUrl implementation is based on Jon Stroop's Python version, + * which is released under the New BSD license: + * https://gist.github.com/jpstroop/4624253 + */ + + +(function( $ ){ + +/** + * A client implementation of the International Image Interoperability + * Format: Image API Draft 0.2 - Please read more about the specification + * at + * + * @class + * @extends OpenSeadragon.TileSource + * @see http://library.stanford.edu/iiif/image-api/ + */ +$.IIIFTileSource = function( options ){ + + $.extend( true, this, options ); + + if( !(this.height && this.width && this.identifier && this.tilesUrl ) ){ + throw new Error('IIIF required parameters not provided.'); + } + + //TODO: at this point the base tile source implementation assumes + // a tile is a square and so only has one property tileSize + // to store it. It may be possible to make tileSize a vector + // OpenSeadraon.Point but would require careful implementation + // to preserve backward compatibility. + options.tileSize = this.tile_width; + + if (! options.maxLevel ) { + var mf = -1; + var scfs = this.scale_factors || this.scale_factor; + if ( scfs instanceof Array ) { + for ( var i = 0; i < scfs.length; i++ ) { + var cf = Number( scfs[i] ); + if ( !isNaN( cf ) && cf > mf ) { mf = cf; } + } + } + if ( mf < 0 ) { options.maxLevel = Number(Math.ceil(Math.log(Math.max(this.width, this.height), 2))); } + else { options.maxLevel = mf; } + } + + $.TileSource.apply( this, [ options ] ); +}; + +$.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, { + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @name OpenSeadragon.IIIFTileSource.prototype.supports + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function( data, url ){ + return ( + data.ns && + "http://library.stanford.edu/iiif/image-api/ns/" == data.ns + ) || ( + data.profile && ( + "http://library.stanford.edu/iiif/image-api/compliance.html#level1" == data.profile || + "http://library.stanford.edu/iiif/image-api/compliance.html#level2" == data.profile || + "http://library.stanford.edu/iiif/image-api/compliance.html#level3" == data.profile || + "http://library.stanford.edu/iiif/image-api/compliance.html" == data.profile + ) + ) || ( + data.documentElement && + "info" == data.documentElement.tagName && + "http://library.stanford.edu/iiif/image-api/ns/" == + data.documentElement.namespaceURI + ); + }, + + /** + * + * @function + * @name OpenSeadragon.IIIFTileSource.prototype.configure + * @param {Object|XMLDocument} data - the raw configuration + * @param {String} url - the url the data was retreived from if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile source via it's constructor. + */ + configure: function( data, url ){ + var service, + options, + host; + + if( !$.isPlainObject(data) ){ + + options = configureFromXml( this, data ); + + }else{ + + options = configureFromObject( this, data ); + } + + if( url && !options.tilesUrl ){ + service = url.split('/'); + service.pop(); //info.json or info.xml + service = service.join('/'); + if( 'http' !== url.substring( 0, 4 ) ){ + host = location.protocol + '//' + location.host; + service = host + service; + } + options.tilesUrl = service.replace( + data.identifier, + '' + ); + } + + return options; + }, + + /** + * Responsible for retreiving the url which will return an image for the + * region speified by the given x, y, and level components. + * @function + * @name OpenSeadragon.IIIFTileSource.prototype.getTileUrl + * @param {Number} level - z index + * @param {Number} x + * @param {Number} y + * @throws {Error} + */ + getTileUrl: function( level, x, y ){ + + //# constants + var IIIF_ROTATION = '0', + IIIF_QUALITY = 'native.jpg', + + //## get the scale (level as a decimal) + scale = Math.pow( 0.5, this.maxLevel - level ), + + //## get iiif size + // iiif_size = 'pct:' + ( scale * 100 ), + + //# image dimensions at this level + level_width = Math.ceil( this.width * scale ), + level_height = Math.ceil( this.height * scale ), + + //## iiif region + iiif_tile_size_width = Math.ceil( this.tileSize / scale ), + iiif_tile_size_height = Math.ceil( this.tileSize / scale ), + iiif_region, + iiif_tile_x, + iiif_tile_y, + iiif_tile_w, + iiif_tile_h, + iiif_size; + + + if ( level_width < this.tile_width && level_height < this.tile_height ){ + iiif_size = level_width + ","; // + level_height; only one dim. for IIIF level 1 compliance + iiif_region = 'full'; + } else { + iiif_tile_x = x * iiif_tile_size_width; + iiif_tile_y = y * iiif_tile_size_height; + iiif_tile_w = Math.min( iiif_tile_size_width, this.width - iiif_tile_x ); + iiif_tile_h = Math.min( iiif_tile_size_height, this.height - iiif_tile_y ); + iiif_size = Math.ceil(iiif_tile_w * scale) + ","; + iiif_region = [ iiif_tile_x, iiif_tile_y, iiif_tile_w, iiif_tile_h ].join(','); + } + + return [ + this.tilesUrl, + this.identifier, + iiif_region, + iiif_size, + IIIF_ROTATION, + IIIF_QUALITY + ].join('/'); + } + + +}); + +/** + * @private + * @inner + * @function + * + + + 1E34750D-38DB-4825-A38A-B60A345E591C + 6000 + 4000 + + 1 + 2 + 4 + + 1024 + 1024 + + jpg + png + + + native + grey + + + */ +function configureFromXml( tileSource, xmlDoc ){ + + //parse the xml + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + var root = xmlDoc.documentElement, + rootName = root.tagName, + configuration = null; + + if ( rootName == "info" ) { + + try { + + configuration = { + "ns": root.namespaceURI + }; + + parseXML( root, configuration ); + + return configureFromObject( tileSource, configuration ); + + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( $.getString("Errors.IIIF") ); + } + } + + throw new Error( $.getString( "Errors.IIIF" ) ); + +} + + +/** + * @private + * @inner + * @function + */ +function parseXML( node, configuration, property ){ + var i, + value; + if( node.nodeType == 3 && property ){//text node + value = node.nodeValue.trim(); + if( value.match(/^\d*$/)){ + value = Number( value ); + } + if( !configuration[ property ] ){ + configuration[ property ] = value; + }else{ + if( !$.isArray( configuration[ property ] ) ){ + configuration[ property ] = [ configuration[ property ] ]; + } + configuration[ property ].push( value ); + } + } else if( node.nodeType == 1 ){ + for( i = 0; i < node.childNodes.length; i++ ){ + parseXML( node.childNodes[ i ], configuration, node.nodeName ); + } + } +} + + +/** + * @private + * @inner + * @function + * + { + "profile" : "http://library.stanford.edu/iiif/image-api/compliance.html#level1", + "identifier" : "1E34750D-38DB-4825-A38A-B60A345E591C", + "width" : 6000, + "height" : 4000, + "scale_factors" : [ 1, 2, 4 ], + "tile_width" : 1024, + "tile_height" : 1024, + "formats" : [ "jpg", "png" ], + "quality" : [ "native", "grey" ] + } + */ +function configureFromObject( tileSource, configuration ){ + //the image_host property is not part of the iiif standard but is included here to + //allow the info.json and info.xml specify a different server to load the + //images from so we can test the implementation. + if( configuration.image_host ){ + configuration.tilesUrl = configuration.image_host; + } + return configuration; +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - IIIF1_1TileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + +/** + * A client implementation of the International Image Interoperability + * Format: Image API 1.1 - Please read more about the specification + * at + * + * @class + * @extends OpenSeadragon.TileSource + * @see http://library.stanford.edu/iiif/image-api/ + */ +$.IIIF1_1TileSource = function( options ){ + + $.extend( true, this, options ); + + if( !(this.height && this.width && this['@id'] ) ){ + throw new Error('IIIF required parameters not provided.'); + } + + options.tileSize = this.tile_width; + + if (! options.maxLevel ) { + var mf = -1; + var scfs = this.scale_factors || this.scale_factor; + if ( scfs instanceof Array ) { + for ( var i = 0; i < scfs.length; i++ ) { + var cf = Number( scfs[i] ); + if ( !isNaN( cf ) && cf > mf ) { mf = cf; } + } + } + if ( mf < 0 ) { options.maxLevel = Number(Math.ceil(Math.log(Math.max(this.width, this.height), 2))); } + else { options.maxLevel = mf; } + } + + $.TileSource.apply( this, [ options ] ); +}; + +$.extend( $.IIIF1_1TileSource.prototype, $.TileSource.prototype, { + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @name OpenSeadragon.IIIF1_1TileSource.prototype.supports + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function( data, url ){ + return data.profile && ( + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0" == data.profile || + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level1" == data.profile || + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level2" == data.profile || + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level3" == data.profile || + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html" == data.profile + ); + }, + + /** + * + * @function + * @name OpenSeadragon.IIIF1_1TileSource.prototype.configure + * @param {Object} data - the raw configuration + */ + // IIIF 1.1 Info Looks like this (XML syntax is no more): + // { + // "@context" : "http://library.stanford.edu/iiif/image-api/1.1/context.json", + // "@id" : "http://iiif.example.com/prefix/1E34750D-38DB-4825-A38A-B60A345E591C", + // "width" : 6000, + // "height" : 4000, + // "scale_factors" : [ 1, 2, 4 ], + // "tile_width" : 1024, + // "tile_height" : 1024, + // "formats" : [ "jpg", "png" ], + // "qualities" : [ "native", "grey" ] + // "profile" : "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0" + // } + configure: function( data ){ + return data; + }, + /** + * Responsible for retreiving the url which will return an image for the + * region specified by the given x, y, and level components. + * @function + * @name OpenSeadragon.IIIF1_1TileSource.prototype.getTileUrl + * @param {Number} level - z index + * @param {Number} x + * @param {Number} y + * @throws {Error} + */ + getTileUrl: function( level, x, y ){ + + //# constants + var IIIF_ROTATION = '0', + IIIF_QUALITY = 'native.jpg', + + //## get the scale (level as a decimal) + scale = Math.pow( 0.5, this.maxLevel - level ), + + //# image dimensions at this level + level_width = Math.ceil( this.width * scale ), + level_height = Math.ceil( this.height * scale ), + + //## iiif region + iiif_tile_size_width = Math.ceil( this.tileSize / scale ), + iiif_tile_size_height = Math.ceil( this.tileSize / scale ), + iiif_region, + iiif_tile_x, + iiif_tile_y, + iiif_tile_w, + iiif_tile_h, + iiif_size, + uri; + + if ( level_width < this.tile_width && level_height < this.tile_height ){ + iiif_size = level_width + "," + level_height; + iiif_region = 'full'; + } else { + iiif_tile_x = x * iiif_tile_size_width; + iiif_tile_y = y * iiif_tile_size_height; + iiif_tile_w = Math.min( iiif_tile_size_width, this.width - iiif_tile_x ); + iiif_tile_h = Math.min( iiif_tile_size_height, this.height - iiif_tile_y ); + iiif_size = Math.ceil(iiif_tile_w * scale) + "," + Math.ceil(iiif_tile_h * scale); + iiif_region = [ iiif_tile_x, iiif_tile_y, iiif_tile_w, iiif_tile_h ].join(','); + } + uri = [ this['@id'], iiif_region, iiif_size, IIIF_ROTATION, IIIF_QUALITY ].join('/'); + return uri; + } + }); + +}( OpenSeadragon )); + +/* + * OpenSeadragon - OsmTileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Derived from the OSM tile source in Rainer Simon's seajax-utils project + * . Rainer Simon has contributed + * the included code to the OpenSeadragon project under the New BSD license; + * see . + */ + + +(function( $ ){ + +/** + * A tilesource implementation for OpenStreetMap. + * + * Note 1. Zoomlevels. Deep Zoom and OSM define zoom levels differently. In Deep + * Zoom, level 0 equals an image of 1x1 pixels. In OSM, level 0 equals an image of + * 256x256 levels (see http://gasi.ch/blog/inside-deep-zoom-2). I.e. there is a + * difference of log2(256)=8 levels. + * + * Note 2. Image dimension. According to the OSM Wiki + * (http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels) + * the highest Mapnik zoom level has 256.144x256.144 tiles, with a 256x256 + * pixel size. I.e. the Deep Zoom image dimension is 65.572.864x65.572.864 + * pixels. + * + * @class + * @extends OpenSeadragon.TileSource + * @param {Number|Object} width - the pixel width of the image or the idiomatic + * options object which is used instead of positional arguments. + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + */ +$.OsmTileSource = function( width, height, tileSize, tileOverlap, tilesUrl ) { + var options; + + if( $.isPlainObject( width ) ){ + options = width; + }else{ + options = { + width: arguments[0], + height: arguments[1], + tileSize: arguments[2], + tileOverlap: arguments[3], + tilesUrl: arguments[4] + }; + } + //apply default setting for standard public OpenStreatMaps service + //but allow them to be specified so fliks can host there own instance + //or apply against other services supportting the same standard + if( !options.width || !options.height ){ + options.width = 65572864; + options.height = 65572864; + } + if( !options.tileSize ){ + options.tileSize = 256; + options.tileOverlap = 0; + } + if( !options.tilesUrl ){ + options.tilesUrl = "http://tile.openstreetmap.org/"; + } + options.minLevel = 8; + + $.TileSource.apply( this, [ options ] ); + +}; + +$.extend( $.OsmTileSource.prototype, $.TileSource.prototype, { + + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @name OpenSeadragon.OsmTileSource.prototype.supports + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function( data, url ){ + return ( + data.type && + "openstreetmaps" == data.type + ); + }, + + /** + * + * @function + * @name OpenSeadragon.OsmTileSource.prototype.configure + * @param {Object} data - the raw configuration + * @param {String} url - the url the data was retreived from if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( data, url ){ + return data; + }, + + + /** + * @function + * @name OpenSeadragon.OsmTileSource.prototype.getTileUrl + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function( level, x, y ) { + return this.tilesUrl + (level - 8) + "/" + x + "/" + y + ".png"; + } +}); + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - TmsTileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Derived from the TMS tile source in Rainer Simon's seajax-utils project + * . Rainer Simon has contributed + * the included code to the OpenSeadragon project under the New BSD license; + * see . + */ + + +(function( $ ){ + +/** + * A tilesource implementation for Tiled Map Services (TMS). + * TMS tile scheme ( [ as supported by OpenLayers ] is described here + * ( http://openlayers.org/dev/examples/tms.html ). + * + * @class + * @extends OpenSeadragon.TileSource + * @param {Number|Object} width - the pixel width of the image or the idiomatic + * options object which is used instead of positional arguments. + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + */ +$.TmsTileSource = function( width, height, tileSize, tileOverlap, tilesUrl ) { + var options; + + if( $.isPlainObject( width ) ){ + options = width; + }else{ + options = { + width: arguments[0], + height: arguments[1], + tileSize: arguments[2], + tileOverlap: arguments[3], + tilesUrl: arguments[4] + }; + } + // TMS has integer multiples of 256 for width/height and adds buffer + // if necessary -> account for this! + var bufferedWidth = Math.ceil(options.width / 256) * 256, + bufferedHeight = Math.ceil(options.height / 256) * 256, + max; + + // Compute number of zoomlevels in this tileset + if (bufferedWidth > bufferedHeight) { + max = bufferedWidth / 256; + } else { + max = bufferedHeight / 256; + } + options.maxLevel = Math.ceil(Math.log(max)/Math.log(2)) - 1; + options.tileSize = 256; + options.width = bufferedWidth; + options.height = bufferedHeight; + + $.TileSource.apply( this, [ options ] ); + +}; + +$.extend( $.TmsTileSource.prototype, $.TileSource.prototype, { + + + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @name OpenSeadragon.TmsTileSource.prototype.supports + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function( data, url ){ + return ( data.type && "tiledmapservice" == data.type ); + }, + + /** + * + * @function + * @name OpenSeadragon.TmsTileSource.prototype.configure + * @param {Object} data - the raw configuration + * @param {String} url - the url the data was retreived from if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( data, url ){ + return data; + }, + + + /** + * @function + * @name OpenSeadragon.TmsTileSource.prototype.getTileUrl + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function( level, x, y ) { + // Convert from Deep Zoom definition to TMS zoom definition + var yTiles = this.getNumTiles( level ).y - 1; + + return this.tilesUrl + level + "/" + x + "/" + (yTiles - y) + ".png"; + } +}); + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - LegacyTileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + +/** + * The LegacyTileSource allows simple, traditional image pyramids to be loaded + * into an OpenSeadragon Viewer. Basically, this translates to the historically + * common practice of starting with a 'master' image, maybe a tiff for example, + * and generating a set of 'service' images like one or more thumbnails, a medium + * resolution image and a high resolution image in standard web formats like + * png or jpg. + * @class + * @extends OpenSeadragon.TileSource + * @param {Array} levels An array of file descriptions, each is an object with + * a 'url', a 'width', and a 'height'. Overriding classes can expect more + * properties but these properties are sufficient for this implementation. + * Additionally, the levels are required to be listed in order from + * smallest to largest. + * @property {Number} aspectRatio + * @property {Number} dimensions + * @property {Number} tileSize + * @property {Number} tileOverlap + * @property {Number} minLevel + * @property {Number} maxLevel + * @property {Array} levels + */ +$.LegacyTileSource = function( levels ) { + + var options, + width, + height; + + if( $.isArray( levels ) ){ + options = { + type: 'legacy-image-pyramid', + levels: levels + }; + } + + //clean up the levels to make sure we support all formats + options.levels = filterFiles( options.levels ); + + if ( options.levels.length > 0 ) { + width = options.levels[ options.levels.length - 1 ].width; + height = options.levels[ options.levels.length - 1 ].height; + } + else { + width = 0; + height = 0; + $.console.error( "No supported image formats found" ); + } + + $.extend( true, options, { + width: width, + height: height, + tileSize: Math.max( height, width ), + tileOverlap: 0, + minLevel: 0, + maxLevel: options.levels.length > 0 ? options.levels.length - 1 : 0 + } ); + + $.TileSource.apply( this, [ options ] ); + + this.levels = options.levels; +}; + +$.extend( $.LegacyTileSource.prototype, $.TileSource.prototype, { + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.supports + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function( data, url ){ + return ( + data.type && + "legacy-image-pyramid" == data.type + ) || ( + data.documentElement && + "legacy-image-pyramid" == data.documentElement.getAttribute('type') + ); + }, + + + /** + * + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.configure + * @param {Object|XMLDocument} configuration - the raw configuration + * @param {String} dataUrl - the url the data was retreived from if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function( configuration, dataUrl ){ + + var options; + + if( !$.isPlainObject(configuration) ){ + + options = configureFromXML( this, configuration ); + + }else{ + + options = configureFromObject( this, configuration ); + } + + return options; + + }, + + /** + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.getLevelScale + * @param {Number} level + */ + getLevelScale: function ( level ) { + var levelScale = NaN; + if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) { + levelScale = + this.levels[ level ].width / + this.levels[ this.maxLevel ].width; + } + return levelScale; + }, + + /** + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.getNumTiles + * @param {Number} level + */ + getNumTiles: function( level ) { + var scale = this.getLevelScale( level ); + if ( scale ){ + return new $.Point( 1, 1 ); + } else { + return new $.Point( 0, 0 ); + } + }, + + /** + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.getTileAtPoint + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function( level, point ) { + return new $.Point( 0, 0 ); + }, + + + /** + * This method is not implemented by this class other than to throw an Error + * announcing you have to implement it. Because of the variety of tile + * server technologies, and various specifications for building image + * pyramids, this method is here to allow easy integration. + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.getTileUrl + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @throws {Error} + */ + getTileUrl: function ( level, x, y ) { + var url = null; + if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) { + url = this.levels[ level ].url; + } + return url; + } +} ); + +/** + * This method removes any files from the Array which dont conform to our + * basic requirements for a 'level' in the LegacyTileSource. + * @private + * @inner + * @function + */ +function filterFiles( files ){ + var filtered = [], + file, + i; + for( i = 0; i < files.length; i++ ){ + file = files[ i ]; + if( file.height && + file.width && + file.url && ( + file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)$/) || ( + file.mimetype && + file.mimetype.toLowerCase().match(/^.*\/(png|jpg|jpeg|gif)$/) + ) + ) ){ + //This is sufficient to serve as a level + filtered.push({ + url: file.url, + width: Number( file.width ), + height: Number( file.height ) + }); + } + else { + $.console.error( 'Unsupported image format: %s', file.url ? file.url : '' ); + } + } + + return filtered.sort(function(a,b){ + return a.height - b.height; + }); + +} + +/** + * @private + * @inner + * @function + */ +function configureFromXML( tileSource, xmlDoc ){ + + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + var root = xmlDoc.documentElement, + rootName = root.tagName, + conf = null, + levels = [], + level, + i; + + if ( rootName == "image" ) { + + try { + conf = { + type: root.getAttribute( "type" ), + levels: [] + }; + + levels = root.getElementsByTagName( "level" ); + for ( i = 0; i < levels.length; i++ ) { + level = levels[ i ]; + + conf.levels .push({ + url: level.getAttribute( "url" ), + width: parseInt( level.getAttribute( "width" ), 10 ), + height: parseInt( level.getAttribute( "height" ), 10 ) + }); + } + + return configureFromObject( tileSource, conf ); + + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( 'Unknown error parsing Legacy Image Pyramid XML.' ); + } + } else if ( rootName == "collection" ) { + throw new Error( 'Legacy Image Pyramid Collections not yet supported.' ); + } else if ( rootName == "error" ) { + throw new Error( 'Error: ' + xmlDoc ); + } + + throw new Error( 'Unknown element ' + rootName ); +} + +/** + * @private + * @inner + * @function + */ +function configureFromObject( tileSource, configuration ){ + + return configuration.levels; + +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - TileSourceCollection + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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 + * @extends OpenSeadragon.TileSource + */ +$.TileSourceCollection = function( tileSize, tileSources, rows, layout ) { + var options; + + if( $.isPlainObject( tileSize ) ){ + options = tileSize; + }else{ + options = { + tileSize: arguments[ 0 ], + tileSources: arguments[ 1 ], + rows: arguments[ 2 ], + layout: arguments[ 3 ] + }; + } + + if( !options.layout ){ + options.layout = 'horizontal'; + } + + var minLevel = 0, + levelSize = 1.0, + tilesPerRow = Math.ceil( options.tileSources.length / options.rows ), + longSide = tilesPerRow >= options.rows ? + tilesPerRow : + options.rows; + + if( 'horizontal' == options.layout ){ + options.width = ( options.tileSize ) * tilesPerRow; + options.height = ( options.tileSize ) * options.rows; + } else { + options.height = ( options.tileSize ) * tilesPerRow; + options.width = ( options.tileSize ) * options.rows; + } + + options.tileOverlap = -options.tileMargin; + options.tilesPerRow = tilesPerRow; + + //Set min level to avoid loading sublevels since collection is a + //different kind of abstraction + + while( levelSize < ( options.tileSize ) * longSide ){ + //$.console.log( '%s levelSize %s minLevel %s', options.tileSize * longSide, levelSize, minLevel ); + levelSize = levelSize * 2.0; + minLevel++; + } + options.minLevel = minLevel; + + //for( var name in options ){ + // $.console.log( 'Collection %s %s', name, options[ name ] ); + //} + + $.TileSource.apply( this, [ options ] ); + +}; + +$.extend( $.TileSourceCollection.prototype, $.TileSource.prototype, { + + /** + * @function + * @name OpenSeadragon.TileSourceCollection.prototype.getTileBounds + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileBounds: function( level, x, y ) { + var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), + px = this.tileSize * x - this.tileOverlap, + py = this.tileSize * y - this.tileOverlap, + sx = this.tileSize + 1 * this.tileOverlap, + sy = this.tileSize + 1 * this.tileOverlap, + scale = 1.0 / dimensionsScaled.x; + + sx = Math.min( sx, dimensionsScaled.x - px ); + sy = Math.min( sy, dimensionsScaled.y - py ); + + return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); + }, + + /** + * + * @function + * @name OpenSeadragon.TileSourceCollection.prototype.configure + */ + configure: function( data, url ){ + return; + }, + + + /** + * @function + * @name OpenSeadragon.TileSourceCollection.prototype.getTileUrl + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ + getTileUrl: function( level, x, y ) { + //$.console.log([ level, '/', x, '_', y ].join( '' )); + return null; + } + + + +}); + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Button + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + +/** + * An enumeration of button states including, REST, GROUP, HOVER, and DOWN + * @static + */ +$.ButtonState = { + REST: 0, + GROUP: 1, + HOVER: 2, + DOWN: 3 +}; + +/** + * Manages events, hover states for individual buttons, tool-tips, as well + * as fading the bottons out when the user has not interacted with them + * for a specified period. + * @class + * @extends OpenSeadragon.EventSource + * @param {Object} options + * @param {String} options.tooltip Provides context help for the button we the + * user hovers over it. + * @param {String} options.srcRest URL of image to use in 'rest' state + * @param {String} options.srcGroup URL of image to use in 'up' state + * @param {String} options.srcHover URL of image to use in 'hover' state + * @param {String} options.srcDown URL of image to use in 'down' state + * @param {Element} [options.element] Element to use as a container for the + * button. + * @property {String} tooltip Provides context help for the button we the + * user hovers over it. + * @property {String} srcRest URL of image to use in 'rest' state + * @property {String} srcGroup URL of image to use in 'up' state + * @property {String} srcHover URL of image to use in 'hover' state + * @property {String} srcDown URL of image to use in 'down' state + * @property {Object} config Configurable settings for this button. DEPRECATED. + * @property {Element} [element] Element to use as a container for the + * button. + * @property {Number} fadeDelay How long to wait before fading + * @property {Number} fadeLength How long should it take to fade the button. + * @property {Number} fadeBeginTime When the button last began to fade. + * @property {Boolean} shouldFade Whether this button should fade after user + * stops interacting with the viewport. + this.fadeDelay = 0; // begin fading immediately + this.fadeLength = 2000; // fade over a period of 2 seconds + this.fadeBeginTime = null; + this.shouldFade = false; + */ +$.Button = function( options ) { + + var _this = this; + + $.EventSource.call( this ); + + $.extend( true, this, { + + tooltip: null, + srcRest: null, + srcGroup: null, + srcHover: null, + srcDown: null, + clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, + clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold, + // begin fading immediately + fadeDelay: 0, + // fade over a period of 2 seconds + fadeLength: 2000, + onPress: null, + onRelease: null, + onClick: null, + onEnter: null, + onExit: null, + onFocus: null, + onBlur: null + + }, options ); + + this.element = options.element || $.makeNeutralElement( "button" ); + this.element.href = this.element.href || '#'; + + //if the user has specified the element to bind the control to explicitly + //then do not add the default control images + if( !options.element ){ + this.imgRest = $.makeTransparentImage( this.srcRest ); + this.imgGroup = $.makeTransparentImage( this.srcGroup ); + this.imgHover = $.makeTransparentImage( this.srcHover ); + this.imgDown = $.makeTransparentImage( this.srcDown ); + + this.element.appendChild( this.imgRest ); + this.element.appendChild( this.imgGroup ); + this.element.appendChild( this.imgHover ); + this.element.appendChild( this.imgDown ); + + this.imgGroup.style.position = + this.imgHover.style.position = + this.imgDown.style.position = + "absolute"; + + this.imgGroup.style.top = + this.imgHover.style.top = + this.imgDown.style.top = + "0px"; + + this.imgGroup.style.left = + this.imgHover.style.left = + this.imgDown.style.left = + "0px"; + + this.imgHover.style.visibility = + this.imgDown.style.visibility = + "hidden"; + + if ( $.Browser.vendor == $.BROWSERS.FIREFOX && $.Browser.version < 3 ){ + this.imgGroup.style.top = + this.imgHover.style.top = + this.imgDown.style.top = + ""; + } + } + + + this.addHandler( "press", this.onPress ); + this.addHandler( "release", this.onRelease ); + this.addHandler( "click", this.onClick ); + this.addHandler( "enter", this.onEnter ); + this.addHandler( "exit", this.onExit ); + this.addHandler( "focus", this.onFocus ); + this.addHandler( "blur", this.onBlur ); + + this.currentState = $.ButtonState.GROUP; + + this.fadeBeginTime = null; + this.shouldFade = false; + + this.element.style.display = "inline-block"; + this.element.style.position = "relative"; + this.element.title = this.tooltip; + + this.tracker = new $.MouseTracker({ + + element: this.element, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + + enterHandler: function( event ) { + if ( event.insideElementPressed ) { + inTo( _this, $.ButtonState.DOWN ); + _this.raiseEvent( "enter", { originalEvent: event.originalEvent } ); + } else if ( !event.buttonDownAny ) { + inTo( _this, $.ButtonState.HOVER ); + } + }, + + focusHandler: function ( event ) { + this.enterHandler( event ); + _this.raiseEvent( "focus", { originalEvent: event.originalEvent } ); + }, + + exitHandler: function( event ) { + outTo( _this, $.ButtonState.GROUP ); + if ( event.insideElementPressed ) { + _this.raiseEvent( "exit", { originalEvent: event.originalEvent } ); + } + }, + + blurHandler: function ( event ) { + this.exitHandler( event ); + _this.raiseEvent( "blur", { originalEvent: event.originalEvent } ); + }, + + pressHandler: function ( event ) { + inTo( _this, $.ButtonState.DOWN ); + _this.raiseEvent( "press", { originalEvent: event.originalEvent } ); + }, + + releaseHandler: function( event ) { + if ( event.insideElementPressed && event.insideElementReleased ) { + outTo( _this, $.ButtonState.HOVER ); + _this.raiseEvent( "release", { originalEvent: event.originalEvent } ); + } else if ( event.insideElementPressed ) { + outTo( _this, $.ButtonState.GROUP ); + } else { + inTo( _this, $.ButtonState.HOVER ); + } + }, + + clickHandler: function( event ) { + if ( event.quick ) { + _this.raiseEvent("click", { originalEvent: event.originalEvent }); + } + }, + + keyHandler: function( event ){ + //console.log( "%s : handling key %s!", _this.tooltip, event.keyCode); + if( 13 === event.keyCode ){ + _this.raiseEvent( "click", { originalEvent: event.originalEvent } ); + _this.raiseEvent( "release", { originalEvent: event.originalEvent } ); + return false; + } + return true; + } + + }).setTracking( true ); + + outTo( this, $.ButtonState.REST ); +}; + +$.extend( $.Button.prototype, $.EventSource.prototype, { + + /** + * TODO: Determine what this function is intended to do and if it's actually + * useful as an API point. + * @function + * @name OpenSeadragon.Button.prototype.notifyGroupEnter + */ + notifyGroupEnter: function() { + inTo( this, $.ButtonState.GROUP ); + }, + + /** + * TODO: Determine what this function is intended to do and if it's actually + * useful as an API point. + * @function + * @name OpenSeadragon.Button.prototype.notifyGroupExit + */ + notifyGroupExit: function() { + outTo( this, $.ButtonState.REST ); + }, + + disable: function(){ + this.notifyGroupExit(); + this.element.disabled = true; + $.setElementOpacity( this.element, 0.2, true ); + }, + + enable: function(){ + this.element.disabled = false; + $.setElementOpacity( this.element, 1.0, true ); + this.notifyGroupEnter(); + } + +}); + + +function scheduleFade( button ) { + $.requestAnimationFrame(function(){ + updateFade( button ); + }); +} + +function updateFade( button ) { + var currentTime, + deltaTime, + opacity; + + if ( button.shouldFade ) { + currentTime = $.now(); + deltaTime = currentTime - button.fadeBeginTime; + opacity = 1.0 - deltaTime / button.fadeLength; + opacity = Math.min( 1.0, opacity ); + opacity = Math.max( 0.0, opacity ); + + if( button.imgGroup ){ + $.setElementOpacity( button.imgGroup, opacity, true ); + } + if ( opacity > 0 ) { + // fade again + scheduleFade( button ); + } + } +} + +function beginFading( button ) { + button.shouldFade = true; + button.fadeBeginTime = $.now() + button.fadeDelay; + window.setTimeout( function(){ + scheduleFade( button ); + }, button.fadeDelay ); +} + +function stopFading( button ) { + button.shouldFade = false; + if( button.imgGroup ){ + $.setElementOpacity( button.imgGroup, 1.0, true ); + } +} + +function inTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + + if ( newState >= $.ButtonState.GROUP && + button.currentState == $.ButtonState.REST ) { + stopFading( button ); + button.currentState = $.ButtonState.GROUP; + } + + if ( newState >= $.ButtonState.HOVER && + button.currentState == $.ButtonState.GROUP ) { + if( button.imgHover ){ + button.imgHover.style.visibility = ""; + } + button.currentState = $.ButtonState.HOVER; + } + + if ( newState >= $.ButtonState.DOWN && + button.currentState == $.ButtonState.HOVER ) { + if( button.imgDown ){ + button.imgDown.style.visibility = ""; + } + button.currentState = $.ButtonState.DOWN; + } +} + + +function outTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + + if ( newState <= $.ButtonState.HOVER && + button.currentState == $.ButtonState.DOWN ) { + if( button.imgDown ){ + button.imgDown.style.visibility = "hidden"; + } + button.currentState = $.ButtonState.HOVER; + } + + if ( newState <= $.ButtonState.GROUP && + button.currentState == $.ButtonState.HOVER ) { + if( button.imgHover ){ + button.imgHover.style.visibility = "hidden"; + } + button.currentState = $.ButtonState.GROUP; + } + + if ( newState <= $.ButtonState.REST && + button.currentState == $.ButtonState.GROUP ) { + beginFading( button ); + button.currentState = $.ButtonState.REST; + } +} + + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - ButtonGroup + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ +/** + * Manages events on groups of buttons. + * @class + * @param {Object} options - a dictionary of settings applied against the entire + * group of buttons + * @param {Array} options.buttons Array of buttons + * @param {Element} [options.group] Element to use as the container, + * @param {Object} options.config Object with Viewer settings ( TODO: is + * this actually used anywhere? ) + * @param {Function} [options.enter] Function callback for when the mouse + * enters group + * @param {Function} [options.exit] Function callback for when mouse leaves + * the group + * @param {Function} [options.release] Function callback for when mouse is + * released + * @property {Array} buttons - An array containing the buttons themselves. + * @property {Element} element - The shared container for the buttons. + * @property {Object} config - Configurable settings for the group of buttons. + * @property {OpenSeadragon.MouseTracker} tracker - Tracks mouse events accross + * the group of buttons. + **/ +$.ButtonGroup = function( options ) { + + $.extend( true, this, { + buttons: [], + clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, + clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold, + labelText: "" + }, options ); + + // copy the botton elements + var buttons = this.buttons.concat([]), + _this = this, + i; + + this.element = options.element || $.makeNeutralElement( "fieldgroup" ); + + if( !options.group ){ + this.label = $.makeNeutralElement( "label" ); + //TODO: support labels for ButtonGroups + //this.label.innerHTML = this.labelText; + this.element.style.display = "inline-block"; + this.element.appendChild( this.label ); + for ( i = 0; i < buttons.length; i++ ) { + this.element.appendChild( buttons[ i ].element ); + } + } + + this.tracker = new $.MouseTracker({ + element: this.element, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + enterHandler: function ( event ) { + var i; + for ( i = 0; i < _this.buttons.length; i++ ) { + _this.buttons[ i ].notifyGroupEnter(); + } + }, + exitHandler: function ( event ) { + var i; + if ( !event.insideElementPressed ) { + for ( i = 0; i < _this.buttons.length; i++ ) { + _this.buttons[ i ].notifyGroupExit(); + } + } + }, + releaseHandler: function ( event ) { + var i; + if ( !event.insideElementReleased ) { + for ( i = 0; i < _this.buttons.length; i++ ) { + _this.buttons[ i ].notifyGroupExit(); + } + } + } + }).setTracking( true ); +}; + +$.ButtonGroup.prototype = { + + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @name OpenSeadragon.ButtonGroup.prototype.emulateEnter + */ + emulateEnter: function() { + this.tracker.enterHandler( { eventSource: this.tracker } ); + }, + + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @name OpenSeadragon.ButtonGroup.prototype.emulateExit + */ + emulateExit: function() { + this.tracker.exitHandler( { eventSource: this.tracker } ); + } +}; + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Rect + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + +/** + * A Rectangle really represents a 2x2 matrix where each row represents a + * 2 dimensional vector component, the first is (x,y) and the second is + * (width, height). The latter component implies the equation of a simple + * plane. + * + * @class + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {Number} width The vector component 'height'. + * @param {Number} height The vector component 'width'. + * @property {Number} x The vector component 'x'. + * @property {Number} y The vector component 'y'. + * @property {Number} width The vector component 'width'. + * @property {Number} height The vector component 'height'. + */ +$.Rect = function( x, y, width, height ) { + this.x = typeof ( x ) == "number" ? x : 0; + this.y = typeof ( y ) == "number" ? y : 0; + this.width = typeof ( width ) == "number" ? width : 0; + this.height = typeof ( height ) == "number" ? height : 0; +}; + +$.Rect.prototype = { + + /** + * The aspect ratio is simply the ratio of width to height. + * @function + * @returns {Number} The ratio of width to height. + */ + getAspectRatio: function() { + return this.width / this.height; + }, + + /** + * Provides the coordinates of the upper-left corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of + * the rectangle. + */ + getTopLeft: function() { + return new $.Point( + this.x, + this.y + ); + }, + + /** + * Provides the coordinates of the bottom-right corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of + * the rectangle. + */ + getBottomRight: function() { + return new $.Point( + this.x + this.width, + this.y + this.height + ); + }, + + /** + * Provides the coordinates of the top-right corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the top-right corner of + * the rectangle. + */ + getTopRight: function() { + return new $.Point( + this.x + this.width, + this.y + ); + }, + + /** + * Provides the coordinates of the bottom-left corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the bottom-left corner of + * the rectangle. + */ + getBottomLeft: function() { + return new $.Point( + this.x, + this.y + this.height + ); + }, + + /** + * Computes the center of the rectangle. + * @function + * @returns {OpenSeadragon.Point} The center of the rectangle as represented + * as represented by a 2-dimensional vector (x,y) + */ + getCenter: function() { + return new $.Point( + this.x + this.width / 2.0, + this.y + this.height / 2.0 + ); + }, + + /** + * Returns the width and height component as a vector OpenSeadragon.Point + * @function + * @returns {OpenSeadragon.Point} The 2 dimensional vector representing the + * the width and height of the rectangle. + */ + getSize: function() { + return new $.Point( this.width, this.height ); + }, + + /** + * Determines if two Rectangles have equivalent components. + * @function + * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to. + * @return {Boolean} 'true' if all components are equal, otherwise 'false'. + */ + equals: function( other ) { + return ( other instanceof $.Rect ) && + ( this.x === other.x ) && + ( this.y === other.y ) && + ( this.width === other.width ) && + ( this.height === other.height ); + }, + + /** + * Rotates a rectangle around a point. Currently only 90, 180, and 270 + * degrees are supported. + * @function + * @param {Number} degrees The angle in degrees to rotate. + * @param {OpenSeadragon.Point} pivot The point about which to rotate. + * Defaults to the center of the rectangle. + * @return {OpenSeadragon.Rect} + */ + rotate: function( degrees, pivot ) { + // TODO support arbitrary rotation + var width = this.width, + height = this.height, + newTopLeft; + + degrees = ( degrees + 360 ) % 360; + if( degrees % 90 !== 0 ) { + throw new Error('Currently only 0, 90, 180, and 270 degrees are supported.'); + } + + if( degrees === 0 ){ + return new $.Rect( + this.x, + this.y, + this.width, + this.height + ); + } + + pivot = pivot || this.getCenter(); + + switch ( degrees ) { + case 90: + newTopLeft = this.getBottomLeft(); + width = this.height; + height = this.width; + break; + case 180: + newTopLeft = this.getBottomRight(); + break; + case 270: + newTopLeft = this.getTopRight(); + width = this.height; + height = this.width; + break; + default: + newTopLeft = this.getTopLeft(); + break; + } + + newTopLeft = newTopLeft.rotate(degrees, pivot); + + return new $.Rect(newTopLeft.x, newTopLeft.y, width, height); + }, + + /** + * Provides a string representation of the rectangle which is useful for + * debugging. + * @function + * @returns {String} A string representation of the rectangle. + */ + toString: function() { + return "[" + + Math.round(this.x*100) + "," + + Math.round(this.y*100) + "," + + Math.round(this.width*100) + "x" + + Math.round(this.height*100) + + "]"; + } +}; + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - ReferenceStrip + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function ( $ ) { + +// dictionary from id to private properties +var THIS = {}; + +/** + * The CollectionDrawer is a reimplementation if the Drawer API that + * focuses on allowing a viewport to be redefined as a collection + * of smaller viewports, defined by a clear number of rows and / or + * columns of which each item in the matrix of viewports has its own + * source. + * + * This idea is a reexpression of the idea of dzi collections + * which allows a clearer algorithm to reuse the tile sources already + * supported by OpenSeadragon, in heterogenious or homogenious + * sequences just like mixed groups already supported by the viewer + * for the purpose of image sequnces. + * + * TODO: The difficult part of this feature is figuring out how to express + * this functionality as a combination of the functionality already + * provided by Drawer, Viewport, TileSource, and Navigator. It may + * require better abstraction at those points in order to effeciently + * reuse those paradigms. + */ +$.ReferenceStrip = function ( options ) { + + var _this = this, + viewer = options.viewer, + viewerSize = $.getElementSize( viewer.element ), + element, + style, + i; + + //We may need to create a new element and id if they did not + //provide the id for the existing element + if ( !options.id ) { + options.id = 'referencestrip-' + $.now(); + this.element = $.makeNeutralElement( "div" ); + this.element.id = options.id; + this.element.className = 'referencestrip'; + } + + options = $.extend( true, { + sizeRatio: $.DEFAULT_SETTINGS.referenceStripSizeRatio, + position: $.DEFAULT_SETTINGS.referenceStripPosition, + scroll: $.DEFAULT_SETTINGS.referenceStripScroll, + clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold + }, options, { + //required overrides + element: this.element, + //These need to be overridden to prevent recursion since + //the navigator is a viewer and a viewer has a navigator + showNavigator: false, + mouseNavEnabled: false, + showNavigationControl: false, + showSequenceControl: false + } ); + + $.extend( this, options ); + //Private state properties + THIS[this.id] = { + "animating": false + }; + + this.minPixelRatio = this.viewer.minPixelRatio; + + style = this.element.style; + style.marginTop = '0px'; + style.marginRight = '0px'; + style.marginBottom = '0px'; + style.marginLeft = '0px'; + style.left = '0px'; + style.bottom = '0px'; + style.border = '0px'; + style.background = '#000'; + style.position = 'relative'; + + $.setElementOpacity( this.element, 0.8 ); + + this.viewer = viewer; + this.innerTracker = new $.MouseTracker( { + element: this.element, + dragHandler: $.delegate( this, onStripDrag ), + scrollHandler: $.delegate( this, onStripScroll ), + enterHandler: $.delegate( this, onStripEnter ), + exitHandler: $.delegate( this, onStripExit ), + keyHandler: $.delegate( this, onKeyPress ) + } ).setTracking( true ); + + //Controls the position and orientation of the reference strip and sets the + //appropriate width and height + if ( options.width && options.height ) { + this.element.style.width = options.width + 'px'; + this.element.style.height = options.height + 'px'; + viewer.addControl( + this.element, + { anchor: $.ControlAnchor.BOTTOM_LEFT } + ); + } else { + if ( "horizontal" == options.scroll ) { + this.element.style.width = ( + viewerSize.x * + options.sizeRatio * + viewer.tileSources.length + ) + ( 12 * viewer.tileSources.length ) + 'px'; + + this.element.style.height = ( + viewerSize.y * + options.sizeRatio + ) + 'px'; + + viewer.addControl( + this.element, + { anchor: $.ControlAnchor.BOTTOM_LEFT } + ); + } else { + this.element.style.height = ( + viewerSize.y * + options.sizeRatio * + viewer.tileSources.length + ) + ( 12 * viewer.tileSources.length ) + 'px'; + + this.element.style.width = ( + viewerSize.x * + options.sizeRatio + ) + 'px'; + + viewer.addControl( + this.element, + { anchor: $.ControlAnchor.TOP_LEFT } + ); + + } + } + + this.panelWidth = ( viewerSize.x * this.sizeRatio ) + 8; + this.panelHeight = ( viewerSize.y * this.sizeRatio ) + 8; + this.panels = []; + + /*jshint loopfunc:true*/ + for ( i = 0; i < viewer.tileSources.length; i++ ) { + + element = $.makeNeutralElement( 'div' ); + element.id = this.element.id + "-" + i; + + element.style.width = _this.panelWidth + 'px'; + element.style.height = _this.panelHeight + 'px'; + element.style.display = 'inline'; + element.style.float = 'left'; //Webkit + element.style.cssFloat = 'left'; //Firefox + element.style.styleFloat = 'left'; //IE + element.style.padding = '2px'; + + element.innerTracker = new $.MouseTracker( { + element: element, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + pressHandler: function ( event ) { + event.eventSource.dragging = $.now(); + }, + releaseHandler: function ( event ) { + var tracker = event.eventSource, + id = tracker.element.id, + page = Number( id.split( '-' )[2] ), + now = $.now(); + + if ( event.insideElementPressed && + event.insideElementReleased && + tracker.dragging && + ( now - tracker.dragging ) < tracker.clickTimeThreshold ) { + tracker.dragging = null; + viewer.goToPage( page ); + } + } + } ).setTracking( true ); + + this.element.appendChild( element ); + + element.activePanel = false; + + this.panels.push( element ); + + } + loadPanels( this, this.scroll == 'vertical' ? viewerSize.y : viewerSize.y, 0 ); + this.setFocus( 0 ); + +}; + +$.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototype, { + + setFocus: function ( page ) { + var element = $.getElement( this.element.id + '-' + page ), + viewerSize = $.getElementSize( this.viewer.canvas ), + scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ), + scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ), + offsetLeft = -Number( this.element.style.marginLeft.replace( 'px', '' ) ), + offsetTop = -Number( this.element.style.marginTop.replace( 'px', '' ) ), + offset; + + if ( this.currentSelected !== element ) { + if ( this.currentSelected ) { + this.currentSelected.style.background = '#000'; + } + this.currentSelected = element; + this.currentSelected.style.background = '#999'; + + if ( 'horizontal' == this.scroll ) { + //right left + offset = ( Number( page ) ) * ( this.panelWidth + 3 ); + if ( offset > offsetLeft + viewerSize.x - this.panelWidth ) { + offset = Math.min( offset, ( scrollWidth - viewerSize.x ) ); + this.element.style.marginLeft = -offset + 'px'; + loadPanels( this, viewerSize.x, -offset ); + } else if ( offset < offsetLeft ) { + offset = Math.max( 0, offset - viewerSize.x / 2 ); + this.element.style.marginLeft = -offset + 'px'; + loadPanels( this, viewerSize.x, -offset ); + } + } else { + offset = ( Number( page ) ) * ( this.panelHeight + 3 ); + if ( offset > offsetTop + viewerSize.y - this.panelHeight ) { + offset = Math.min( offset, ( scrollHeight - viewerSize.y ) ); + this.element.style.marginTop = -offset + 'px'; + loadPanels( this, viewerSize.y, -offset ); + } else if ( offset < offsetTop ) { + offset = Math.max( 0, offset - viewerSize.y / 2 ); + this.element.style.marginTop = -offset + 'px'; + loadPanels( this, viewerSize.y, -offset ); + } + } + + this.currentPage = page; + $.getElement( element.id + '-displayregion' ).focus(); + onStripEnter.call( this, { eventSource: this.innerTracker } ); + } + }, + /** + * @function + * @name OpenSeadragon.ReferenceStrip.prototype.update + */ + update: function () { + if ( THIS[this.id].animating ) { + $.console.log( 'image reference strip update' ); + return true; + } + return false; + } + +} ); + + + + +/** + * @private + * @inner + * @function + */ +function onStripDrag( event ) { + + var offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ), + offsetTop = Number( this.element.style.marginTop.replace( 'px', '' ) ), + scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ), + scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ), + viewerSize = $.getElementSize( this.viewer.canvas ); + this.dragging = true; + if ( this.element ) { + if ( 'horizontal' == this.scroll ) { + if ( -event.delta.x > 0 ) { + //forward + if ( offsetLeft > -( scrollWidth - viewerSize.x ) ) { + this.element.style.marginLeft = ( offsetLeft + ( event.delta.x * 2 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft + ( event.delta.x * 2 ) ); + } + } else if ( -event.delta.x < 0 ) { + //reverse + if ( offsetLeft < 0 ) { + this.element.style.marginLeft = ( offsetLeft + ( event.delta.x * 2 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft + ( event.delta.x * 2 ) ); + } + } + } else { + if ( -event.delta.y > 0 ) { + //forward + if ( offsetTop > -( scrollHeight - viewerSize.y ) ) { + this.element.style.marginTop = ( offsetTop + ( event.delta.y * 2 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.delta.y * 2 ) ); + } + } else if ( -event.delta.y < 0 ) { + //reverse + if ( offsetTop < 0 ) { + this.element.style.marginTop = ( offsetTop + ( event.delta.y * 2 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.delta.y * 2 ) ); + } + } + } + } + return false; + +} + + + +/** + * @private + * @inner + * @function + */ +function onStripScroll( event ) { + var offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ), + offsetTop = Number( this.element.style.marginTop.replace( 'px', '' ) ), + scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ), + scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ), + viewerSize = $.getElementSize( this.viewer.canvas ); + if ( this.element ) { + if ( 'horizontal' == this.scroll ) { + if ( event.scroll > 0 ) { + //forward + if ( offsetLeft > -( scrollWidth - viewerSize.x ) ) { + this.element.style.marginLeft = ( offsetLeft - ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft - ( event.scroll * 60 ) ); + } + } else if ( event.scroll < 0 ) { + //reverse + if ( offsetLeft < 0 ) { + this.element.style.marginLeft = ( offsetLeft - ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.x, offsetLeft - ( event.scroll * 60 ) ); + } + } + } else { + if ( event.scroll < 0 ) { + //scroll up + if ( offsetTop > viewerSize.y - scrollHeight ) { + this.element.style.marginTop = ( offsetTop + ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.scroll * 60 ) ); + } + } else if ( event.scroll > 0 ) { + //scroll dowm + if ( offsetTop < 0 ) { + this.element.style.marginTop = ( offsetTop + ( event.scroll * 60 ) ) + 'px'; + loadPanels( this, viewerSize.y, offsetTop + ( event.scroll * 60 ) ); + } + } + } + } + //cancels event + return false; +} + + +function loadPanels( strip, viewerSize, scroll ) { + var panelSize, + activePanelsStart, + activePanelsEnd, + miniViewer, + style, + i, + element; + if ( 'horizontal' == strip.scroll ) { + panelSize = strip.panelWidth; + } else { + panelSize = strip.panelHeight; + } + activePanelsStart = Math.ceil( viewerSize / panelSize ) + 5; + activePanelsEnd = Math.ceil( ( Math.abs( scroll ) + viewerSize ) / panelSize ) + 1; + activePanelsStart = activePanelsEnd - activePanelsStart; + activePanelsStart = activePanelsStart < 0 ? 0 : activePanelsStart; + + for ( i = activePanelsStart; i < activePanelsEnd && i < strip.panels.length; i++ ) { + element = strip.panels[i]; + if ( !element.activePanel ) { + miniViewer = new $.Viewer( { + id: element.id, + tileSources: [strip.viewer.tileSources[i]], + element: element, + navigatorSizeRatio: strip.sizeRatio, + showNavigator: false, + mouseNavEnabled: false, + showNavigationControl: false, + showSequenceControl: false, + immediateRender: true, + blendTime: 0, + animationTime: 0 + } ); + + miniViewer.displayRegion = $.makeNeutralElement( "textarea" ); + miniViewer.displayRegion.id = element.id + '-displayregion'; + miniViewer.displayRegion.className = 'displayregion'; + + style = miniViewer.displayRegion.style; + style.position = 'relative'; + style.top = '0px'; + style.left = '0px'; + style.fontSize = '0px'; + style.overflow = 'hidden'; + style.float = 'left'; //Webkit + style.cssFloat = 'left'; //Firefox + style.styleFloat = 'left'; //IE + style.zIndex = 999999999; + style.cursor = 'default'; + style.width = ( strip.panelWidth - 4 ) + 'px'; + style.height = ( strip.panelHeight - 4 ) + 'px'; + + miniViewer.displayRegion.innerTracker = new $.MouseTracker( { + element: miniViewer.displayRegion + } ); + + element.getElementsByTagName( 'form' )[0].appendChild( + miniViewer.displayRegion + ); + + element.activePanel = true; + } + } +} + + +/** + * @private + * @inner + * @function + */ +function onStripEnter( event ) { + var element = event.eventSource.element; + + //$.setElementOpacity(element, 0.8); + + //element.style.border = '1px solid #555'; + //element.style.background = '#000'; + + if ( 'horizontal' == this.scroll ) { + + //element.style.paddingTop = "0px"; + element.style.marginBottom = "0px"; + + } else { + + //element.style.paddingRight = "0px"; + element.style.marginLeft = "0px"; + + } + return false; +} + + +/** + * @private + * @inner + * @function + */ +function onStripExit( event ) { + var element = event.eventSource.element; + + if ( 'horizontal' == this.scroll ) { + + //element.style.paddingTop = "10px"; + element.style.marginBottom = "-" + ( $.getElementSize( element ).y / 2 ) + "px"; + + } else { + + //element.style.paddingRight = "10px"; + element.style.marginLeft = "-" + ( $.getElementSize( element ).x / 2 ) + "px"; + + } + return false; +} + + + +/** + * @private + * @inner + * @function + */ +function onKeyPress( event ) { + //console.log( event.keyCode ); + + switch ( event.keyCode ) { + case 61: //=|+ + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + case 45: //-|_ + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 48: //0|) + case 119: //w + case 87: //W + case 38: //up arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + case 115: //s + case 83: //S + case 40: //down arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 97: //a + case 37: //left arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 100: //d + case 39: //right arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + return true; + } +} + + + +} ( OpenSeadragon ) ); + +/* + * OpenSeadragon - DisplayRect + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + +/** + * A display rectanlge is very similar to the OpenSeadragon.Rect but adds two + * fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels + * for this rectangle. + * @class + * @extends OpenSeadragon.Rect + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {Number} width The vector component 'height'. + * @param {Number} height The vector component 'width'. + * @param {Number} minLevel The lowest zoom level supported. + * @param {Number} maxLevel The highest zoom level supported. + * @property {Number} minLevel The lowest zoom level supported. + * @property {Number} maxLevel The highest zoom level supported. + */ +$.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) { + $.Rect.apply( this, [ x, y, width, height ] ); + + this.minLevel = minLevel; + this.maxLevel = maxLevel; +}; + +$.extend( $.DisplayRect.prototype, $.Rect.prototype ); + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Spring + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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 + * @param {Object} options - Spring configuration settings. + * @param {Number} options.initial - Initial value of spring, default to 0 so + * spring is not in motion initally by default. + * @param {Number} options.springStiffness - Spring stiffness. + * @param {Number} options.animationTime - Animation duration per spring. + * + * @property {Number} initial - Initial value of spring, default to 0 so + * spring is not in motion initally by default. + * @property {Number} springStiffness - Spring stiffness. + * @property {Number} animationTime - Animation duration per spring. + * @property {Object} current + * @property {Number} start + * @property {Number} target + */ +$.Spring = function( options ) { + var args = arguments; + + if( typeof( options ) != 'object' ){ + //allows backward compatible use of ( initialValue, config ) as + //constructor parameters + options = { + initial: args.length && typeof ( args[ 0 ] ) == "number" ? + args[ 0 ] : + 0, + springStiffness: args.length > 1 ? + args[ 1 ].springStiffness : + 5.0, + animationTime: args.length > 1 ? + args[ 1 ].animationTime : + 1.5 + }; + } + + $.extend( true, this, options); + + + this.current = { + value: typeof ( this.initial ) == "number" ? + this.initial : + 0, + time: $.now() // always work in milliseconds + }; + + this.start = { + value: this.current.value, + time: this.current.time + }; + + this.target = { + value: this.current.value, + time: this.current.time + }; +}; + +$.Spring.prototype = { + + /** + * @function + * @param {Number} target + */ + resetTo: function( target ) { + this.target.value = target; + this.target.time = this.current.time; + this.start.value = this.target.value; + this.start.time = this.target.time; + }, + + /** + * @function + * @param {Number} target + */ + springTo: function( target ) { + this.start.value = this.current.value; + this.start.time = this.current.time; + this.target.value = target; + this.target.time = this.start.time + 1000 * this.animationTime; + }, + + /** + * @function + * @param {Number} delta + */ + shiftBy: function( delta ) { + this.start.value += delta; + this.target.value += delta; + }, + + /** + * @function + */ + update: function() { + this.current.time = $.now(); + this.current.value = (this.current.time >= this.target.time) ? + this.target.value : + this.start.value + + ( this.target.value - this.start.value ) * + transform( + this.springStiffness, + ( this.current.time - this.start.time ) / + ( this.target.time - this.start.time ) + ); + } +}; + +/** + * @private + */ +function transform( stiffness, x ) { + return ( 1.0 - Math.exp( stiffness * -x ) ) / + ( 1.0 - Math.exp( -stiffness ) ); +} + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Tile + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + var TILE_CACHE = {}; +/** + * @class + * @param {Number} level The zoom level this tile belongs to. + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {OpenSeadragon.Point} bounds Where this tile fits, in normalized + * coordinates. + * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has + * this tile failed to load? ) + * @param {String} url The URL of this tile's image. + * + * @property {Number} level The zoom level this tile belongs to. + * @property {Number} x The vector component 'x'. + * @property {Number} y The vector component 'y'. + * @property {OpenSeadragon.Point} bounds Where this tile fits, in normalized + * coordinates + * @property {Boolean} exists Is this tile a part of a sparse image? ( Also has + * this tile failed to load? + * @property {String} url The URL of this tile's image. + * @property {Boolean} loaded Is this tile loaded? + * @property {Boolean} loading Is this tile loading? + * @property {Element} element The HTML div element for this tile + * @property {Element} imgElement The HTML img element for this tile + * @property {Image} image The Image object for this tile + * @property {String} style The alias of this.element.style. + * @property {String} position This tile's position on screen, in pixels. + * @property {String} size This tile's size on screen, in pixels + * @property {String} blendStart The start time of this tile's blending + * @property {String} opacity The current opacity this tile should be. + * @property {String} distance The distance of this tile to the viewport center + * @property {String} visibility The visibility score of this tile. + * @property {Boolean} beingDrawn Whether this tile is currently being drawn + * @property {Number} lastTouchTime Timestamp the tile was last touched. + */ +$.Tile = function(level, x, y, bounds, exists, url) { + this.level = level; + this.x = x; + this.y = y; + this.bounds = bounds; + this.exists = exists; + this.url = url; + this.loaded = false; + this.loading = false; + + this.element = null; + this.imgElement = null; + this.image = null; + + this.style = null; + this.position = null; + this.size = null; + this.blendStart = null; + this.opacity = null; + this.distance = null; + this.visibility = null; + + this.beingDrawn = false; + this.lastTouchTime = 0; +}; + +$.Tile.prototype = { + + /** + * Provides a string representation of this tiles level and (x,y) + * components. + * @function + * @returns {String} + */ + toString: function() { + return this.level + "/" + this.x + "_" + this.y; + }, + + /** + * Renders the tile in an html container. + * @function + * @param {Element} container + */ + drawHTML: function( container ) { + if ( !this.loaded || !this.image ) { + $.console.warn( + "Attempting to draw tile %s when it's not yet loaded.", + this.toString() + ); + return; + } + + //EXPERIMENTAL - trying to figure out how to scale the container + // content during animation of the container size. + + if ( !this.element ) { + this.element = $.makeNeutralElement( "div" ); + this.imgElement = $.makeNeutralElement( "img" ); + this.imgElement.src = this.url; + this.imgElement.style.msInterpolationMode = "nearest-neighbor"; + this.imgElement.style.width = "100%"; + this.imgElement.style.height = "100%"; + + this.style = this.element.style; + this.style.position = "absolute"; + } + if ( this.element.parentNode != container ) { + container.appendChild( this.element ); + } + if ( this.imgElement.parentNode != this.element ) { + this.element.appendChild( this.imgElement ); + } + + this.style.top = this.position.y + "px"; + this.style.left = this.position.x + "px"; + this.style.height = this.size.y + "px"; + this.style.width = this.size.x + "px"; + + $.setElementOpacity( this.element, this.opacity ); + }, + + /** + * Renders the tile in a canvas-based context. + * @function + * @param {Canvas} context + */ + drawCanvas: function( context ) { + + var position = this.position, + size = this.size, + rendered, + canvas; + + if ( !this.loaded || !( this.image || TILE_CACHE[ this.url ] ) ){ + $.console.warn( + "Attempting to draw tile %s when it's not yet loaded.", + this.toString() + ); + return; + } + context.globalAlpha = this.opacity; + + //context.save(); + + //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.url.match('.png') ){ + //clearing only the inside of the rectangle occupied + //by the png prevents edge flikering + context.clearRect( + position.x+1, + position.y+1, + size.x-2, + size.y-2 + ); + + } + + if( !TILE_CACHE[ this.url ] ){ + canvas = document.createElement( 'canvas' ); + canvas.width = this.image.width; + canvas.height = this.image.height; + rendered = canvas.getContext('2d'); + rendered.drawImage( this.image, 0, 0 ); + TILE_CACHE[ this.url ] = rendered; + //since we are caching the prerendered image on a canvas + //allow the image to not be held in memory + this.image = null; + } + + rendered = TILE_CACHE[ this.url ]; + + //rendered.save(); + context.drawImage( + rendered.canvas, + 0, + 0, + rendered.canvas.width, + rendered.canvas.height, + position.x, + position.y, + size.x, + size.y + ); + //rendered.restore(); + + //context.restore(); + }, + + /** + * Removes tile from it's contianer. + * @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 ); + } + if ( TILE_CACHE[ this.url ]){ + delete TILE_CACHE[ this.url ]; + } + + this.element = null; + this.imgElement = null; + this.image = null; + this.loaded = false; + this.loading = false; + } +}; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Overlay + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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 including CENTER, TOP_LEFT (default), TOP, TOP_RIGHT, + * RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT, and LEFT. + * @static + */ + $.OverlayPlacement = { + CENTER: 0, + TOP_LEFT: 1, + TOP: 2, + TOP_RIGHT: 3, + RIGHT: 4, + BOTTOM_RIGHT: 5, + BOTTOM: 6, + BOTTOM_LEFT: 7, + LEFT: 8 + }; + + /** + * An Overlay provides a + * @class + */ + $.Overlay = function( element, location, placement ) { + var options; + if( $.isPlainObject( element ) ){ + options = element; + } else{ + options = { + element: element, + location: location, + placement: placement + }; + } + + this.element = options.element; + this.scales = options.location instanceof $.Rect; + this.bounds = new $.Rect( + options.location.x, + options.location.y, + options.location.width, + options.location.height + ); + this.position = new $.Point( + options.location.x, + options.location.y + ); + this.size = new $.Point( + options.location.width, + options.location.height + ); + this.style = options.element.style; + // rects are always top-left + this.placement = options.location instanceof $.Point ? + options.placement : + $.OverlayPlacement.TOP_LEFT; + this.onDraw = options.onDraw; + }; + + $.Overlay.prototype = { + + /** + * @function + * @param {OpenSeadragon.OverlayPlacement} position + * @param {OpenSeadragon.Point} size + */ + adjust: function( position, size ) { + switch ( this.placement ) { + case $.OverlayPlacement.TOP_LEFT: + break; + case $.OverlayPlacement.TOP: + position.x -= size.x / 2; + break; + case $.OverlayPlacement.TOP_RIGHT: + position.x -= size.x; + break; + case $.OverlayPlacement.RIGHT: + position.x -= size.x; + position.y -= size.y / 2; + break; + case $.OverlayPlacement.BOTTOM_RIGHT: + position.x -= size.x; + position.y -= size.y; + break; + case $.OverlayPlacement.BOTTOM: + position.x -= size.x / 2; + position.y -= size.y; + break; + case $.OverlayPlacement.BOTTOM_LEFT: + position.y -= size.y; + break; + case $.OverlayPlacement.LEFT: + position.y -= size.y / 2; + break; + default: + case $.OverlayPlacement.CENTER: + position.x -= size.x / 2; + position.y -= size.y / 2; + break; + } + }, + + /** + * @function + */ + destroy: function() { + var element = this.element, + 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.scales ) { + style.width = ""; + style.height = ""; + } + }, + + /** + * @function + * @param {Element} container + */ + drawHTML: function( container, viewport ) { + var element = this.element, + style = this.style, + scales = this.scales, + drawerCenter = new $.Point( + viewport.viewer.drawer.canvas.width / 2, + viewport.viewer.drawer.canvas.height / 2 + ), + degrees = viewport.degrees, + position, + size, + overlayCenter; + + 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 ); + } + + if ( !scales ) { + this.size = $.getElementSize( element ); + } + + position = this.position; + size = this.size; + + this.adjust( position, size ); + + position = position.apply( Math.floor ); + size = size.apply( Math.ceil ); + + // rotate the position of the overlay + // TODO only rotate overlays if in canvas mode + // TODO replace the size rotation with CSS3 transforms + // TODO add an option to overlays to not rotate with the image + // Currently only rotates position and size + if( degrees !== 0 && this.scales ) { + overlayCenter = new $.Point( size.x / 2, size.y / 2 ); + + position = position.plus( overlayCenter ).rotate( + degrees, + drawerCenter + ).minus( overlayCenter ); + + size = size.rotate( degrees, new $.Point( 0, 0 ) ); + size = new $.Point( Math.abs( size.x ), Math.abs( size.y ) ); + } + + // call the onDraw callback if there is one to allow, this allows someone to overwrite + // the drawing/positioning/sizing of the overlay + if (this.onDraw) { + this.onDraw(position, size, element); + } else { + style.left = position.x + "px"; + style.top = position.y + "px"; + style.position = "absolute"; + style.display = 'block'; + + if ( scales ) { + style.width = size.x + "px"; + style.height = size.y + "px"; + } + } + }, + + /** + * @function + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location + * @param {OpenSeadragon.OverlayPlacement} position + */ + update: function( location, placement ) { + this.scales = location instanceof $.Rect; + this.bounds = new $.Rect( + location.x, + location.y, + location.width, + location.height + ); + // rects are always top-left + this.placement = location instanceof $.Point ? + placement : + $.OverlayPlacement.TOP_LEFT; + } + + }; + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Drawer + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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( $ ){ + +var DEVICE_SCREEN = $.getWindowSize(), + BROWSER = $.Browser.vendor, + BROWSER_VERSION = $.Browser.version, + + SUBPIXEL_RENDERING = ( + ( BROWSER == $.BROWSERS.FIREFOX ) || + ( BROWSER == $.BROWSERS.OPERA ) || + ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) || + ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) || + ( BROWSER == $.BROWSERS.IE && BROWSER_VERSION >= 9 ) + ), + + USE_CANVAS = SUBPIXEL_RENDERING && + !( DEVICE_SCREEN.x <= 400 || DEVICE_SCREEN.y <= 400 ) && + !( navigator.appVersion.match( 'Mobile' ) ) && + $.isFunction( document.createElement( "canvas" ).getContext ); + +//console.error( 'USE_CANVAS ' + USE_CANVAS ); + +/** + * @class + * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source. + * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. + * @param {Element} element - Reference to Viewer 'canvas'. + * @property {OpenSeadragon.TileSource} source - Reference to Viewer tile source. + * @property {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. + * @property {Element} container - Reference to Viewer 'canvas'. + * @property {Element|Canvas} canvas - TODO + * @property {CanvasContext} context - TODO + * @property {Object} config - Reference to Viewer config. + * @property {Number} downloading - How many images are currently being loaded in parallel. + * @property {Number} normHeight - Ratio of zoomable image height to width. + * @property {Object} tilesMatrix - A '3d' dictionary [level][x][y] --> Tile. + * @property {Array} tilesLoaded - An unordered list of Tiles with loaded images. + * @property {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @property {Array} overlays - An unordered list of Overlays added. + * @property {Array} lastDrawn - An unordered list of Tiles drawn last frame. + * @property {Number} lastResetTime - Last time for which the drawer was reset. + * @property {Boolean} midUpdate - Is the drawer currently updating the viewport? + * @property {Boolean} updateAgain - Does the drawer need to update the viewort again? + * @property {Element} element - DEPRECATED Alias for container. + */ +$.Drawer = function( options ) { + + //backward compatibility for positional args while prefering more + //idiomatic javascript options object as the only argument + var args = arguments, + i; + + if( !$.isPlainObject( options ) ){ + options = { + source: args[ 0 ], + viewport: args[ 1 ], + element: args[ 2 ] + }; + } + + $.extend( true, this, { + + //internal state properties + viewer: null, + downloading: 0, + tilesMatrix: {}, + tilesLoaded: [], + coverage: {}, + lastDrawn: [], + lastResetTime: 0, + midUpdate: false, + updateAgain: true, + + + //internal state / configurable settings + overlays: [], + collectionOverlays: {}, + + //configurable settings + maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount, + imageLoaderLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, + 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, + debugMode: $.DEFAULT_SETTINGS.debugMode, + timeout: $.DEFAULT_SETTINGS.timeout + + }, options ); + + this.container = $.getElement( this.element ); + this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" ); + this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null; + this.normHeight = this.source.dimensions.y / this.source.dimensions.x; + 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'; + + this.canvas.style.width = "100%"; + this.canvas.style.height = "100%"; + this.canvas.style.position = "absolute"; + + // explicit left-align + this.container.style.textAlign = "left"; + this.container.appendChild( this.canvas ); + + //create the correct type of overlay by convention if the overlays + //are not already OpenSeadragon.Overlays + for( i = 0; i < this.overlays.length; i++ ){ + if( $.isPlainObject( this.overlays[ i ] ) ){ + + this.overlays[ i ] = addOverlayFromConfiguration( this, this.overlays[ i ]); + + } else if ( $.isFunction( this.overlays[ i ] ) ){ + //TODO + } + } + + //this.profiler = new $.Profiler(); +}; + +$.Drawer.prototype = { + + /** + * Adds an html element as an overlay to the current viewport. Useful for + * highlighting words or areas of interest on an image or other zoomable + * interface. + * @method + * @param {Element|String|Object} element - A reference to an element or an id for + * the element which will overlayed. Or an Object specifying the configuration for the overlay + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or + * rectangle which will be overlayed. + * @param {OpenSeadragon.OverlayPlacement} placement - The position of the + * viewport which the location coordinates will be treated as relative + * to. + * @param {function} onDraw - If supplied the callback is called when the overlay + * needs to be drawn. It it the responsibility of the callback to do any drawing/positioning. + * It is passed position, size and element. + */ + addOverlay: function( element, location, placement, onDraw ) { + var options; + if( $.isPlainObject( element ) ){ + options = element; + } else { + options = { + element: element, + location: location, + placement: placement, + onDraw: onDraw + }; + } + + element = $.getElement(options.element); + + if ( getOverlayIndex( this.overlays, element ) >= 0 ) { + // they're trying to add a duplicate overlay + return; + } + this.overlays.push( new $.Overlay({ + element: element, + location: options.location, + placement: options.placement, + onDraw: options.onDraw + }) ); + this.updateAgain = true; + if( this.viewer ){ + this.viewer.raiseEvent( 'add-overlay', { + element: element, + location: options.location, + placement: options.placement + }); + } + return this; + }, + + /** + * Updates the overlay represented by the reference to the element or + * element id moving it to the new location, relative to the new placement. + * @method + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or + * rectangle which will be overlayed. + * @param {OpenSeadragon.OverlayPlacement} placement - The position of the + * viewport which the location coordinates will be treated as relative + * to. + * @return {OpenSeadragon.Drawer} Chainable. + */ + updateOverlay: function( element, location, placement ) { + var i; + + element = $.getElement( element ); + i = getOverlayIndex( this.overlays, element ); + + if ( i >= 0 ) { + this.overlays[ i ].update( location, placement ); + this.updateAgain = true; + } + if( this.viewer ){ + this.viewer.raiseEvent( 'update-overlay', { + element: element, + location: location, + placement: placement + }); + } + return this; + }, + + /** + * Removes and overlay identified by the reference element or element id + * and schedules and update. + * @method + * @param {Element|String} element - A reference to the element or an + * element id which represent the ovelay content to be removed. + * @return {OpenSeadragon.Drawer} Chainable. + */ + removeOverlay: function( element ) { + var i; + + element = $.getElement( element ); + i = getOverlayIndex( this.overlays, element ); + + if ( i >= 0 ) { + this.overlays[ i ].destroy(); + this.overlays.splice( i, 1 ); + this.updateAgain = true; + } + if( this.viewer ){ + this.viewer.raiseEvent( 'remove-overlay', { + element: element + }); + } + return this; + }, + + /** + * Removes all currently configured Overlays from this Drawer and schedules + * and update. + * @method + * @return {OpenSeadragon.Drawer} Chainable. + */ + clearOverlays: function() { + while ( this.overlays.length > 0 ) { + this.overlays.pop().destroy(); + this.updateAgain = true; + } + if( this.viewer ){ + this.viewer.raiseEvent( 'clear-overlay', {} ); + } + return this; + }, + + + /** + * Returns whether the Drawer is scheduled for an update at the + * soonest possible opportunity. + * @method + * @returns {Boolean} - Whether the Drawer is scheduled for an update at the + * soonest possible opportunity. + */ + needsUpdate: function() { + return this.updateAgain; + }, + + /** + * Returns the total number of tiles that have been loaded by this Drawer. + * @method + * @returns {Number} - The total number of tiles that have been loaded by + * this Drawer. + */ + numTilesLoaded: function() { + return this.tilesLoaded.length; + }, + + /** + * Clears all tiles and triggers an update on the next call to + * Drawer.prototype.update(). + * @method + * @return {OpenSeadragon.Drawer} Chainable. + */ + reset: function() { + clearTiles( this ); + this.lastResetTime = $.now(); + this.updateAgain = true; + return this; + }, + + /** + * Forces the Drawer to update. + * @method + * @return {OpenSeadragon.Drawer} Chainable. + */ + update: function() { + //this.profiler.beginUpdate(); + this.midUpdate = true; + updateViewport( this ); + this.midUpdate = false; + //this.profiler.endUpdate(); + return this; + }, + + /** + * Used internally to load images when required. May also be used to + * preload a set of images so the browser will have them available in + * the local cache to optimize user experience in certain cases. Because + * the number of parallel image loads is configurable, if too many images + * are currently being loaded, the request will be ignored. Since by + * default drawer.imageLoaderLimit is 0, the native browser parallel + * image loading policy will be used. + * @method + * @param {String} src - The url of the image to load. + * @param {Function} callback - The function that will be called with the + * Image object as the only parameter if it was loaded successfully. + * If an error occured, or the request timed out or was aborted, + * the parameter is null instead. + * @return {Boolean} loading - Whether the request was submitted or ignored + * based on OpenSeadragon.DEFAULT_SETTINGS.imageLoaderLimit. + */ + loadImage: function( src, callback ) { + var _this = this, + loading = false, + image, + jobid, + complete; + + if ( !this.imageLoaderLimit || + this.downloading < this.imageLoaderLimit ) { + + this.downloading++; + + image = new Image(); + + complete = function( imagesrc, resultingImage ){ + _this.downloading--; + if (typeof ( callback ) == "function") { + try { + callback( resultingImage ); + } catch ( e ) { + $.console.error( + "%s while executing %s callback: %s", + e.name, + src, + e.message, + e + ); + } + } + }; + + image.onload = function(){ + finishLoadingImage( image, complete, true, jobid ); + }; + + image.onabort = image.onerror = function(){ + finishLoadingImage( image, complete, false, jobid ); + }; + + jobid = window.setTimeout( function(){ + finishLoadingImage( image, complete, false, jobid ); + }, this.timeout ); + + loading = true; + image.src = src; + } + + return loading; + }, + + canRotate: function() { + return USE_CANVAS; + } +}; + +/** + * @private + * @inner + */ + function addOverlayFromConfiguration( drawer, overlay ){ + + var element = null, + rect = ( overlay.height && overlay.width ) ? new $.Rect( + overlay.x || overlay.px, + overlay.y || overlay.py, + overlay.width, + overlay.height + ) : new $.Point( + overlay.x || overlay.px, + overlay.y || overlay.py + ), + id = overlay.id ? + overlay.id : + "openseadragon-overlay-"+Math.floor(Math.random()*10000000); + + element = $.getElement(overlay.id); + if( !element ){ + element = document.createElement("a"); + element.href = "#/overlay/"+id; + } + element.id = id; + $.addClass( element, overlay.className ? + overlay.className : + "openseadragon-overlay" + ); + + + if(overlay.px !== undefined){ + //if they specified 'px' so it's in pixel coordinates so + //we need to translate to viewport coordinates + rect = drawer.viewport.imageToViewportRectangle( rect ); + } + + if( overlay.placement ){ + return new $.Overlay({ + element: element, + location: drawer.viewport.pointFromPixel(rect), + placement: $.OverlayPlacement[overlay.placement.toUpperCase()], + onDraw: overlay.onDraw + }); + }else{ + return new $.Overlay({ + element: element, + location: rect, + onDraw: overlay.onDraw + }); + } + +} + +/** + * @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. + */ +function updateViewport( drawer ) { + + drawer.updateAgain = false; + + if( drawer.viewer ){ + drawer.viewer.raiseEvent( 'update-viewport', {} ); + } + + var tile, + level, + best = null, + haveDrawn = false, + currentTime = $.now(), + viewportSize = drawer.viewport.getContainerSize(), + viewportBounds = drawer.viewport.getBounds( true ), + viewportTL = viewportBounds.getTopLeft(), + viewportBR = viewportBounds.getBottomRight(), + zeroRatioC = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( 0 ), + true + ).x, + lowestLevel = Math.max( + drawer.source.minLevel, + Math.floor( + Math.log( drawer.minZoomImageRatio ) / + Math.log( 2 ) + ) + ), + highestLevel = Math.min( + Math.abs(drawer.source.maxLevel), + Math.abs(Math.floor( + Math.log( zeroRatioC / drawer.minPixelRatio ) / + Math.log( 2 ) + )) + ), + degrees = drawer.viewport.degrees, + renderPixelRatioC, + renderPixelRatioT, + zeroRatioT, + optimalRatio, + levelOpacity, + levelVisibility; + + //TODO + while ( drawer.lastDrawn.length > 0 ) { + tile = drawer.lastDrawn.pop(); + tile.beingDrawn = false; + } + + //TODO + drawer.canvas.innerHTML = ""; + if ( USE_CANVAS ) { + if( drawer.canvas.width != viewportSize.x || + drawer.canvas.height != viewportSize.y ){ + drawer.canvas.width = viewportSize.x; + drawer.canvas.height = viewportSize.y; + } + drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y ); + } + + //Change bounds for rotation + if (degrees === 90 || degrees === 270) { + var rotatedBounds = viewportBounds.rotate( degrees ); + viewportTL = rotatedBounds.getTopLeft(); + viewportBR = rotatedBounds.getBottomRight(); + } + + //Don't draw if completely outside of the viewport + if ( !drawer.wrapHorizontal && + ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { + return; + } else if + ( !drawer.wrapVertical && + ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) { + return; + } + + //TODO + if ( !drawer.wrapHorizontal ) { + viewportTL.x = Math.max( viewportTL.x, 0 ); + viewportBR.x = Math.min( viewportBR.x, 1 ); + } + if ( !drawer.wrapVertical ) { + viewportTL.y = Math.max( viewportTL.y, 0 ); + viewportBR.y = Math.min( viewportBR.y, drawer.normHeight ); + } + + //TODO + lowestLevel = Math.min( lowestLevel, highestLevel ); + + //TODO + var drawLevel; // FIXME: drawLevel should have a more explanatory name + for ( level = highestLevel; level >= lowestLevel; level-- ) { + drawLevel = false; + + //Avoid calculations for draw if we have already drawn this + renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( level ), + true + ).x; + + if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) || + ( level == lowestLevel ) ) { + drawLevel = true; + haveDrawn = true; + } else if ( !haveDrawn ) { + continue; + } + + //Perform calculations for draw if we haven't drawn this + renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( level ), + false + ).x; + + zeroRatioT = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( + Math.max( + drawer.source.getClosestLevel( drawer.viewport.containerSize ) - 1, + 0 + ) + ), + false + ).x; + + optimalRatio = drawer.immediateRender ? + 1 : + zeroRatioT; + + levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); + + levelVisibility = optimalRatio / Math.abs( + optimalRatio - renderPixelRatioT + ); + + //TODO + best = updateLevel( + drawer, + haveDrawn, + drawLevel, + level, + levelOpacity, + levelVisibility, + viewportTL, + viewportBR, + currentTime, + best + ); + + //TODO + if ( providesCoverage( drawer.coverage, level ) ) { + break; + } + } + + //TODO + drawTiles( drawer, drawer.lastDrawn ); + drawOverlays( drawer.viewport, drawer.overlays, drawer.container ); + + //TODO + if ( best ) { + loadTile( drawer, best, currentTime ); + // because we haven't finished drawing, so + drawer.updateAgain = true; + } + +} + + +function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ + + var x, y, + tileTL, + tileBR, + numberOfTiles, + viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() ); + + + if( drawer.viewer ){ + drawer.viewer.raiseEvent( 'update-level', { + havedrawn: haveDrawn, + level: level, + opacity: levelOpacity, + visibility: levelVisibility, + topleft: viewportTL, + bottomright: viewportBR, + currenttime: currentTime, + best: best + }); + } + + //OK, a new drawing so do your calculations + tileTL = drawer.source.getTileAtPoint( level, viewportTL ); + tileBR = drawer.source.getTileAtPoint( level, viewportBR ); + numberOfTiles = drawer.source.getNumTiles( level ); + + resetCoverage( drawer.coverage, level ); + + if ( !drawer.wrapHorizontal ) { + tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); + } + if ( !drawer.wrapVertical ) { + tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); + } + + for ( x = tileTL.x; x <= tileBR.x; x++ ) { + for ( y = tileTL.y; y <= tileBR.y; y++ ) { + + best = updateTile( + drawer, + drawLevel, + haveDrawn, + x, y, + level, + levelOpacity, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + + } + } + + return best; +} + +function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = getTile( + x, y, + level, + drawer.source, + drawer.tilesMatrix, + currentTime, + numberOfTiles, + drawer.normHeight + ), + drawTile = drawLevel; + + if( drawer.viewer ){ + drawer.viewer.raiseEvent( 'update-tile', { + tile: tile + }); + } + + setCoverage( drawer.coverage, level, x, y, false ); + + if ( !tile.exists ) { + return best; + } + + if ( haveDrawn && !drawTile ) { + if ( isCovered( drawer.coverage, level, x, y ) ) { + setCoverage( drawer.coverage, level, x, y, true ); + } else { + drawTile = true; + } + } + + if ( !drawTile ) { + return best; + } + + positionTile( + tile, + drawer.source.tileOverlap, + drawer.viewport, + viewportCenter, + levelVisibility + ); + + if ( tile.loaded ) { + var needsUpdate = blendTile( + drawer, + tile, + x, y, + level, + levelOpacity, + currentTime + ); + + if ( needsUpdate ) { + drawer.updateAgain = true; + } + } else if ( tile.loading ) { + // the tile is already in the download queue + // thanks josh1093 for finally translating this typo + } else { + best = compareTiles( best, tile ); + } + + return best; +} + +function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) { + var xMod, + yMod, + bounds, + exists, + url, + tile; + + if ( !tilesMatrix[ level ] ) { + tilesMatrix[ level ] = {}; + } + if ( !tilesMatrix[ level ][ x ] ) { + tilesMatrix[ level ][ x ] = {}; + } + + if ( !tilesMatrix[ level ][ x ][ y ] ) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + bounds = tileSource.getTileBounds( level, xMod, yMod ); + exists = tileSource.tileExists( level, xMod, yMod ); + url = tileSource.getTileUrl( level, xMod, yMod ); + + bounds.x += 1.0 * ( x - xMod ) / numTiles.x; + bounds.y += normHeight * ( y - yMod ) / numTiles.y; + + tilesMatrix[ level ][ x ][ y ] = new $.Tile( + level, + x, + y, + bounds, + exists, + url + ); + } + + tile = tilesMatrix[ level ][ x ][ y ]; + tile.lastTouchTime = time; + + return tile; +} + + +function loadTile( drawer, tile, time ) { + if( drawer.viewport.collectionMode ){ + drawer.midUpdate = false; + onTileLoad( drawer, tile, time ); + } else { + tile.loading = drawer.loadImage( + tile.url, + function( image ){ + onTileLoad( drawer, tile, time, image ); + } + ); + } +} + +function onTileLoad( drawer, tile, time, image ) { + var insertionIndex, + cutoff, + worstTile, + worstTime, + worstLevel, + worstTileIndex, + prevTile, + prevTime, + prevLevel, + i; + + tile.loading = false; + + if ( drawer.midUpdate ) { + $.console.warn( "Tile load callback in middle of drawing routine." ); + return; + } else if ( !image && !drawer.viewport.collectionMode ) { + $.console.log( "Tile %s failed to load: %s", tile, tile.url ); + if( !drawer.debugMode ){ + tile.exists = false; + return; + } + } else if ( time < drawer.lastResetTime ) { + $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); + return; + } + + tile.loaded = true; + tile.image = image; + + + insertionIndex = drawer.tilesLoaded.length; + + if ( drawer.tilesLoaded.length >= drawer.maxImageCacheCount ) { + cutoff = Math.ceil( Math.log( drawer.source.tileSize ) / Math.log( 2 ) ); + + worstTile = null; + worstTileIndex = -1; + + for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) { + prevTile = drawer.tilesLoaded[ i ]; + + if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) { + continue; + } else if ( !worstTile ) { + worstTile = prevTile; + worstTileIndex = i; + continue; + } + + prevTime = prevTile.lastTouchTime; + worstTime = worstTile.lastTouchTime; + prevLevel = prevTile.level; + worstLevel = worstTile.level; + + if ( prevTime < worstTime || + ( prevTime == worstTime && prevLevel > worstLevel ) ) { + worstTile = prevTile; + worstTileIndex = i; + } + } + + if ( worstTile && worstTileIndex >= 0 ) { + worstTile.unload(); + insertionIndex = worstTileIndex; + } + } + + drawer.tilesLoaded[ insertionIndex ] = tile; + drawer.updateAgain = true; +} + + +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){ + var boundsTL = tile.bounds.getTopLeft(), + boundsSize = tile.bounds.getSize(), + positionC = viewport.pixelFromPoint( boundsTL, true ), + positionT = viewport.pixelFromPoint( boundsTL, false ), + sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ), + sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ), + tileCenter = positionT.plus( sizeT.divide( 2 ) ), + tileDistance = viewportCenter.distanceTo( tileCenter ); + + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + } + + tile.position = positionC; + tile.size = sizeC; + tile.distance = tileDistance; + tile.visibility = levelVisibility; +} + + +function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * drawer.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + if ( drawer.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + drawer.lastDrawn.push( tile ); + + if ( opacity == 1 ) { + setCoverage( drawer.coverage, level, x, y, true ); + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; +} + + +function clearTiles( drawer ) { + drawer.tilesMatrix = {}; + drawer.tilesLoaded = []; +} + +/** + * @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. + */ +function providesCoverage( 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 ( rows.hasOwnProperty( i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( cols.hasOwnProperty( 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. + */ +function isCovered( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return providesCoverage( coverage, level + 1 ); + } else { + return ( + providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) + ); + } +} + +/** + * @private + * @inner + * Sets whether the given tile provides coverage or not. + */ +function setCoverage( 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. + */ +function resetCoverage( coverage, level ) { + coverage[ level ] = {}; +} + +/** + * @private + * @inner + * Determines the 'z-index' of the given overlay. Overlays are ordered in + * a z-index based on the order they are added to the Drawer. + */ +function getOverlayIndex( overlays, element ) { + var i; + for ( i = overlays.length - 1; i >= 0; i-- ) { + if ( overlays[ i ].element == element ) { + return i; + } + } + + return -1; +} + +/** + * @private + * @inner + * Determines whether the 'last best' tile for the area is better than the + * tile in question. + */ +function compareTiles( previousBest, tile ) { + if ( !previousBest ) { + return tile; + } + + if ( tile.visibility > previousBest.visibility ) { + return tile; + } else if ( tile.visibility == previousBest.visibility ) { + if ( tile.distance < previousBest.distance ) { + return tile; + } + } + + return previousBest; +} + +function finishLoadingImage( image, callback, successful, jobid ){ + + image.onload = null; + image.onabort = null; + image.onerror = null; + + if ( jobid ) { + window.clearTimeout( jobid ); + } + $.requestAnimationFrame( function() { + callback( image.src, successful ? image : null); + }); + +} + + +function drawOverlays( viewport, overlays, container ){ + var i, + length = overlays.length; + for ( i = 0; i < length; i++ ) { + drawOverlay( viewport, overlays[ i ], container ); + } +} + +function drawOverlay( viewport, overlay, container ){ + + overlay.position = viewport.pixelFromPoint( + overlay.bounds.getTopLeft(), + true + ); + overlay.size = viewport.deltaPixelsFromPoints( + overlay.bounds.getSize(), + true + ); + overlay.drawHTML( container, viewport ); +} + +function drawTiles( drawer, lastDrawn ){ + var i, + tile, + tileKey, + viewer, + viewport, + position, + tileSource, + collectionTileSource; + + for ( i = lastDrawn.length - 1; i >= 0; i-- ) { + tile = lastDrawn[ i ]; + + //We dont actually 'draw' a collection tile, rather its used to house + //an overlay which does the drawing in its own viewport + if( drawer.viewport.collectionMode ){ + + tileKey = tile.x + '/' + tile.y; + viewport = drawer.viewport; + collectionTileSource = viewport.collectionTileSource; + + if( !drawer.collectionOverlays[ tileKey ] ){ + + position = collectionTileSource.layout == 'horizontal' ? + tile.y + ( tile.x * collectionTileSource.rows ) : + tile.x + ( tile.y * collectionTileSource.rows ); + + if (position < collectionTileSource.tileSources.length) { + tileSource = collectionTileSource.tileSources[ position ]; + } else { + tileSource = null; + } + + //$.console.log("Rendering collection tile %s | %s | %s", tile.y, tile.y, position); + if( tileSource ){ + drawer.collectionOverlays[ tileKey ] = viewer = new $.Viewer({ + hash: viewport.viewer.hash + "-" + tileKey, + element: $.makeNeutralElement( "div" ), + mouseNavEnabled: false, + showNavigator: false, + showSequenceControl: false, + showNavigationControl: false, + tileSources: [ + tileSource + ] + }); + + //TODO: IE seems to barf on this, not sure if its just the border + // but we probably need to clear this up with a better + // test of support for various css features + if( SUBPIXEL_RENDERING ){ + viewer.element.style.border = '1px solid rgba(255,255,255,0.38)'; + viewer.element.style['-webkit-box-reflect'] = + 'below 0px -webkit-gradient('+ + 'linear,left '+ + 'top,left '+ + 'bottom,from(transparent),color-stop(62%,transparent),to(rgba(255,255,255,0.62))'+ + ')'; + } + + drawer.addOverlay( + viewer.element, + tile.bounds + ); + } + + }else{ + viewer = drawer.collectionOverlays[ tileKey ]; + if( viewer.viewport ){ + viewer.viewport.resize( tile.size, true ); + viewer.viewport.goHome( true ); + } + } + + } else { + + if ( USE_CANVAS ) { + // TODO do this in a more performant way + // specifically, don't save,rotate,restore every time we draw a tile + if( drawer.viewport.degrees !== 0 ) { + offsetForRotation( tile, drawer.canvas, drawer.context, drawer.viewport.degrees ); + tile.drawCanvas( drawer.context ); + restoreRotationChanges( tile, drawer.canvas, drawer.context ); + } else { + tile.drawCanvas( drawer.context ); + } + } else { + tile.drawHTML( drawer.canvas ); + } + + + tile.beingDrawn = true; + } + + if( drawer.debugMode ){ + try{ + drawDebugInfo( drawer, tile, lastDrawn.length, i ); + }catch(e){ + $.console.error(e); + } + } + + if( drawer.viewer ){ + drawer.viewer.raiseEvent( 'tile-drawn', { + tile: tile + }); + } + } +} + +function offsetForRotation( tile, canvas, context, degrees ){ + var cx = canvas.width / 2, + cy = canvas.height / 2, + px = tile.position.x - cx, + py = tile.position.y - cy; + + context.save(); + + context.translate(cx, cy); + context.rotate( Math.PI / 180 * degrees); + tile.position.x = px; + tile.position.y = py; +} + +function restoreRotationChanges( tile, canvas, context ){ + var cx = canvas.width / 2, + cy = canvas.height / 2, + px = tile.position.x + cx, + py = tile.position.y + cy; + + tile.position.x = px; + tile.position.y = py; + + context.restore(); +} + + +function drawDebugInfo( drawer, tile, count, i ){ + + if ( USE_CANVAS ) { + drawer.context.save(); + drawer.context.lineWidth = 2; + drawer.context.font = 'small-caps bold 13px ariel'; + drawer.context.strokeStyle = drawer.debugGridColor; + drawer.context.fillStyle = drawer.debugGridColor; + drawer.context.strokeRect( + tile.position.x, + tile.position.y, + tile.size.x, + tile.size.y + ); + if( tile.x === 0 && tile.y === 0 ){ + drawer.context.fillText( + "Zoom: " + drawer.viewport.getZoom(), + tile.position.x, + tile.position.y - 30 + ); + drawer.context.fillText( + "Pan: " + drawer.viewport.getBounds().toString(), + tile.position.x, + tile.position.y - 20 + ); + } + drawer.context.fillText( + "Level: " + tile.level, + tile.position.x + 10, + tile.position.y + 20 + ); + drawer.context.fillText( + "Column: " + tile.x, + tile.position.x + 10, + tile.position.y + 30 + ); + drawer.context.fillText( + "Row: " + tile.y, + tile.position.x + 10, + tile.position.y + 40 + ); + drawer.context.fillText( + "Order: " + i + " of " + count, + tile.position.x + 10, + tile.position.y + 50 + ); + drawer.context.fillText( + "Size: " + tile.size.toString(), + tile.position.x + 10, + tile.position.y + 60 + ); + drawer.context.fillText( + "Position: " + tile.position.toString(), + tile.position.x + 10, + tile.position.y + 70 + ); + drawer.context.restore(); + } +} + + +}( OpenSeadragon )); + +/* + * OpenSeadragon - Viewport + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 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 = function( options ) { + + //backward compatibility for positional args while prefering 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; + } + + $.extend( true, this, { + + //required settings + containerSize: null, + contentSize: null, + + //internal state properties + zoomPoint: 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, + degrees: $.DEFAULT_SETTINGS.degrees + + }, options ); + + 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({ + initial: 1, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this.resetContentSize( this.contentSize ); + this.goHome( true ); + this.update(); +}; + +$.Viewport.prototype = { + + /** + * @function + * @return {OpenSeadragon.Viewport} Chainable. + */ + resetContentSize: function( contentSize ){ + this.contentSize = contentSize; + this.contentAspectX = this.contentSize.x / this.contentSize.y; + this.contentAspectY = this.contentSize.y / this.contentSize.x; + this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); + this.fitHeightBounds = new $.Rect( 0, 0, this.contentAspectY, this.contentAspectY); + + this.homeBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); + + if( this.viewer ){ + this.viewer.raiseEvent( 'reset-size', { + contentSize: contentSize + }); + } + + return this; + }, + + /** + * @function + */ + getHomeZoom: function() { + var aspectFactor = + this.contentAspectX / this.getAspectRatio(); + + if( this.defaultZoomLevel ){ + return this.defaultZoomLevel; + } else { + return ( aspectFactor >= 1 ) ? + 1 : + aspectFactor; + } + }, + + /** + * @function + */ + getHomeBounds: function() { + var center = this.homeBounds.getCenter( ), + width = 1.0 / this.getHomeZoom( ), + height = width / this.getAspectRatio(); + + return new $.Rect( + center.x - ( width / 2.0 ), + center.y - ( height / 2.0 ), + width, + height + ); + }, + + /** + * @function + * @param {Boolean} immediately + */ + goHome: function( immediately ) { + if( this.viewer ){ + 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 Math.min( zoom, homeZoom ); + }, + + /** + * @function + */ + getMaxZoom: function() { + var zoom = this.maxZoomLevel ? + this.maxZoomLevel : + ( this.contentSize.x * this.maxZoomPixelRatio / this.containerSize.x ); + + return Math.max( zoom, this.getHomeZoom() ); + }, + + /** + * @function + */ + getAspectRatio: function() { + return this.containerSize.x / this.containerSize.y; + }, + + /** + * @function + */ + getContainerSize: function() { + return new $.Point( + this.containerSize.x, + this.containerSize.y + ); + }, + + /** + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + */ + getBounds: function( current ) { + var center = this.getCenter( current ), + width = 1.0 / this.getZoom( current ), + 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). + */ + 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.zoomPoint.minus( + bounds.getTopLeft() + ).times( + this.containerSize.x / bounds.width + ); + deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); + deltaZoomPoints = deltaZoomPixels.divide( this.containerSize.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; + } + }, + + /** + * @function + * @return {OpenSeadragon.Viewport} Chainable. + */ + applyConstraints: function( immediately ) { + var actualZoom = this.getZoom(), + constrainedZoom = Math.max( + Math.min( actualZoom, this.getMaxZoom() ), + this.getMinZoom() + ), + bounds, + horizontalThreshold, + verticalThreshold, + left, + right, + top, + bottom, + dx = 0, + dy = 0; + + if ( actualZoom != constrainedZoom ) { + this.zoomTo( constrainedZoom, this.zoomPoint, immediately ); + } + + bounds = this.getBounds(); + + horizontalThreshold = this.visibilityRatio * bounds.width; + verticalThreshold = this.visibilityRatio * bounds.height; + + left = bounds.x + bounds.width; + right = 1 - bounds.x; + top = bounds.y + bounds.height; + bottom = this.contentAspectY - bounds.y; + + if ( this.wrapHorizontal ) { + //do nothing + } else { + if ( left < horizontalThreshold ) { + dx = horizontalThreshold - left; + } + if ( right < horizontalThreshold ) { + dx = dx ? + ( dx + right - horizontalThreshold ) / 2 : + ( right - horizontalThreshold ); + } + } + + if ( this.wrapVertical ) { + //do nothing + } else { + if ( top < verticalThreshold ) { + dy = ( verticalThreshold - top ); + } + if ( bottom < verticalThreshold ) { + dy = dy ? + ( dy + bottom - verticalThreshold ) / 2 : + ( bottom - verticalThreshold ); + } + } + + if ( dx || dy || immediately ) { + bounds.x += dx; + bounds.y += dy; + if( bounds.width > 1 ){ + bounds.x = 0.5 - bounds.width/2; + } + if( bounds.height > this.contentAspectY ){ + bounds.y = this.contentAspectY/2 - bounds.height/2; + } + this.fitBounds( bounds, immediately ); + } + + if( this.viewer ){ + this.viewer.raiseEvent( 'constrain', { + immediately: immediately + }); + } + + return this; + }, + + /** + * @function + * @param {Boolean} immediately + */ + ensureVisible: function( immediately ) { + return this.applyConstraints( immediately ); + }, + + /** + * @function + * @param {OpenSeadragon.Rect} bounds + * @param {Boolean} immediately + * @return {OpenSeadragon.Viewport} Chainable. + */ + fitBounds: function( bounds, immediately ) { + var aspect = this.getAspectRatio(), + center = bounds.getCenter(), + newBounds = new $.Rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height + ), + oldBounds, + oldZoom, + newZoom, + referencePoint; + + if ( newBounds.getAspectRatio() >= aspect ) { + newBounds.height = bounds.width / aspect; + newBounds.y = center.y - newBounds.height / 2; + } else { + newBounds.width = bounds.height * aspect; + newBounds.x = center.x - newBounds.width / 2; + } + + this.panTo( this.getCenter( true ), true ); + this.zoomTo( this.getZoom( true ), null, true ); + + oldBounds = this.getBounds(); + oldZoom = this.getZoom(); + newZoom = 1.0 / newBounds.width; + if ( newZoom == oldZoom || newBounds.width == oldBounds.width ) { + return this.panTo( center, immediately ); + } + + referencePoint = oldBounds.getTopLeft().times( + this.containerSize.x / oldBounds.width + ).minus( + newBounds.getTopLeft().times( + this.containerSize.x / newBounds.width + ) + ).divide( + this.containerSize.x / oldBounds.width - + this.containerSize.x / newBounds.width + ); + + return this.zoomTo( newZoom, referencePoint, immediately ); + }, + + + /** + * @function + * @param {Boolean} immediately + * @return {OpenSeadragon.Viewport} Chainable. + */ + fitVertically: function( immediately ) { + var center = this.getCenter(); + + if ( this.wrapHorizontal ) { + center.x = ( 1 + ( center.x % 1 ) ) % 1; + this.centerSpringX.resetTo( center.x ); + this.centerSpringX.update(); + } + + if ( this.wrapVertical ) { + center.y = ( + this.contentAspectY + ( center.y % this.contentAspectY ) + ) % this.contentAspectY; + this.centerSpringY.resetTo( center.y ); + this.centerSpringY.update(); + } + + return this.fitBounds( this.fitHeightBounds, immediately ); + }, + + /** + * @function + * @param {Boolean} immediately + * @return {OpenSeadragon.Viewport} Chainable. + */ + fitHorizontally: function( immediately ) { + var center = this.getCenter(); + + if ( this.wrapHorizontal ) { + center.x = ( + this.contentAspectX + ( center.x % this.contentAspectX ) + ) % this.contentAspectX; + this.centerSpringX.resetTo( center.x ); + this.centerSpringX.update(); + } + + if ( this.wrapVertical ) { + center.y = ( 1 + ( center.y % 1 ) ) % 1; + this.centerSpringY.resetTo( center.y ); + this.centerSpringY.update(); + } + + return this.fitBounds( this.fitWidthBounds, immediately ); + }, + + + /** + * @function + * @param {OpenSeadragon.Point} delta + * @param {Boolean} immediately + * @return {OpenSeadragon.Viewport} Chainable. + */ + panBy: function( delta, immediately ) { + var center = new $.Point( + this.centerSpringX.target.value, + this.centerSpringY.target.value + ); + delta = delta.rotate( -this.degrees, new $.Point( 0, 0 ) ); + return this.panTo( center.plus( delta ), immediately ); + }, + + /** + * @function + * @param {OpenSeadragon.Point} center + * @param {Boolean} immediately + * @return {OpenSeadragon.Viewport} Chainable. + */ + 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 ){ + this.viewer.raiseEvent( 'pan', { + center: center, + immediately: immediately + }); + } + + return this; + }, + + /** + * @function + * @return {OpenSeadragon.Viewport} Chainable. + */ + zoomBy: function( factor, refPoint, immediately ) { + if( refPoint instanceof $.Point && !isNaN( refPoint.x ) && !isNaN( refPoint.y ) ) { + refPoint = refPoint.rotate( + -this.degrees, + new $.Point( this.centerSpringX.target.value, this.centerSpringY.target.value ) + ); + } + return this.zoomTo( this.zoomSpring.target.value * factor, refPoint, immediately ); + }, + + /** + * @function + * @return {OpenSeadragon.Viewport} Chainable. + */ + zoomTo: function( zoom, refPoint, immediately ) { + + this.zoomPoint = refPoint instanceof $.Point && + !isNaN(refPoint.x) && + !isNaN(refPoint.y) ? + refPoint : + null; + + if ( immediately ) { + this.zoomSpring.resetTo( zoom ); + } else { + this.zoomSpring.springTo( zoom ); + } + + if( this.viewer ){ + this.viewer.raiseEvent( 'zoom', { + zoom: zoom, + refPoint: refPoint, + immediately: immediately + }); + } + + return this; + }, + + /** + * Currently only 90 degree rotation is supported and it only works + * with the canvas. Additionally, the navigator does not rotate yet, + * debug mode doesn't rotate yet, and overlay rotation is only + * partially supported. + * @function + * @name OpenSeadragon.Viewport.prototype.setRotation + * @return {OpenSeadragon.Viewport} Chainable. + */ + setRotation: function( degrees ) { + if( !( this.viewer && this.viewer.drawer.canRotate() ) ) { + return this; + } + + degrees = ( degrees + 360 ) % 360; + if( degrees % 90 !== 0 ) { + throw new Error('Currently only 0, 90, 180, and 270 degrees are supported.'); + } + this.degrees = degrees; + this.viewer.drawer.update(); + + return this; + }, + + /** + * Gets the current rotation in degrees. + * @function + * @name OpenSeadragon.Viewport.prototype.getRotation + * @return {Number} The current rotation in degrees. + */ + getRotation: function() { + return this.degrees; + }, + + /** + * @function + * @return {OpenSeadragon.Viewport} Chainable. + */ + resize: function( newContainerSize, maintain ) { + var oldBounds = this.getBounds(), + newBounds = oldBounds, + widthDeltaFactor = newContainerSize.x / this.containerSize.x; + + this.containerSize = new $.Point( + newContainerSize.x, + newContainerSize.y + ); + + if (maintain) { + newBounds.width = oldBounds.width * widthDeltaFactor; + newBounds.height = newBounds.width / this.getAspectRatio(); + } + + if( this.viewer ){ + this.viewer.raiseEvent( 'resize', { + newContainerSize: newContainerSize, + maintain: maintain + }); + } + + return this.fitBounds( newBounds, true ); + }, + + /** + * @function + */ + update: function() { + var oldCenterX = this.centerSpringX.current.value, + oldCenterY = this.centerSpringY.current.value, + oldZoom = this.zoomSpring.current.value, + oldZoomPixel, + newZoomPixel, + deltaZoomPixels, + deltaZoomPoints; + + if (this.zoomPoint) { + oldZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); + } + + this.zoomSpring.update(); + + if (this.zoomPoint && this.zoomSpring.current.value != oldZoom) { + newZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); + deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); + deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true ); + + this.centerSpringX.shiftBy( deltaZoomPoints.x ); + this.centerSpringY.shiftBy( deltaZoomPoints.y ); + } else { + this.zoomPoint = null; + } + + this.centerSpringX.update(); + this.centerSpringY.update(); + + return this.centerSpringX.current.value != oldCenterX || + this.centerSpringY.current.value != oldCenterY || + this.zoomSpring.current.value != oldZoom; + }, + + + /** + * Convert a delta (translation vector) from pixels coordinates to viewport coordinates + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + */ + deltaPixelsFromPoints: function( deltaPoints, current ) { + return deltaPoints.times( + this.containerSize.x * this.getZoom( current ) + ); + }, + + /** + * Convert a delta (translation vector) from viewport coordinates to pixels coordinates. + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + */ + deltaPointsFromPixels: function( deltaPixels, current ) { + return deltaPixels.divide( + this.containerSize.x * this.getZoom( current ) + ); + }, + + /** + * Convert image pixel coordinates to viewport coordinates. + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + */ + pixelFromPoint: function( point, current ) { + var bounds = this.getBounds( current ); + return point.minus( + bounds.getTopLeft() + ).times( + this.containerSize.x / bounds.width + ); + }, + + /** + * Convert viewport coordinates to image pixel coordinates. + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + */ + pointFromPixel: function( pixel, current ) { + var bounds = this.getBounds( current ); + return pixel.divide( + this.containerSize.x / bounds.width + ).plus( + bounds.getTopLeft() + ); + }, + + /** + * 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 + * @function + * @param {OpenSeadragon.Point} viewerX the point in viewport coordinate system. + * @param {Number} viewerX X coordinate in viewport coordinate system. + * @param {Number} viewerY Y coordinate in viewport coordinate system. + * @return {OpenSeadragon.Point} a point representing the coordinates in the image. + */ + viewportToImageCoordinates: function( viewerX, viewerY ) { + if ( arguments.length == 1 ) { + //they passed a point instead of individual components + return this.viewportToImageCoordinates( viewerX.x, viewerX.y ); + } + return new $.Point( viewerX * this.contentSize.x, viewerY * this.contentSize.y * this.contentAspectX ); + }, + + /** + * 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 + * @function + * @param {OpenSeadragon.Point} imageX the point in image coordinate system. + * @param {Number} imageX X coordinate in image coordinate system. + * @param {Number} imageY Y coordinate in image coordinate system. + * @return {OpenSeadragon.Point} a point representing the coordinates in the viewport. + */ + imageToViewportCoordinates: function( imageX, imageY ) { + if ( arguments.length == 1 ) { + //they passed a point instead of individual components + return this.imageToViewportCoordinates( imageX.x, imageX.y ); + } + return new $.Point( imageX / this.contentSize.x, imageY / this.contentSize.y / this.contentAspectX ); + }, + + /** + * 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 + * @function + * @param {OpenSeadragon.Rect} imageX the rectangle in image coordinate system. + * @param {Number} imageX 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. + */ + imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight ) { + var coordA, + coordB, + rect; + if( arguments.length == 1 ) { + //they passed a rectangle instead of individual components + rect = imageX; + return this.imageToViewportRectangle( + rect.x, rect.y, rect.width, rect.height + ); + } + coordA = this.imageToViewportCoordinates( + imageX, imageY + ); + coordB = this.imageToViewportCoordinates( + pixelWidth, pixelHeight + ); + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y + ); + }, + + /** + * 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 + * @function + * @param {OpenSeadragon.Rect} viewerX the rectangle in viewport coordinate system. + * @param {Number} viewerX the X coordinate of the top left corner of the rectangle + * in viewport coordinate system. + * @param {Number} imageY 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 coordA, + coordB, + rect; + if ( arguments.length == 1 ) { + //they passed a rectangle instead of individual components + rect = viewerX; + return this.viewportToImageRectangle( + rect.x, rect.y, rect.width, rect.height + ); + } + coordA = this.viewportToImageCoordinates( viewerX, viewerY ); + coordB = this.viewportToImageCoordinates( pointWidth, pointHeight ); + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y + ); + }, + + /** + * Convert pixel coordinates relative to the viewer element to image + * coordinates. + * @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. + * @param {OpenSeadragon.Point} point + * @returns {OpenSeadragon.Point} + */ + imageToViewerElementCoordinates: function( point ) { + var pixel = this.pixelFromPoint( point, true ); + return this.imageToViewportCoordinates( pixel ); + }, + + /** + * 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 )); + }, + + /** + * 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 pixel coordinates relative to the window to viewport coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + windowToViewportCoordinates: function( pixel ) { + var viewerCoordinates = pixel.minus( + OpenSeadragon.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 ) { + var viewerCoordinates = this.viewportToViewerElementCoordinates( point ); + return viewerCoordinates.plus( + OpenSeadragon.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... + * @function + * @param {Number} viewportZoom The viewport zoom + * target zoom. + * @returns {Number} imageZoom The image zoom + */ + viewportToImageZoom: function( viewportZoom ) { + var imageWidth = this.viewer.source.dimensions.x; + var containerWidth = this.getContainerSize().x; + var viewportToImageZoomRatio = containerWidth / imageWidth; + 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... + * @function + * @param {Number} imageZoom The image zoom + * target zoom. + * @returns {Number} viewportZoom The viewport zoom + */ + imageToViewportZoom: function( imageZoom ) { + var imageWidth = this.viewer.source.dimensions.x; + var containerWidth = this.getContainerSize().x; + var viewportToImageZoomRatio = imageWidth / containerWidth; + return imageZoom * viewportToImageZoomRatio; + } +}; + +}( OpenSeadragon )); \ No newline at end of file diff --git a/lms/envs/common.py b/lms/envs/common.py index ef4e52fecd..12de13987a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -845,6 +845,8 @@ main_vendor_js = [ 'js/vendor/ova/tags-annotator.js', 'js/vendor/ova/flagging-annotator.js', 'js/vendor/ova/jquery-Watch.js', + 'js/vendor/ova/openseadragon.js', + 'js/vendor/ova/OpenSeaDragonAnnotation.js', 'js/vendor/ova/ova.js', 'js/vendor/ova/catch/js/catch.js', 'js/vendor/ova/catch/js/handlebars-1.1.2.js', diff --git a/lms/templates/imageannotation.html b/lms/templates/imageannotation.html new file mode 100644 index 0000000000..a4c121cac8 --- /dev/null +++ b/lms/templates/imageannotation.html @@ -0,0 +1,209 @@ +<%! from django.utils.translation import ugettext as _ %> +<%namespace name='static' file='/static_content.html'/> +${static.css(group='style-vendor-tinymce-content', raw=True)} +${static.css(group='style-vendor-tinymce-skin', raw=True)} + \ No newline at end of file From 008bbc224b908c85098358c8ad41d4aa92375589 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Wed, 21 May 2014 16:19:15 -0400 Subject: [PATCH 3/5] Image Annotation Tool: Add static images for openseadragon navigation --- .../js/vendor/ova/images/fullpage_grouphover.png | Bin 0 -> 4907 bytes .../js/vendor/ova/images/fullpage_hover.png | Bin 0 -> 5214 bytes .../js/vendor/ova/images/fullpage_pressed.png | Bin 0 -> 5213 bytes .../js/vendor/ova/images/fullpage_rest.png | Bin 0 -> 5155 bytes .../js/vendor/ova/images/home_grouphover.png | Bin 0 -> 4808 bytes .../static/js/vendor/ova/images/home_hover.png | Bin 0 -> 5107 bytes .../static/js/vendor/ova/images/home_pressed.png | Bin 0 -> 5138 bytes common/static/js/vendor/ova/images/home_rest.png | Bin 0 -> 5061 bytes .../js/vendor/ova/images/newan_grouphover.png | Bin 0 -> 4859 bytes .../static/js/vendor/ova/images/newan_hover.png | Bin 0 -> 5027 bytes .../js/vendor/ova/images/newan_pressed.png | Bin 0 -> 4898 bytes .../static/js/vendor/ova/images/newan_rest.png | Bin 0 -> 4772 bytes .../js/vendor/ova/images/next_grouphover.png | Bin 0 -> 3004 bytes .../static/js/vendor/ova/images/next_hover.png | Bin 0 -> 3433 bytes .../static/js/vendor/ova/images/next_pressed.png | Bin 0 -> 3503 bytes common/static/js/vendor/ova/images/next_rest.png | Bin 0 -> 3061 bytes .../js/vendor/ova/images/previous_grouphover.png | Bin 0 -> 2987 bytes .../js/vendor/ova/images/previous_hover.png | Bin 0 -> 3461 bytes .../js/vendor/ova/images/previous_pressed.png | Bin 0 -> 3499 bytes .../js/vendor/ova/images/previous_rest.png | Bin 0 -> 3064 bytes .../js/vendor/ova/images/zoomin_grouphover.png | Bin 0 -> 4794 bytes .../static/js/vendor/ova/images/zoomin_hover.png | Bin 0 -> 5126 bytes .../js/vendor/ova/images/zoomin_pressed.png | Bin 0 -> 5172 bytes .../static/js/vendor/ova/images/zoomin_rest.png | Bin 0 -> 5041 bytes .../js/vendor/ova/images/zoomout_grouphover.png | Bin 0 -> 4596 bytes .../js/vendor/ova/images/zoomout_hover.png | Bin 0 -> 4931 bytes .../js/vendor/ova/images/zoomout_pressed.png | Bin 0 -> 5007 bytes .../static/js/vendor/ova/images/zoomout_rest.png | Bin 0 -> 4811 bytes 28 files changed, 0 insertions(+), 0 deletions(-) create mode 100755 common/static/js/vendor/ova/images/fullpage_grouphover.png create mode 100755 common/static/js/vendor/ova/images/fullpage_hover.png create mode 100755 common/static/js/vendor/ova/images/fullpage_pressed.png create mode 100755 common/static/js/vendor/ova/images/fullpage_rest.png create mode 100755 common/static/js/vendor/ova/images/home_grouphover.png create mode 100755 common/static/js/vendor/ova/images/home_hover.png create mode 100755 common/static/js/vendor/ova/images/home_pressed.png create mode 100755 common/static/js/vendor/ova/images/home_rest.png create mode 100755 common/static/js/vendor/ova/images/newan_grouphover.png create mode 100755 common/static/js/vendor/ova/images/newan_hover.png create mode 100755 common/static/js/vendor/ova/images/newan_pressed.png create mode 100755 common/static/js/vendor/ova/images/newan_rest.png create mode 100755 common/static/js/vendor/ova/images/next_grouphover.png create mode 100755 common/static/js/vendor/ova/images/next_hover.png create mode 100755 common/static/js/vendor/ova/images/next_pressed.png create mode 100755 common/static/js/vendor/ova/images/next_rest.png create mode 100755 common/static/js/vendor/ova/images/previous_grouphover.png create mode 100755 common/static/js/vendor/ova/images/previous_hover.png create mode 100755 common/static/js/vendor/ova/images/previous_pressed.png create mode 100755 common/static/js/vendor/ova/images/previous_rest.png create mode 100755 common/static/js/vendor/ova/images/zoomin_grouphover.png create mode 100755 common/static/js/vendor/ova/images/zoomin_hover.png create mode 100755 common/static/js/vendor/ova/images/zoomin_pressed.png create mode 100755 common/static/js/vendor/ova/images/zoomin_rest.png create mode 100755 common/static/js/vendor/ova/images/zoomout_grouphover.png create mode 100755 common/static/js/vendor/ova/images/zoomout_hover.png create mode 100755 common/static/js/vendor/ova/images/zoomout_pressed.png create mode 100755 common/static/js/vendor/ova/images/zoomout_rest.png diff --git a/common/static/js/vendor/ova/images/fullpage_grouphover.png b/common/static/js/vendor/ova/images/fullpage_grouphover.png new file mode 100755 index 0000000000000000000000000000000000000000..3ca4e1e3c366d44e9dbbde415f6addaef941b129 GIT binary patch literal 4907 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 R`k?>-002ovPDHLkV1ijn#q9t9 literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/home_grouphover.png b/common/static/js/vendor/ova/images/home_grouphover.png new file mode 100755 index 0000000000000000000000000000000000000000..204e1cc94b5057e04a42f0d4483ba0c96c05e7cc GIT binary patch literal 4808 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 literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/newan_hover.png b/common/static/js/vendor/ova/images/newan_hover.png new file mode 100755 index 0000000000000000000000000000000000000000..d34f9f5d55fe680eabb99a83ebf25601ee801f59 GIT binary patch literal 5027 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 literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/newan_pressed.png b/common/static/js/vendor/ova/images/newan_pressed.png new file mode 100755 index 0000000000000000000000000000000000000000..d0ad47d963b30ec962cf54b85362312bd2e9bf9f GIT binary patch literal 4898 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 LNkvXXu0mjf%T$KU literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/next_pressed.png b/common/static/js/vendor/ova/images/next_pressed.png new file mode 100755 index 0000000000000000000000000000000000000000..95f169d65150b1e5162585bd0695e4617363afbe GIT binary patch literal 3503 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 literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/next_rest.png b/common/static/js/vendor/ova/images/next_rest.png new file mode 100755 index 0000000000000000000000000000000000000000..5ead544b5d7ef6de16e83f9d28aee515666fcb89 GIT binary patch literal 3061 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 DA#uj9 literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/previous_grouphover.png b/common/static/js/vendor/ova/images/previous_grouphover.png new file mode 100755 index 0000000000000000000000000000000000000000..016e63957a87778ba2562ec7a91dee22248590d8 GIT binary patch literal 2987 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 literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/previous_hover.png b/common/static/js/vendor/ova/images/previous_hover.png new file mode 100755 index 0000000000000000000000000000000000000000..d4a5c1552c7a8a5e7ddae0d2a94f235e00e44c62 GIT binary patch literal 3461 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 Z{{Y0&G1asaZ{h#|002ovPDHLkV1h%en=b$W literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/previous_rest.png b/common/static/js/vendor/ova/images/previous_rest.png new file mode 100755 index 0000000000000000000000000000000000000000..9716dac67b7ccd4c68b427eeca7de3548eed0726 GIT binary patch literal 3064 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 U<@`%&ga7~l07*qoM6N<$f_Nk7;{X5v literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/zoomin_hover.png b/common/static/js/vendor/ova/images/zoomin_hover.png new file mode 100755 index 0000000000000000000000000000000000000000..3cab721f1bd5b0360dd1eac094c36a409b517738 GIT binary patch literal 5126 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 Hu0mjf@M4@K literal 0 HcmV?d00001 diff --git a/common/static/js/vendor/ova/images/zoomout_grouphover.png b/common/static/js/vendor/ova/images/zoomout_grouphover.png new file mode 100755 index 0000000000000000000000000000000000000000..46d21b3e507e7363b4e8afd0e77343814ad63748 GIT binary patch literal 4596 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 literal 0 HcmV?d00001 From 0318c955376334293182376a8bc268bce2ed9789 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Wed, 21 May 2014 16:25:37 -0400 Subject: [PATCH 4/5] Image Annotation Tool Fixes - Added component to view - Added correct images for OSD - Fixed path to codemirror - Tinymce fullscreen works now - Pep8/Pylint Fixes - Default image changed and added call to super --- .../contentstore/views/component.py | 1 + common/lib/xmodule/xmodule/annotator_mixin.py | 24 +++++++++++++----- common/lib/xmodule/xmodule/annotator_token.py | 2 +- .../xmodule/xmodule/imageannotation_module.py | 20 +++------------ .../xmodule/tests/test_annotator_mixin.py | 5 ++-- .../xmodule/tests/test_imageannotation.py | 12 ++++----- .../js/vendor/ova/OpenSeaDragonAnnotation.js | 22 ++++++++++++++++ .../vendor/ova/images/fullpage_grouphover.png | Bin 4907 -> 698 bytes .../js/vendor/ova/images/fullpage_hover.png | Bin 5214 -> 831 bytes .../js/vendor/ova/images/fullpage_pressed.png | Bin 5213 -> 817 bytes .../js/vendor/ova/images/fullpage_rest.png | Bin 5155 -> 698 bytes .../js/vendor/ova/images/home_grouphover.png | Bin 4808 -> 672 bytes .../js/vendor/ova/images/home_hover.png | Bin 5107 -> 737 bytes .../js/vendor/ova/images/home_pressed.png | Bin 5138 -> 741 bytes .../static/js/vendor/ova/images/home_rest.png | Bin 5061 -> 672 bytes .../js/vendor/ova/images/newan_grouphover.png | Bin 4859 -> 746 bytes .../js/vendor/ova/images/newan_hover.png | Bin 5027 -> 815 bytes .../js/vendor/ova/images/newan_pressed.png | Bin 4898 -> 828 bytes .../js/vendor/ova/images/newan_rest.png | Bin 4772 -> 746 bytes .../js/vendor/ova/images/next_grouphover.png | Bin 3004 -> 656 bytes .../js/vendor/ova/images/next_hover.png | Bin 3433 -> 753 bytes .../js/vendor/ova/images/next_pressed.png | Bin 3503 -> 752 bytes .../static/js/vendor/ova/images/next_rest.png | Bin 3061 -> 656 bytes .../vendor/ova/images/previous_grouphover.png | Bin 2987 -> 661 bytes .../js/vendor/ova/images/previous_hover.png | Bin 3461 -> 755 bytes .../js/vendor/ova/images/previous_pressed.png | Bin 3499 -> 755 bytes .../js/vendor/ova/images/previous_rest.png | Bin 3064 -> 661 bytes .../vendor/ova/images/zoomin_grouphover.png | Bin 4794 -> 612 bytes .../js/vendor/ova/images/zoomin_hover.png | Bin 5126 -> 608 bytes .../js/vendor/ova/images/zoomin_pressed.png | Bin 5172 -> 615 bytes .../js/vendor/ova/images/zoomin_rest.png | Bin 5041 -> 612 bytes .../vendor/ova/images/zoomout_grouphover.png | Bin 4596 -> 504 bytes .../js/vendor/ova/images/zoomout_hover.png | Bin 4931 -> 559 bytes .../js/vendor/ova/images/zoomout_pressed.png | Bin 5007 -> 561 bytes .../js/vendor/ova/images/zoomout_rest.png | Bin 4811 -> 504 bytes .../js/vendor/ova/richText-annotator.js | 2 +- lms/templates/imageannotation.html | 10 ++++---- 37 files changed, 60 insertions(+), 38 deletions(-) mode change 100755 => 100644 common/static/js/vendor/ova/images/fullpage_grouphover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/fullpage_hover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/fullpage_pressed.png mode change 100755 => 100644 common/static/js/vendor/ova/images/fullpage_rest.png mode change 100755 => 100644 common/static/js/vendor/ova/images/home_grouphover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/home_hover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/home_pressed.png mode change 100755 => 100644 common/static/js/vendor/ova/images/home_rest.png mode change 100755 => 100644 common/static/js/vendor/ova/images/newan_grouphover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/newan_hover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/newan_pressed.png mode change 100755 => 100644 common/static/js/vendor/ova/images/newan_rest.png mode change 100755 => 100644 common/static/js/vendor/ova/images/next_grouphover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/next_hover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/next_pressed.png mode change 100755 => 100644 common/static/js/vendor/ova/images/next_rest.png mode change 100755 => 100644 common/static/js/vendor/ova/images/previous_grouphover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/previous_hover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/previous_pressed.png mode change 100755 => 100644 common/static/js/vendor/ova/images/previous_rest.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomin_grouphover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomin_hover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomin_pressed.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomin_rest.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomout_grouphover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomout_hover.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomout_pressed.png mode change 100755 => 100644 common/static/js/vendor/ova/images/zoomout_rest.png diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index a2acd86d4f..dd7c7c3815 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -54,6 +54,7 @@ else: 'annotatable', 'textannotation', # module for annotating text (with annotation table) 'videoannotation', # module for annotating video (with annotation table) + 'imageannotation', # module for annotating image (with annotation table) 'word_cloud', 'graphical_slider_tool', 'lti', diff --git a/common/lib/xmodule/xmodule/annotator_mixin.py b/common/lib/xmodule/xmodule/annotator_mixin.py index 8b12263580..9c4875e462 100644 --- a/common/lib/xmodule/xmodule/annotator_mixin.py +++ b/common/lib/xmodule/xmodule/annotator_mixin.py @@ -2,12 +2,12 @@ Annotations Tool Mixin This file contains global variables and functions used in the various Annotation Tools. """ -from pkg_resources import resource_string from lxml import etree from urlparse import urlparse from os.path import splitext, basename from HTMLParser import HTMLParser + def get_instructions(xmltree): """ Removes from the xmltree and returns them as a string, otherwise None. """ instructions = xmltree.find('instructions') @@ -17,8 +17,9 @@ def get_instructions(xmltree): return etree.tostring(instructions, encoding='unicode') return None + def get_extension(srcurl): - ''' get the extension of a given url ''' + """get the extension of a given url """ if 'youtu' in srcurl: return 'video/youtube' else: @@ -26,20 +27,29 @@ def get_extension(srcurl): file_ext = splitext(basename(disassembled.path))[1] return 'video/' + file_ext.replace('.', '') + class MLStripper(HTMLParser): "helper function for html_to_text below" def __init__(self): + HTMLParser.__init__(self) self.reset() self.fed = [] - def handle_data(self, d): - self.fed.append(d) + + def handle_data(self, data): + """takes the data in separate chunks""" + self.fed.append(data) + def handle_entityref(self, name): + """appends the reference to the body""" self.fed.append('&%s;' % name) + def get_data(self): + """joins together the seperate chunks into one cohesive string""" return ''.join(self.fed) + def html_to_text(html): "strips the html tags off of the text to return plaintext" - s = MLStripper() - s.feed(html) - return s.get_data() \ No newline at end of file + htmlStripper = MLStripper() + htmlStripper.feed(html) + return htmlStripper.get_data() diff --git a/common/lib/xmodule/xmodule/annotator_token.py b/common/lib/xmodule/xmodule/annotator_token.py index 129315739c..501917cc3d 100644 --- a/common/lib/xmodule/xmodule/annotator_token.py +++ b/common/lib/xmodule/xmodule/annotator_token.py @@ -25,7 +25,7 @@ def retrieve_token(userid, secret): delta = dtnow - dtutcnow newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60) # pylint: disable=E1103 newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin) # pylint: disable=E1103 - # uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a + # uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a # federated system in the annotation backend server custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400} newtoken = create_token(secret, custom_data) diff --git a/common/lib/xmodule/xmodule/imageannotation_module.py b/common/lib/xmodule/xmodule/imageannotation_module.py index 1875b3d668..31f86a0f82 100644 --- a/common/lib/xmodule/xmodule/imageannotation_module.py +++ b/common/lib/xmodule/xmodule/imageannotation_module.py @@ -1,3 +1,4 @@ +# pylint: disable=W0223 """ Module for Image annotations using annotator. """ @@ -31,20 +32,7 @@ class AnnotatableFields(object): showNavigator: true, navigatorPosition: "BOTTOM_LEFT", showNavigationControl: true, - tileSources: [{ - Image: { - xmlns: "http://schemas.microsoft.com/deepzoom/2009", - Url: "http://static.seadragon.com/content/misc/milwaukee_files/", - TileSize: "254", - Overlap: "1", - Format: "jpg", - ServerFormat: "Default", - Size: { - Width: "15497", - Height: "5378" - } - } - },], + tileSources: [{"profile": "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level2", "scale_factors": [1, 2, 4, 8, 16, 32, 64], "tile_height": 1024, "height": 3466, "width": 113793, "tile_width": 1024, "qualities": ["native", "bitonal", "grey", "color"], "formats": ["jpg", "png", "gif"], "@context": "http://library.stanford.edu/iiif/image-api/1.1/context.json", "@id": "http://54.187.32.48/loris/suzhou_orig.jp2"}], """)) @@ -100,7 +88,7 @@ class ImageAnnotationModule(AnnotatableFields, XModule): 'display_name': self.display_name_with_default, 'instructions_html': self.instructions, 'annotation_storage': self.annotation_storage_url, - 'token':retrieve_token(self.user, self.annotation_token_secret), + 'token': retrieve_token(self.user, self.annotation_token_secret), 'tag': self.instructor_tags, 'openseadragonjson': self.openseadragonjson, } @@ -120,4 +108,4 @@ class ImageAnnotationDescriptor(AnnotatableFields, RawDescriptor): ImageAnnotationDescriptor.annotation_storage_url, ImageAnnotationDescriptor.annotation_token_secret, ]) - return non_editable_fields \ No newline at end of file + return non_editable_fields diff --git a/common/lib/xmodule/xmodule/tests/test_annotator_mixin.py b/common/lib/xmodule/xmodule/tests/test_annotator_mixin.py index 09d216b709..a33c05c32d 100644 --- a/common/lib/xmodule/xmodule/tests/test_annotator_mixin.py +++ b/common/lib/xmodule/xmodule/tests/test_annotator_mixin.py @@ -7,6 +7,7 @@ from lxml import etree from xmodule.annotator_mixin import get_instructions, get_extension, html_to_text + class HelperFunctionTest(unittest.TestCase): """ Tests to ensure that the following helper functions work for the annotation tool @@ -47,6 +48,6 @@ class HelperFunctionTest(unittest.TestCase): self.assertEqual(expectednotyoutube, result1) def test_html_to_text(self): - expectedText = "Testing here and not bolded here" + expectedtext = "Testing here and not bolded here" result = html_to_text(self.sample_html) - self.assertEqual(expectedText, result) \ No newline at end of file + self.assertEqual(expectedtext, result) diff --git a/common/lib/xmodule/xmodule/tests/test_imageannotation.py b/common/lib/xmodule/xmodule/tests/test_imageannotation.py index 5a6710b7a0..c3cb9aaef5 100644 --- a/common/lib/xmodule/xmodule/tests/test_imageannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_imageannotation.py @@ -28,11 +28,11 @@ class ImageAnnotationModuleTestCase(unittest.TestCase): Image: { xmlns: "http://schemas.microsoft.com/deepzoom/2009", Url: "http://static.seadragon.com/content/misc/milwaukee_files/", - TileSize: "254", - Overlap: "1", - Format: "jpg", + TileSize: "254", + Overlap: "1", + Format: "jpg", ServerFormat: "Default", - Size: { + Size: { Width: "15497", Height: "5378" } @@ -41,7 +41,7 @@ class ImageAnnotationModuleTestCase(unittest.TestCase): ''' - + def setUp(self): """ Makes sure that the Module is declared and mocked with the sample xml above. @@ -75,4 +75,4 @@ class ImageAnnotationModuleTestCase(unittest.TestCase): """ context = self.mod.get_html() for key in ['display_name', 'instructions_html', 'annotation_storage', 'token', 'tag', 'openseadragonjson']: - self.assertIn(key, context) \ No newline at end of file + self.assertIn(key, context) diff --git a/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js b/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js index 8a1b939340..0fdd10b70a 100644 --- a/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js +++ b/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js @@ -782,6 +782,28 @@ OpenSeadragonAnnotation = function (element, options) { //Set annotator.editor.OpenSeaDragon by default this.annotator.editor.OpenSeaDragon=-1; + + function reloadEditor(){ + tinymce.EditorManager.execCommand('mceRemoveEditor',true, "annotator-field-0"); + tinymce.EditorManager.execCommand('mceAddEditor',true, "annotator-field-0"); + } + + var self = this; + document.addEventListener("fullscreenchange", function () { + reloadEditor(); + }, false); + + document.addEventListener("mozfullscreenchange", function () { + reloadEditor(); + }, false); + + document.addEventListener("webkitfullscreenchange", function () { + reloadEditor(); + }, false); + + document.addEventListener("msfullscreenchange", function () { + reloadEditor(); + }, false); this.options = options; diff --git a/common/static/js/vendor/ova/images/fullpage_grouphover.png b/common/static/js/vendor/ova/images/fullpage_grouphover.png old mode 100755 new mode 100644 index 3ca4e1e3c366d44e9dbbde415f6addaef941b129..2cf72a3d73059148379a595f01ebabca22a673bc GIT binary patch literal 698 zcmeAS@N?(olHy`uVBq!ia0vp^%0R5d!3-oHMNc>fq?n7HJVQ7*IBq}me*okj2=EDU z{r~?zkckG~ym|Bd`SYhwpFV#4_~FBc_wL=hef##!n>Vjsy?XKD#WQEloIH8*`0?XM zj~?B-ckj-fJGXD&zIN@})vH&pT)A?=f(3Ku&Yd)A(!_}q+uPe)TU(o(o9pW8s;a8W z%F0SgN^){?va+(0l9Cb<5uV23&XeLl` zT3Q-VaZF51baZq?M1-%euZxR|jg5_!m6fXc;e|lgJCp?Z1v3aL8M}n0?Ir@eg94!U z5kQu+svBq)XMsm#F#`j)5C}6~x?A@LC@5Fr8c`CQpH@h(+(&E3wf{1`-X4vWqA9hew5JxP)905?t=o@$Y~A zRWoJ5n-*^^tN7n#-#)l)-a=lU%ULJ4&3$-w#r?&+>r7))F8ts#Sd#5==ZRzDi3_UF zPPU$zq2{z>Rpv6|6#>bSSKjmsMn!F1a_sY!$(Av*rbyjiKeP5=)WhKa_XIXQy2_*Y zoK3K4cIVmaN%u0veyZ8|y*^eMFzer}pUX~IczG>6SQwV0>1EV;@Z+>aDyLK17RG*y z_2BI*PmL>)*cEs&QD|3SyXCq|SruQT)}3B8Yh$jJ{(qmUviYywLIesPTL=la-QiJX zam~MTr%(OemCyRsd~x=6%6V?z``@j(+w%S*cl~mPRpF09{@rV2TByb4^e_JlW55wc aw>pNElNxSxuo%dI(wnENpUXO@geCw3Bu~iz literal 4907 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 aw>pNElNxSxuo%dI(wnENpUXO@geCw3Bu~iz literal 5155 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 R`k?>-002ovPDHLkV1ijn#q9t9 diff --git a/common/static/js/vendor/ova/images/home_grouphover.png b/common/static/js/vendor/ova/images/home_grouphover.png old mode 100755 new mode 100644 index 204e1cc94b5057e04a42f0d4483ba0c96c05e7cc..bba978a57aa5722aa53d965019ae00adc3363b35 GIT binary patch literal 672 zcmeAS@N?(olHy`uVBq!ia0vp^%0R5d!3-oHMNc>fq?n7HJVQ7*IBq}me*okj2=EDU z{r~?zkckG~ym|Bd`SYhwpFV#4_~FBc_wL=hef##!n>Vjsy?XKD#WQEloIH8*`0?XM zj~+dE@ZjFPdw1^KxqbWgwQJX|S+i!viWLhMESNiY?!<``+uPe)TU(o(o12=N>gwvM zs;bJ$%1TN~a&mIAva*trk`fXUJUl#r8ka0tGIQokpq7Gy0-#`IWMp`Fcu-J~kB^V5 zt1Hlapn*U`fU-_bPL7U_Ha0evmX>B_X3tMbUIn_|p(MyJm_bm<*d;V=H!%R{i8FWK zfdMcGAfWF=bUe^3&H|6fVg?3oArNM~bhqvgP*ASKHKHUqKdq!Zu_%?nF(p4KRlzN@ zD78GlD7#p}IoMRsaJ9V0YX$~JMo$;V5R2ZuSF-b)3`80dHB+6}8M)4#-H@EK=n?B( z!TiVn|7&|+Wl4*kKJ(4r8#X61?dM!8*}6_zP5JaDX@A|7ZmaB9Nu9TN%cnJAnbn_p z)kTdZz6o)^cNnxO8q8}?P4Wp&sEOa1+4P|Qm~ZfPMgd84R*%?)GuJe1xy5( literal 4808 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 Nc)I$ztaD0e0ssiSHEI9= literal 5107 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 Yw4HU{D{j&ZVAwHuy85}Sb4q9e0H?=h6951J literal 5138 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 literal 5061 zcmV;$6FTgPP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000Q^Nkl>4uobimum$@duR0J|H3S5FQXuAgWe8v_NSQtSY5ZM7L_PNlP!mi5=Uq$8$ZGePG6& zxCU0sNIxBWe0Jwx zc`E!IK`Gz&RWCp^N+|*0e}joZ5C9Ya3&-d)iNPoU5`c)0x~?k#YGY$VMF4U6Lzq4g zS||wsi5FgYA${!Fu_530Dd(JAx^$`e%{SjzApi#ugu+9Sxm->GP!AtIRJ}0dw|piB z5{EF(oHfLd5s z=mJeoPxpt3fv^xlf9l#+1HscqZMAAa~@ z_T0I1*DqhbTm@hQZ~^!skN}`&XJ?g^63;*X{3Dp?=@?_ZAjVf;eYNlK;luxAjOD}M zd?{rRJz?aektPWt8UWpK99FB<)<65~vrF&4|NadCoe;{416{dtMIAkQ^pVd5K@iz4 zZW`nA<;#EDvuDpgD5XUyrLUCoLah)02|@^s9c2YThM<9A82a$=@Zj;|$6utBrrvw+ zy?@0{Uw3||(@_XF1ps)S2h%j8i4;KMz7FpQ#<(v?z5rIbMk$x=!Sl+q$&Y>+WF#26c-lokjf10kdiKvzm7gbnQ%WTOn2fP4AAb1Zc1_dp`s=SdSFc_*?%lgbZ{NP1 zedU!`a-~vfH3)*bQmTbt9Y_cv0?xTTIXPJ&~m&t0iN=?&*=XrEyW+p#3H#e^9`V*R_HS1UhWD#Wy6c|j*-}c)FpMod4ov&yCIWzR&NG~I-e@%HwrxK& zP4iBvRN8+2{Q0RHH*Oe1LqixF8-uRvn_!~QUaQrxu&}@`Uc6X3bLPzQ-9Pzgd{xAgKgWg*=&04cH8niuhYATl+usoD8yoy)oQiuR;%T^t_wm) z;=zLlq|sHV92ths1Q;y>Z*L4HW^PJvIgCKB%Ah3Jy zd!Famw(YyFt2pPxFpSjo>(_%yrBVy)91%~py=IaC_>VvSSSgpwzwF+=n+d@w*Mr9FV$vaGgk+uZkk3=9ky>+9>q+1XjE)oRsZr9@QU zY=ViGi{8@G()_h+*G3N=I#k%$*r+&;BO8r|T`U%tKKkgRYne z%*<2_!!S7KJP3ld)z#H%tJU(AQmjxY&($VHkta^wiU|Ni?Z0BK>kP6HSi85wytolgJdSuw2&-q?D3U zN=?($A3S)FxpnK7{>2wxc(q!s5<;wp5Y5mukpuHsta+rFa=8pCB^DPK<1I=e)Q}mL zi&;u((KO9xD5YZnvSVXoG?U4YR;#6c{`qGqgs`NP4JqaQm|wKQF*1+W1F{)r+qP}p zM7OfC(%T_KPHl#oiX=%0$pc6#r9`+HkM&7*Oq_nLN<4CRVMazqHnp40X0x}QO@#G_ z5feRDSV!%x2p!Y!Y2f@mQFl`c>B8i4xyPDv*LC}~w-Hz()@kfq?n7HJVQ7*IBq}me*olO3h)VW z{r~?zkckG~ym|Bd`SYhwpFV#4_~FBc_wL=hef##!n>Vjsy?XKD#WQEloIH8*`0?XM zj~?B-ckj-fJGXD&zIN@}HEY(aSg~UM{P}a|&Yd`MVtadgYinzBb8}r?T~$?8Sy@?0 zNl8vlPF7Y{Qc_YvLV|~f$JVV|f$ICg0H~~>pdc+REh;K1C@9Fw%ge>Z1!(w&4I3cF z&YU?Ds1Rr-P$kebpiw}@At50^Wi~c8R#sN#=H|x6#{Px(jspGTRubeF%pj;_>=K%` zn=k-+>h9gU??3<;B&a}T+ObxkiJS!IQ+eo=O@f^)E`p5bbFkJk(gjK-cWjv*Gktyglxn+!zQ3uG5haMxVx z)7a6)q{z}~r2XXK@A{=To_Vg!{TqMa+|K=nciu5%>oN%n=@p7KKbAiE;eyjILMBBX zn-j;-l5#TTd6t^Ok`pq<@eU~=QD!q5G5)c|e%~FZUg`8&q(yF5DLrH4 zS$(1_kK^W4(S7PB!jsr@KKXCr&`j&q4&A5Q5gE+0vPSf+x92pU&$~45-}{+1eQ`+4 z=f+u@1=o33KTov5g)D9eh5_TZ`R!EeqRWc}vMs1`J}ogDbx#N&Ia(RtOsw+$An z_&(mdPV$+$Oz@Z0f2-7z;ux~O9+4z06=<WDR*FRcSTFz- zW=q650N5=6FiBTtNC2?60Km==3$g$R3;-}uh=nNt1bYBr$Ri_o0EC$U6h`t_Jn<{8 z5a%iY0C<_QJh>z}MS)ugEpZ1|S1ukX&Pf+56gFW3VVXcL!g-k)GJ!M?;PcD?0HBc- z5#WRK{dmp}uFlRjj{U%*%WZ25jX z{P*?XzTzZ-GF^d31o+^>%=Ap99M6&ogks$0k4OBs3;+Bb(;~!4V!2o<6ys46agIcq zjPo+3B8fthDa9qy|77CdEc*jK-!%ZRYCZvbku9iQV*~a}ClFY4z~c7+0P?$U!PF=S z1Au6Q;m>#f??3%Vpd|o+W=WE9003S@Bra6Svp>fO002awfhw>;8}z{#EWidF!3EsG z3;bXU&9EIRU@z1_9W=mEXoiz;4lcq~xDGvV5BgyU zp1~-*fe8db$Osc*A=-!mVv1NJjtCc-h4>-CNCXm#Bp}I%6j35eku^v$Qi@a{RY)E3 zJ#qp$hg?Rwkvqr$GJ^buyhkyVfwECO)C{#lxu`c9ghrwZ&}4KmnvWKso6vH!8a<3Q zq36)6Xb;+tK10Vaz~~qUGsJ8#F2=(`u{bOVlVi)VBCHIn#u~6ztOL7=^<&SmcLWlF zMZgI*1b0FpVIDz9SWH+>*hr`#93(Um+6gxa1B6k+CnA%mOSC4s5&6UzVlpv@SV$}* z))J2sFA#f(L&P^E5{W}HC%KRUNwK6<(h|}}(r!{C=`5+6G)NjFlgZj-YqAG9lq?`C z$c5yc>d>VnA`E_*3F2Qp##d8RZb=H01_mm@+|Cqnc9PsG(F5HIG_C zt)aG3uTh7n6Et<2In9F>NlT@zqLtGcXcuVrX|L#Xx)I%#9!{6gSJKPrN9dR61N3(c z4Tcqi$B1Vr8Jidf7-t!G7_XR2rWwr)$3XQ?}=hpK0&Z&W{| zep&sA23f;Q!%st`QJ}G3cbou<7-yIK2z4nfCCCtN2-XOGSWo##{8Q{ATurxr~;I`ytDs%xbip}RzP zziy}Qn4Z2~fSycmr`~zJ=lUFdFa1>gZThG6M+{g7vkW8#+YHVaJjFF}Z#*3@$J_By zLtVo_L#1JrVVB{Ak-5=4qt!-@Mh}c>#$4kh<88)m#-k<%CLtzEP3leVno>={htGUuD;o7bD)w_sX$S}eAxwzy?UvgBH(S?;#HZiQMoS*2K2 zT3xe7t(~nU*1N5{rxB;QPLocnp4Ml>u<^FZwyC!nu;thW+pe~4wtZn|Vi#w(#jeBd zlf9FDx_yoPJqHbk*$%56S{;6Kv~mM9!g3B(KJ}#RZ#@)!hR|78Dq|Iq-afF%KE1Brn_fm;Im z_u$xr8UFki1L{Ox>G0o)(&RAZ;=|I=wN2l97;cLaHH6leTB-XXa*h%dBOEvi`+x zi?=Txl?TadvyiL>SuF~-LZ;|cS}4~l2eM~nS7yJ>iOM;atDY;(?aZ^v+mJV$@1Ote z62cPUlD4IWOIIx&SmwQ~YB{nzae3Pc;}r!fhE@iwJh+OsDs9zItL;~pu715HdQEGA zUct(O!LkCy1<%NCg+}G`0PgpNm-?d@-hMgNe6^V+j6x$b<6@S<$+<4_1hi}Ti zncS4LsjI}fWY1>OX6feMEuLErma3QLmkw?X+1j)X-&VBk_4Y;EFPF_I+q;9dL%E~B zJh;4Nr^(LEJ3myURP{Rblsw%57T)g973R8o)DE9*xN#~;4_o$q%o z4K@u`jhx2fBXC4{U8Qn{*%*B$Ge=nny$HAYq{=vy|sI0 z_vss+H_qMky?OB#|JK!>IX&II^LlUh#rO5!7TtbwC;iULyV-Xq?ybB}ykGP{?LpZ? z-G|jbTmIbG@7#ZCz;~eY(cDM(28Dyq{*m>M4?_iynUBkc4TkHUI6gT!;y-fz>HMcd z&t%Ugo)`Y2{>!cx7B7DI)$7;J(U{Spm-3gBzioV_{p!H$8L!*M!p0uH$#^p{Ui4P` z?ZJ24cOCDe-w#jZd?0@)|7iKK^;6KN`;!@ylm7$*nDhK&GcDTy000JJOGiWi{{a60 z|De66lK=n!32;bRa{vGf6951U69E94oEQKA00(qQO+^RY3kwkn4#E~6ivR!!ElET{ zR9M5Un15`Pn8-#gqLa4kiwKqaYQhH=$sGT6-Ik0lN;>f%g*>zoj{|+VuApi}~y|aHULNE$&fm-++i^UY6ip8Sx0mAZ?FbhCL zJc$C)9Xoa`*}Qr48r!xt*LB5v@4Yv2^ytw^4?qHrC(;u)l}af=&Db>A$y_Y@5uee}_jZEbC@X__|2 z`NBctUu&9%5CS0trfD)cIZ01X&o7^S_SxgWEKmU~&ule7_4M?p2OoS8&0oxM9BkXJ zH&4U)^XGrs+S>Y>e~)EZIF5sql3J}6*xZMdQU>=$A`zOJnz;Y|`+s%#@Zo1X%`5^M z!seQtot>+ha=A=0ne??10gSG$u1)Rj?I#k61X9Yt(flW0D364->}yIXMSp)kCr_UI z`GErm&HyD(OP1&03Q$2bS(Zh}JW-(G_S}&NZ_05w2 zmi6}bUdre5YIt~94G#~iFTVIfH8wV>BS(&?n;oO0qw49WpHYpCO{%^9UNtl{qy`5E z19|DCmj;0iKpU{k+b;r$`lKg-$o~EN?~cV{cZIbgr9=pUZQBeC3{YQwG)?2|*&g0} z^LM=d`eD-PG`ZXeuImPlZWsnlO-*-f*s$T7*=+XjUSvf)4b92GK@_cCy?RqaLj$&L zQ>)bwLLj9C2qGp76NyAPckUdgPru9EcdzHU=bq!Ox85R^N>Zs*QA#1D#57HoFJI2e zl`FrU&1OFaV&SOL>NMlmnst_CF*!NO?Cfk1wjk!!lvpfAcXv14-QBdbw9wVnMO#}N z0Q>gs52DF5O-iK_nx>)a`W>P0_Ujzl0%p=|+isFlQYw|m=kp8>4k8@~A)r=sk&Z)6 zN^Hv_lgV)UbQibXb{p@!bCQQ1dWc=ScG2D4&9P(07#tiVpU*QsK2E7rLI{E5I7{k4 z#6pEq0HRzjyS8m(+cuVEA*H1M(@z0`lwT$^0la(qG~?GMXkNCAKOE^~`;WJ=qoaeV zLIGXZi9{k)Dir`fq2jtOGcz-4fr>v*GXNBe#VOM?snu%OwvB0;;m`*lbLI@aANB!? z%=_>2_zxfFvBw@`x>!Uh6*M{ie|`Tem&=sP-9F^)TUf(aPm=e>`{Y z-1-L|cz~Ii82~yuIym_2S1=6YD_d2|vUuvLr-I_Tc=2L7J3E=2ocx=Y>`bpv2HIK3 zXC&ZcGMQsasqd~`yY~Bw7A^7?O!4B2zu?-niJO8brC7Rj35H?dI1Y;!FJ^dnm~1xt z;rRG?2AJ`H{Mt0vXC8g@(XeK0z|7@xM>{(^F$^PcWXEwxrOK8>sg8_h)mt+}=Zn4iSw;X=-Z1_aW0XsaC5C zifNh%A!uxDL`uo7UArh03cs&ZD*phcJ(#Me88=9BevR3|Z024r= zuDSBfqym`s_I7Hu8lQafNx1(tyb}M7NF>snPN$zwCX-J*{P4r9Tepr>DutgMbzLVO zk25wl#-&S_ICbh2g+d`CrTmjpYRrR}2Ff7^mZ4m8T{COfuElj-1_lO1-E`34b@aD* zP`Ae8@h5~3-%(1nu2`{xL?S`CTxMisgj%gOpp?4cy6zdJRNmXX!?t_cY~;p=sJGAgPq9Dy6P^n^>MyLQ)Mm?L3eFN4$ zHtO{s4Iznn-|M0Aw&ppe9fGn#ZDu&@&gswsm{ck?ZzwlSbD{Cp2aATr-TGNu){VEd zP^MK2fy{w13}fDaR@Wbgq3VZ!t*)fFb;JKnA?Cs8x;`gNZ*>1nUWv@B{^3_5H-nh> h0_Xp5h_AcAe*oK7cPO`-jQs!r002ovPDHLkV1fgqM&bYf diff --git a/common/static/js/vendor/ova/images/newan_hover.png b/common/static/js/vendor/ova/images/newan_hover.png old mode 100755 new mode 100644 index d34f9f5d55fe680eabb99a83ebf25601ee801f59..997c848dd7deda92c873c4fe8329fc2ecb279755 GIT binary patch literal 815 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 diff --git a/common/static/js/vendor/ova/images/newan_pressed.png b/common/static/js/vendor/ova/images/newan_pressed.png old mode 100755 new mode 100644 index d0ad47d963b30ec962cf54b85362312bd2e9bf9f..9f4a4eb830eef0cbec90d877fa6a5126281f09f1 GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^%0R5d!3-oHMNc>fq?n7HJVQ7*IBq}mf55=N$Qs}i z;`;ypf1nH+xcYy`$^T1_{-1yF|MY|ZXYTz!Y2W|JyZ`s?{NJ_pf6JEtjjR6`ul--P z>VM(#|2fP4XD|AfvfyvhoZm4$|Lte}h@Sa9a@zN>DPM#8zj{x27u5eIp!2my`%Cxc zhc1l|oEz>t*57rkx^7o>#lGsYec4I-(&P3;`|S(&+86G&&);sJx79v(qkYbL`|Nf0 z8B6Wc7u%=IwojgEAJuGM|G>8Bh)u>?DKJ5d<7pE2 zkR#?ASL7Mqu!902TZIBwi~22)@R=&**(K{%ui#v&Y!|0t;i+e4XJBq?Vrpp&1PhH! z&j5oUyd=mkm_bm<*d;V=H|gNanKO6q-hBrKz+goM>o2E-0IlFG@Q5sCVBi)4Va7{$ z>;3=*{(+%k(&%kzt}ixr%MP4x^{%X_>AMwz3hi(`mI zZ|jxp;3fwVwuIKDTh@peT-GbwzoKF4(nbwdq0}&rkN@kv-bAWjF5CW{`OTTiHp}9& z>1V>~_67t!*&ld%>Z>~2*`AZ*&*q9qrhWeqaUerCJo)*Tm?IgR4;W78ZSh@s!tkt^ zQ1D_6nZ=nxW}D6!?%J2LMqU3~%PHvbXcyz+4^a(cYDJM=HpcL=v>gTe~DWM4fo<)J^ literal 4898 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 literal 3433 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 LNkvXXu0mjf%T$KU diff --git a/common/static/js/vendor/ova/images/next_pressed.png b/common/static/js/vendor/ova/images/next_pressed.png old mode 100755 new mode 100644 index 95f169d65150b1e5162585bd0695e4617363afbe..173734c7771b6fabf943951a29960e50be76a852 GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^%0R5d!3-oHMNc>fq?n7HJVQ7*IBq}mf55=N$Q0lc z;`;ypf1nH;OrD$yWas7iMnu^A`dWH>+qt`&ySSJ;Ihi{;S~@tG|Np<^|Nr^_|1bUj zfAatTGynf@`TxJ`|Nja9|M&g>U-tih{_uU~sSeCYDvf%E{i|1Y=g-@mK5cXI zq|L#D)(aL`_V!wIbXc^vTQoOYG&Wk))|zKzn8n4Jg@u`hgqQ{enFa)y`uLc*x|%pS znOImD^R(<;4D@qYNswPKgP@YJOK50XITY+B5u5>myAbdW0)PR$cJ|`^KpQvL0dmt5lE zB+)O$>?}4xN;A+VtEVS&Mq`$W>cF>W%2&Sm(f@Q|Ion&GC=CX=$;uasyLbot<7mU3yYl;$hy115eqMg^leF9650U Zm>fl?zl(kS8I*(>JYD@<);T3K0RReST;Biy literal 3503 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 diff --git a/common/static/js/vendor/ova/images/next_rest.png b/common/static/js/vendor/ova/images/next_rest.png old mode 100755 new mode 100644 index 5ead544b5d7ef6de16e83f9d28aee515666fcb89..de92d99d134552234286043b3d12ae52b1ac4cf2 GIT binary patch literal 656 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 DA#uj9 diff --git a/common/static/js/vendor/ova/images/previous_grouphover.png b/common/static/js/vendor/ova/images/previous_grouphover.png old mode 100755 new mode 100644 index 016e63957a87778ba2562ec7a91dee22248590d8..7b7f6db7f6fdb68c7edcaf04285d6dcf2dbb7d2e GIT binary patch literal 661 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 diff --git a/common/static/js/vendor/ova/images/previous_hover.png b/common/static/js/vendor/ova/images/previous_hover.png old mode 100755 new mode 100644 index d4a5c1552c7a8a5e7ddae0d2a94f235e00e44c62..9547e65b79cff5f23bc0dfb3b8c2a47964886acc GIT binary patch literal 755 zcmeAS@N?(olHy`uVBq!ia0vp^%0R5d!3-oHMNc>fq?n7HJVQ7*IBq}me*onE5AX?b z{r~?zkO>D`S|Fx^0;7R}vW||5mX?B+mYkNBl$MsLmX?f$2EU35$AJTt2M#10I1qa9 zpvQp&-Ukl2A2?uk;DE(}12zW^*dI7xv~Qo<>(7;ev z$52zlP*TEBRK!qN$dH!Ckd(v_8_N(L&fx6KuxS(P{P{eyXY;hnhKG4dN}ED9I(5F;k+|}Pn2y2}-8t?f^-g)icE|)FdzBRgW!9M}l^1C9On{wM$hN&d&vTfU`$10Os8-gU-jZd#ho zI%e=m)3p41_|-+LT6SwZd9udm`IiR++vBP9T!g=KNoV Y(>~KV4<+@k1cn-er>mdKI;Vst084K_m;e9( literal 3461 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 literal 3499 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 Z{{Y0&G1asaZ{h#|002ovPDHLkV1h%en=b$W diff --git a/common/static/js/vendor/ova/images/previous_rest.png b/common/static/js/vendor/ova/images/previous_rest.png old mode 100755 new mode 100644 index 9716dac67b7ccd4c68b427eeca7de3548eed0726..7b7f6db7f6fdb68c7edcaf04285d6dcf2dbb7d2e GIT binary patch literal 661 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 literal 4794 zcmV;r5=HHaP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000N&Nkl4Zdn$eo}T{K!-o$q96We%D}ZhQ zZ2*?W|JURBBmm^;)2A38AII3(*t=m;APN8jKSa0U-pg>tb$h4nspjKR^1_7+KaMri z06lYC)p69_Z46#im0Lrafw?102V#Tj0rSN?pK@cPh#|~a~78(+OK@fm* zj*gBF^!E1t?C8;>AIH8UOho~pzrUZO>JpuIizVC zT-U7w5h6(v48uS}Ljw%MsDcq13d0bLG1#_^?(S~1wY7a3`;7rWO$AUQ)y6ph@|iPd zzOAb2hr$`t+DHXN5JI478VtjLuItH85{Gz!Fboj{0Vt)&=kp(I*|OzZDHyrVOzB;; zYSpTbHBGBBj{u;QLQxbbiULY0R8@tp>(F%_lv2pDocK{X6fnl%x-ME8P?ris43KJ3`B1I1zyzVAcRG!Q}%1VKv;2zeI_DVNKU@B8q5 zAD-udF@|!vjERW}T)A?kE}^!!x1&%f)IHB;v)Ht06CB3@V+@24L{WrFrNS4<_={)l zQmIrFyU6$b#2h_6Jve^+I21*Js;U5hix)4VP$;0OsR_r99ZRBl5CmAgdNpj@hUa+* z!w`z1pi-%zTrQUvTelX51K_i>vtOB}`9l%#2q9pMp|P0oZ2M*xXt5>L0Dv4Gt%Ss|~CX>PBFe!jmUYCT3@6p91jH zMOST{NtcV>)YR0U?%%(U=H})^Gih;M7nWs#F_uK#QmF*Taq7lKQH18^W?Z{=4RdpI ze~ZfqH!dX-?T}Ok7D-Z#Giz?=&Yi#8w{PDMq9}rCns3WPAn}{#=4NP`h9C$~EEW@g zNdlK7Vabvuuq+D$0|WnjN}=mIve_(JT3T@L-aTBsdKHf2{5A-JI{;<@m;kJ}1Qq2cukx9` zzCMIuh{ul~r|VxmE`*w7Szf+$>C&HOv)Rx3`}@(`+lyQ-my{TkQW%DTnVA`kj*jBy z&6_YybC5CiXU_Rd3^5O&oN{26iZyRFvu@owL{Wri&z_N*?m&yvePax@!Z3`_2qE9+ zoOidiwIP$qpj<9va&i)37(U~ie;Gy59nSf5ytoygg?YRhkZPEfD_17<$IF*5YdZwd zvKXNAEOttql+)IMRKYA?zPzg4oS&bsZD*Bu+iNLNQ-!r? zZ^PIz{S=fJ-`7Uns){Xv$>nl&1-I+Ei?z1`Oi8u7wY@g0X>Y?+q~(i&RKe)FUe}=2 zbeL(V+U8%XNhwiH^Z#CmIv7f6)i!liDo#5XC0P96f U<@`%&ga7~l07*qoM6N<$f_Nk7;{X5v diff --git a/common/static/js/vendor/ova/images/zoomin_hover.png b/common/static/js/vendor/ova/images/zoomin_hover.png old mode 100755 new mode 100644 index 3cab721f1bd5b0360dd1eac094c36a409b517738..206d2055409ae677eecd516c3bb0f5f038c6b7b4 GIT binary patch literal 608 zcmeAS@N?(olHy`uVBq!ia0vp^%0R5d!3-oHMNc>fq?n7HJVQ7*IBq}me*om}4e$wZ z{r~?zkO>D`S|Fx^0;7R}vW||5mX?B+mYkNBl$Mr^mX@f72EU35$AJTt2M#10I1qa9 zpvQp&-Ukl2A2?uk;DE(}12zW^*dI7xv~Qo<>(7;ev$52zlP*TEBRK!qN$dH!C zkd(v_8_N(L&fx6K(9t2Ak)iM6qU-EzZDwY~{qABG(7E;{L4Lsuf=b3Np`mHzFtD3g zaQEH2|Nj@QNq+`3inG8YvY3H^TL^?1FWs&C0~C}iag8Vm&QB{TPb^Aha7@WhN>y;n zEJ`iUFUl@fa1J)rGh8k2@fxV|sHcl#h(+(yEAISF1`-X4({_mSxdaq)cj$1R`1Rl3 zJIYd^@zv3cN7Y%gpI=%M&w0DJF?r_Ep3{-G?>v;w?*928D$gYJX0X7{e~Wg#oz=#{ z&@(Zd$7EZxp?BxP;N(aN!c&HrSn=3EPQuh7SKZsp00i_>zopr06bLot^fc4 literal 5126 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 BBfbCt literal 5172 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 literal 5041 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 Hu0mjf@M4@K diff --git a/common/static/js/vendor/ova/images/zoomout_grouphover.png b/common/static/js/vendor/ova/images/zoomout_grouphover.png old mode 100755 new mode 100644 index 46d21b3e507e7363b4e8afd0e77343814ad63748..f9661201dfc8cf4de02013c429997eeced0f6103 GIT binary patch literal 504 zcmeAS@N?(olHy`uVBq!ia0vp^%0R5d!3-oHMNc>fq?n7HJVQ7*IBq}me*omR2l#}z z{{R0U$V3Bg-n@DK{Q1+TPai*i{P5w!d-v|$zJ2@V&6`)RUcGqn;+ZpNPM$n@{P^*s zN008?w{P#>y*qdA+`fJL+O=!ftXZ>S#fk+B7R;SHcjCl}?d|QYt*y>K$S~9T^vI!df#5zDb{2l(3c1lQS(T;^Vfmc?%|6em{L4LQYk0ahP;cppa(y?nzNo)Z~g+9FRI^yKG$}pW11g zWtlhJFK?^=Y}6mP-#B=8t&v-Ye($x6C0}OjbDvz@VI*#Hx-{~tuk||XdHLnd%Wk}r zb^9J(d}yvvf^j*|cRvk=V%COwWtEm)%F}EZeK~y{kI9M303FQW>FVdQ&MBb@0A1SQ ASpWb4 literal 4596 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 ASpWb4 literal 4811 zcmV;+5;X0JP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000N}NklZpvOEENIVZ<#4OM(NW>^YgQ7%< z@&(wCU8IN)5SFa7NTNj9WtjzpKoXG=k~2bK$UqE%*x(WEw)sAlV?ds zRb5?G_gCkhd(QcnU}pT5O$Z_S&+!u;%FJtwhB?` z{Hao@RELI!CYV`TF+j`9%PTi-+_?4j+i%Z(`st@D020f3PyqPm&6_=-nVFfvFcyeo zApF-~e|`M)>C=DPv17*}Ddh%cW+4RS@oM5k5|L0!$!@p1dgaQMxpU{vUA=JO!b1RE z05O2n0BHa`H#f&bgy)`n?lDaMc8=rpg9M*^^2zL>Lx=w9I8Mp@ml9EyzhNGvxhI7X zE&xA@BBxfXEr0aUN9WI+IrBAuj)6*Spi7r7@sT4(9>+|UW%=CMQxjacaN)1}_wWCQ zlyaPiQf5w!R|Ei02qCSd%mB!MW45-@XM?)RD5(^@MbM0?w|Z5s|8IIs`E7=WU=$F)+!W0*LOVSDAn4?q0#rcImn z1JFt-#muf5s0Uzv`xaP`pJ!%|h+HCavMfu&FpQpk_SxUR_S$RD02ndXdp1KaW-676 zozVR0>FGZf3WZ^%R96V$FtaCw@QimHA%r7@kk-TOWS74!6OqJe zG)*HSa)Th)&= zeh7k~;5d%+Bp4#1BuV0SyWL)1UT$~0U9Gi-5W-tnSP&~KD{U(c^qR0iF!aqg-`w7@ zV@EbSJ6qIRH_|kvG)>b^r=x1MTHA3P*;ixMb(mUfQc972w6T;@20>7~di83yw6s(+ zeU7$^Z9fbFP+xuZ)l#)ueXwuezD=Iz)uSki%ztF6?JwR&M@I|G%gf_)b93!xvstrR ziM+pA17l~O6SxpsBT{Ukc(lj@-C$N$E*=I}jdi^$l zwgJjp(zPwbi!Z)tdnN@C-@SYH>-XP(KlVItYpGNkAtITkDSoaIkt~%;Bh0+@?Afz% zv)Q}{pkZKQt1i)++GN ycj~=+2!x-_6g@Upja0oOT>%d!O}=~gE@`dXM6^OgKNx_P z@kGm-o}>?CEzH)fTYG}){{8#?3c)i#q48911tCNUK!KUH8BAF^scTs@NUB)s;xWwR z%l9@qm0hbQQx@$yR4`ys^Y@H~N<9Lv~&A~iNY4&Sv z)hBPYtt1D5tbr*M3Tr@Y>-*v$2Z(>!S5l}?{QoG#IvB@sdIJ2%z6JcqCy{m4zfW!c l^oJnp?SJbge(MANKLGgW6Oj(y!CC+S002ovPDHLkV1hX!DhdDq diff --git a/common/static/js/vendor/ova/richText-annotator.js b/common/static/js/vendor/ova/richText-annotator.js index af3410faab..b5add53706 100644 --- a/common/static/js/vendor/ova/richText-annotator.js +++ b/common/static/js/vendor/ova/richText-annotator.js @@ -38,7 +38,7 @@ Annotator.Plugin.RichText = (function(_super) { } }, codemirror: { - path: "static/js/vendor" + path: "/static/js/vendor" }, plugins: "image link codemirror", menubar: false, diff --git a/lms/templates/imageannotation.html b/lms/templates/imageannotation.html index a4c121cac8..72b73e7188 100644 --- a/lms/templates/imageannotation.html +++ b/lms/templates/imageannotation.html @@ -1,9 +1,4 @@ <%! from django.utils.translation import ugettext as _ %> -<%namespace name='static' file='/static_content.html'/> -${static.css(group='style-vendor-tinymce-content', raw=True)} -${static.css(group='style-vendor-tinymce-skin', raw=True)} -