1 /** 2 * @version OpenSeadragon 0.9.50 3 * 4 * @fileOverview 5 * <h2> 6 * <strong> 7 * OpenSeadragon - Javascript Deep Zooming 8 * </strong> 9 * </h2> 10 * <p> 11 * OpenSeadragon is provides an html interface for creating 12 * deep zoom user interfaces. The simplest examples include deep 13 * zoom for large resolution images, and complex examples include 14 * zoomable map interfaces driven by SVG files. 15 * </p> 16 * 17 * @author <br/>(c) 2011, 2012 Christopher Thatcher 18 * @author <br/>(c) 2010 OpenSeadragon Team 19 * @author <br/>(c) 2010 CodePlex Foundation 20 * 21 * <p> 22 * <strong>Original license preserved below: </strong><br/> 23 * <pre> 24 * ---------------------------------------------------------------------------- 25 * 26 * License: New BSD License (BSD) 27 * Copyright (c) 2010, OpenSeadragon 28 * All rights reserved. 29 * 30 * Redistribution and use in source and binary forms, with or without 31 * modification, are permitted provided that the following conditions are met: 32 * 33 * * Redistributions of source code must retain the above copyright notice, this 34 * list of conditions and the following disclaimer. 35 * 36 * * Redistributions in binary form must reproduce the above copyright notice, 37 * this list of conditions and the following disclaimer in the documentation 38 * and/or other materials provided with the distribution. 39 * 40 * * Neither the name of OpenSeadragon nor the names of its contributors may be 41 * used to endorse or promote products derived from this software without 42 * specific prior written permission. 43 * 44 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 45 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 46 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 47 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 48 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 49 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 50 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 51 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 52 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 53 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 54 * POSSIBILITY OF SUCH DAMAGE. 55 * 56 * --------------------------------------------------------------------------- 57 * </pre> 58 * </p> 59 * <p> 60 * <strong> Work done by Chris Thatcher adds an MIT license </strong><br/> 61 * <pre> 62 * ---------------------------------------------------------------------------- 63 * (c) Christopher Thatcher 2011, 2012. All rights reserved. 64 * 65 * Licensed with the MIT License 66 * 67 * Permission is hereby granted, free of charge, to any person obtaining a copy 68 * of this software and associated documentation files (the "Software"), to deal 69 * in the Software without restriction, including without limitation the rights 70 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 71 * copies of the Software, and to permit persons to whom the Software is 72 * furnished to do so, subject to the following conditions: 73 * 74 * The above copyright notice and this permission notice shall be included in 75 * all copies or substantial portions of the Software. 76 * 77 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 78 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 79 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 80 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 81 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 82 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 83 * THE SOFTWARE. 84 * --------------------------------------------------------------------------- 85 * </pre> 86 * </p> 87 **/ 88 89 /** 90 * The root namespace for OpenSeadragon, this function also serves as a single 91 * point of instantiation for an {@link OpenSeadragon.Viewer}, including all 92 * combinations of out-of-the-box configurable features. All utility methods 93 * and classes are defined on or below this namespace. 94 * 95 * @namespace 96 * @function 97 * @name OpenSeadragon 98 * @exports $ as OpenSeadragon 99 * 100 * @param {Object} options All required and optional settings for instantiating 101 * a new instance of an OpenSeadragon image viewer. 102 * 103 * @param {String} options.xmlPath 104 * DEPRECATED. A relative path to load a DZI file from the server. 105 * Prefer the newer options.tileSources. 106 * 107 * @param {Array|String|Function|Object[]|Array[]|String[]|Function[]} options.tileSources 108 * As an Array, the tileSource can hold either be all Objects or mixed 109 * types of Arrays of Objects, String, Function. When a value is a String, 110 * the tileSource is used to create a {@link OpenSeadragon.DziTileSource}. 111 * When a value is a Function, the function is used to create a new 112 * {@link OpenSeadragon.TileSource} whose abstract method 113 * getUrl( level, x, y ) is implemented by the function. Finally, when it 114 * is an Array of objects, it is used to create a 115 * {@link OpenSeadragon.LegacyTileSource}. 116 * 117 * @param {Boolean} [options.debugMode=true] 118 * Currently does nothing. TODO: provide an in-screen panel providing event 119 * detail feedback. 120 * 121 * @param {Number} [options.animationTime=1.5] 122 * Specifies the animation duration per each {@link OpenSeadragon.Spring} 123 * which occur when the image is dragged or zoomed. 124 * 125 * @param {Number} [options.blendTime=0.5] 126 * Specifies the duration of animation as higher or lower level tiles are 127 * replacing the existing tile. 128 * 129 * @param {Boolean} [options.alwaysBlend=false] 130 * Forces the tile to always blend. By default the tiles skip blending 131 * when the blendTime is surpassed and the current animation frame would 132 * not complete the blend. 133 * 134 * @param {Boolean} [options.autoHideControls=true] 135 * If the user stops interacting with the viewport, fade the navigation 136 * controls. Useful for presentation since the controls are by default 137 * floated on top of the image the user is viewing. 138 * 139 * @param {Boolean} [options.immediateRender=false] 140 * Render the best closest level first, ignoring the lowering levels which 141 * provide the effect of very blurry to sharp. It is recommended to change 142 * setting to true for mobile devices. 143 * 144 * @param {Boolean} [options.wrapHorizontal=false] 145 * Set to true to force the image to wrap horizontally within the viewport. 146 * Useful for maps or images representing the surface of a sphere or cylinder. 147 * 148 * @param {Boolean} [options.wrapVertical=false] 149 * Set to true to force the image to wrap vertically within the viewport. 150 * Useful for maps or images representing the surface of a sphere or cylinder. 151 * 152 * @param {Number} [options.minZoomImageRatio=0.8] 153 * The minimum percentage ( expressed as a number between 0 and 1 ) of 154 * the viewport height or width at which the zoom out will be constrained. 155 * Setting it to 0, for example will allow you to zoom out infinitly. 156 * 157 * @param {Number} [options.maxZoomPixelRatio=2] 158 * The maximum ratio to allow a zoom-in to affect the highest level pixel 159 * ratio. This can be set to Infinity to allow 'infinite' zooming into the 160 * image though it is less effective visually if the HTML5 Canvas is not 161 * availble on the viewing device. 162 * 163 * @param {Number} [options.visibilityRatio=0.5] 164 * The percentage ( as a number from 0 to 1 ) of the source image which 165 * must be kept within the viewport. If the image is dragged beyond that 166 * limit, it will 'bounce' back until the minimum visibility ration is 167 * achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to 168 * true will provide the effect of an infinitely scrolling viewport. 169 * 170 * @param {Number} [options.springStiffness=5.0] 171 * 172 * @param {Number} [options.imageLoaderLimit=0] 173 * The maximum number of image requests to make concurrently. By default 174 * it is set to 0 allowing the browser to make the maximum number of 175 * image requests in parallel as allowed by the browsers policy. 176 * 177 * @param {Number} [options.clickTimeThreshold=200] 178 * If multiple mouse clicks occurs within less than this number of 179 * milliseconds, treat them as a single click. 180 * 181 * @param {Number} [options.clickDistThreshold=5] 182 * If a mouse or touch drag occurs and the distance to the starting drag 183 * point is less than this many pixels, ignore the drag event. 184 * 185 * @param {Number} [options.zoomPerClick=2.0] 186 * The "zoom distance" per mouse click or touch tap. 187 * 188 * @param {Number} [options.zoomPerScroll=1.2] 189 * The "zoom distance" per mouse scroll or touch pinch. 190 * 191 * @param {Number} [options.zoomPerSecond=2.0] 192 * The number of seconds to animate a single zoom event over. 193 * 194 * @param {Boolean} [options.showNavigationControl=true] 195 * Set to false to prevent the appearance of the default navigation controls. 196 * 197 * @param {Number} [options.controlsFadeDelay=2000] 198 * The number of milliseconds to wait once the user has stopped interacting 199 * with the interface before begining to fade the controls. Assumes 200 * showNavigationControl and autoHideControls are both true. 201 * 202 * @param {Number} [options.controlsFadeLength=1500] 203 * The number of milliseconds to animate the controls fading out. 204 * 205 * @param {Number} [options.maxImageCacheCount=100] 206 * The max number of images we should keep in memory (per drawer). 207 * 208 * @param {Number} [options.minPixelRatio=0.5] 209 * The higher the minPixelRatio, the lower the quality of the image that 210 * is considered sufficient to stop rendering a given zoom level. For 211 * example, if you are targeting mobile devices with less bandwith you may 212 * try setting this to 1.5 or higher. 213 * 214 * @param {Boolean} [options.mouseNavEnabled=true] 215 * Is the user able to interact with the image via mouse or touch. Default 216 * interactions include draging the image in a plane, and zooming in toward 217 * and away from the image. 218 * 219 * @param {Boolean} [options.preserveViewport=false] 220 * If the viewer has been configured with a sequence of tile sources, then 221 * normally navigating to through each image resets the viewport to 'home' 222 * position. If preserveViewport is set to true, then the viewport position 223 * is preserved when navigating between images in the sequence. 224 * 225 * @param {String} [options.prefixUrl=''] 226 * Appends the prefixUrl to navImages paths, which is very useful 227 * since the default paths are rarely useful for production 228 * environments. 229 * 230 * @param {Object} [options.navImages=] 231 * An object with a property for each button or other built-in navigation 232 * control, eg the current 'zoomIn', 'zoomOut', 'home', and 'fullpage'. 233 * Each of those in turn provides an image path for each state of the botton 234 * or navigation control, eg 'REST', 'GROUP', 'HOVER', 'PRESS'. Finally the 235 * image paths, by default assume there is a folder on the servers root path 236 * called '/images', eg '/images/zoomin_rest.png'. If you need to adjust 237 * these paths, prefer setting the option.prefixUrl rather than overriding 238 * every image path directly through this setting. 239 * 240 * @returns {OpenSeadragon.Viewer} 241 */ 242 OpenSeadragon = window.OpenSeadragon || function( options ){ 243 244 return new OpenSeadragon.Viewer( options ); 245 246 }; 247 248 (function( $ ){ 249 250 251 /** 252 * Taken from jquery 1.6.1 253 * [[Class]] -> type pairs 254 * @private 255 */ 256 var class2type = { 257 '[object Boolean]': 'boolean', 258 '[object Number]': 'number', 259 '[object String]': 'string', 260 '[object Function]': 'function', 261 '[object Array]': 'array', 262 '[object Date]': 'date', 263 '[object RegExp]': 'regexp', 264 '[object Object]': 'object' 265 }, 266 // Save a reference to some core methods 267 toString = Object.prototype.toString, 268 hasOwn = Object.prototype.hasOwnProperty, 269 push = Array.prototype.push, 270 slice = Array.prototype.slice, 271 trim = String.prototype.trim, 272 indexOf = Array.prototype.indexOf; 273 274 275 /** 276 * Taken from jQuery 1.6.1 277 * @name $.isFunction 278 * @function 279 * @see <a href='http://www.jquery.com/'>jQuery</a> 280 */ 281 $.isFunction = function( obj ) { 282 return $.type(obj) === "function"; 283 }; 284 285 286 /** 287 * Taken from jQuery 1.6.1 288 * @name $.isArray 289 * @function 290 * @see <a href='http://www.jquery.com/'>jQuery</a> 291 */ 292 $.isArray = Array.isArray || function( obj ) { 293 return $.type(obj) === "array"; 294 }; 295 296 297 /** 298 * A crude way of determining if an object is a window. 299 * Taken from jQuery 1.6.1 300 * @name $.isWindow 301 * @function 302 * @see <a href='http://www.jquery.com/'>jQuery</a> 303 */ 304 $.isWindow = function( obj ) { 305 return obj && typeof obj === "object" && "setInterval" in obj; 306 }; 307 308 309 /** 310 * Taken from jQuery 1.6.1 311 * @name $.type 312 * @function 313 * @see <a href='http://www.jquery.com/'>jQuery</a> 314 */ 315 $.type = function( obj ) { 316 return obj == null ? 317 String( obj ) : 318 class2type[ toString.call(obj) ] || "object"; 319 }; 320 321 322 /** 323 * Taken from jQuery 1.6.1 324 * @name $.isPlainObject 325 * @function 326 * @see <a href='http://www.jquery.com/'>jQuery</a> 327 */ 328 $.isPlainObject = function( obj ) { 329 // Must be an Object. 330 // Because of IE, we also have to check the presence of the constructor property. 331 // Make sure that DOM nodes and window objects don't pass through, as well 332 if ( !obj || OpenSeadragon.type(obj) !== "object" || obj.nodeType || $.isWindow( obj ) ) { 333 return false; 334 } 335 336 // Not own constructor property must be Object 337 if ( obj.constructor && 338 !hasOwn.call(obj, "constructor") && 339 !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { 340 return false; 341 } 342 343 // Own properties are enumerated firstly, so to speed up, 344 // if last one is own, then all properties are own. 345 346 var key; 347 for ( key in obj ) {} 348 349 return key === undefined || hasOwn.call( obj, key ); 350 }; 351 352 353 /** 354 * Taken from jQuery 1.6.1 355 * @name $.isEmptyObject 356 * @function 357 * @see <a href='http://www.jquery.com/'>jQuery</a> 358 */ 359 $.isEmptyObject = function( obj ) { 360 for ( var name in obj ) { 361 return false; 362 } 363 return true; 364 }; 365 366 367 }( OpenSeadragon )); 368 369 /** 370 * This closure defines all static methods available to the OpenSeadragon 371 * namespace. Many, if not most, are taked directly from jQuery for use 372 * to simplify and reduce common programming patterns. More static methods 373 * from jQuery may eventually make their way into this though we are 374 * attempting to avoid substaintial plagarism or the more explicit dependency 375 * on jQuery only because OpenSeadragon is a broadly useful code base and 376 * would be made less broad by requiring jQuery fully. 377 * 378 * Some static methods have also been refactored from the original OpenSeadragon 379 * project. 380 */ 381 (function( $ ){ 382 383 /** 384 * Taken from jQuery 1.6.1 385 * @see <a href='http://www.jquery.com/'>jQuery</a> 386 */ 387 $.extend = function() { 388 var options, 389 name, 390 src, 391 copy, 392 copyIsArray, 393 clone, 394 target = arguments[ 0 ] || {}, 395 length = arguments.length, 396 deep = false, 397 i = 1; 398 399 // Handle a deep copy situation 400 if ( typeof target === "boolean" ) { 401 deep = target; 402 target = arguments[ 1 ] || {}; 403 // skip the boolean and the target 404 i = 2; 405 } 406 407 // Handle case when target is a string or something (possible in deep copy) 408 if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) { 409 target = {}; 410 } 411 412 // extend jQuery itself if only one argument is passed 413 if ( length === i ) { 414 target = this; 415 --i; 416 } 417 418 for ( ; i < length; i++ ) { 419 // Only deal with non-null/undefined values 420 if ( ( options = arguments[ i ] ) != null ) { 421 // Extend the base object 422 for ( name in options ) { 423 src = target[ name ]; 424 copy = options[ name ]; 425 426 // Prevent never-ending loop 427 if ( target === copy ) { 428 continue; 429 } 430 431 // Recurse if we're merging plain objects or arrays 432 if ( deep && copy && ( OpenSeadragon.isPlainObject( copy ) || ( copyIsArray = OpenSeadragon.isArray( copy ) ) ) ) { 433 if ( copyIsArray ) { 434 copyIsArray = false; 435 clone = src && OpenSeadragon.isArray( src ) ? src : []; 436 437 } else { 438 clone = src && OpenSeadragon.isPlainObject( src ) ? src : {}; 439 } 440 441 // Never move original objects, clone them 442 target[ name ] = OpenSeadragon.extend( deep, clone, copy ); 443 444 // Don't bring in undefined values 445 } else if ( copy !== undefined ) { 446 target[ name ] = copy; 447 } 448 } 449 } 450 } 451 452 // Return the modified object 453 return target; 454 }; 455 456 457 $.extend( $, { 458 /** 459 * These are the default values for the optional settings documented 460 * in the {@link OpenSeadragon} constructor detail. 461 * @name $.DEFAULT_SETTINGS 462 * @static 463 */ 464 DEFAULT_SETTINGS: { 465 //DATA SOURCE DETAILS 466 xmlPath: null, 467 tileSources: null, 468 tileHost: null, 469 470 //INTERFACE FEATURES 471 debugMode: true, 472 animationTime: 1.5, 473 blendTime: 0.5, 474 alwaysBlend: false, 475 autoHideControls: true, 476 immediateRender: false, 477 wrapHorizontal: false, 478 wrapVertical: false, 479 panHorizontal: true, 480 panVertical: true, 481 visibilityRatio: 0.5, 482 springStiffness: 5.0, 483 clickTimeThreshold: 200, 484 clickDistThreshold: 5, 485 zoomPerClick: 2.0, 486 zoomPerScroll: 1.2, 487 zoomPerSecond: 2.0, 488 showNavigationControl: true, 489 showSequenceControl: true, 490 controlsFadeDelay: 2000, 491 controlsFadeLength: 1500, 492 mouseNavEnabled: true, 493 showNavigator: false, 494 navigatorElement: null, 495 navigatorHeight: null, 496 navigatorWidth: null, 497 navigatorPosition: null, 498 navigatorSizeRatio: 0.25, 499 preserveViewport: false, 500 501 //PERFORMANCE SETTINGS 502 minPixelRatio: 0.5, 503 imageLoaderLimit: 0, 504 maxImageCacheCount: 200, 505 minZoomImageRatio: 0.8, 506 maxZoomPixelRatio: 2, 507 508 //INTERFACE RESOURCE SETTINGS 509 prefixUrl: null, 510 navImages: { 511 zoomIn: { 512 REST: '/images/zoomin_rest.png', 513 GROUP: '/images/zoomin_grouphover.png', 514 HOVER: '/images/zoomin_hover.png', 515 DOWN: '/images/zoomin_pressed.png' 516 }, 517 zoomOut: { 518 REST: '/images/zoomout_rest.png', 519 GROUP: '/images/zoomout_grouphover.png', 520 HOVER: '/images/zoomout_hover.png', 521 DOWN: '/images/zoomout_pressed.png' 522 }, 523 home: { 524 REST: '/images/home_rest.png', 525 GROUP: '/images/home_grouphover.png', 526 HOVER: '/images/home_hover.png', 527 DOWN: '/images/home_pressed.png' 528 }, 529 fullpage: { 530 REST: '/images/fullpage_rest.png', 531 GROUP: '/images/fullpage_grouphover.png', 532 HOVER: '/images/fullpage_hover.png', 533 DOWN: '/images/fullpage_pressed.png' 534 }, 535 previous: { 536 REST: '/images/previous_rest.png', 537 GROUP: '/images/previous_grouphover.png', 538 HOVER: '/images/previous_hover.png', 539 DOWN: '/images/previous_pressed.png' 540 }, 541 next: { 542 REST: '/images/next_rest.png', 543 GROUP: '/images/next_grouphover.png', 544 HOVER: '/images/next_hover.png', 545 DOWN: '/images/next_pressed.png' 546 } 547 } 548 }, 549 550 551 /** 552 * TODO: get rid of this. I can't see how it's required at all. Looks 553 * like an early legacy code artifact. 554 * @static 555 * @ignore 556 */ 557 SIGNAL: "----seadragon----", 558 559 560 /** 561 * Invokes the the method as if it where a method belonging to the object. 562 * @name $.delegate 563 * @function 564 * @param {Object} object 565 * @param {Function} method 566 */ 567 delegate: function( object, method ) { 568 return function() { 569 if ( arguments === undefined ) 570 arguments = []; 571 return method.apply( object, arguments ); 572 }; 573 }, 574 575 576 /** 577 * An enumeration of Browser vendors including UNKNOWN, IE, FIREFOX, 578 * SAFARI, CHROME, and OPERA. 579 * @name $.BROWSERS 580 * @static 581 */ 582 BROWSERS: { 583 UNKNOWN: 0, 584 IE: 1, 585 FIREFOX: 2, 586 SAFARI: 3, 587 CHROME: 4, 588 OPERA: 5 589 }, 590 591 592 /** 593 * Returns a DOM Element for the given id or element. 594 * @function 595 * @name OpenSeadragon.getElement 596 * @param {String|Element} element Accepts an id or element. 597 * @returns {Element} The element with the given id, null, or the element itself. 598 */ 599 getElement: function( element ) { 600 if ( typeof ( element ) == "string" ) { 601 element = document.getElementById( element ); 602 } 603 return element; 604 }, 605 606 607 /** 608 * Determines the position of the upper-left corner of the element. 609 * @function 610 * @name OpenSeadragon.getElementPosition 611 * @param {Element|String} element - the elemenet we want the position for. 612 * @returns {Point} - the position of the upper left corner of the element. 613 */ 614 getElementPosition: function( element ) { 615 var result = new $.Point(), 616 isFixed, 617 offsetParent; 618 619 element = $.getElement( element ); 620 isFixed = $.getElementStyle( element ).position == "fixed"; 621 offsetParent = getOffsetParent( element, isFixed ); 622 623 while ( offsetParent ) { 624 625 result.x += element.offsetLeft; 626 result.y += element.offsetTop; 627 628 if ( isFixed ) { 629 result = result.plus( $.getPageScroll() ); 630 } 631 632 element = offsetParent; 633 isFixed = $.getElementStyle( element ).position == "fixed"; 634 offsetParent = getOffsetParent( element, isFixed ); 635 } 636 637 return result; 638 }, 639 640 641 /** 642 * Determines the height and width of the given element. 643 * @function 644 * @name OpenSeadragon.getElementSize 645 * @param {Element|String} element 646 * @returns {Point} 647 */ 648 getElementSize: function( element ) { 649 element = $.getElement( element ); 650 651 return new $.Point( 652 element.clientWidth, 653 element.clientHeight 654 ); 655 }, 656 657 658 /** 659 * Returns the CSSStyle object for the given element. 660 * @function 661 * @name OpenSeadragon.getElementStyle 662 * @param {Element|String} element 663 * @returns {CSSStyle} 664 */ 665 getElementStyle: 666 document.documentElement.currentStyle ? 667 function( element ) { 668 element = $.getElement( element ); 669 return element.currentStyle; 670 } : 671 function( element ) { 672 element = $.getElement( element ); 673 return window.getComputedStyle( element, "" ); 674 }, 675 676 677 /** 678 * Gets the latest event, really only useful internally since its 679 * specific to IE behavior. TODO: Deprecate this from the api and 680 * use it internally. 681 * @function 682 * @name OpenSeadragon.getEvent 683 * @param {Event} [event] 684 * @returns {Event} 685 */ 686 getEvent: function( event ) { 687 if( event ){ 688 $.getEvent = function( event ){ 689 return event; 690 }; 691 } else { 692 $.getEvent = function( event ){ 693 return window.event; 694 }; 695 } 696 return $.getEvent( event ); 697 }, 698 699 700 /** 701 * Gets the position of the mouse on the screen for a given event. 702 * @function 703 * @name OpenSeadragon.getMousePosition 704 * @param {Event} [event] 705 * @returns {Point} 706 */ 707 getMousePosition: function( event ) { 708 709 if ( typeof( event.pageX ) == "number" ) { 710 $.getMousePosition = function( event ){ 711 var result = new $.Point(); 712 713 event = $.getEvent( event ); 714 result.x = event.pageX; 715 result.y = event.pageY; 716 717 return result; 718 }; 719 } else if ( typeof( event.clientX ) == "number" ) { 720 $.getMousePosition = function( event ){ 721 var result = new $.Point(); 722 723 event = $.getEvent( event ); 724 result.x = 725 event.clientX + 726 document.body.scrollLeft + 727 document.documentElement.scrollLeft; 728 result.y = 729 event.clientY + 730 document.body.scrollTop + 731 document.documentElement.scrollTop; 732 733 return result; 734 }; 735 } else { 736 throw new Error( 737 "Unknown event mouse position, no known technique." 738 ); 739 } 740 741 return $.getMousePosition( event ); 742 }, 743 744 745 /** 746 * Determines the pages current scroll position. 747 * @function 748 * @name OpenSeadragon.getPageScroll 749 * @returns {Point} 750 */ 751 getPageScroll: function() { 752 var docElement = document.documentElement || {}, 753 body = document.body || {}; 754 755 if ( typeof( window.pageXOffset ) == "number" ) { 756 $.getPageScroll = function(){ 757 return new $.Point( 758 window.pageXOffset, 759 window.pageYOffset 760 ); 761 }; 762 } else if ( body.scrollLeft || body.scrollTop ) { 763 $.getPageScroll = function(){ 764 return new $.Point( 765 document.body.scrollLeft, 766 document.body.scrollTop 767 ); 768 }; 769 } else if ( docElement.scrollLeft || docElement.scrollTop ) { 770 $.getPageScroll = function(){ 771 return new $.Point( 772 document.documentElement.scrollLeft, 773 document.documentElement.scrollTop 774 ); 775 }; 776 } 777 778 return $.getPageScroll(); 779 }, 780 781 782 /** 783 * Determines the size of the browsers window. 784 * @function 785 * @name OpenSeadragon.getWindowSize 786 * @returns {Point} 787 */ 788 getWindowSize: function() { 789 var docElement = document.documentElement || {}, 790 body = document.body || {}; 791 792 if ( typeof( window.innerWidth ) == 'number' ) { 793 $.getWindowSize = function(){ 794 return new $.Point( 795 window.innerWidth, 796 window.innerHeight 797 ); 798 } 799 } else if ( docElement.clientWidth || docElement.clientHeight ) { 800 $.getWindowSize = function(){ 801 return new $.Point( 802 document.documentElement.clientWidth, 803 document.documentElement.clientHeight 804 ); 805 } 806 } else if ( body.clientWidth || body.clientHeight ) { 807 $.getWindowSize = function(){ 808 return new $.Point( 809 document.body.clientWidth, 810 document.body.clientHeight 811 ); 812 } 813 } else { 814 throw new Error("Unknown window size, no known technique."); 815 } 816 817 return $.getWindowSize(); 818 }, 819 820 821 /** 822 * Wraps the given element in a nest of divs so that the element can 823 * be easily centered. 824 * @function 825 * @name OpenSeadragon.makeCenteredNode 826 * @param {Element|String} element 827 * @returns {Element} 828 */ 829 makeCenteredNode: function( element ) { 830 831 var div = $.makeNeutralElement( "div" ), 832 html = [], 833 innerDiv, 834 innerDivs; 835 836 element = $.getElement( element ); 837 838 //TODO: I dont understand the use of # inside the style attributes 839 // below. Invetigate the results of the constructed html in 840 // the browser and clean up the mark-up to make this clearer. 841 html.push('<div style="display:table; height:100%; width:100%;'); 842 html.push('border:none; margin:0px; padding:0px;'); // neutralizing 843 html.push('#position:relative; overflow:hidden; text-align:left;">'); 844 html.push('<div style="#position:absolute; #top:50%; width:100%; '); 845 html.push('border:none; margin:0px; padding:0px;'); // neutralizing 846 html.push('display:table-cell; vertical-align:middle;">'); 847 html.push('<div style="#position:relative; #top:-50%; width:100%; '); 848 html.push('border:none; margin:0px; padding:0px;'); // neutralizing 849 html.push('text-align:center;"></div></div></div>'); 850 851 div.innerHTML = html.join( '' ); 852 div = div.firstChild; 853 854 innerDiv = div; 855 innerDivs = div.getElementsByTagName( "div" ); 856 while ( innerDivs.length > 0 ) { 857 innerDiv = innerDivs[ 0 ]; 858 innerDivs = innerDiv.getElementsByTagName( "div" ); 859 } 860 861 innerDiv.appendChild( element ); 862 863 return div; 864 }, 865 866 867 /** 868 * Creates an easily positionable element of the given type that therefor 869 * serves as an excellent container element. 870 * @function 871 * @name OpenSeadragon.makeNeutralElement 872 * @param {String} tagName 873 * @returns {Element} 874 */ 875 makeNeutralElement: function( tagName ) { 876 var element = document.createElement( tagName ), 877 style = element.style; 878 879 style.background = "transparent none"; 880 style.border = "none"; 881 style.margin = "0px"; 882 style.padding = "0px"; 883 style.position = "static"; 884 885 return element; 886 }, 887 888 889 /** 890 * Ensures an image is loaded correctly to support alpha transparency. 891 * Generally only IE has issues doing this correctly for formats like 892 * png. 893 * @function 894 * @name OpenSeadragon.makeTransparentImage 895 * @param {String} src 896 * @returns {Element} 897 */ 898 makeTransparentImage: function( src ) { 899 900 $.makeTransparentImage = function( src ){ 901 var img = $.makeNeutralElement( "img" ); 902 903 img.src = src; 904 905 return img; 906 }; 907 908 if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 7 ) { 909 910 $.makeTransparentImage = function( src ){ 911 var img = $.makeNeutralElement( "img" ), 912 element = null; 913 914 element = $.makeNeutralElement("span"); 915 element.style.display = "inline-block"; 916 917 img.onload = function() { 918 element.style.width = element.style.width || img.width + "px"; 919 element.style.height = element.style.height || img.height + "px"; 920 921 img.onload = null; 922 img = null; // to prevent memory leaks in IE 923 }; 924 925 img.src = src; 926 element.style.filter = 927 "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + 928 src + 929 "', sizingMethod='scale')"; 930 931 return element; 932 }; 933 934 } 935 936 return $.makeTransparentImage( src ); 937 }, 938 939 940 /** 941 * Sets the opacity of the specified element. 942 * @function 943 * @name OpenSeadragon.setElementOpacity 944 * @param {Element|String} element 945 * @param {Number} opacity 946 * @param {Boolean} [usesAlpha] 947 */ 948 setElementOpacity: function( element, opacity, usesAlpha ) { 949 950 var previousFilter, 951 ieOpacity, 952 ieFilter; 953 954 element = $.getElement( element ); 955 956 if ( usesAlpha && !$.Browser.alpha ) { 957 opacity = Math.round( opacity ); 958 } 959 960 if ( opacity < 1 ) { 961 element.style.opacity = opacity; 962 } else { 963 element.style.opacity = ""; 964 } 965 966 if ( opacity == 1 ) { 967 prevFilter = element.style.filter || ""; 968 element.style.filter = prevFilter.replace(/alpha\(.*?\)/g, ""); 969 return; 970 } 971 972 ieOpacity = Math.round( 100 * opacity ); 973 ieFilter = " alpha(opacity=" + ieOpacity + ") "; 974 975 //TODO: find out why this uses a try/catch instead of a predetermined 976 // routine or at least an if/elseif/else 977 try { 978 if ( element.filters && element.filters.alpha ) { 979 element.filters.alpha.opacity = ieOpacity; 980 } else { 981 element.style.filter += ieFilter; 982 } 983 } catch ( e ) { 984 element.style.filter += ieFilter; 985 } 986 }, 987 988 989 /** 990 * Adds an event listener for the given element, eventName and handler. 991 * @function 992 * @name OpenSeadragon.addEvent 993 * @param {Element|String} element 994 * @param {String} eventName 995 * @param {Function} handler 996 * @param {Boolean} [useCapture] 997 * @throws {Error} 998 */ 999 addEvent: function( element, eventName, handler, useCapture ) { 1000 element = $.getElement( element ); 1001 1002 //TODO: Why do this if/else on every method call instead of just 1003 // defining this function once based on the same logic 1004 if ( element.addEventListener ) { 1005 $.addEvent = function( element, eventName, handler, useCapture ){ 1006 element = $.getElement( element ); 1007 element.addEventListener( eventName, handler, useCapture ); 1008 }; 1009 } else if ( element.attachEvent ) { 1010 $.addEvent = function( element, eventName, handler, useCapture ){ 1011 element = $.getElement( element ); 1012 element.attachEvent( "on" + eventName, handler ); 1013 if ( useCapture && element.setCapture ) { 1014 element.setCapture(); 1015 } 1016 }; 1017 } else { 1018 throw new Error( 1019 "Unable to attach event handler, no known technique." 1020 ); 1021 } 1022 1023 return $.addEvent( element, eventName, handler, useCapture ); 1024 }, 1025 1026 1027 /** 1028 * Remove a given event listener for the given element, event type and 1029 * handler. 1030 * @function 1031 * @name OpenSeadragon.removeEvent 1032 * @param {Element|String} element 1033 * @param {String} eventName 1034 * @param {Function} handler 1035 * @param {Boolean} [useCapture] 1036 * @throws {Error} 1037 */ 1038 removeEvent: function( element, eventName, handler, useCapture ) { 1039 element = $.getElement( element ); 1040 1041 //TODO: Why do this if/else on every method call instead of just 1042 // defining this function once based on the same logic 1043 if ( element.removeEventListener ) { 1044 $.removeEvent = function( element, eventName, handler, useCapture ) { 1045 element = $.getElement( element ); 1046 element.removeEventListener( eventName, handler, useCapture ); 1047 }; 1048 } else if ( element.detachEvent ) { 1049 $.removeEvent = function( element, eventName, handler, useCapture ) { 1050 element = $.getElement( element ); 1051 element.detachEvent("on" + eventName, handler); 1052 if ( useCapture && element.releaseCapture ) { 1053 element.releaseCapture(); 1054 } 1055 }; 1056 } else { 1057 throw new Error( 1058 "Unable to detach event handler, no known technique." 1059 ); 1060 } 1061 return $.removeEvent( element, eventName, handler, useCapture ); 1062 }, 1063 1064 1065 /** 1066 * Cancels the default browser behavior had the event propagated all 1067 * the way up the DOM to the window object. 1068 * @function 1069 * @name OpenSeadragon.cancelEvent 1070 * @param {Event} [event] 1071 */ 1072 cancelEvent: function( event ) { 1073 event = $.getEvent( event ); 1074 1075 if ( event.preventDefault ) { 1076 $.cancelEvent = function( event ){ 1077 // W3C for preventing default 1078 event.preventDefault(); 1079 } 1080 } else { 1081 $.cancelEvent = function( event ){ 1082 event = $.getEvent( event ); 1083 // legacy for preventing default 1084 event.cancel = true; 1085 // IE for preventing default 1086 event.returnValue = false; 1087 }; 1088 } 1089 $.cancelEvent( event ); 1090 }, 1091 1092 1093 /** 1094 * Stops the propagation of the event up the DOM. 1095 * @function 1096 * @name OpenSeadragon.stopEvent 1097 * @param {Event} [event] 1098 */ 1099 stopEvent: function( event ) { 1100 event = $.getEvent( event ); 1101 1102 if ( event.stopPropagation ) { 1103 // W3C for stopping propagation 1104 $.stopEvent = function( event ){ 1105 event.stopPropagation(); 1106 }; 1107 } else { 1108 // IE for stopping propagation 1109 $.stopEvent = function( event ){ 1110 event = $.getEvent( event ); 1111 event.cancelBubble = true; 1112 }; 1113 1114 } 1115 1116 $.stopEvent( event ); 1117 }, 1118 1119 1120 /** 1121 * Similar to OpenSeadragon.delegate, but it does not immediately call 1122 * the method on the object, returning a function which can be called 1123 * repeatedly to delegate the method. It also allows additonal arguments 1124 * to be passed during construction which will be added during each 1125 * invocation, and each invocation can add additional arguments as well. 1126 * 1127 * @function 1128 * @name OpenSeadragon.createCallback 1129 * @param {Object} object 1130 * @param {Function} method 1131 * @param [args] any additional arguments are passed as arguments to the 1132 * created callback 1133 * @returns {Function} 1134 */ 1135 createCallback: function( object, method ) { 1136 //TODO: This pattern is painful to use and debug. It's much cleaner 1137 // to use pinning plus anonymous functions. Get rid of this 1138 // pattern! 1139 var initialArgs = [], 1140 i; 1141 for ( i = 2; i < arguments.length; i++ ) { 1142 initialArgs.push( arguments[ i ] ); 1143 } 1144 1145 return function() { 1146 var args = initialArgs.concat( [] ), 1147 i; 1148 for ( i = 0; i < arguments.length; i++ ) { 1149 args.push( arguments[ i ] ); 1150 } 1151 1152 return method.apply( object, args ); 1153 }; 1154 }, 1155 1156 1157 /** 1158 * Retreives the value of a url parameter from the window.location string. 1159 * @function 1160 * @name OpenSeadragon.getUrlParameter 1161 * @param {String} key 1162 * @returns {String} The value of the url parameter or null if no param matches. 1163 */ 1164 getUrlParameter: function( key ) { 1165 var value = URLPARAMS[ key ]; 1166 return value ? value : null; 1167 }, 1168 1169 1170 createAjaxRequest: function(){ 1171 var request; 1172 1173 if ( window.ActiveXObject ) { 1174 //TODO: very bad...Why check every time using try/catch when 1175 // we could determine once at startup which activeX object 1176 // was supported. This will have significant impact on 1177 // performance for IE Browsers 1178 for ( i = 0; i < ACTIVEX.length; i++ ) { 1179 try { 1180 request = new ActiveXObject( ACTIVEX[ i ] ); 1181 $.createAjaxRequest = function( ){ 1182 return new ActiveXObject( ACTIVEX[ i ] ); 1183 }; 1184 break; 1185 } catch (e) { 1186 continue; 1187 } 1188 } 1189 } else if ( window.XMLHttpRequest ) { 1190 $.createAjaxRequest = function( ){ 1191 return new XMLHttpRequest(); 1192 }; 1193 request = new XMLHttpRequest(); 1194 } 1195 1196 if ( !request ) { 1197 throw new Error( "Browser doesn't support XMLHttpRequest." ); 1198 } 1199 1200 return request; 1201 }, 1202 /** 1203 * Makes an AJAX request. 1204 * @function 1205 * @name OpenSeadragon.makeAjaxRequest 1206 * @param {String} url - the url to request 1207 * @param {Function} [callback] - a function to call when complete 1208 * @throws {Error} 1209 */ 1210 makeAjaxRequest: function( url, callback ) { 1211 var async = typeof( callback ) == "function", 1212 request = $.createAjaxRequest(), 1213 actual, 1214 i; 1215 1216 if ( async ) { 1217 actual = callback; 1218 callback = function() { 1219 window.setTimeout( 1220 $.createCallback( null, actual, request ), 1221 1 1222 ); 1223 }; 1224 /** @ignore */ 1225 request.onreadystatechange = function() { 1226 if ( request.readyState == 4) { 1227 request.onreadystatechange = new function() { }; 1228 callback(); 1229 } 1230 }; 1231 } 1232 1233 try { 1234 request.open( "GET", url, async ); 1235 request.send( null ); 1236 } catch (e) { 1237 $.console.log( 1238 "%s while making AJAX request: %s", 1239 e.name, 1240 e.message 1241 ); 1242 1243 request.onreadystatechange = null; 1244 request = null; 1245 1246 if ( async ) { 1247 callback(); 1248 } 1249 } 1250 1251 return async ? null : request; 1252 }, 1253 1254 1255 /** 1256 * Taken from jQuery 1.6.1 1257 * @function 1258 * @name OpenSeadragon.jsonp 1259 * @param {Object} options 1260 * @param {String} options.url 1261 * @param {Function} options.callback 1262 * @param {String} [options.param='callback'] The name of the url parameter 1263 * to request the jsonp provider with. 1264 * @param {String} [options.callbackName=] The name of the callback to 1265 * request the jsonp provider with. 1266 */ 1267 jsonp: function( options ){ 1268 var script, 1269 url = options.url, 1270 head = document.head || 1271 document.getElementsByTagName( "head" )[ 0 ] || 1272 document.documentElement, 1273 jsonpCallback = options.callbackName || 'openseadragon' + (+new Date()), 1274 previous = window[ jsonpCallback ], 1275 replace = "$1" + jsonpCallback + "$2", 1276 callbackParam = options.param || 'callback', 1277 callback = options.callback; 1278 1279 url = url.replace( /(\=)\?(&|$)|\?\?/i, replace ); 1280 // Add callback manually 1281 url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback; 1282 1283 // Install callback 1284 window[ jsonpCallback ] = function( response ) { 1285 if ( !previous ){ 1286 delete window[ jsonpCallback ]; 1287 } else { 1288 window[ jsonpCallback ] = previous; 1289 } 1290 if( callback && $.isFunction( callback ) ){ 1291 callback( response ); 1292 } 1293 }; 1294 1295 script = document.createElement( "script" ); 1296 1297 script.async = "async"; 1298 1299 if ( options.scriptCharset ) { 1300 script.charset = options.scriptCharset; 1301 } 1302 1303 script.src = url; 1304 1305 // Attach handlers for all browsers 1306 script.onload = script.onreadystatechange = function( _, isAbort ) { 1307 1308 if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { 1309 1310 // Handle memory leak in IE 1311 script.onload = script.onreadystatechange = null; 1312 1313 // Remove the script 1314 if ( head && script.parentNode ) { 1315 head.removeChild( script ); 1316 } 1317 1318 // Dereference the script 1319 script = undefined; 1320 } 1321 }; 1322 // Use insertBefore instead of appendChild to circumvent an IE6 bug. 1323 // This arises when a base node is used (#2709 and #4378). 1324 head.insertBefore( script, head.firstChild ); 1325 1326 }, 1327 1328 1329 /** 1330 * Loads a Deep Zoom Image description from a url or XML string and 1331 * provides a callback hook for the resulting Document 1332 * @function 1333 * @name OpenSeadragon.createFromDZI 1334 * @param {String} xmlUrl 1335 * @param {String} xmlString 1336 * @param {Function} callback 1337 */ 1338 createFromDZI: function( dzi, callback, tileHost ) { 1339 var async = typeof ( callback ) == "function", 1340 xmlUrl = dzi.substring(0,1) != '<' ? dzi : null, 1341 xmlString = xmlUrl ? null : dzi, 1342 error = null, 1343 urlParts, 1344 filename, 1345 lastDot, 1346 tilesUrl; 1347 1348 1349 if( tileHost ){ 1350 1351 tilesUrl = tileHost + "/_files/"; 1352 1353 } else if( xmlUrl ) { 1354 1355 urlParts = xmlUrl.split( '/' ); 1356 filename = urlParts[ urlParts.length - 1 ]; 1357 lastDot = filename.lastIndexOf( '.' ); 1358 1359 if ( lastDot > -1 ) { 1360 urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); 1361 } 1362 1363 tilesUrl = urlParts.join( '/' ) + "_files/"; 1364 1365 } 1366 1367 function finish( func, obj ) { 1368 try { 1369 return func( obj, tilesUrl ); 1370 } catch ( e ) { 1371 if ( async ) { 1372 return null; 1373 } else { 1374 throw e; 1375 } 1376 } 1377 } 1378 1379 if ( async ) { 1380 if ( xmlString ) { 1381 window.setTimeout( function() { 1382 var source = finish( processDZIXml, parseXml( xmlString ) ); 1383 // call after finish sets error 1384 callback( source, error ); 1385 }, 1); 1386 } else { 1387 $.makeAjaxRequest( xmlUrl, function( xhr ) { 1388 var source = finish( processDZIResponse, xhr ); 1389 // call after finish sets error 1390 callback( source, error ); 1391 }); 1392 } 1393 1394 return null; 1395 } 1396 1397 if ( xmlString ) { 1398 return finish( 1399 processDZIXml, 1400 parseXml( xmlString ) 1401 ); 1402 } else { 1403 return finish( 1404 processDZIResponse, 1405 $.makeAjaxRequest( xmlUrl ) 1406 ); 1407 } 1408 } 1409 1410 }); 1411 1412 1413 /** 1414 * The current browser vendor, version, and related information regarding 1415 * detected features. Features include <br/> 1416 * <strong>'alpha'</strong> - Does the browser support image alpha 1417 * transparency.<br/> 1418 * @name $.Browser 1419 * @static 1420 */ 1421 $.Browser = { 1422 vendor: $.BROWSERS.UNKNOWN, 1423 version: 0, 1424 alpha: true 1425 }; 1426 1427 1428 var ACTIVEX = [ 1429 "Msxml2.XMLHTTP", 1430 "Msxml3.XMLHTTP", 1431 "Microsoft.XMLHTTP" 1432 ], 1433 FILEFORMATS = { 1434 "bmp": false, 1435 "jpeg": true, 1436 "jpg": true, 1437 "png": true, 1438 "tif": false, 1439 "wdp": false 1440 }, 1441 URLPARAMS = {}; 1442 1443 (function() { 1444 //A small auto-executing routine to determine the browser vendor, 1445 //version and supporting feature sets. 1446 var app = navigator.appName, 1447 ver = navigator.appVersion, 1448 ua = navigator.userAgent; 1449 1450 //console.error( 'appName: ' + navigator.appName ); 1451 //console.error( 'appVersion: ' + navigator.appVersion ); 1452 //console.error( 'userAgent: ' + navigator.userAgent ); 1453 1454 switch( navigator.appName ){ 1455 case "Microsoft Internet Explorer": 1456 if( !!window.attachEvent && 1457 !!window.ActiveXObject ) { 1458 1459 $.Browser.vendor = $.BROWSERS.IE; 1460 $.Browser.version = parseFloat( 1461 ua.substring( 1462 ua.indexOf( "MSIE" ) + 5, 1463 ua.indexOf( ";", ua.indexOf( "MSIE" ) ) ) 1464 ); 1465 } 1466 break; 1467 case "Netscape": 1468 if( !!window.addEventListener ){ 1469 if ( ua.indexOf( "Firefox" ) >= 0 ) { 1470 $.Browser.vendor = $.BROWSERS.FIREFOX; 1471 $.Browser.version = parseFloat( 1472 ua.substring( ua.indexOf( "Firefox" ) + 8 ) 1473 ); 1474 } else if ( ua.indexOf( "Safari" ) >= 0 ) { 1475 $.Browser.vendor = ua.indexOf( "Chrome" ) >= 0 ? 1476 $.BROWSERS.CHROME : 1477 $.BROWSERS.SAFARI; 1478 $.Browser.version = parseFloat( 1479 ua.substring( 1480 ua.substring( 0, ua.indexOf( "Safari" ) ).lastIndexOf( "/" ) + 1, 1481 ua.indexOf( "Safari" ) 1482 ) 1483 ); 1484 } 1485 } 1486 break; 1487 case "Opera": 1488 $.Browser.vendor = $.BROWSERS.OPERA; 1489 $.Browser.version = parseFloat( ver ); 1490 break; 1491 } 1492 1493 // ignore '?' portion of query string 1494 var query = window.location.search.substring( 1 ), 1495 parts = query.split('&'), 1496 part, 1497 sep, 1498 i; 1499 1500 for ( i = 0; i < parts.length; i++ ) { 1501 part = parts[ i ]; 1502 sep = part.indexOf( '=' ); 1503 1504 if ( sep > 0 ) { 1505 URLPARAMS[ part.substring( 0, sep ) ] = 1506 decodeURIComponent( part.substring( sep + 1 ) ); 1507 } 1508 } 1509 1510 //determine if this browser supports image alpha transparency 1511 $.Browser.alpha = !( 1512 ( 1513 $.Browser.vendor == $.BROWSERS.IE && 1514 $.Browser.version < 9 1515 ) || ( 1516 $.Browser.vendor == $.BROWSERS.CHROME && 1517 $.Browser.version < 2 1518 ) 1519 ); 1520 1521 })(); 1522 1523 //TODO: $.console is often used inside a try/catch block which generally 1524 // prevents allowings errors to occur with detection until a debugger 1525 // is attached. Although I've been guilty of the same anti-pattern 1526 // I eventually was convinced that errors should naturally propogate in 1527 // all but the most special cases. 1528 /** 1529 * A convenient alias for console when available, and a simple null 1530 * function when console is unavailable. 1531 * @static 1532 * @private 1533 */ 1534 var nullfunction = function( msg ){ 1535 //document.location.hash = msg; 1536 }; 1537 1538 $.console = window.console || { 1539 log: nullfunction, 1540 debug: nullfunction, 1541 info: nullfunction, 1542 warn: nullfunction, 1543 error: nullfunction 1544 }; 1545 1546 1547 /** 1548 * @private 1549 * @inner 1550 * @function 1551 * @param {Element} element 1552 * @param {Boolean} [isFixed] 1553 * @returns {Element} 1554 */ 1555 function getOffsetParent( element, isFixed ) { 1556 if ( isFixed && element != document.body ) { 1557 return document.body; 1558 } else { 1559 return element.offsetParent; 1560 } 1561 }; 1562 1563 /** 1564 * @private 1565 * @inner 1566 * @function 1567 * @param {XMLHttpRequest} xhr 1568 * @param {String} tilesUrl 1569 */ 1570 function processDZIResponse( xhr, tilesUrl ) { 1571 var status, 1572 statusText, 1573 doc = null; 1574 1575 if ( !xhr ) { 1576 throw new Error( $.getString( "Errors.Security" ) ); 1577 } else if ( xhr.status !== 200 && xhr.status !== 0 ) { 1578 status = xhr.status; 1579 statusText = ( status == 404 ) ? 1580 "Not Found" : 1581 xhr.statusText; 1582 throw new Error( $.getString( "Errors.Status", status, statusText ) ); 1583 } 1584 1585 if ( xhr.responseXML && xhr.responseXML.documentElement ) { 1586 doc = xhr.responseXML; 1587 } else if ( xhr.responseText ) { 1588 doc = parseXml( xhr.responseText ); 1589 } 1590 1591 return processDZIXml( doc, tilesUrl ); 1592 }; 1593 1594 /** 1595 * @private 1596 * @inner 1597 * @function 1598 * @param {Document} xmlDoc 1599 * @param {String} tilesUrl 1600 */ 1601 function processDZIXml( xmlDoc, tilesUrl ) { 1602 1603 if ( !xmlDoc || !xmlDoc.documentElement ) { 1604 throw new Error( $.getString( "Errors.Xml" ) ); 1605 } 1606 1607 var root = xmlDoc.documentElement, 1608 rootName = root.tagName; 1609 1610 if ( rootName == "Image" ) { 1611 try { 1612 return processDZI( root, tilesUrl ); 1613 } catch ( e ) { 1614 throw (e instanceof Error) ? 1615 e : 1616 new Error( $.getString("Errors.Dzi") ); 1617 } 1618 } else if ( rootName == "Collection" ) { 1619 throw new Error( $.getString( "Errors.Dzc" ) ); 1620 } else if ( rootName == "Error" ) { 1621 return processDZIError( root ); 1622 } 1623 1624 throw new Error( $.getString( "Errors.Dzi" ) ); 1625 }; 1626 1627 /** 1628 * @private 1629 * @inner 1630 * @function 1631 * @param {Element} imageNode 1632 * @param {String} tilesUrl 1633 */ 1634 function processDZI( imageNode, tilesUrl ) { 1635 var fileFormat = imageNode.getAttribute( "Format" ), 1636 sizeNode = imageNode.getElementsByTagName( "Size" )[ 0 ], 1637 dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ), 1638 width = parseInt( sizeNode.getAttribute( "Width" ) ), 1639 height = parseInt( sizeNode.getAttribute( "Height" ) ), 1640 tileSize = parseInt( imageNode.getAttribute( "TileSize" ) ), 1641 tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ) ), 1642 dispRects = [], 1643 dispRectNode, 1644 rectNode, 1645 i; 1646 1647 if ( !imageFormatSupported( fileFormat ) ) { 1648 throw new Error( 1649 $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) 1650 ); 1651 } 1652 1653 for ( i = 0; i < dispRectNodes.length; i++ ) { 1654 dispRectNode = dispRectNodes[ i ]; 1655 rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ]; 1656 1657 dispRects.push( new $.DisplayRect( 1658 parseInt( rectNode.getAttribute( "X" ) ), 1659 parseInt( rectNode.getAttribute( "Y" ) ), 1660 parseInt( rectNode.getAttribute( "Width" ) ), 1661 parseInt( rectNode.getAttribute( "Height" ) ), 1662 0, // ignore MinLevel attribute, bug in Deep Zoom Composer 1663 parseInt( dispRectNode.getAttribute( "MaxLevel" ) ) 1664 )); 1665 } 1666 return new $.DziTileSource( 1667 width, 1668 height, 1669 tileSize, 1670 tileOverlap, 1671 tilesUrl, 1672 fileFormat, 1673 dispRects 1674 ); 1675 }; 1676 1677 /** 1678 * @private 1679 * @inner 1680 * @function 1681 * @param {Document} errorNode 1682 * @throws {Error} 1683 */ 1684 function processDZIError( errorNode ) { 1685 var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ], 1686 message = messageNode.firstChild.nodeValue; 1687 1688 throw new Error(message); 1689 }; 1690 1691 /** 1692 * Reports whether the image format is supported for tiling in this 1693 * version. 1694 * @private 1695 * @inner 1696 * @function 1697 * @param {String} [extension] 1698 * @returns {Boolean} 1699 */ 1700 function imageFormatSupported( extension ) { 1701 extension = extension ? extension : ""; 1702 return !!FILEFORMATS[ extension.toLowerCase() ]; 1703 }; 1704 1705 /** 1706 * Parses an XML string into a DOM Document. 1707 * @private 1708 * @inner 1709 * @function 1710 * @name OpenSeadragon.parseXml 1711 * @param {String} string 1712 * @returns {Document} 1713 */ 1714 function parseXml( string ) { 1715 //TODO: yet another example where we can determine the correct 1716 // implementation once at start-up instead of everytime we use 1717 // the function. DONE. 1718 if ( window.ActiveXObject ) { 1719 1720 $.parseXml = function( string ){ 1721 var xmlDoc = null, 1722 parser; 1723 1724 xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); 1725 xmlDoc.async = false; 1726 xmlDoc.loadXML( string ); 1727 return xmlDoc; 1728 }; 1729 1730 } else if ( window.DOMParser ) { 1731 1732 $.parseXml = function( string ){ 1733 var xmlDoc = null, 1734 parser; 1735 1736 parser = new DOMParser(); 1737 xmlDoc = parser.parseFromString( string, "text/xml" ); 1738 return xmlDoc; 1739 }; 1740 1741 } else { 1742 throw new Error( "Browser doesn't support XML DOM." ); 1743 } 1744 1745 return $.parseXml( string ); 1746 }; 1747 1748 }( OpenSeadragon )); 1749 1750 (function($){ 1751 1752 /** 1753 * For use by classes which want to support custom, non-browser events. 1754 * @class 1755 */ 1756 $.EventHandler = function() { 1757 this.events = {}; 1758 }; 1759 1760 $.EventHandler.prototype = { 1761 1762 /** 1763 * Add an event handler for a given event. 1764 * @function 1765 * @param {String} eventName - Name of event to register. 1766 * @param {Function} handler - Function to call when event is triggered. 1767 */ 1768 addHandler: function( eventName, handler ) { 1769 var events = this.events[ eventName ]; 1770 if( !events ){ 1771 this.events[ eventName ] = events = []; 1772 } 1773 if( handler && $.isFunction( handler ) ){ 1774 events[ events.length ] = handler; 1775 } 1776 }, 1777 1778 /** 1779 * Remove a specific event handler for a given event. 1780 * @function 1781 * @param {String} eventName - Name of event for which the handler is to be removed. 1782 * @param {Function} handler - Function to be removed. 1783 */ 1784 removeHandler: function( eventName, handler ) { 1785 //Start Thatcher - unneccessary indirection. Also, because events were 1786 // - not actually being removed, we need to add the code 1787 // - to do the removal ourselves. TODO 1788 var events = this.events[ eventName ]; 1789 if ( !events ){ 1790 return; 1791 } 1792 //End Thatcher 1793 }, 1794 1795 /** 1796 * Retrive the list of all handlers registered for a given event. 1797 * @function 1798 * @param {String} eventName - Name of event to get handlers for. 1799 */ 1800 getHandler: function( eventName ) { 1801 var events = this.events[ eventName ]; 1802 if ( !events || !events.length ){ 1803 return null; 1804 } 1805 events = events.length === 1 ? 1806 [ events[ 0 ] ] : 1807 Array.apply( null, events ); 1808 return function( source, args ) { 1809 var i, 1810 length = events.length; 1811 for ( i = 0; i < length; i++ ) { 1812 if( events[ i ] ){ 1813 events[ i ]( source, args ); 1814 } 1815 } 1816 }; 1817 }, 1818 1819 /** 1820 * Trigger an event, optionally passing additional information. 1821 * @function 1822 * @param {String} eventName - Name of event to register. 1823 * @param {Function} handler - Function to call when event is triggered. 1824 */ 1825 raiseEvent: function( eventName, eventArgs ) { 1826 var handler = this.getHandler( eventName ); 1827 1828 if ( handler ) { 1829 if ( !eventArgs ) { 1830 eventArgs = new Object(); 1831 } 1832 1833 handler( this, eventArgs ); 1834 } 1835 } 1836 }; 1837 1838 }( OpenSeadragon )); 1839 1840 (function( $ ){ 1841 1842 // is any button currently being pressed while mouse events occur 1843 var IS_BUTTON_DOWN = false, 1844 // is any tracker currently capturing? 1845 IS_CAPTURING = false, 1846 // dictionary from hash to MouseTracker 1847 ACTIVE = {}, 1848 // list of trackers interested in capture 1849 CAPTURING = [], 1850 // dictionary from hash to private properties 1851 THIS = {}; 1852 1853 /** 1854 * The MouseTracker allows other classes to set handlers for common mouse 1855 * events on a specific element like, 'enter', 'exit', 'press', 'release', 1856 * 'scroll', 'click', and 'drag'. 1857 * @class 1858 * @param {Object} options 1859 * Allows configurable properties to be entirely specified by passing 1860 * an options object to the constructor. The constructor also supports 1861 * the original positional arguments 'elements', 'clickTimeThreshold', 1862 * and 'clickDistThreshold' in that order. 1863 * @param {Element|String} options.element 1864 * A reference to an element or an element id for which the mouse 1865 * events will be monitored. 1866 * @param {Number} options.clickTimeThreshold 1867 * The number of milliseconds within which mutliple mouse clicks 1868 * will be treated as a single event. 1869 * @param {Number} options.clickDistThreshold 1870 * The distance between mouse click within multiple mouse clicks 1871 * will be treated as a single event. 1872 * @param {Function} options.enterHandler 1873 * An optional handler for mouse enter. 1874 * @param {Function} options.exitHandler 1875 * An optional handler for mouse exit. 1876 * @param {Function} options.pressHandler 1877 * An optional handler for mouse press. 1878 * @param {Function} options.releaseHandler 1879 * An optional handler for mouse release. 1880 * @param {Function} options.scrollHandler 1881 * An optional handler for mouse scroll. 1882 * @param {Function} options.clickHandler 1883 * An optional handler for mouse click. 1884 * @param {Function} options.dragHandler 1885 * An optional handler for mouse drag. 1886 * @property {Number} hash 1887 * An unique hash for this tracker. 1888 * @property {Element} element 1889 * The element for which mouse event are being monitored. 1890 * @property {Number} clickTimeThreshold 1891 * The number of milliseconds within which mutliple mouse clicks 1892 * will be treated as a single event. 1893 * @property {Number} clickDistThreshold 1894 * The distance between mouse click within multiple mouse clicks 1895 * will be treated as a single event. 1896 */ 1897 $.MouseTracker = function ( options ) { 1898 1899 var args = arguments; 1900 1901 if( !$.isPlainObject( options ) ){ 1902 options = { 1903 element: args[ 0 ], 1904 clickTimeThreshold: args[ 1 ], 1905 clickDistThreshold: args[ 2 ] 1906 }; 1907 } 1908 1909 this.hash = Math.random(); 1910 this.element = $.getElement( options.element ); 1911 this.clickTimeThreshold = options.clickTimeThreshold; 1912 this.clickDistThreshold = options.clickDistThreshold; 1913 1914 1915 this.enterHandler = options.enterHandler || null; 1916 this.exitHandler = options.exitHandler || null; 1917 this.pressHandler = options.pressHandler || null; 1918 this.releaseHandler = options.releaseHandler || null; 1919 this.scrollHandler = options.scrollHandler || null; 1920 this.clickHandler = options.clickHandler || null; 1921 this.dragHandler = options.dragHandler || null; 1922 this.keyHandler = options.keyHandler || null; 1923 this.focusHandler = options.focusHandler || null; 1924 this.blurHandler = options.blurHandler || null; 1925 1926 //Store private properties in a scope sealed hash map 1927 var _this = this; 1928 1929 /** 1930 * @private 1931 * @property {Boolean} tracking 1932 * Are we currently tracking mouse events. 1933 * @property {Boolean} capturing 1934 * Are we curruently capturing mouse events. 1935 * @property {Boolean} buttonDown 1936 * True if the left mouse button is currently being pressed and was 1937 * initiated inside the tracked element, otherwise false. 1938 * @property {Boolean} insideElement 1939 * Are we currently inside the screen area of the tracked element. 1940 * @property {OpenSeadragon.Point} lastPoint 1941 * Position of last mouse down/move 1942 * @property {Number} lastMouseDownTime 1943 * Time of last mouse down. 1944 * @property {OpenSeadragon.Point} lastMouseDownPoint 1945 * Position of last mouse down 1946 */ 1947 THIS[ this.hash ] = { 1948 "mouseover": function( event ){ onMouseOver( _this, event ); }, 1949 "mouseout": function( event ){ onMouseOut( _this, event ); }, 1950 "mousedown": function( event ){ onMouseDown( _this, event ); }, 1951 "mouseup": function( event ){ onMouseUp( _this, event ); }, 1952 "click": function( event ){ onMouseClick( _this, event ); }, 1953 "DOMMouseScroll": function( event ){ onMouseWheelSpin( _this, event ); }, 1954 "mousewheel": function( event ){ onMouseWheelSpin( _this, event ); }, 1955 "mouseupie": function( event ){ onMouseUpIE( _this, event ); }, 1956 "mousemoveie": function( event ){ onMouseMoveIE( _this, event ); }, 1957 "mouseupwindow": function( event ){ onMouseUpWindow( _this, event ); }, 1958 "mousemove": function( event ){ onMouseMove( _this, event ); }, 1959 "touchstart": function( event ){ onTouchStart( _this, event ); }, 1960 "touchmove": function( event ){ onTouchMove( _this, event ); }, 1961 "touchend": function( event ){ onTouchEnd( _this, event ); }, 1962 "keypress": function( event ){ onKeyPress( _this, event ); }, 1963 "focus": function( event ){ onFocus( _this, event ); }, 1964 "blur": function( event ){ onBlur( _this, event ); }, 1965 tracking: false, 1966 capturing: false, 1967 buttonDown: false, 1968 insideElement: false, 1969 lastPoint: null, 1970 lastMouseDownTime: null, 1971 lastMouseDownPoint: null, 1972 lastPinchDelta: 0 1973 }; 1974 1975 }; 1976 1977 $.MouseTracker.prototype = { 1978 1979 /** 1980 * Are we currently tracking events on this element. 1981 * @deprecated Just use this.tracking 1982 * @function 1983 * @returns {Boolean} Are we currently tracking events on this element. 1984 */ 1985 isTracking: function () { 1986 return THIS[ this.hash ].tracking; 1987 }, 1988 1989 /** 1990 * Enable or disable whether or not we are tracking events on this element. 1991 * @function 1992 * @param {Boolean} track True to start tracking, false to stop tracking. 1993 * @returns {OpenSeadragon.MouseTracker} Chainable. 1994 */ 1995 setTracking: function ( track ) { 1996 if ( track ) { 1997 startTracking( this ); 1998 } else { 1999 stopTracking( this ); 2000 } 2001 //chain 2002 return this; 2003 }, 2004 2005 /** 2006 * Implement or assign implmentation to these handlers during or after 2007 * calling the constructor. 2008 * @function 2009 * @param {OpenSeadragon.MouseTracker} tracker 2010 * A reference to the tracker instance. 2011 * @param {OpenSeadragon.Point} position 2012 * The poistion of the event on the screen. 2013 * @param {Boolean} buttonDown 2014 * True if the left mouse button is currently being pressed and was 2015 * initiated inside the tracked element, otherwise false. 2016 * @param {Boolean} buttonDownAny 2017 * Was the button down anywhere in the screen during the event. 2018 */ 2019 enterHandler: function(){}, 2020 2021 /** 2022 * Implement or assign implmentation to these handlers during or after 2023 * calling the constructor. 2024 * @function 2025 * @param {OpenSeadragon.MouseTracker} tracker 2026 * A reference to the tracker instance. 2027 * @param {OpenSeadragon.Point} position 2028 * The poistion of the event on the screen. 2029 * @param {Boolean} buttonDown 2030 * True if the left mouse button is currently being pressed and was 2031 * initiated inside the tracked element, otherwise false. 2032 * @param {Boolean} buttonDownAny 2033 * Was the button down anywhere in the screen during the event. 2034 */ 2035 exitHandler: function(){}, 2036 2037 /** 2038 * Implement or assign implmentation to these handlers during or after 2039 * calling the constructor. 2040 * @function 2041 * @param {OpenSeadragon.MouseTracker} tracker 2042 * A reference to the tracker instance. 2043 * @param {OpenSeadragon.Point} position 2044 * The poistion of the event on the screen. 2045 */ 2046 pressHandler: function(){}, 2047 2048 /** 2049 * Implement or assign implmentation to these handlers during or after 2050 * calling the constructor. 2051 * @function 2052 * @param {OpenSeadragon.MouseTracker} tracker 2053 * A reference to the tracker instance. 2054 * @param {OpenSeadragon.Point} position 2055 * The poistion of the event on the screen. 2056 * @param {Boolean} buttonDown 2057 * True if the left mouse button is currently being pressed and was 2058 * initiated inside the tracked element, otherwise false. 2059 * @param {Boolean} insideElementRelease 2060 * Was the mouse still inside the tracked element when the button 2061 * was released. 2062 */ 2063 releaseHandler: function(){}, 2064 2065 /** 2066 * Implement or assign implmentation to these handlers during or after 2067 * calling the constructor. 2068 * @function 2069 * @param {OpenSeadragon.MouseTracker} tracker 2070 * A reference to the tracker instance. 2071 * @param {OpenSeadragon.Point} position 2072 * The poistion of the event on the screen. 2073 * @param {Number} scroll 2074 * The scroll delta for the event. 2075 * @param {Boolean} shift 2076 * Was the shift key being pressed during this event? 2077 */ 2078 scrollHandler: function(){}, 2079 2080 /** 2081 * Implement or assign implmentation to these handlers during or after 2082 * calling the constructor. 2083 * @function 2084 * @param {OpenSeadragon.MouseTracker} tracker 2085 * A reference to the tracker instance. 2086 * @param {OpenSeadragon.Point} position 2087 * The poistion of the event on the screen. 2088 * @param {Boolean} quick 2089 * True only if the clickDistThreshold and clickDeltaThreshold are 2090 * both pased. Useful for ignoring events. 2091 * @param {Boolean} shift 2092 * Was the shift key being pressed during this event? 2093 */ 2094 clickHandler: function(){}, 2095 2096 /** 2097 * Implement or assign implmentation to these handlers during or after 2098 * calling the constructor. 2099 * @function 2100 * @param {OpenSeadragon.MouseTracker} tracker 2101 * A reference to the tracker instance. 2102 * @param {OpenSeadragon.Point} position 2103 * The poistion of the event on the screen. 2104 * @param {OpenSeadragon.Point} delta 2105 * The x,y components of the difference between start drag and 2106 * end drag. Usefule for ignoring or weighting the events. 2107 * @param {Boolean} shift 2108 * Was the shift key being pressed during this event? 2109 */ 2110 dragHandler: function(){}, 2111 2112 /** 2113 * Implement or assign implmentation to these handlers during or after 2114 * calling the constructor. 2115 * @function 2116 * @param {OpenSeadragon.MouseTracker} tracker 2117 * A reference to the tracker instance. 2118 * @param {Number} keyCode 2119 * The key code that was pressed. 2120 * @param {Boolean} shift 2121 * Was the shift key being pressed during this event? 2122 */ 2123 keyHandler: function(){}, 2124 2125 focusHandler: function(){}, 2126 2127 blurHandler: function(){} 2128 }; 2129 2130 /** 2131 * Starts tracking mouse events on this element. 2132 * @private 2133 * @inner 2134 */ 2135 function startTracking( tracker ) { 2136 var events = [ 2137 "mouseover", "mouseout", "mousedown", "mouseup", 2138 "click", 2139 "DOMMouseScroll", "mousewheel", 2140 "touchstart", "touchmove", "touchend", 2141 "keypress", 2142 "focus", "blur" 2143 ], 2144 delegate = THIS[ tracker.hash ], 2145 event, 2146 i; 2147 2148 if ( !delegate.tracking ) { 2149 for( i = 0; i < events.length; i++ ){ 2150 event = events[ i ]; 2151 $.addEvent( 2152 tracker.element, 2153 event, 2154 delegate[ event ], 2155 false 2156 ); 2157 } 2158 delegate.tracking = true; 2159 ACTIVE[ tracker.hash ] = tracker; 2160 } 2161 }; 2162 2163 /** 2164 * Stops tracking mouse events on this element. 2165 * @private 2166 * @inner 2167 */ 2168 function stopTracking( tracker ) { 2169 var events = [ 2170 "mouseover", "mouseout", "mousedown", "mouseup", 2171 "click", 2172 "DOMMouseScroll", "mousewheel", 2173 "touchstart", "touchmove", "touchend", 2174 "keypress", 2175 "focus", "blur" 2176 ], 2177 delegate = THIS[ tracker.hash ], 2178 event, 2179 i; 2180 2181 if ( delegate.tracking ) { 2182 for( i = 0; i < events.length; i++ ){ 2183 event = events[ i ]; 2184 $.removeEvent( 2185 tracker.element, 2186 event, 2187 delegate[ event ], 2188 false 2189 ); 2190 } 2191 2192 releaseMouse( tracker ); 2193 delegate.tracking = false; 2194 delete ACTIVE[ tracker.hash ]; 2195 } 2196 }; 2197 2198 /** 2199 * @private 2200 * @inner 2201 */ 2202 function hasMouse( tracker ) { 2203 return THIS[ tracker.hash ].insideElement; 2204 }; 2205 2206 /** 2207 * Begin capturing mouse events on this element. 2208 * @private 2209 * @inner 2210 */ 2211 function captureMouse( tracker ) { 2212 var delegate = THIS[ tracker.hash ]; 2213 if ( !delegate.capturing ) { 2214 2215 if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { 2216 $.removeEvent( 2217 tracker.element, 2218 "mouseup", 2219 delegate[ "mouseup" ], 2220 false 2221 ); 2222 $.addEvent( 2223 tracker.element, 2224 "mouseup", 2225 delegate[ "mouseupie" ], 2226 true 2227 ); 2228 $.addEvent( 2229 tracker.element, 2230 "mousemove", 2231 delegate[ "mousemoveie" ], 2232 true 2233 ); 2234 } else { 2235 $.addEvent( 2236 window, 2237 "mouseup", 2238 delegate[ "mouseupwindow" ], 2239 true 2240 ); 2241 $.addEvent( 2242 window, 2243 "mousemove", 2244 delegate[ "mousemove" ], 2245 true 2246 ); 2247 } 2248 delegate.capturing = true; 2249 } 2250 }; 2251 2252 2253 /** 2254 * Stop capturing mouse events on this element. 2255 * @private 2256 * @inner 2257 */ 2258 function releaseMouse( tracker ) { 2259 var delegate = THIS[ tracker.hash ]; 2260 if ( delegate.capturing ) { 2261 2262 if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { 2263 $.removeEvent( 2264 tracker.element, 2265 "mousemove", 2266 delegate[ "mousemoveie" ], 2267 true 2268 ); 2269 $.removeEvent( 2270 tracker.element, 2271 "mouseup", 2272 delegate[ "mouseupie" ], 2273 true 2274 ); 2275 $.addEvent( 2276 tracker.element, 2277 "mouseup", 2278 delegate[ "mouseup" ], 2279 false 2280 ); 2281 } else { 2282 $.removeEvent( 2283 window, 2284 "mousemove", 2285 delegate[ "mousemove" ], 2286 true 2287 ); 2288 $.removeEvent( 2289 window, 2290 "mouseup", 2291 delegate[ "mouseupwindow" ], 2292 true 2293 ); 2294 } 2295 delegate.capturing = false; 2296 } 2297 }; 2298 2299 2300 /** 2301 * @private 2302 * @inner 2303 */ 2304 function triggerOthers( tracker, handler, event ) { 2305 var otherHash; 2306 for ( otherHash in ACTIVE ) { 2307 if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { 2308 handler( ACTIVE[ otherHash ], event ); 2309 } 2310 } 2311 }; 2312 2313 2314 /** 2315 * @private 2316 * @inner 2317 */ 2318 function onFocus( tracker, event ){ 2319 //console.log( "focus %s", event ); 2320 var propagate; 2321 if ( tracker.focusHandler ) { 2322 propagate = tracker.focusHandler( 2323 tracker, 2324 event 2325 ); 2326 if( propagate === false ){ 2327 $.cancelEvent( event ); 2328 } 2329 } 2330 }; 2331 2332 2333 /** 2334 * @private 2335 * @inner 2336 */ 2337 function onBlur( tracker, event ){ 2338 //console.log( "blur %s", event ); 2339 var propagate; 2340 if ( tracker.blurHandler ) { 2341 propagate = tracker.blurHandler( 2342 tracker, 2343 event 2344 ); 2345 if( propagate === false ){ 2346 $.cancelEvent( event ); 2347 } 2348 } 2349 }; 2350 2351 2352 /** 2353 * @private 2354 * @inner 2355 */ 2356 function onKeyPress( tracker, event ){ 2357 //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); 2358 var propagate; 2359 if ( tracker.keyHandler ) { 2360 propagate = tracker.keyHandler( 2361 tracker, 2362 event.keyCode ? event.keyCode : event.charCode, 2363 event.shiftKey 2364 ); 2365 if( propagate === false ){ 2366 $.cancelEvent( event ); 2367 } 2368 } 2369 }; 2370 2371 2372 /** 2373 * @private 2374 * @inner 2375 */ 2376 function onMouseOver( tracker, event ) { 2377 2378 var event = $.getEvent( event ), 2379 delegate = THIS[ tracker.hash ], 2380 propagate; 2381 2382 if ( $.Browser.vendor == $.BROWSERS.IE && 2383 $.Browser.version < 9 && 2384 delegate.capturing && 2385 !isChild( event.srcElement, tracker.element ) ) { 2386 2387 triggerOthers( tracker, onMouseOver, event ); 2388 2389 } 2390 2391 var to = event.target ? 2392 event.target : 2393 event.srcElement, 2394 from = event.relatedTarget ? 2395 event.relatedTarget : 2396 event.fromElement; 2397 2398 if ( !isChild( tracker.element, to ) || 2399 isChild( tracker.element, from ) ) { 2400 return; 2401 } 2402 2403 delegate.insideElement = true; 2404 2405 if ( tracker.enterHandler ) { 2406 propagate = tracker.enterHandler( 2407 tracker, 2408 getMouseRelative( event, tracker.element ), 2409 delegate.buttonDown, 2410 IS_BUTTON_DOWN 2411 ); 2412 if( propagate === false ){ 2413 $.cancelEvent( event ); 2414 } 2415 } 2416 }; 2417 2418 2419 /** 2420 * @private 2421 * @inner 2422 */ 2423 function onMouseOut( tracker, event ) { 2424 var event = $.getEvent( event ), 2425 delegate = THIS[ tracker.hash ], 2426 propagate; 2427 2428 if ( $.Browser.vendor == $.BROWSERS.IE && 2429 $.Browser.version < 9 && 2430 delegate.capturing && 2431 !isChild( event.srcElement, tracker.element ) ) { 2432 2433 triggerOthers( tracker, onMouseOut, event ); 2434 2435 } 2436 2437 var from = event.target ? 2438 event.target : 2439 event.srcElement, 2440 to = event.relatedTarget ? 2441 event.relatedTarget : 2442 event.toElement; 2443 2444 if ( !isChild( tracker.element, from ) || 2445 isChild( tracker.element, to ) ) { 2446 return; 2447 } 2448 2449 delegate.insideElement = false; 2450 2451 if ( tracker.exitHandler ) { 2452 propagate = tracker.exitHandler( 2453 tracker, 2454 getMouseRelative( event, tracker.element ), 2455 delegate.buttonDown, 2456 IS_BUTTON_DOWN 2457 ); 2458 2459 if( propagate === false ){ 2460 $.cancelEvent( event ); 2461 } 2462 } 2463 }; 2464 2465 2466 /** 2467 * @private 2468 * @inner 2469 */ 2470 function onMouseDown( tracker, event ) { 2471 var event = $.getEvent( event ), 2472 delegate = THIS[ tracker.hash ], 2473 propagate; 2474 2475 if ( event.button == 2 ) { 2476 return; 2477 } 2478 2479 delegate.buttonDown = true; 2480 2481 delegate.lastPoint = getMouseAbsolute( event ); 2482 delegate.lastMouseDownPoint = delegate.lastPoint; 2483 delegate.lastMouseDownTime = +new Date(); 2484 2485 if ( tracker.pressHandler ) { 2486 propagate = tracker.pressHandler( 2487 tracker, 2488 getMouseRelative( event, tracker.element ) 2489 ); 2490 if( propagate === false ){ 2491 $.cancelEvent( event ); 2492 } 2493 } 2494 2495 if ( tracker.pressHandler || tracker.dragHandler ) { 2496 $.cancelEvent( event ); 2497 } 2498 2499 if ( !( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) || 2500 !IS_CAPTURING ) { 2501 captureMouse( tracker ); 2502 IS_CAPTURING = true; 2503 // reset to empty & add us 2504 CAPTURING = [ tracker ]; 2505 } else if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { 2506 // add us to the list 2507 CAPTURING.push( tracker ); 2508 } 2509 }; 2510 2511 /** 2512 * @private 2513 * @inner 2514 */ 2515 function onTouchStart( tracker, event ) { 2516 var touchA, 2517 touchB; 2518 2519 if( event.touches.length == 1 && 2520 event.targetTouches.length == 1 && 2521 event.changedTouches.length == 1 ){ 2522 2523 THIS[ tracker.hash ].lastTouch = event.touches[ 0 ]; 2524 onMouseOver( tracker, event.changedTouches[ 0 ] ); 2525 onMouseDown( tracker, event.touches[ 0 ] ); 2526 } 2527 2528 if( event.touches.length == 2 ){ 2529 2530 touchA = getMouseAbsolute( event.touches[ 0 ] ); 2531 touchB = getMouseAbsolute( event.touches[ 1 ] ); 2532 THIS[ tracker.hash ].lastPinchDelta = 2533 Math.abs( touchA.x - touchB.x ) + 2534 Math.abs( touchA.y - touchB.y ); 2535 //$.console.debug("pinch start : "+THIS[ tracker.hash ].lastPinchDelta); 2536 } 2537 2538 event.preventDefault(); 2539 }; 2540 2541 2542 /** 2543 * @private 2544 * @inner 2545 */ 2546 function onMouseUp( tracker, event ) { 2547 var event = $.getEvent( event ), 2548 delegate = THIS[ tracker.hash ], 2549 //were we inside the tracked element when we were pressed 2550 insideElementPress = delegate.buttonDown, 2551 //are we still inside the tracked element when we released 2552 insideElementRelease = delegate.insideElement, 2553 propagate; 2554 2555 if ( event.button == 2 ) { 2556 return; 2557 } 2558 2559 delegate.buttonDown = false; 2560 2561 if ( tracker.releaseHandler ) { 2562 propagate = tracker.releaseHandler( 2563 tracker, 2564 getMouseRelative( event, tracker.element ), 2565 insideElementPress, 2566 insideElementRelease 2567 ); 2568 if( propagate === false ){ 2569 $.cancelEvent( event ); 2570 } 2571 } 2572 2573 if ( insideElementPress && insideElementRelease ) { 2574 handleMouseClick( tracker, event ); 2575 } 2576 }; 2577 2578 2579 /** 2580 * @private 2581 * @inner 2582 */ 2583 function onTouchEnd( tracker, event ) { 2584 2585 if( event.touches.length == 0 && 2586 event.targetTouches.length == 0 && 2587 event.changedTouches.length == 1 ){ 2588 2589 THIS[ tracker.hash ].lastTouch = null; 2590 onMouseUp( tracker, event.changedTouches[ 0 ] ); 2591 onMouseOut( tracker, event.changedTouches[ 0 ] ); 2592 } 2593 if( event.touches.length + event.changedTouches.length == 2 ){ 2594 THIS[ tracker.hash ].lastPinchDelta = null; 2595 //$.console.debug("pinch end"); 2596 } 2597 event.preventDefault(); 2598 }; 2599 2600 2601 /** 2602 * Only triggered once by the deepest element that initially received 2603 * the mouse down event. We want to make sure THIS event doesn't bubble. 2604 * Instead, we want to trigger the elements that initially received the 2605 * mouse down event (including this one) only if the mouse is no longer 2606 * inside them. Then, we want to release capture, and emulate a regular 2607 * mouseup on the event that this event was meant for. 2608 * @private 2609 * @inner 2610 */ 2611 function onMouseUpIE( tracker, event ) { 2612 var event = $.getEvent( event ), 2613 othertracker, 2614 i; 2615 2616 if ( event.button == 2 ) { 2617 return; 2618 } 2619 2620 for ( i = 0; i < CAPTURING.length; i++ ) { 2621 othertracker = CAPTURING[ i ]; 2622 if ( !hasMouse( othertracker ) ) { 2623 onMouseUp( othertracker, event ); 2624 } 2625 } 2626 2627 releaseMouse( tracker ); 2628 IS_CAPTURING = false; 2629 event.srcElement.fireEvent( 2630 "on" + event.type, 2631 document.createEventObject( event ) 2632 ); 2633 2634 $.stopEvent( event ); 2635 }; 2636 2637 2638 /** 2639 * Only triggered in W3C browsers by elements within which the mouse was 2640 * initially pressed, since they are now listening to the window for 2641 * mouseup during the capture phase. We shouldn't handle the mouseup 2642 * here if the mouse is still inside this element, since the regular 2643 * mouseup handler will still fire. 2644 * @private 2645 * @inner 2646 */ 2647 function onMouseUpWindow( tracker, event ) { 2648 if ( ! THIS[ tracker.hash ].insideElement ) { 2649 onMouseUp( tracker, event ); 2650 } 2651 releaseMouse( tracker ); 2652 }; 2653 2654 2655 /** 2656 * @private 2657 * @inner 2658 */ 2659 function onMouseClick( tracker, event ) { 2660 if ( tracker.clickHandler ) { 2661 $.cancelEvent( event ); 2662 } 2663 }; 2664 2665 2666 /** 2667 * @private 2668 * @inner 2669 */ 2670 function onMouseWheelSpin( tracker, event ) { 2671 var nDelta = 0, 2672 propagate; 2673 2674 if ( !event ) { // For IE, access the global (window) event object 2675 event = window.event; 2676 } 2677 2678 if ( event.wheelDelta ) { // IE and Opera 2679 nDelta = event.wheelDelta; 2680 if ( window.opera ) { // Opera has the values reversed 2681 nDelta = -nDelta; 2682 } 2683 } else if (event.detail) { // Mozilla FireFox 2684 nDelta = -event.detail; 2685 } 2686 //The nDelta variable is gated to provide smooth z-index scrolling 2687 //since the mouse wheel allows for substantial deltas meant for rapid 2688 //y-index scrolling. 2689 nDelta = nDelta > 0 ? 1 : -1; 2690 2691 if ( tracker.scrollHandler ) { 2692 propagate = tracker.scrollHandler( 2693 tracker, 2694 getMouseRelative( event, tracker.element ), 2695 nDelta, 2696 event.shiftKey 2697 ); 2698 if( propagate === false ){ 2699 $.cancelEvent( event ); 2700 } 2701 } 2702 }; 2703 2704 2705 /** 2706 * @private 2707 * @inner 2708 */ 2709 function handleMouseClick( tracker, event ) { 2710 var event = $.getEvent( event ), 2711 delegate = THIS[ tracker.hash ], 2712 propagate; 2713 2714 if ( event.button == 2 ) { 2715 return; 2716 } 2717 2718 var time = +new Date() - delegate.lastMouseDownTime, 2719 point = getMouseAbsolute( event ), 2720 distance = delegate.lastMouseDownPoint.distanceTo( point ), 2721 quick = time <= tracker.clickTimeThreshold && 2722 distance <= tracker.clickDistThreshold; 2723 2724 if ( tracker.clickHandler ) { 2725 propagate = tracker.clickHandler( 2726 tracker, 2727 getMouseRelative( event, tracker.element ), 2728 quick, 2729 event.shiftKey 2730 ); 2731 if( propagate === false ){ 2732 $.cancelEvent( event ); 2733 } 2734 } 2735 }; 2736 2737 2738 /** 2739 * @private 2740 * @inner 2741 */ 2742 function onMouseMove( tracker, event ) { 2743 var event = $.getEvent( event ), 2744 delegate = THIS[ tracker.hash ], 2745 point = getMouseAbsolute( event ), 2746 delta = point.minus( delegate.lastPoint ), 2747 propagate; 2748 2749 delegate.lastPoint = point; 2750 2751 if ( tracker.dragHandler ) { 2752 propagate = tracker.dragHandler( 2753 tracker, 2754 getMouseRelative( event, tracker.element ), 2755 delta, 2756 event.shiftKey 2757 ); 2758 if( propagate === false ){ 2759 $.cancelEvent( event ); 2760 } 2761 } 2762 }; 2763 2764 2765 /** 2766 * @private 2767 * @inner 2768 */ 2769 function onTouchMove( tracker, event ) { 2770 var touchA, 2771 touchB, 2772 pinchDelta; 2773 2774 if( event.touches.length === 1 && 2775 event.targetTouches.length === 1 && 2776 event.changedTouches.length === 1 && 2777 THIS[ tracker.hash ].lastTouch === event.touches[ 0 ]){ 2778 2779 onMouseMove( tracker, event.touches[ 0 ] ); 2780 2781 } else if ( event.touches.length === 2 ){ 2782 2783 touchA = getMouseAbsolute( event.touches[ 0 ] ); 2784 touchB = getMouseAbsolute( event.touches[ 1 ] ); 2785 pinchDelta = 2786 Math.abs( touchA.x - touchB.x ) + 2787 Math.abs( touchA.y - touchB.y ); 2788 2789 //TODO: make the 75px pinch threshold configurable 2790 if( Math.abs( THIS[ tracker.hash ].lastPinchDelta - pinchDelta ) > 75 ){ 2791 //$.console.debug( "pinch delta : " + pinchDelta + " | previous : " + THIS[ tracker.hash ].lastPinchDelta); 2792 2793 onMouseWheelSpin( tracker, { 2794 shift: false, 2795 pageX: ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2, 2796 pageY: ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2, 2797 detail:( 2798 THIS[ tracker.hash ].lastPinchDelta > pinchDelta 2799 ) ? 1 : -1 2800 }); 2801 2802 THIS[ tracker.hash ].lastPinchDelta = pinchDelta; 2803 } 2804 } 2805 event.preventDefault(); 2806 }; 2807 2808 /** 2809 * Only triggered once by the deepest element that initially received 2810 * the mouse down event. Since no other element has captured the mouse, 2811 * we want to trigger the elements that initially received the mouse 2812 * down event (including this one). The the param tracker isn't used 2813 * but for consistency with the other event handlers we include it. 2814 * @private 2815 * @inner 2816 */ 2817 function onMouseMoveIE( tracker, event ) { 2818 var i; 2819 for ( i = 0; i < CAPTURING.length; i++ ) { 2820 onMouseMove( CAPTURING[ i ], event ); 2821 } 2822 2823 $.stopEvent( event ); 2824 }; 2825 2826 /** 2827 * @private 2828 * @inner 2829 */ 2830 function getMouseAbsolute( event ) { 2831 return $.getMousePosition( event ); 2832 }; 2833 2834 /** 2835 * @private 2836 * @inner 2837 */ 2838 function getMouseRelative( event, element ) { 2839 var mouse = $.getMousePosition( event ), 2840 offset = $.getElementPosition( element ); 2841 2842 return mouse.minus( offset ); 2843 }; 2844 2845 /** 2846 * @private 2847 * @inner 2848 * Returns true if elementB is a child node of elementA, or if they're equal. 2849 */ 2850 function isChild( elementA, elementB ) { 2851 var body = document.body; 2852 while ( elementB && elementA != elementB && body != elementB ) { 2853 try { 2854 elementB = elementB.parentNode; 2855 } catch (e) { 2856 return false; 2857 } 2858 } 2859 return elementA == elementB; 2860 }; 2861 2862 /** 2863 * @private 2864 * @inner 2865 */ 2866 function onGlobalMouseDown() { 2867 IS_BUTTON_DOWN = true; 2868 }; 2869 2870 /** 2871 * @private 2872 * @inner 2873 */ 2874 function onGlobalMouseUp() { 2875 IS_BUTTON_DOWN = false; 2876 }; 2877 2878 2879 (function () { 2880 if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { 2881 $.addEvent( document, "mousedown", onGlobalMouseDown, false ); 2882 $.addEvent( document, "mouseup", onGlobalMouseUp, false ); 2883 } else { 2884 $.addEvent( window, "mousedown", onGlobalMouseDown, true ); 2885 $.addEvent( window, "mouseup", onGlobalMouseUp, true ); 2886 } 2887 })(); 2888 2889 }( OpenSeadragon )); 2890 2891 (function( $ ){ 2892 2893 /** 2894 * An enumeration of supported locations where controls can be anchored, 2895 * including NONE, TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, and BOTTOM_LEFT. 2896 * The anchoring is always relative to the container 2897 * @static 2898 */ 2899 $.ControlAnchor = { 2900 NONE: 0, 2901 TOP_LEFT: 1, 2902 TOP_RIGHT: 2, 2903 BOTTOM_RIGHT: 3, 2904 BOTTOM_LEFT: 4 2905 }; 2906 2907 /** 2908 * A Control represents any interface element which is meant to allow the user 2909 * to interact with the zoomable interface. Any control can be anchored to any 2910 * element. 2911 * @class 2912 * @param {Element} element - the contol element to be anchored in the container. 2913 * @param {OpenSeadragon.ControlAnchor} anchor - the location to anchor at. 2914 * @param {Element} container - the element to control will be anchored too. 2915 * 2916 * @property {Element} element - the element providing the user interface with 2917 * some type of control. Eg a zoom-in button 2918 * @property {OpenSeadragon.ControlAnchor} anchor - the position of the control 2919 * relative to the container. 2920 * @property {Element} container - the element within with the control is 2921 * positioned. 2922 * @property {Element} wrapper - a nuetral element surrounding the control 2923 * element. 2924 */ 2925 $.Control = function ( element, anchor, container ) { 2926 this.element = element; 2927 this.anchor = anchor; 2928 this.container = container; 2929 this.wrapper = $.makeNeutralElement( "span" ); 2930 this.wrapper.style.display = "inline-block"; 2931 this.wrapper.appendChild( this.element ); 2932 2933 if ( this.anchor == $.ControlAnchor.NONE ) { 2934 // IE6 fix 2935 this.wrapper.style.width = this.wrapper.style.height = "100%"; 2936 } 2937 2938 if ( this.anchor == $.ControlAnchor.TOP_RIGHT || 2939 this.anchor == $.ControlAnchor.BOTTOM_RIGHT ) { 2940 this.container.insertBefore( 2941 this.wrapper, 2942 this.container.firstChild 2943 ); 2944 } else { 2945 this.container.appendChild( this.wrapper ); 2946 } 2947 }; 2948 2949 $.Control.prototype = { 2950 2951 /** 2952 * Removes the control from the container. 2953 * @function 2954 */ 2955 destroy: function() { 2956 this.wrapper.removeChild( this.element ); 2957 this.container.removeChild( this.wrapper ); 2958 }, 2959 2960 /** 2961 * Determines if the control is currently visible. 2962 * @function 2963 * @return {Boolean} true if currenly visible, false otherwise. 2964 */ 2965 isVisible: function() { 2966 return this.wrapper.style.display != "none"; 2967 }, 2968 2969 /** 2970 * Toggles the visibility of the control. 2971 * @function 2972 * @param {Boolean} visible - true to make visible, false to hide. 2973 */ 2974 setVisible: function( visible ) { 2975 this.wrapper.style.display = visible ? 2976 "inline-block" : 2977 "none"; 2978 }, 2979 2980 /** 2981 * Sets the opacity level for the control. 2982 * @function 2983 * @param {Number} opactiy - a value between 1 and 0 inclusively. 2984 */ 2985 setOpacity: function( opacity ) { 2986 if ( this.element[ $.SIGNAL ] && $.Browser.vendor == $.BROWSERS.IE ) { 2987 $.setElementOpacity( this.element, opacity, true ); 2988 } else { 2989 $.setElementOpacity( this.wrapper, opacity, true ); 2990 } 2991 } 2992 }; 2993 2994 }( OpenSeadragon )); 2995 (function( $ ){ 2996 2997 //id hash for private properties; 2998 var THIS = {}; 2999 3000 /** 3001 * @class 3002 */ 3003 $.ControlDock = function( options ){ 3004 var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'], 3005 layout, 3006 i; 3007 3008 $.extend( true, this, { 3009 id: 'controldock-'+(+new Date())+'-'+Math.floor(Math.random()*1000000), 3010 container: $.makeNeutralElement('form'), 3011 controls: [] 3012 }, options ); 3013 3014 if( this.element ){ 3015 this.element = $.getElement( this.element ); 3016 this.element.appendChild( this.container ); 3017 this.element.style.position = 'relative'; 3018 this.container.style.width = '100%'; 3019 this.container.style.height = '100%'; 3020 } 3021 3022 for( i = 0; i < layouts.length; i++ ){ 3023 layout = layouts[ i ]; 3024 this.controls[ layout ] = $.makeNeutralElement( "div" ); 3025 this.controls[ layout ].style.position = 'absolute'; 3026 if ( layout.match( 'left' ) ){ 3027 this.controls[ layout ].style.left = '0px'; 3028 } 3029 if ( layout.match( 'right' ) ){ 3030 this.controls[ layout ].style.right = '0px'; 3031 } 3032 if ( layout.match( 'top' ) ){ 3033 this.controls[ layout ].style.top = '0px'; 3034 } 3035 if ( layout.match( 'bottom' ) ){ 3036 this.controls[ layout ].style.bottom = '0px'; 3037 } 3038 } 3039 3040 this.container.appendChild( this.controls.topleft ); 3041 this.container.appendChild( this.controls.topright ); 3042 this.container.appendChild( this.controls.bottomright ); 3043 this.container.appendChild( this.controls.bottomleft ); 3044 }; 3045 3046 $.ControlDock.prototype = { 3047 3048 /** 3049 * @function 3050 */ 3051 addControl: function ( element, anchor ) { 3052 var element = $.getElement( element ), 3053 div = null; 3054 3055 if ( getControlIndex( this, element ) >= 0 ) { 3056 return; // they're trying to add a duplicate control 3057 } 3058 3059 switch ( anchor ) { 3060 case $.ControlAnchor.TOP_RIGHT: 3061 div = this.controls.topright; 3062 element.style.position = "relative"; 3063 element.style.marginRight = "4px"; 3064 element.style.marginTop = "4px"; 3065 break; 3066 case $.ControlAnchor.BOTTOM_RIGHT: 3067 div = this.controls.bottomright; 3068 element.style.position = "relative"; 3069 element.style.marginRight = "4px"; 3070 element.style.marginBottom = "4px"; 3071 break; 3072 case $.ControlAnchor.BOTTOM_LEFT: 3073 div = this.controls.bottomleft; 3074 element.style.position = "relative"; 3075 element.style.marginLeft = "4px"; 3076 element.style.marginBottom = "4px"; 3077 break; 3078 case $.ControlAnchor.TOP_LEFT: 3079 div = this.controls.topleft; 3080 element.style.position = "relative"; 3081 element.style.marginLeft = "4px"; 3082 element.style.marginTop = "4px"; 3083 break; 3084 case $.ControlAnchor.NONE: 3085 default: 3086 div = this.container; 3087 element.style.margin = "0px"; 3088 element.style.padding = "0px"; 3089 break; 3090 } 3091 3092 this.controls.push( 3093 new $.Control( element, anchor, div ) 3094 ); 3095 element.style.display = "inline-block"; 3096 }, 3097 3098 3099 /** 3100 * @function 3101 * @return {OpenSeadragon.ControlDock} Chainable. 3102 */ 3103 removeControl: function ( element ) { 3104 var element = $.getElement( element ), 3105 i = getControlIndex( this, element ); 3106 3107 if ( i >= 0 ) { 3108 this.controls[ i ].destroy(); 3109 this.controls.splice( i, 1 ); 3110 } 3111 3112 return this; 3113 }, 3114 3115 /** 3116 * @function 3117 * @return {OpenSeadragon.ControlDock} Chainable. 3118 */ 3119 clearControls: function () { 3120 while ( this.controls.length > 0 ) { 3121 this.controls.pop().destroy(); 3122 } 3123 3124 return this; 3125 }, 3126 3127 3128 /** 3129 * @function 3130 * @return {Boolean} 3131 */ 3132 areControlsEnabled: function () { 3133 var i; 3134 3135 for ( i = this.controls.length - 1; i >= 0; i-- ) { 3136 if ( this.controls[ i ].isVisible() ) { 3137 return true; 3138 } 3139 } 3140 3141 return false; 3142 }, 3143 3144 3145 /** 3146 * @function 3147 * @return {OpenSeadragon.ControlDock} Chainable. 3148 */ 3149 setControlsEnabled: function( enabled ) { 3150 var i; 3151 3152 for ( i = this.controls.length - 1; i >= 0; i-- ) { 3153 this.controls[ i ].setVisible( enabled ); 3154 } 3155 3156 return this; 3157 } 3158 3159 }; 3160 3161 3162 /////////////////////////////////////////////////////////////////////////////// 3163 // Utility methods 3164 /////////////////////////////////////////////////////////////////////////////// 3165 function getControlIndex( dock, element ) { 3166 var controls = dock.controls, 3167 i; 3168 3169 for ( i = controls.length - 1; i >= 0; i-- ) { 3170 if ( controls[ i ].element == element ) { 3171 return i; 3172 } 3173 } 3174 3175 return -1; 3176 }; 3177 3178 }( OpenSeadragon )); 3179 (function( $ ){ 3180 3181 // dictionary from hash to private properties 3182 var THIS = {}, 3183 // We keep a list of viewers so we can 'wake-up' each viewer on 3184 // a page after toggling between fullpage modes 3185 VIEWERS = {}; 3186 3187 /** 3188 * 3189 * The main point of entry into creating a zoomable image on the page. 3190 * 3191 * We have provided an idiomatic javascript constructor which takes 3192 * a single object, but still support the legacy positional arguments. 3193 * 3194 * The options below are given in order that they appeared in the constructor 3195 * as arguments and we translate a positional call into an idiomatic call. 3196 * 3197 * @class 3198 * @extends OpenSeadragon.EventHandler 3199 * @extends OpenSeadragon.ControlDock 3200 * @param {Object} options 3201 * @param {String} options.element Id of Element to attach to, 3202 * @param {String} options.xmlPath Xpath ( TODO: not sure! ), 3203 * @param {String} options.prefixUrl Url used to prepend to paths, eg button 3204 * images, etc. 3205 * @param {Seadragon.Controls[]} options.controls Array of Seadragon.Controls, 3206 * @param {Seadragon.Overlays[]} options.overlays Array of Seadragon.Overlays, 3207 * @param {Seadragon.Controls[]} options.overlayControls An Array of ( TODO: 3208 * not sure! ) 3209 * 3210 **/ 3211 $.Viewer = function( options ) { 3212 3213 var args = arguments, 3214 _this = this, 3215 i; 3216 3217 3218 //backward compatibility for positional args while prefering more 3219 //idiomatic javascript options object as the only argument 3220 if( !$.isPlainObject( options ) ){ 3221 options = { 3222 id: args[ 0 ], 3223 xmlPath: args.length > 1 ? args[ 1 ] : undefined, 3224 prefixUrl: args.length > 2 ? args[ 2 ] : undefined, 3225 controls: args.length > 3 ? args[ 3 ] : undefined, 3226 overlays: args.length > 4 ? args[ 4 ] : undefined, 3227 overlayControls: args.length > 5 ? args[ 5 ] : undefined 3228 }; 3229 } 3230 3231 //options.config and the general config argument are deprecated 3232 //in favor of the more direct specification of optional settings 3233 //being pass directly on the options object 3234 if ( options.config ){ 3235 $.extend( true, options, options.config ); 3236 delete options.config; 3237 } 3238 3239 //Public properties 3240 //Allow the options object to override global defaults 3241 $.extend( true, this, { 3242 3243 //internal state and dom identifiers 3244 id: options.id, 3245 hash: options.id, 3246 3247 //dom nodes 3248 element: null, 3249 canvas: null, 3250 container: null, 3251 3252 //TODO: not sure how to best describe these 3253 overlays: [], 3254 overlayControls:[], 3255 3256 //private state properties 3257 previousBody: [], 3258 3259 //This was originally initialized in the constructor and so could never 3260 //have anything in it. now it can because we allow it to be specified 3261 //in the options and is only empty by default if not specified. Also 3262 //this array was returned from get_controls which I find confusing 3263 //since this object has a controls property which is treated in other 3264 //functions like clearControls. I'm removing the accessors. 3265 customControls: [], 3266 3267 //These are originally not part options but declared as members 3268 //in initialize. Its still considered idiomatic to put them here 3269 source: null, 3270 drawer: null, 3271 viewport: null, 3272 navigator: null, 3273 3274 //UI image resources 3275 //TODO: rename navImages to uiImages 3276 navImages: null, 3277 3278 //interface button controls 3279 buttons: null, 3280 3281 //TODO: this is defunct so safely remove it 3282 profiler: null 3283 3284 }, $.DEFAULT_SETTINGS, options ); 3285 3286 //Private state properties 3287 THIS[ this.hash ] = { 3288 "fsBoundsDelta": new $.Point( 1, 1 ), 3289 "prevContainerSize": null, 3290 "lastOpenStartTime": 0, 3291 "lastOpenEndTime": 0, 3292 "animating": false, 3293 "forceRedraw": false, 3294 "mouseInside": false, 3295 "group": null, 3296 // whether we should be continuously zooming 3297 "zooming": false, 3298 // how much we should be continuously zooming by 3299 "zoomFactor": null, 3300 "lastZoomTime": null, 3301 // did we decide this viewer has a sequence of tile sources 3302 "sequenced": false, 3303 "sequence": 0 3304 }; 3305 3306 //Inherit some behaviors and properties 3307 $.EventHandler.call( this ); 3308 $.ControlDock.call( this, options ); 3309 3310 //Deal with tile sources 3311 var initialTileSource, 3312 customTileSource; 3313 3314 if ( this.xmlPath ){ 3315 //Deprecated option. Now it is preferred to use the tileSources option 3316 this.tileSources = [ this.xmlPath ]; 3317 } 3318 3319 if ( this.tileSources ){ 3320 //tileSources is a complex option... 3321 //It can be a string, object, function, or an array of any of these. 3322 // - A String implies a DZI 3323 // - An Srray of Objects implies a simple image 3324 // - A Function implies a custom tile source callback 3325 // - An Array that is not an Array of simple Objects implies a sequence 3326 // of tile sources which can be any of the above 3327 if( $.isArray( this.tileSources ) ){ 3328 if( $.isPlainObject( this.tileSources[ 0 ] ) ){ 3329 //This is a non-sequenced legacy tile source 3330 initialTileSource = this.tileSources; 3331 } else { 3332 //Sequenced tile source 3333 initialTileSource = this.tileSources[ 0 ]; 3334 THIS[ this.hash ].sequenced = true; 3335 } 3336 } else { 3337 initialTileSource = this.tileSources; 3338 } 3339 3340 this.openTileSource( initialTileSource ); 3341 } 3342 3343 this.element = this.element || document.getElementById( this.id ); 3344 this.canvas = $.makeNeutralElement( "div" ); 3345 3346 (function( canvas ){ 3347 canvas.width = "100%"; 3348 canvas.height = "100%"; 3349 canvas.overflow = "hidden"; 3350 canvas.position = "absolute"; 3351 canvas.top = "0px"; 3352 canvas.left = "0px"; 3353 }( this.canvas.style )); 3354 3355 (function( container ){ 3356 container.width = "100%"; 3357 container.height = "100%"; 3358 container.position = "relative"; 3359 container.left = "0px"; 3360 container.top = "0px"; 3361 container.textAlign = "left"; // needed to protect against 3362 }( this.container.style )); 3363 3364 this.container.insertBefore( this.canvas, this.container.firstChild ); 3365 this.element.appendChild( this.container ); 3366 3367 //Used for toggling between fullscreen and default container size 3368 //TODO: these can be closure private and shared across Viewer 3369 // instances. 3370 this.bodyWidth = document.body.style.width; 3371 this.bodyHeight = document.body.style.height; 3372 this.bodyOverflow = document.body.style.overflow; 3373 this.docOverflow = document.documentElement.style.overflow; 3374 3375 this.innerTracker = new $.MouseTracker({ 3376 element: this.canvas, 3377 clickTimeThreshold: this.clickTimeThreshold, 3378 clickDistThreshold: this.clickDistThreshold, 3379 clickHandler: $.delegate( this, onCanvasClick ), 3380 dragHandler: $.delegate( this, onCanvasDrag ), 3381 releaseHandler: $.delegate( this, onCanvasRelease ), 3382 scrollHandler: $.delegate( this, onCanvasScroll ) 3383 }).setTracking( this.mouseNavEnabled ? true : false ); // default state 3384 3385 this.outerTracker = new $.MouseTracker({ 3386 element: this.container, 3387 clickTimeThreshold: this.clickTimeThreshold, 3388 clickDistThreshold: this.clickDistThreshold, 3389 enterHandler: $.delegate( this, onContainerEnter ), 3390 exitHandler: $.delegate( this, onContainerExit ), 3391 releaseHandler: $.delegate( this, onContainerRelease ) 3392 }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking 3393 3394 if( this.toolbar ){ 3395 this.toolbar = new $.ControlDock({ element: this.toolbar }); 3396 } 3397 3398 this.bindStandardControls(); 3399 this.bindSequenceControls(); 3400 3401 for ( i = 0; i < this.customControls.length; i++ ) { 3402 this.addControl( 3403 this.customControls[ i ].id, 3404 this.customControls[ i ].anchor 3405 ); 3406 } 3407 3408 window.setTimeout( function(){ 3409 beginControlsAutoHide( _this ); 3410 }, 1 ); // initial fade out 3411 3412 }; 3413 3414 $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, { 3415 3416 3417 /** 3418 * @function 3419 * @name OpenSeadragon.Viewer.prototype.isOpen 3420 * @return {Boolean} 3421 */ 3422 isOpen: function () { 3423 return !!this.source; 3424 }, 3425 3426 /** 3427 * If the string is xml is simply parsed and opened, otherwise the string 3428 * is treated as an URL and an xml document is requested via ajax, parsed 3429 * and then opened in the viewer. 3430 * @function 3431 * @name OpenSeadragon.Viewer.prototype.openDzi 3432 * @param {String} dzi and xml string or the url to a DZI xml document. 3433 * @return {OpenSeadragon.Viewer} Chainable. 3434 */ 3435 openDzi: function ( dzi ) { 3436 var _this = this; 3437 $.createFromDZI( 3438 dzi, 3439 function( source ){ 3440 _this.open( source ); 3441 }, 3442 this.tileHost 3443 ); 3444 return this; 3445 }, 3446 3447 /** 3448 * @function 3449 * @name OpenSeadragon.Viewer.prototype.openTileSource 3450 * @return {OpenSeadragon.Viewer} Chainable. 3451 */ 3452 openTileSource: function ( tileSource ) { 3453 var _this = this, 3454 customTileSource; 3455 3456 setTimeout(function(){ 3457 if ( $.type( tileSource ) == 'string') { 3458 //Standard DZI format 3459 _this.openDzi( tileSource ); 3460 } else if ( $.isArray( tileSource ) ){ 3461 //Legacy image pyramid 3462 _this.open( new $.LegacyTileSource( tileSource ) ); 3463 } else if ( $.isPlainObject( tileSource ) && $.isFunction( tileSource.getTileUrl ) ){ 3464 //Custom tile source 3465 customTileSource = new $.TileSource( 3466 tileSource.width, 3467 tileSource.height, 3468 tileSource.tileSize, 3469 tileSource.tileOverlap, 3470 tileSource.minLevel, 3471 tileSource.maxLevel 3472 ); 3473 customTileSource.getTileUrl = tileSource.getTileUrl; 3474 _this.open( customTileSource ); 3475 } else { 3476 //can assume it's already a tile source implementation 3477 _this.open( tileSource ); 3478 } 3479 }, 1); 3480 3481 return this; 3482 }, 3483 3484 /** 3485 * @function 3486 * @name OpenSeadragon.Viewer.prototype.open 3487 * @return {OpenSeadragon.Viewer} Chainable. 3488 */ 3489 open: function( source ) { 3490 var _this = this, 3491 overlay, 3492 i; 3493 3494 if ( this.source ) { 3495 this.close( ); 3496 } 3497 3498 // to ignore earlier opens 3499 THIS[ this.hash ].lastOpenStartTime = +new Date(); 3500 3501 window.setTimeout( function () { 3502 if ( THIS[ _this.hash ].lastOpenStartTime > THIS[ _this.hash ].lastOpenEndTime ) { 3503 THIS[ _this.hash ].setMessage( $.getString( "Messages.Loading" ) ); 3504 } 3505 }, 2000); 3506 3507 THIS[ this.hash ].lastOpenEndTime = +new Date(); 3508 this.canvas.innerHTML = ""; 3509 THIS[ this.hash ].prevContainerSize = $.getElementSize( this.container ); 3510 3511 if( source ){ 3512 this.source = source; 3513 } 3514 3515 this.viewport = this.viewport ? this.viewport : new $.Viewport({ 3516 containerSize: THIS[ this.hash ].prevContainerSize, 3517 contentSize: this.source.dimensions, 3518 springStiffness: this.springStiffness, 3519 animationTime: this.animationTime, 3520 minZoomImageRatio: this.minZoomImageRatio, 3521 maxZoomPixelRatio: this.maxZoomPixelRatio, 3522 visibilityRatio: this.visibilityRatio, 3523 wrapHorizontal: this.wrapHorizontal, 3524 wrapVertical: this.wrapVertical 3525 }); 3526 if( this.preserveVewport ){ 3527 this.viewport.resetContentSize( this.source.dimensions ); 3528 } 3529 3530 this.drawer = new $.Drawer({ 3531 source: this.source, 3532 viewport: this.viewport, 3533 element: this.canvas, 3534 overlays: this.overlays, 3535 maxImageCacheCount: this.maxImageCacheCount, 3536 imageLoaderLimit: this.imageLoaderLimit, 3537 minZoomImageRatio: this.minZoomImageRatio, 3538 wrapHorizontal: this.wrapHorizontal, 3539 wrapVertical: this.wrapVertical, 3540 immediateRender: this.immediateRender, 3541 blendTime: this.blendTime, 3542 alwaysBlend: this.alwaysBlend, 3543 minPixelRatio: this.minPixelRatio 3544 }); 3545 3546 //Instantiate a navigator if configured 3547 if ( this.showNavigator && ! this.navigator ){ 3548 this.navigator = new $.Navigator({ 3549 id: this.navigatorElement, 3550 position: this.navigatorPosition, 3551 sizeRatio: this.navigatorSizeRatio, 3552 height: this.navigatorHeight, 3553 width: this.navigatorWidth, 3554 tileSources: this.tileSources, 3555 tileHost: this.tileHost, 3556 prefixUrl: this.prefixUrl, 3557 overlays: this.overlays, 3558 viewer: this 3559 }); 3560 } 3561 3562 //this.profiler = new $.Profiler(); 3563 3564 THIS[ this.hash ].animating = false; 3565 THIS[ this.hash ].forceRedraw = true; 3566 scheduleUpdate( this, updateMulti ); 3567 3568 //Assuming you had programatically created a bunch of overlays 3569 //and added them via configuration 3570 for ( i = 0; i < this.overlayControls.length; i++ ) { 3571 3572 overlay = this.overlayControls[ i ]; 3573 3574 if ( overlay.point != null ) { 3575 3576 this.drawer.addOverlay( 3577 overlay.id, 3578 new $.Point( 3579 overlay.point.X, 3580 overlay.point.Y 3581 ), 3582 $.OverlayPlacement.TOP_LEFT 3583 ); 3584 3585 } else { 3586 3587 this.drawer.addOverlay( 3588 overlay.id, 3589 new $.Rect( 3590 overlay.rect.Point.X, 3591 overlay.rect.Point.Y, 3592 overlay.rect.Width, 3593 overlay.rect.Height 3594 ), 3595 overlay.placement 3596 ); 3597 3598 } 3599 } 3600 VIEWERS[ this.hash ] = this; 3601 this.raiseEvent( "open" ); 3602 3603 if( this.navigator ){ 3604 this.navigator.open( source ); 3605 } 3606 3607 return this; 3608 }, 3609 3610 /** 3611 * @function 3612 * @name OpenSeadragon.Viewer.prototype.close 3613 * @return {OpenSeadragon.Viewer} Chainable. 3614 */ 3615 close: function ( ) { 3616 this.source = null; 3617 this.drawer = null; 3618 this.viewport = this.preserveViewport ? this.viewport : null; 3619 //this.profiler = null; 3620 this.canvas.innerHTML = ""; 3621 3622 VIEWERS[ this.hash ] = null; 3623 delete VIEWERS[ this.hash ]; 3624 3625 return this; 3626 }, 3627 3628 3629 /** 3630 * @function 3631 * @name OpenSeadragon.Viewer.prototype.isMouseNavEnabled 3632 * @return {Boolean} 3633 */ 3634 isMouseNavEnabled: function () { 3635 return this.innerTracker.isTracking(); 3636 }, 3637 3638 /** 3639 * @function 3640 * @name OpenSeadragon.Viewer.prototype.setMouseNavEnabled 3641 * @return {OpenSeadragon.Viewer} Chainable. 3642 */ 3643 setMouseNavEnabled: function( enabled ){ 3644 this.innerTracker.setTracking( enabled ); 3645 return this; 3646 }, 3647 3648 3649 /** 3650 * @function 3651 * @name OpenSeadragon.Viewer.prototype.isDashboardEnabled 3652 * @return {Boolean} 3653 */ 3654 areControlsEnabled: function () { 3655 return this.controls.length && this.controls[ i ].isVisibile(); 3656 }, 3657 3658 3659 /** 3660 * @function 3661 * @name OpenSeadragon.Viewer.prototype.setDashboardEnabled 3662 * @return {OpenSeadragon.Viewer} Chainable. 3663 */ 3664 setControlsEnabled: function( enabled ) { 3665 if( enabled ){ 3666 abortControlsAutoHide( this ); 3667 } else { 3668 beginControlsAutoHide( this ); 3669 }; 3670 }, 3671 3672 3673 /** 3674 * @function 3675 * @name OpenSeadragon.Viewer.prototype.isFullPage 3676 * @return {Boolean} 3677 */ 3678 isFullPage: function () { 3679 return this.container.parentNode == document.body; 3680 }, 3681 3682 3683 /** 3684 * Toggle full page mode. 3685 * @function 3686 * @name OpenSeadragon.Viewer.prototype.setFullPage 3687 * @param {Boolean} fullPage 3688 * If true, enter full page mode. If false, exit full page mode. 3689 * @return {OpenSeadragon.Viewer} Chainable. 3690 */ 3691 setFullPage: function( fullPage ) { 3692 3693 var body = document.body, 3694 bodyStyle = body.style, 3695 docStyle = document.documentElement.style, 3696 containerStyle = this.container.style, 3697 canvasStyle = this.canvas.style, 3698 oldBounds, 3699 newBounds, 3700 viewer, 3701 hash, 3702 nodes, 3703 i; 3704 3705 //dont bother modifying the DOM if we are already in full page mode. 3706 if ( fullPage == this.isFullPage() ) { 3707 return; 3708 } 3709 3710 if ( fullPage ) { 3711 3712 this.bodyOverflow = bodyStyle.overflow; 3713 this.docOverflow = docStyle.overflow; 3714 bodyStyle.overflow = "hidden"; 3715 docStyle.overflow = "hidden"; 3716 3717 this.bodyWidth = bodyStyle.width; 3718 this.bodyHeight = bodyStyle.height; 3719 bodyStyle.width = "100%"; 3720 bodyStyle.height = "100%"; 3721 3722 canvasStyle.backgroundColor = "black"; 3723 canvasStyle.color = "white"; 3724 3725 containerStyle.position = "fixed"; 3726 3727 //when entering full screen on the ipad it wasnt sufficient to leave 3728 //the body intact as only only the top half of the screen would 3729 //respond to touch events on the canvas, while the bottom half treated 3730 //them as touch events on the document body. Thus we remove and store 3731 //the bodies elements and replace them when we leave full screen. 3732 this.previousBody = []; 3733 nodes = body.childNodes.length; 3734 for ( i = 0; i < nodes; i ++ ){ 3735 this.previousBody.push( body.childNodes[ 0 ] ); 3736 body.removeChild( body.childNodes[ 0 ] ); 3737 } 3738 3739 //If we've got a toolbar, we need to enable the user to use css to 3740 //preserve it in fullpage mode 3741 if( this.toolbar && this.toolbar.element ){ 3742 //save a reference to the parent so we can put it back 3743 //in the long run we need a better strategy 3744 this.toolbar.parentNode = this.toolbar.element.parentNode; 3745 this.toolbar.nextSibling = this.toolbar.element.nextSibling; 3746 body.appendChild( this.toolbar.element ); 3747 3748 //Make sure the user has some ability to style the toolbar based 3749 //on the mode 3750 this.toolbar.element.setAttribute( 3751 'class', 3752 this.toolbar.element.className +" fullpage" 3753 ); 3754 this.toolbar.element.style.position = 'fixed'; 3755 3756 this.container.style.top = $.getElementSize( 3757 this.toolbar.element 3758 ).y + 'px'; 3759 } 3760 body.appendChild( this.container ); 3761 THIS[ this.hash ].prevContainerSize = $.getWindowSize(); 3762 3763 // mouse will be inside container now 3764 $.delegate( this, onContainerEnter )(); 3765 3766 3767 } else { 3768 3769 bodyStyle.overflow = this.bodyOverflow; 3770 docStyle.overflow = this.docOverflow; 3771 3772 bodyStyle.width = this.bodyWidth; 3773 bodyStyle.height = this.bodyHeight; 3774 3775 canvasStyle.backgroundColor = ""; 3776 canvasStyle.color = ""; 3777 3778 containerStyle.position = "relative"; 3779 containerStyle.zIndex = ""; 3780 3781 //If we've got a toolbar, we need to enable the user to use css to 3782 //reset it to its original state 3783 if( this.toolbar && this.toolbar.element ){ 3784 body.removeChild( this.toolbar.element ); 3785 3786 //Make sure the user has some ability to style the toolbar based 3787 //on the mode 3788 this.toolbar.element.setAttribute( 3789 'class', 3790 this.toolbar.element.className.replace('fullpage','') 3791 ); 3792 this.toolbar.element.style.position = 'relative'; 3793 this.toolbar.parentNode.insertBefore( 3794 this.toolbar.element, 3795 this.toolbar.nextSibling 3796 ); 3797 delete this.toolbar.parentNode; 3798 delete this.toolbar.nextSibling; 3799 3800 this.container.style.top = 'auto'; 3801 } 3802 3803 body.removeChild( this.container ); 3804 nodes = this.previousBody.length; 3805 for ( i = 0; i < nodes; i++ ){ 3806 body.appendChild( this.previousBody.shift() ); 3807 } 3808 this.element.appendChild( this.container ); 3809 THIS[ this.hash ].prevContainerSize = $.getElementSize( this.element ); 3810 3811 // mouse will likely be outside now 3812 $.delegate( this, onContainerExit )(); 3813 3814 } 3815 3816 if ( this.viewport ) { 3817 oldBounds = this.viewport.getBounds(); 3818 this.viewport.resize( THIS[ this.hash ].prevContainerSize ); 3819 newBounds = this.viewport.getBounds(); 3820 3821 if ( fullPage ) { 3822 THIS[ this.hash ].fsBoundsDelta = new $.Point( 3823 newBounds.width / oldBounds.width, 3824 newBounds.height / oldBounds.height 3825 ); 3826 } else { 3827 this.viewport.update(); 3828 this.viewport.zoomBy( 3829 Math.max( 3830 THIS[ this.hash ].fsBoundsDelta.x, 3831 THIS[ this.hash ].fsBoundsDelta.y 3832 ), 3833 null, 3834 true 3835 ); 3836 //Ensures that if multiple viewers are on a page, the viewers that 3837 //were hidden during fullpage are 'reopened' 3838 for( hash in VIEWERS ){ 3839 viewer = VIEWERS[ hash ]; 3840 if( viewer !== this && viewer != this.navigator ){ 3841 viewer.open( viewer.source ); 3842 if( viewer.navigator ){ 3843 viewer.navigator.open( viewer.source ); 3844 } 3845 } 3846 } 3847 } 3848 3849 THIS[ this.hash ].forceRedraw = true; 3850 this.raiseEvent( "resize", this ); 3851 updateOnce( this ); 3852 3853 } 3854 return this; 3855 }, 3856 3857 3858 /** 3859 * @function 3860 * @name OpenSeadragon.Viewer.prototype.isVisible 3861 * @return {Boolean} 3862 */ 3863 isVisible: function () { 3864 return this.container.style.visibility != "hidden"; 3865 }, 3866 3867 3868 /** 3869 * @function 3870 * @name OpenSeadragon.Viewer.prototype.setVisible 3871 * @return {OpenSeadragon.Viewer} Chainable. 3872 */ 3873 setVisible: function( visible ){ 3874 this.container.style.visibility = visible ? "" : "hidden"; 3875 return this; 3876 }, 3877 3878 bindSequenceControls: function(){ 3879 3880 ////////////////////////////////////////////////////////////////////////// 3881 // Image Sequence Controls 3882 ////////////////////////////////////////////////////////////////////////// 3883 var onFocusHandler = $.delegate( this, onFocus ), 3884 onBlurHandler = $.delegate( this, onBlur ), 3885 onNextHandler = $.delegate( this, onNext ), 3886 onPreviousHandler = $.delegate( this, onPrevious ), 3887 navImages = this.navImages, 3888 buttons = [], 3889 useGroup = true ; 3890 3891 if( this.showSequenceControl && THIS[ this.hash ].sequenced ){ 3892 3893 if( this.previousButton || this.nextButton ){ 3894 //if we are binding to custom buttons then layout and 3895 //grouping is the responsibility of the page author 3896 useGroup = false; 3897 } 3898 3899 this.previousButton = new $.Button({ 3900 element: this.previousButton ? $.getElement( this.previousButton ) : null, 3901 clickTimeThreshold: this.clickTimeThreshold, 3902 clickDistThreshold: this.clickDistThreshold, 3903 tooltip: $.getString( "Tooltips.PreviousPage" ), 3904 srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ), 3905 srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ), 3906 srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ), 3907 srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ), 3908 onRelease: onPreviousHandler, 3909 onFocus: onFocusHandler, 3910 onBlur: onBlurHandler 3911 }); 3912 3913 this.nextButton = new $.Button({ 3914 element: this.nextButton ? $.getElement( this.nextButton ) : null, 3915 clickTimeThreshold: this.clickTimeThreshold, 3916 clickDistThreshold: this.clickDistThreshold, 3917 tooltip: $.getString( "Tooltips.NextPage" ), 3918 srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ), 3919 srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ), 3920 srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ), 3921 srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ), 3922 onRelease: onNextHandler, 3923 onFocus: onFocusHandler, 3924 onBlur: onBlurHandler 3925 }); 3926 3927 this.previousButton.disable(); 3928 3929 if( useGroup ){ 3930 this.paging = new $.ButtonGroup({ 3931 buttons: [ 3932 this.previousButton, 3933 this.nextButton 3934 ], 3935 clickTimeThreshold: this.clickTimeThreshold, 3936 clickDistThreshold: this.clickDistThreshold 3937 }); 3938 3939 this.pagingControl = this.paging.element; 3940 3941 if( this.toolbar ){ 3942 this.toolbar.addControl( 3943 this.navControl, 3944 $.ControlAnchor.BOTTOM_RIGHT 3945 ); 3946 }else{ 3947 this.addControl( 3948 this.pagingControl, 3949 $.ControlAnchor.TOP_LEFT 3950 ); 3951 } 3952 } 3953 } 3954 }, 3955 3956 bindStandardControls: function(){ 3957 ////////////////////////////////////////////////////////////////////////// 3958 // Navigation Controls 3959 ////////////////////////////////////////////////////////////////////////// 3960 var beginZoomingInHandler = $.delegate( this, beginZoomingIn ), 3961 endZoomingHandler = $.delegate( this, endZooming ), 3962 doSingleZoomInHandler = $.delegate( this, doSingleZoomIn ), 3963 beginZoomingOutHandler = $.delegate( this, beginZoomingOut ), 3964 doSingleZoomOutHandler = $.delegate( this, doSingleZoomOut ), 3965 onHomeHandler = $.delegate( this, onHome ), 3966 onFullPageHandler = $.delegate( this, onFullPage ), 3967 onFocusHandler = $.delegate( this, onFocus ), 3968 onBlurHandler = $.delegate( this, onBlur ), 3969 navImages = this.navImages, 3970 buttons = [], 3971 useGroup = true ; 3972 3973 3974 if( this.showNavigationControl ){ 3975 3976 if( this.zoomInButton || this.zoomOutButton || this.homeButton || this.fullPageButton ){ 3977 //if we are binding to custom buttons then layout and 3978 //grouping is the responsibility of the page author 3979 useGroup = false; 3980 } 3981 3982 buttons.push( this.zoomInButton = new $.Button({ 3983 element: this.zoomInButton ? $.getElement( this.zoomInButton ) : null, 3984 clickTimeThreshold: this.clickTimeThreshold, 3985 clickDistThreshold: this.clickDistThreshold, 3986 tooltip: $.getString( "Tooltips.ZoomIn" ), 3987 srcRest: resolveUrl( this.prefixUrl, navImages.zoomIn.REST ), 3988 srcGroup: resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ), 3989 srcHover: resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ), 3990 srcDown: resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ), 3991 onPress: beginZoomingInHandler, 3992 onRelease: endZoomingHandler, 3993 onClick: doSingleZoomInHandler, 3994 onEnter: beginZoomingInHandler, 3995 onExit: endZoomingHandler, 3996 onFocus: onFocusHandler, 3997 onBlur: onBlurHandler 3998 })); 3999 4000 buttons.push( this.zoomOutButton = new $.Button({ 4001 element: this.zoomOutButton ? $.getElement( this.zoomOutButton ) : null, 4002 clickTimeThreshold: this.clickTimeThreshold, 4003 clickDistThreshold: this.clickDistThreshold, 4004 tooltip: $.getString( "Tooltips.ZoomOut" ), 4005 srcRest: resolveUrl( this.prefixUrl, navImages.zoomOut.REST ), 4006 srcGroup: resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ), 4007 srcHover: resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ), 4008 srcDown: resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ), 4009 onPress: beginZoomingOutHandler, 4010 onRelease: endZoomingHandler, 4011 onClick: doSingleZoomOutHandler, 4012 onEnter: beginZoomingOutHandler, 4013 onExit: endZoomingHandler, 4014 onFocus: onFocusHandler, 4015 onBlur: onBlurHandler 4016 })); 4017 4018 buttons.push( this.homeButton = new $.Button({ 4019 element: this.homeButton ? $.getElement( this.homeButton ) : null, 4020 clickTimeThreshold: this.clickTimeThreshold, 4021 clickDistThreshold: this.clickDistThreshold, 4022 tooltip: $.getString( "Tooltips.Home" ), 4023 srcRest: resolveUrl( this.prefixUrl, navImages.home.REST ), 4024 srcGroup: resolveUrl( this.prefixUrl, navImages.home.GROUP ), 4025 srcHover: resolveUrl( this.prefixUrl, navImages.home.HOVER ), 4026 srcDown: resolveUrl( this.prefixUrl, navImages.home.DOWN ), 4027 onRelease: onHomeHandler, 4028 onFocus: onFocusHandler, 4029 onBlur: onBlurHandler 4030 })); 4031 4032 buttons.push( this.fullPageButton = new $.Button({ 4033 element: this.fullPageButton ? $.getElement( this.fullPageButton ) : null, 4034 clickTimeThreshold: this.clickTimeThreshold, 4035 clickDistThreshold: this.clickDistThreshold, 4036 tooltip: $.getString( "Tooltips.FullPage" ), 4037 srcRest: resolveUrl( this.prefixUrl, navImages.fullpage.REST ), 4038 srcGroup: resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ), 4039 srcHover: resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ), 4040 srcDown: resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ), 4041 onRelease: onFullPageHandler, 4042 onFocus: onFocusHandler, 4043 onBlur: onBlurHandler 4044 })); 4045 4046 if( useGroup ){ 4047 this.buttons = new $.ButtonGroup({ 4048 buttons: buttons, 4049 clickTimeThreshold: this.clickTimeThreshold, 4050 clickDistThreshold: this.clickDistThreshold 4051 }); 4052 4053 this.navControl = this.buttons.element; 4054 this.addHandler( 'open', $.delegate( this, lightUp ) ); 4055 4056 if( this.toolbar ){ 4057 this.toolbar.addControl( 4058 this.navControl, 4059 $.ControlAnchor.TOP_LEFT 4060 ); 4061 }else{ 4062 this.addControl( 4063 this.navControl, 4064 $.ControlAnchor.BOTTOM_RIGHT 4065 ); 4066 } 4067 } 4068 4069 4070 } 4071 } 4072 4073 }); 4074 4075 4076 4077 /////////////////////////////////////////////////////////////////////////////// 4078 // Schedulers provide the general engine for animation 4079 /////////////////////////////////////////////////////////////////////////////// 4080 function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){ 4081 var currentTime, 4082 targetTime, 4083 deltaTime; 4084 4085 if ( THIS[ viewer.hash ].animating ) { 4086 return window.setTimeout( function(){ 4087 updateFunc( viewer ); 4088 }, 1 ); 4089 } 4090 4091 currentTime = +new Date(); 4092 prevUpdateTime = prevUpdateTime ? prevUpdateTime : currentTime; 4093 // 60 frames per second is ideal 4094 targetTime = prevUpdateTime + 1000 / 60; 4095 deltaTime = Math.max( 1, targetTime - currentTime ); 4096 4097 return window.setTimeout( function(){ 4098 updateFunc( viewer ); 4099 }, deltaTime ); 4100 }; 4101 4102 4103 //provides a sequence in the fade animation 4104 function scheduleControlsFade( viewer ) { 4105 window.setTimeout( function(){ 4106 updateControlsFade( viewer ); 4107 }, 20); 4108 }; 4109 4110 4111 //initiates an animation to hide the controls 4112 function beginControlsAutoHide( viewer ) { 4113 if ( !viewer.autoHideControls ) { 4114 return; 4115 } 4116 viewer.controlsShouldFade = true; 4117 viewer.controlsFadeBeginTime = 4118 +new Date() + 4119 viewer.controlsFadeDelay; 4120 4121 window.setTimeout( function(){ 4122 scheduleControlsFade( viewer ); 4123 }, viewer.controlsFadeDelay ); 4124 }; 4125 4126 4127 //determines if fade animation is done or continues the animation 4128 function updateControlsFade( viewer ) { 4129 var currentTime, 4130 deltaTime, 4131 opacity, 4132 i; 4133 if ( viewer.controlsShouldFade ) { 4134 currentTime = new Date().getTime(); 4135 deltaTime = currentTime - viewer.controlsFadeBeginTime; 4136 opacity = 1.0 - deltaTime / viewer.controlsFadeLength; 4137 4138 opacity = Math.min( 1.0, opacity ); 4139 opacity = Math.max( 0.0, opacity ); 4140 4141 for ( i = viewer.controls.length - 1; i >= 0; i--) { 4142 viewer.controls[ i ].setOpacity( opacity ); 4143 } 4144 4145 if ( opacity > 0 ) { 4146 // fade again 4147 scheduleControlsFade( viewer ); 4148 } 4149 } 4150 }; 4151 4152 4153 //stop the fade animation on the controls and show them 4154 function abortControlsAutoHide( viewer ) { 4155 var i; 4156 viewer.controlsShouldFade = false; 4157 for ( i = viewer.controls.length - 1; i >= 0; i-- ) { 4158 viewer.controls[ i ].setOpacity( 1.0 ); 4159 } 4160 }; 4161 4162 4163 4164 /////////////////////////////////////////////////////////////////////////////// 4165 // Default view event handlers. 4166 /////////////////////////////////////////////////////////////////////////////// 4167 function onFocus(){ 4168 abortControlsAutoHide( this ); 4169 }; 4170 4171 function onBlur(){ 4172 beginControlsAutoHide( this ); 4173 4174 }; 4175 4176 function onCanvasClick( tracker, position, quick, shift ) { 4177 var zoomPreClick, 4178 factor; 4179 if ( this.viewport && quick ) { // ignore clicks where mouse moved 4180 zoomPerClick = this.zoomPerClick; 4181 factor = shift ? 1.0 / zoomPerClick : zoomPerClick; 4182 this.viewport.zoomBy( 4183 factor, 4184 this.viewport.pointFromPixel( position, true ) 4185 ); 4186 this.viewport.applyConstraints(); 4187 } 4188 }; 4189 4190 function onCanvasDrag( tracker, position, delta, shift ) { 4191 if ( this.viewport ) { 4192 if( !this.panHorizontal ){ 4193 delta.x = 0; 4194 } 4195 if( !this.panVertical ){ 4196 delta.y = 0; 4197 } 4198 this.viewport.panBy( 4199 this.viewport.deltaPointsFromPixels( 4200 delta.negate() 4201 ) 4202 ); 4203 } 4204 }; 4205 4206 function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) { 4207 if ( insideElementPress && this.viewport ) { 4208 this.viewport.applyConstraints(); 4209 } 4210 }; 4211 4212 function onCanvasScroll( tracker, position, scroll, shift ) { 4213 var factor; 4214 if ( this.viewport ) { 4215 factor = Math.pow( this.zoomPerScroll, scroll ); 4216 this.viewport.zoomBy( 4217 factor, 4218 this.viewport.pointFromPixel( position, true ) 4219 ); 4220 this.viewport.applyConstraints(); 4221 } 4222 //cancels event 4223 return false; 4224 }; 4225 4226 function onContainerExit( tracker, position, buttonDownElement, buttonDownAny ) { 4227 if ( !buttonDownElement ) { 4228 THIS[ this.hash ].mouseInside = false; 4229 if ( !THIS[ this.hash ].animating ) { 4230 beginControlsAutoHide( this ); 4231 } 4232 } 4233 }; 4234 4235 function onContainerRelease( tracker, position, insideElementPress, insideElementRelease ) { 4236 if ( !insideElementRelease ) { 4237 THIS[ this.hash ].mouseInside = false; 4238 if ( !THIS[ this.hash ].animating ) { 4239 beginControlsAutoHide( this ); 4240 } 4241 } 4242 }; 4243 4244 function onContainerEnter( tracker, position, buttonDownElement, buttonDownAny ) { 4245 THIS[ this.hash ].mouseInside = true; 4246 abortControlsAutoHide( this ); 4247 }; 4248 4249 4250 /////////////////////////////////////////////////////////////////////////////// 4251 // Page update routines ( aka Views - for future reference ) 4252 /////////////////////////////////////////////////////////////////////////////// 4253 4254 function updateMulti( viewer ) { 4255 4256 var beginTime; 4257 4258 if ( !viewer.source ) { 4259 return; 4260 } 4261 4262 beginTime = +new Date(); 4263 updateOnce( viewer ); 4264 scheduleUpdate( viewer, arguments.callee, beginTime ); 4265 }; 4266 4267 function updateOnce( viewer ) { 4268 4269 var containerSize, 4270 animated; 4271 4272 if ( !viewer.source ) { 4273 return; 4274 } 4275 4276 //viewer.profiler.beginUpdate(); 4277 4278 containerSize = $.getElementSize( viewer.container ); 4279 if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) { 4280 // maintain image position 4281 viewer.viewport.resize( containerSize, true ); 4282 THIS[ viewer.hash ].prevContainerSize = containerSize; 4283 viewer.raiseEvent( "resize" ); 4284 } 4285 4286 animated = viewer.viewport.update(); 4287 if ( !THIS[ viewer.hash ].animating && animated ) { 4288 viewer.raiseEvent( "animationstart" ); 4289 abortControlsAutoHide( viewer ); 4290 } 4291 4292 if ( animated ) { 4293 viewer.drawer.update(); 4294 if( viewer.navigator ){ 4295 viewer.navigator.update( viewer.viewport ); 4296 } 4297 viewer.raiseEvent( "animation" ); 4298 } else if ( THIS[ viewer.hash ].forceRedraw || viewer.drawer.needsUpdate() ) { 4299 viewer.drawer.update(); 4300 if( viewer.navigator ){ 4301 viewer.navigator.update( viewer.viewport ); 4302 } 4303 THIS[ viewer.hash ].forceRedraw = false; 4304 } 4305 4306 if ( THIS[ viewer.hash ].animating && !animated ) { 4307 viewer.raiseEvent( "animationfinish" ); 4308 4309 if ( !THIS[ viewer.hash ].mouseInside ) { 4310 beginControlsAutoHide( viewer ); 4311 } 4312 } 4313 4314 THIS[ viewer.hash ].animating = animated; 4315 4316 //viewer.profiler.endUpdate(); 4317 }; 4318 4319 4320 4321 /////////////////////////////////////////////////////////////////////////////// 4322 // Navigation Controls 4323 /////////////////////////////////////////////////////////////////////////////// 4324 function resolveUrl( prefix, url ) { 4325 return prefix ? prefix + url : url; 4326 }; 4327 4328 4329 4330 function beginZoomingIn() { 4331 THIS[ this.hash ].lastZoomTime = +new Date(); 4332 THIS[ this.hash ].zoomFactor = this.zoomPerSecond; 4333 THIS[ this.hash ].zooming = true; 4334 scheduleZoom( this ); 4335 }; 4336 4337 4338 function beginZoomingOut() { 4339 THIS[ this.hash ].lastZoomTime = +new Date(); 4340 THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond; 4341 THIS[ this.hash ].zooming = true; 4342 scheduleZoom( this ); 4343 }; 4344 4345 4346 function endZooming() { 4347 THIS[ this.hash ].zooming = false; 4348 }; 4349 4350 4351 function scheduleZoom( viewer ) { 4352 window.setTimeout( $.delegate( viewer, doZoom ), 10 ); 4353 }; 4354 4355 4356 function doZoom() { 4357 var currentTime, 4358 deltaTime, 4359 adjustFactor; 4360 4361 if ( THIS[ this.hash ].zooming && this.viewport) { 4362 currentTime = +new Date(); 4363 deltaTime = currentTime - THIS[ this.hash ].lastZoomTime; 4364 adjustedFactor = Math.pow( THIS[ this.hash ].zoomFactor, deltaTime / 1000 ); 4365 4366 this.viewport.zoomBy( adjustedFactor ); 4367 this.viewport.applyConstraints(); 4368 THIS[ this.hash ].lastZoomTime = currentTime; 4369 scheduleZoom( this ); 4370 } 4371 }; 4372 4373 4374 function doSingleZoomIn() { 4375 if ( this.viewport ) { 4376 THIS[ this.hash ].zooming = false; 4377 this.viewport.zoomBy( 4378 this.zoomPerClick / 1.0 4379 ); 4380 this.viewport.applyConstraints(); 4381 } 4382 }; 4383 4384 4385 function doSingleZoomOut() { 4386 if ( this.viewport ) { 4387 THIS[ this.hash ].zooming = false; 4388 this.viewport.zoomBy( 4389 1.0 / this.zoomPerClick 4390 ); 4391 this.viewport.applyConstraints(); 4392 } 4393 }; 4394 4395 4396 function lightUp() { 4397 this.buttons.emulateEnter(); 4398 this.buttons.emulateExit(); 4399 }; 4400 4401 4402 function onHome() { 4403 if ( this.viewport ) { 4404 this.viewport.goHome(); 4405 } 4406 }; 4407 4408 4409 function onFullPage() { 4410 this.setFullPage( !this.isFullPage() ); 4411 // correct for no mouseout event on change 4412 if( this.buttons ){ 4413 this.buttons.emulateExit(); 4414 } 4415 this.fullPageButton.element.focus(); 4416 if ( this.viewport ) { 4417 this.viewport.applyConstraints(); 4418 } 4419 }; 4420 4421 4422 function onPrevious(){ 4423 var previous = THIS[ this.hash ].sequence - 1, 4424 preserveVewport = true; 4425 if( previous >= 0 ){ 4426 4427 THIS[ this.hash ].sequence = previous; 4428 4429 if( 0 === previous ){ 4430 //Disable previous button 4431 this.previousButton.disable(); 4432 } 4433 if( this.tileSources.length > 0 ){ 4434 //Enable next button 4435 this.nextButton.enable(); 4436 } 4437 4438 this.openTileSource( this.tileSources[ previous ] ); 4439 } 4440 }; 4441 4442 4443 function onNext(){ 4444 var next = THIS[ this.hash ].sequence + 1, 4445 preserveVewport = true; 4446 if( this.tileSources.length > next ){ 4447 4448 THIS[ this.hash ].sequence = next; 4449 4450 if( ( this.tileSources.length - 1 ) === next ){ 4451 //Disable next button 4452 this.nextButton.disable(); 4453 } 4454 if( next > 0 ){ 4455 //Enable previous button 4456 this.previousButton.enable(); 4457 } 4458 4459 this.openTileSource( this.tileSources[ next ] ); 4460 } 4461 }; 4462 4463 4464 }( OpenSeadragon )); 4465 (function( $ ){ 4466 4467 /** 4468 * The Navigator provides a small view of the current image as fixed 4469 * while representing the viewport as a moving box serving as a frame 4470 * of reference in the larger viewport as to which portion of the image 4471 * is currently being examined. The navigators viewport can be interacted 4472 * with using the keyboard or the mouse. 4473 * @class 4474 * @name OpenSeadragon.Navigator 4475 * @extends OpenSeadragon.Viewer 4476 * @extends OpenSeadragon.EventHandler 4477 * @param {Object} options 4478 * @param {String} options.viewerId 4479 */ 4480 $.Navigator = function( options ){ 4481 4482 var _this = this, 4483 viewer = options.viewer, 4484 viewerSize = $.getElementSize( viewer.element ); 4485 4486 //We may need to create a new element and id if they did not 4487 //provide the id for the existing element 4488 if( !options.id ){ 4489 options.id = 'navigator-' + (+new Date()); 4490 this.element = $.makeNeutralElement( "div" ); 4491 this.element.id = options.id; 4492 this.element.className = 'navigator'; 4493 } 4494 4495 options = $.extend( true, { 4496 navigatorSizeRatio: $.DEFAULT_SETTINGS.navigatorSizeRatio 4497 }, options, { 4498 element: this.element, 4499 //These need to be overridden to prevent recursion since 4500 //the navigator is a viewer and a viewer has a navigator 4501 showNavigator: false, 4502 mouseNavEnabled: false, 4503 showNavigationControl: false, 4504 showSequenceControl: false 4505 }); 4506 4507 options.minPixelRatio = Math.min( 4508 options.navigatorSizeRatio * $.DEFAULT_SETTINGS.minPixelRatio, 4509 $.DEFAULT_SETTINGS.minPixelRatio 4510 ); 4511 4512 (function( style ){ 4513 style.marginTop = '0px'; 4514 style.marginRight = '0px'; 4515 style.marginBottom = '0px'; 4516 style.marginLeft = '0px'; 4517 style.border = '2px solid #555'; 4518 style.background = '#000'; 4519 style.opacity = 0.8; 4520 style.overflow = 'hidden'; 4521 }( this.element.style )); 4522 4523 this.displayRegion = $.makeNeutralElement( "textarea" ); 4524 this.displayRegion.id = this.element.id + '-displayregion'; 4525 this.displayRegion.className = 'displayregion'; 4526 4527 (function( style ){ 4528 style.position = 'relative'; 4529 style.top = '0px'; 4530 style.left = '0px'; 4531 style.fontSize = '0px'; 4532 style.border = '2px solid #900'; 4533 //TODO: IE doesnt like this property being set 4534 try{ style.outline = '2px auto #900'; }catch(e){/*ignore*/} 4535 style.background = 'transparent'; 4536 style.float = 'left'; //Webkit 4537 style.cssFloat = 'left'; //Firefox 4538 style.styleFloat = 'left'; //IE 4539 style.zIndex = 999999999; 4540 style.cursor = 'default'; 4541 }( this.displayRegion.style )); 4542 4543 this.element.innerTracker = new $.MouseTracker({ 4544 element: this.element, 4545 scrollHandler: function(){ 4546 //dont scroll the page up and down if the user is scrolling 4547 //in the navigator 4548 return false; 4549 } 4550 }).setTracking( true ); 4551 4552 this.displayRegion.innerTracker = new $.MouseTracker({ 4553 element: this.displayRegion, 4554 clickTimeThreshold: this.clickTimeThreshold, 4555 clickDistThreshold: this.clickDistThreshold, 4556 clickHandler: $.delegate( this, onCanvasClick ), 4557 dragHandler: $.delegate( this, onCanvasDrag ), 4558 releaseHandler: $.delegate( this, onCanvasRelease ), 4559 scrollHandler: $.delegate( this, onCanvasScroll ), 4560 focusHandler: function(){ 4561 var point = $.getElementPosition( _this.viewer.element ); 4562 4563 window.scrollTo( 0, point.y ); 4564 4565 _this.viewer.setControlsEnabled( true ); 4566 (function( style ){ 4567 style.border = '2px solid #437AB2'; 4568 style.outline = '2px auto #437AB2'; 4569 }( this.element.style )); 4570 4571 }, 4572 blurHandler: function(){ 4573 _this.viewer.setControlsEnabled( false ); 4574 (function( style ){ 4575 style.border = '2px solid #900'; 4576 style.outline = '2px auto #900'; 4577 }( this.element.style )); 4578 }, 4579 keyHandler: function(tracker, keyCode, shiftKey){ 4580 //console.log( keyCode ); 4581 switch( keyCode ){ 4582 case 61://=|+ 4583 _this.viewer.viewport.zoomBy(1.1); 4584 _this.viewer.viewport.applyConstraints(); 4585 return false; 4586 case 45://-|_ 4587 _this.viewer.viewport.zoomBy(0.9); 4588 _this.viewer.viewport.applyConstraints(); 4589 return false; 4590 case 48://0|) 4591 _this.viewer.viewport.goHome(); 4592 _this.viewer.viewport.applyConstraints(); 4593 return false; 4594 case 119://w 4595 case 87://W 4596 case 38://up arrow 4597 shiftKey ? 4598 _this.viewer.viewport.zoomBy(1.1): 4599 _this.viewer.viewport.panBy(new $.Point(0, -0.05)); 4600 _this.viewer.viewport.applyConstraints(); 4601 return false; 4602 case 115://s 4603 case 83://S 4604 case 40://down arrow 4605 shiftKey ? 4606 _this.viewer.viewport.zoomBy(0.9): 4607 _this.viewer.viewport.panBy(new $.Point(0, 0.05)); 4608 _this.viewer.viewport.applyConstraints(); 4609 return false; 4610 case 97://a 4611 case 37://left arrow 4612 _this.viewer.viewport.panBy(new $.Point(-0.05, 0)); 4613 _this.viewer.viewport.applyConstraints(); 4614 return false; 4615 case 100://d 4616 case 39://right arrow 4617 _this.viewer.viewport.panBy(new $.Point(0.05, 0)); 4618 _this.viewer.viewport.applyConstraints(); 4619 return false; 4620 default: 4621 //console.log( 'navigator keycode %s', keyCode ); 4622 return true; 4623 } 4624 } 4625 }).setTracking( true ); // default state 4626 4627 /*this.displayRegion.outerTracker = new $.MouseTracker({ 4628 element: this.container, 4629 clickTimeThreshold: this.clickTimeThreshold, 4630 clickDistThreshold: this.clickDistThreshold, 4631 enterHandler: $.delegate( this, onContainerEnter ), 4632 exitHandler: $.delegate( this, onContainerExit ), 4633 releaseHandler: $.delegate( this, onContainerRelease ) 4634 }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking*/ 4635 4636 this.element.appendChild( this.displayRegion ); 4637 4638 viewer.addControl( 4639 this.element, 4640 $.ControlAnchor.TOP_RIGHT 4641 ); 4642 4643 if( options.width && options.height ){ 4644 this.element.style.width = options.width + 'px'; 4645 this.element.style.height = options.height + 'px'; 4646 } else { 4647 this.element.style.width = ( viewerSize.x * options.navigatorSizeRatio ) + 'px'; 4648 this.element.style.height = ( viewerSize.y * options.navigatorSizeRatio ) + 'px'; 4649 } 4650 4651 $.Viewer.apply( this, [ options ] ); 4652 4653 }; 4654 4655 $.extend( $.Navigator.prototype, $.EventHandler.prototype, $.Viewer.prototype, { 4656 4657 /** 4658 * @function 4659 * @name OpenSeadragon.Navigator.prototype.update 4660 */ 4661 update: function( viewport ){ 4662 4663 var bounds, 4664 topleft, 4665 bottomright; 4666 4667 if( viewport && this.viewport ){ 4668 bounds = viewport.getBounds( true ); 4669 topleft = this.viewport.pixelFromPoint( bounds.getTopLeft() ); 4670 bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight() ); 4671 4672 //update style for navigator-box 4673 (function(style){ 4674 4675 style.top = topleft.y + 'px'; 4676 style.left = topleft.x + 'px'; 4677 style.width = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px'; 4678 style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px'; 4679 4680 }( this.displayRegion.style )); 4681 } 4682 4683 } 4684 4685 }); 4686 4687 4688 /** 4689 * @private 4690 * @inner 4691 * @function 4692 */ 4693 function onCanvasClick( tracker, position, quick, shift ) { 4694 this.displayRegion.focus(); 4695 }; 4696 4697 4698 /** 4699 * @private 4700 * @inner 4701 * @function 4702 */ 4703 function onCanvasDrag( tracker, position, delta, shift ) { 4704 if ( this.viewer.viewport ) { 4705 if( !this.panHorizontal ){ 4706 delta.x = 0; 4707 } 4708 if( !this.panVertical ){ 4709 delta.y = 0; 4710 } 4711 this.viewer.viewport.panBy( 4712 this.viewport.deltaPointsFromPixels( 4713 delta 4714 ) 4715 ); 4716 } 4717 }; 4718 4719 4720 /** 4721 * @private 4722 * @inner 4723 * @function 4724 */ 4725 function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) { 4726 if ( insideElementPress && this.viewer.viewport ) { 4727 this.viewer.viewport.applyConstraints(); 4728 } 4729 }; 4730 4731 4732 /** 4733 * @private 4734 * @inner 4735 * @function 4736 */ 4737 function onCanvasScroll( tracker, position, scroll, shift ) { 4738 var factor; 4739 if ( this.viewer.viewport ) { 4740 factor = Math.pow( this.zoomPerScroll, scroll ); 4741 this.viewer.viewport.zoomBy( 4742 factor, 4743 //this.viewport.pointFromPixel( position, true ) 4744 this.viewport.getCenter() 4745 ); 4746 this.viewer.viewport.applyConstraints(); 4747 } 4748 //cancels event 4749 return false; 4750 }; 4751 4752 4753 }( OpenSeadragon )); 4754 (function( $ ){ 4755 4756 //TODO: I guess this is where the i18n needs to be reimplemented. I'll look 4757 // into existing patterns for i18n in javascript but i think that mimicking 4758 // pythons gettext might be a reasonable approach. 4759 var I18N = { 4760 Errors: { 4761 Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" + 4762 "Please try using IE 7 or Firefox 3.\n", 4763 Dzc: "Sorry, we don't support Deep Zoom Collections!", 4764 Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", 4765 Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", 4766 Empty: "You asked us to open nothing, so we did just that.", 4767 ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", 4768 Security: "It looks like a security restriction stopped us from " + 4769 "loading this Deep Zoom Image.", 4770 Status: "This space unintentionally left blank ({0} {1}).", 4771 Unknown: "Whoops, something inexplicably went wrong. Sorry!" 4772 }, 4773 4774 Messages: { 4775 Loading: "Loading..." 4776 }, 4777 4778 Tooltips: { 4779 FullPage: "Toggle full page", 4780 Home: "Go home", 4781 ZoomIn: "Zoom in", 4782 ZoomOut: "Zoom out", 4783 NextPage: "Next page", 4784 PreviousPage: "Previous page" 4785 } 4786 }; 4787 4788 $.extend( $, { 4789 4790 /** 4791 * @function 4792 * @name OpenSeadragon.getString 4793 * @param {String} property 4794 */ 4795 getString: function( prop ) { 4796 4797 var props = prop.split('.'), 4798 string = null, 4799 args = arguments, 4800 i; 4801 4802 for ( i = 0; i < props.length; i++ ) { 4803 // in case not a subproperty 4804 string = I18N[ props[ i ] ] || {}; 4805 } 4806 4807 if ( typeof( string ) != "string" ) { 4808 string = ""; 4809 } 4810 4811 return string.replace(/\{\d+\}/g, function(capture) { 4812 var i = parseInt( capture.match( /\d+/ ) ) + 1; 4813 return i < args.length ? 4814 args[ i ] : 4815 ""; 4816 }); 4817 }, 4818 4819 /** 4820 * @function 4821 * @name OpenSeadragon.setString 4822 * @param {String} property 4823 * @param {*} value 4824 */ 4825 setString: function( prop, value ) { 4826 4827 var props = prop.split('.'), 4828 container = $.Strings, 4829 i; 4830 4831 for ( i = 0; i < props.length - 1; i++ ) { 4832 if ( !container[ props[ i ] ] ) { 4833 container[ props[ i ] ] = {}; 4834 } 4835 container = container[ props[ i ] ]; 4836 } 4837 4838 container[ props[ i ] ] = value; 4839 } 4840 4841 }); 4842 4843 }( OpenSeadragon )); 4844 4845 (function( $ ){ 4846 4847 /** 4848 * A Point is really used as a 2-dimensional vector, equally useful for 4849 * representing a point on a plane, or the height and width of a plane 4850 * not requiring any other frame of reference. 4851 * @class 4852 * @param {Number} [x] The vector component 'x'. Defaults to the origin at 0. 4853 * @param {Number} [y] The vector component 'y'. Defaults to the origin at 0. 4854 * @property {Number} [x] The vector component 'x'. 4855 * @property {Number} [y] The vector component 'y'. 4856 */ 4857 $.Point = function( x, y ) { 4858 this.x = typeof ( x ) == "number" ? x : 0; 4859 this.y = typeof ( y ) == "number" ? y : 0; 4860 }; 4861 4862 $.Point.prototype = { 4863 4864 /** 4865 * Add another Point to this point and return a new Point. 4866 * @function 4867 * @param {OpenSeadragon.Point} point The point to add vector components. 4868 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4869 * vector components 4870 */ 4871 plus: function( point ) { 4872 return new $.Point( 4873 this.x + point.x, 4874 this.y + point.y 4875 ); 4876 }, 4877 4878 /** 4879 * Add another Point to this point and return a new Point. 4880 * @function 4881 * @param {OpenSeadragon.Point} point The point to add vector components. 4882 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4883 * vector components 4884 */ 4885 minus: function( point ) { 4886 return new $.Point( 4887 this.x - point.x, 4888 this.y - point.y 4889 ); 4890 }, 4891 4892 /** 4893 * Add another Point to this point and return a new Point. 4894 * @function 4895 * @param {OpenSeadragon.Point} point The point to add vector components. 4896 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4897 * vector components 4898 */ 4899 times: function( factor ) { 4900 return new $.Point( 4901 this.x * factor, 4902 this.y * factor 4903 ); 4904 }, 4905 4906 /** 4907 * Add another Point to this point and return a new Point. 4908 * @function 4909 * @param {OpenSeadragon.Point} point The point to add vector components. 4910 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4911 * vector components 4912 */ 4913 divide: function( factor ) { 4914 return new $.Point( 4915 this.x / factor, 4916 this.y / factor 4917 ); 4918 }, 4919 4920 /** 4921 * Add another Point to this point and return a new Point. 4922 * @function 4923 * @param {OpenSeadragon.Point} point The point to add vector components. 4924 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4925 * vector components 4926 */ 4927 negate: function() { 4928 return new $.Point( -this.x, -this.y ); 4929 }, 4930 4931 /** 4932 * Add another Point to this point and return a new Point. 4933 * @function 4934 * @param {OpenSeadragon.Point} point The point to add vector components. 4935 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4936 * vector components 4937 */ 4938 distanceTo: function( point ) { 4939 return Math.sqrt( 4940 Math.pow( this.x - point.x, 2 ) + 4941 Math.pow( this.y - point.y, 2 ) 4942 ); 4943 }, 4944 4945 /** 4946 * Add another Point to this point and return a new Point. 4947 * @function 4948 * @param {OpenSeadragon.Point} point The point to add vector components. 4949 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4950 * vector components 4951 */ 4952 apply: function( func ) { 4953 return new $.Point( func( this.x ), func( this.y ) ); 4954 }, 4955 4956 /** 4957 * Add another Point to this point and return a new Point. 4958 * @function 4959 * @param {OpenSeadragon.Point} point The point to add vector components. 4960 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4961 * vector components 4962 */ 4963 equals: function( point ) { 4964 return ( 4965 point instanceof $.Point 4966 ) && ( 4967 this.x === point.x 4968 ) && ( 4969 this.y === point.y 4970 ); 4971 }, 4972 4973 /** 4974 * Add another Point to this point and return a new Point. 4975 * @function 4976 * @param {OpenSeadragon.Point} point The point to add vector components. 4977 * @returns {OpenSeadragon.Point} A new point representing the sum of the 4978 * vector components 4979 */ 4980 toString: function() { 4981 return "(" + this.x + "," + this.y + ")"; 4982 } 4983 }; 4984 4985 }( OpenSeadragon )); 4986 4987 (function( $ ){ 4988 4989 4990 /** 4991 * @class 4992 * @param {Number} width 4993 * @param {Number} height 4994 * @param {Number} tileSize 4995 * @param {Number} tileOverlap 4996 * @param {Number} minLevel 4997 * @param {Number} maxLevel 4998 * @property {Number} aspectRatio 4999 * @property {Number} dimensions 5000 * @property {Number} tileSize 5001 * @property {Number} tileOverlap 5002 * @property {Number} minLevel 5003 * @property {Number} maxLevel 5004 */ 5005 $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) { 5006 this.aspectRatio = width / height; 5007 this.dimensions = new $.Point( width, height ); 5008 this.tileSize = tileSize ? tileSize : 0; 5009 this.tileOverlap = tileOverlap ? tileOverlap : 0; 5010 this.minLevel = minLevel ? minLevel : 0; 5011 this.maxLevel = maxLevel ? maxLevel : 5012 Math.ceil( 5013 Math.log( Math.max( width, height ) ) / 5014 Math.log( 2 ) 5015 ); 5016 }; 5017 5018 $.TileSource.prototype = { 5019 5020 /** 5021 * @function 5022 * @param {Number} level 5023 */ 5024 getLevelScale: function( level ) { 5025 return 1 / ( 1 << ( this.maxLevel - level ) ); 5026 }, 5027 5028 /** 5029 * @function 5030 * @param {Number} level 5031 */ 5032 getNumTiles: function( level ) { 5033 var scale = this.getLevelScale( level ), 5034 x = Math.ceil( scale * this.dimensions.x / this.tileSize ), 5035 y = Math.ceil( scale * this.dimensions.y / this.tileSize ); 5036 5037 return new $.Point( x, y ); 5038 }, 5039 5040 /** 5041 * @function 5042 * @param {Number} level 5043 */ 5044 getPixelRatio: function( level ) { 5045 var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ), 5046 rx = 1.0 / imageSizeScaled.x, 5047 ry = 1.0 / imageSizeScaled.y; 5048 5049 return new $.Point(rx, ry); 5050 }, 5051 5052 /** 5053 * @function 5054 * @param {Number} level 5055 * @param {OpenSeadragon.Point} point 5056 */ 5057 getTileAtPoint: function( level, point ) { 5058 var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ), 5059 tx = Math.floor( pixel.x / this.tileSize ), 5060 ty = Math.floor( pixel.y / this.tileSize ); 5061 5062 return new $.Point( tx, ty ); 5063 }, 5064 5065 /** 5066 * @function 5067 * @param {Number} level 5068 * @param {Number} x 5069 * @param {Number} y 5070 */ 5071 getTileBounds: function( level, x, y ) { 5072 var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), 5073 px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap, 5074 py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap, 5075 sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap, 5076 sy = this.tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap, 5077 scale = 1.0 / dimensionsScaled.x; 5078 5079 sx = Math.min( sx, dimensionsScaled.x - px ); 5080 sy = Math.min( sy, dimensionsScaled.y - py ); 5081 5082 return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); 5083 }, 5084 5085 /** 5086 * This method is not implemented by this class other than to throw an Error 5087 * announcing you have to implement it. Because of the variety of tile 5088 * server technologies, and various specifications for building image 5089 * pyramids, this method is here to allow easy integration. 5090 * @function 5091 * @param {Number} level 5092 * @param {Number} x 5093 * @param {Number} y 5094 * @throws {Error} 5095 */ 5096 getTileUrl: function( level, x, y ) { 5097 throw new Error( "Method not implemented." ); 5098 }, 5099 5100 /** 5101 * @function 5102 * @param {Number} level 5103 * @param {Number} x 5104 * @param {Number} y 5105 */ 5106 tileExists: function( level, x, y ) { 5107 var numTiles = this.getNumTiles( level ); 5108 return level >= this.minLevel && 5109 level <= this.maxLevel && 5110 x >= 0 && 5111 y >= 0 && 5112 x < numTiles.x && 5113 y < numTiles.y; 5114 } 5115 }; 5116 5117 }( OpenSeadragon )); 5118 5119 (function( $ ){ 5120 5121 /** 5122 * @class 5123 * @extends OpenSeadragon.TileSource 5124 * @param {Number} width 5125 * @param {Number} height 5126 * @param {Number} tileSize 5127 * @param {Number} tileOverlap 5128 * @param {String} tilesUrl 5129 * @param {String} fileFormat 5130 * @param {OpenSeadragon.DisplayRect[]} displayRects 5131 * @property {String} tilesUrl 5132 * @property {String} fileFormat 5133 * @property {OpenSeadragon.DisplayRect[]} displayRects 5134 */ 5135 $.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects ) { 5136 var i, 5137 rect, 5138 level; 5139 5140 $.TileSource.call( this, width, height, tileSize, tileOverlap, null, null ); 5141 5142 this._levelRects = {}; 5143 this.tilesUrl = tilesUrl; 5144 this.fileFormat = fileFormat; 5145 this.displayRects = displayRects; 5146 5147 if ( this.displayRects ) { 5148 for ( i = this.displayRects.length - 1; i >= 0; i-- ) { 5149 rect = this.displayRects[ i ]; 5150 for ( level = rect.minLevel; level <= rect.maxLevel; level++ ) { 5151 if ( !this._levelRects[ level ] ) { 5152 this._levelRects[ level ] = []; 5153 } 5154 this._levelRects[ level ].push( rect ); 5155 } 5156 } 5157 } 5158 5159 }; 5160 5161 $.extend( $.DziTileSource.prototype, $.TileSource.prototype, { 5162 5163 /** 5164 * @function 5165 * @name OpenSeadragon.DziTileSource.prototype.getTileUrl 5166 * @param {Number} level 5167 * @param {Number} x 5168 * @param {Number} y 5169 */ 5170 getTileUrl: function( level, x, y ) { 5171 return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat ].join( '' ); 5172 }, 5173 5174 /** 5175 * @function 5176 * @name OpenSeadragon.DziTileSource.prototype.tileExists 5177 * @param {Number} level 5178 * @param {Number} x 5179 * @param {Number} y 5180 */ 5181 tileExists: function( level, x, y ) { 5182 var rects = this._levelRects[ level ], 5183 rect, 5184 scale, 5185 xMin, 5186 yMin, 5187 xMax, 5188 yMax, 5189 i; 5190 5191 if ( !rects || !rects.length ) { 5192 return true; 5193 } 5194 5195 for ( i = rects.length - 1; i >= 0; i-- ) { 5196 rect = rects[ i ]; 5197 5198 if ( level < rect.minLevel || level > rect.maxLevel ) { 5199 continue; 5200 } 5201 5202 scale = this.getLevelScale( level ); 5203 xMin = rect.x * scale; 5204 yMin = rect.y * scale; 5205 xMax = xMin + rect.width * scale; 5206 yMax = yMin + rect.height * scale; 5207 5208 xMin = Math.floor( xMin / this.tileSize ); 5209 yMin = Math.floor( yMin / this.tileSize ); 5210 xMax = Math.ceil( xMax / this.tileSize ); 5211 yMax = Math.ceil( yMax / this.tileSize ); 5212 5213 if ( xMin <= x && x < xMax && yMin <= y && y < yMax ) { 5214 return true; 5215 } 5216 } 5217 5218 return false; 5219 } 5220 }); 5221 5222 5223 5224 }( OpenSeadragon )); 5225 5226 (function( $ ){ 5227 5228 5229 /** 5230 * The LegacyTileSource allows simple, traditional image pyramids to be loaded 5231 * into an OpenSeadragon Viewer. Basically, this translates to the historically 5232 * common practice of starting with a 'master' image, maybe a tiff for example, 5233 * and generating a set of 'service' images like one or more thumbnails, a medium 5234 * resolution image and a high resolution image in standard web formats like 5235 * png or jpg. 5236 * @class 5237 * @param {Array} files An array of file descriptions, each is an object with 5238 * a 'url', a 'width', and a 'height'. Overriding classes can expect more 5239 * properties but these properties are sufficient for this implementation. 5240 * Additionally, the files are required to be listed in order from 5241 * smallest to largest. 5242 * @property {Number} aspectRatio 5243 * @property {Number} dimensions 5244 * @property {Number} tileSize 5245 * @property {Number} tileOverlap 5246 * @property {Number} minLevel 5247 * @property {Number} maxLevel 5248 * @property {Array} files 5249 */ 5250 $.LegacyTileSource = function( files ) { 5251 var width = files[ files.length - 1 ].width, 5252 height = files[ files.length - 1 ].height; 5253 5254 $.TileSource.apply( this, [ 5255 width, 5256 height, 5257 Math.max( height, width ), //tileSize 5258 0, //overlap 5259 0, //mimLevel 5260 files.length - 1 //maxLevel 5261 ] ); 5262 5263 this.files = files; 5264 }; 5265 5266 $.LegacyTileSource.prototype = { 5267 5268 /** 5269 * @function 5270 * @param {Number} level 5271 */ 5272 getLevelScale: function( level ) { 5273 var levelScale = NaN; 5274 if ( level >= this.minLevel && level <= this.maxLevel ){ 5275 levelScale = 5276 this.files[ level ].width / 5277 this.files[ this.maxLevel ].width; 5278 } 5279 return levelScale; 5280 }, 5281 5282 /** 5283 * @function 5284 * @param {Number} level 5285 */ 5286 getNumTiles: function( level ) { 5287 var scale = this.getLevelScale( level ); 5288 if ( scale ){ 5289 return new $.Point( 1, 1 ); 5290 } else { 5291 return new $.Point( 0, 0 ); 5292 } 5293 }, 5294 5295 /** 5296 * @function 5297 * @param {Number} level 5298 */ 5299 getPixelRatio: function( level ) { 5300 var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ), 5301 rx = 1.0 / imageSizeScaled.x, 5302 ry = 1.0 / imageSizeScaled.y; 5303 5304 return new $.Point(rx, ry); 5305 }, 5306 5307 /** 5308 * @function 5309 * @param {Number} level 5310 * @param {OpenSeadragon.Point} point 5311 */ 5312 getTileAtPoint: function( level, point ) { 5313 return new $.Point( 0, 0 ); 5314 }, 5315 5316 /** 5317 * @function 5318 * @param {Number} level 5319 * @param {Number} x 5320 * @param {Number} y 5321 */ 5322 getTileBounds: function( level, x, y ) { 5323 var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), 5324 px = ( x === 0 ) ? 0 : this.files[ level ].width, 5325 py = ( y === 0 ) ? 0 : this.files[ level ].height, 5326 sx = this.files[ level ].width, 5327 sy = this.files[ level ].height, 5328 scale = 1.0 / ( this.width >= this.height ? 5329 dimensionsScaled.y : 5330 dimensionsScaled.x 5331 ); 5332 5333 sx = Math.min( sx, dimensionsScaled.x - px ); 5334 sy = Math.min( sy, dimensionsScaled.y - py ); 5335 5336 return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); 5337 }, 5338 5339 /** 5340 * This method is not implemented by this class other than to throw an Error 5341 * announcing you have to implement it. Because of the variety of tile 5342 * server technologies, and various specifications for building image 5343 * pyramids, this method is here to allow easy integration. 5344 * @function 5345 * @param {Number} level 5346 * @param {Number} x 5347 * @param {Number} y 5348 * @throws {Error} 5349 */ 5350 getTileUrl: function( level, x, y ) { 5351 var url = null; 5352 if( level >= this.minLevel && level <= this.maxLevel ){ 5353 url = this.files[ level ].url; 5354 } 5355 return url; 5356 }, 5357 5358 /** 5359 * @function 5360 * @param {Number} level 5361 * @param {Number} x 5362 * @param {Number} y 5363 */ 5364 tileExists: function( level, x, y ) { 5365 var numTiles = this.getNumTiles( level ); 5366 return level >= this.minLevel && 5367 level <= this.maxLevel && 5368 x >= 0 && 5369 y >= 0 && 5370 x < numTiles.x && 5371 y < numTiles.y; 5372 } 5373 }; 5374 5375 }( OpenSeadragon )); 5376 5377 5378 (function( $ ){ 5379 5380 /** 5381 * An enumeration of button states including, REST, GROUP, HOVER, and DOWN 5382 * @static 5383 */ 5384 $.ButtonState = { 5385 REST: 0, 5386 GROUP: 1, 5387 HOVER: 2, 5388 DOWN: 3 5389 }; 5390 5391 /** 5392 * Manages events, hover states for individual buttons, tool-tips, as well 5393 * as fading the bottons out when the user has not interacted with them 5394 * for a specified period. 5395 * @class 5396 * @extends OpenSeadragon.EventHandler 5397 * @param {Object} options 5398 * @param {String} options.tooltip Provides context help for the button we the 5399 * user hovers over it. 5400 * @param {String} options.srcRest URL of image to use in 'rest' state 5401 * @param {String} options.srcGroup URL of image to use in 'up' state 5402 * @param {String} options.srcHover URL of image to use in 'hover' state 5403 * @param {String} options.srcDown URL of image to use in 'domn' state 5404 * @param {Element} [options.element] Element to use as a container for the 5405 * button. 5406 * @property {String} tooltip Provides context help for the button we the 5407 * user hovers over it. 5408 * @property {String} srcRest URL of image to use in 'rest' state 5409 * @property {String} srcGroup URL of image to use in 'up' state 5410 * @property {String} srcHover URL of image to use in 'hover' state 5411 * @property {String} srcDown URL of image to use in 'domn' state 5412 * @property {Object} config Configurable settings for this button. DEPRECATED. 5413 * @property {Element} [element] Element to use as a container for the 5414 * button. 5415 * @property {Number} fadeDelay How long to wait before fading 5416 * @property {Number} fadeLength How long should it take to fade the button. 5417 * @property {Number} fadeBeginTime When the button last began to fade. 5418 * @property {Boolean} shouldFade Whether this button should fade after user 5419 * stops interacting with the viewport. 5420 this.fadeDelay = 0; // begin fading immediately 5421 this.fadeLength = 2000; // fade over a period of 2 seconds 5422 this.fadeBeginTime = null; 5423 this.shouldFade = false; 5424 */ 5425 $.Button = function( options ) { 5426 5427 var _this = this; 5428 5429 $.EventHandler.call( this ); 5430 5431 $.extend( true, this, { 5432 5433 tooltip: null, 5434 srcRest: null, 5435 srcGroup: null, 5436 srcHover: null, 5437 srcDown: null, 5438 clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, 5439 clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold, 5440 // begin fading immediately 5441 fadeDelay: 0, 5442 // fade over a period of 2 seconds 5443 fadeLength: 2000, 5444 onPress: null, 5445 onRelease: null, 5446 onClick: null, 5447 onEnter: null, 5448 onExit: null, 5449 onFocus: null, 5450 onBlur: null 5451 5452 }, options ); 5453 5454 this.element = options.element || $.makeNeutralElement( "button" ); 5455 this.element.href = this.element.href || '#'; 5456 5457 //if the user has specified the element to bind the control to explicitly 5458 //then do not add the default control images 5459 if( !options.element ){ 5460 this.imgRest = $.makeTransparentImage( this.srcRest ); 5461 this.imgGroup = $.makeTransparentImage( this.srcGroup ); 5462 this.imgHover = $.makeTransparentImage( this.srcHover ); 5463 this.imgDown = $.makeTransparentImage( this.srcDown ); 5464 5465 this.element.appendChild( this.imgRest ); 5466 this.element.appendChild( this.imgGroup ); 5467 this.element.appendChild( this.imgHover ); 5468 this.element.appendChild( this.imgDown ); 5469 5470 this.imgGroup.style.position = 5471 this.imgHover.style.position = 5472 this.imgDown.style.position = 5473 "absolute"; 5474 5475 this.imgGroup.style.top = 5476 this.imgHover.style.top = 5477 this.imgDown.style.top = 5478 "0px"; 5479 5480 this.imgGroup.style.left = 5481 this.imgHover.style.left = 5482 this.imgDown.style.left = 5483 "0px"; 5484 5485 this.imgHover.style.visibility = 5486 this.imgDown.style.visibility = 5487 "hidden"; 5488 5489 if ( $.Browser.vendor == $.BROWSERS.FIREFOX && $.Browser.version < 3 ){ 5490 this.imgGroup.style.top = 5491 this.imgHover.style.top = 5492 this.imgDown.style.top = 5493 ""; 5494 } 5495 } 5496 5497 5498 this.addHandler( "onPress", this.onPress ); 5499 this.addHandler( "onRelease", this.onRelease ); 5500 this.addHandler( "onClick", this.onClick ); 5501 this.addHandler( "onEnter", this.onEnter ); 5502 this.addHandler( "onExit", this.onExit ); 5503 this.addHandler( "onFocus", this.onFocus ); 5504 this.addHandler( "onBlur", this.onBlur ); 5505 5506 this.currentState = $.ButtonState.GROUP; 5507 5508 this.fadeBeginTime = null; 5509 this.shouldFade = false; 5510 5511 this.element.style.display = "inline-block"; 5512 this.element.style.position = "relative"; 5513 this.element.title = this.tooltip; 5514 5515 this.tracker = new $.MouseTracker({ 5516 5517 element: this.element, 5518 clickTimeThreshold: this.clickTimeThreshold, 5519 clickDistThreshold: this.clickDistThreshold, 5520 5521 enterHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { 5522 if ( buttonDownElement ) { 5523 inTo( _this, $.ButtonState.DOWN ); 5524 _this.raiseEvent( "onEnter", _this ); 5525 } else if ( !buttonDownAny ) { 5526 inTo( _this, $.ButtonState.HOVER ); 5527 } 5528 }, 5529 5530 focusHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { 5531 this.enterHandler( tracker, position, buttonDownElement, buttonDownAny ); 5532 _this.raiseEvent( "onFocus", _this ); 5533 }, 5534 5535 exitHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { 5536 outTo( _this, $.ButtonState.GROUP ); 5537 if ( buttonDownElement ) { 5538 _this.raiseEvent( "onExit", _this ); 5539 } 5540 }, 5541 5542 blurHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { 5543 this.exitHandler( tracker, position, buttonDownElement, buttonDownAny ); 5544 _this.raiseEvent( "onBlur", _this ); 5545 }, 5546 5547 pressHandler: function( tracker, position ) { 5548 inTo( _this, $.ButtonState.DOWN ); 5549 _this.raiseEvent( "onPress", _this ); 5550 }, 5551 5552 releaseHandler: function( tracker, position, insideElementPress, insideElementRelease ) { 5553 if ( insideElementPress && insideElementRelease ) { 5554 outTo( _this, $.ButtonState.HOVER ); 5555 _this.raiseEvent( "onRelease", _this ); 5556 } else if ( insideElementPress ) { 5557 outTo( _this, $.ButtonState.GROUP ); 5558 } else { 5559 inTo( _this, $.ButtonState.HOVER ); 5560 } 5561 }, 5562 5563 clickHandler: function( tracker, position, quick, shift ) { 5564 if ( quick ) { 5565 _this.raiseEvent("onClick", _this); 5566 } 5567 }, 5568 5569 keyHandler: function( tracker, key ){ 5570 //console.log( "%s : handling key %s!", _this.tooltip, key); 5571 if( 13 === key ){ 5572 _this.raiseEvent( "onClick", _this ); 5573 _this.raiseEvent( "onRelease", _this ); 5574 return false; 5575 } 5576 return true; 5577 } 5578 5579 }).setTracking( true ); 5580 5581 outTo( this, $.ButtonState.REST ); 5582 }; 5583 5584 $.extend( $.Button.prototype, $.EventHandler.prototype, { 5585 5586 /** 5587 * TODO: Determine what this function is intended to do and if it's actually 5588 * useful as an API point. 5589 * @function 5590 * @name OpenSeadragon.Button.prototype.notifyGroupEnter 5591 */ 5592 notifyGroupEnter: function() { 5593 inTo( this, $.ButtonState.GROUP ); 5594 }, 5595 5596 /** 5597 * TODO: Determine what this function is intended to do and if it's actually 5598 * useful as an API point. 5599 * @function 5600 * @name OpenSeadragon.Button.prototype.notifyGroupExit 5601 */ 5602 notifyGroupExit: function() { 5603 outTo( this, $.ButtonState.REST ); 5604 }, 5605 5606 disable: function(){ 5607 this.notifyGroupExit(); 5608 this.element.disabled = true; 5609 $.setElementOpacity( this.element, 0.2, true ); 5610 }, 5611 5612 enable: function(){ 5613 this.element.disabled = false; 5614 $.setElementOpacity( this.element, 1.0, true ); 5615 this.notifyGroupEnter(); 5616 } 5617 5618 }); 5619 5620 5621 function scheduleFade( button ) { 5622 window.setTimeout(function(){ 5623 updateFade( button ); 5624 }, 20 ); 5625 }; 5626 5627 function updateFade( button ) { 5628 var currentTime, 5629 deltaTime, 5630 opacity; 5631 5632 if ( button.shouldFade ) { 5633 currentTime = +new Date(); 5634 deltaTime = currentTime - button.fadeBeginTime; 5635 opacity = 1.0 - deltaTime / button.fadeLength; 5636 opacity = Math.min( 1.0, opacity ); 5637 opacity = Math.max( 0.0, opacity ); 5638 5639 if( button.imgGroup ){ 5640 $.setElementOpacity( button.imgGroup, opacity, true ); 5641 } 5642 if ( opacity > 0 ) { 5643 // fade again 5644 scheduleFade( button ); 5645 } 5646 } 5647 }; 5648 5649 function beginFading( button ) { 5650 button.shouldFade = true; 5651 button.fadeBeginTime = +new Date() + button.fadeDelay; 5652 window.setTimeout( function(){ 5653 scheduleFade( button ); 5654 }, button.fadeDelay ); 5655 }; 5656 5657 function stopFading( button ) { 5658 button.shouldFade = false; 5659 if( button.imgGroup ){ 5660 $.setElementOpacity( button.imgGroup, 1.0, true ); 5661 } 5662 }; 5663 5664 function inTo( button, newState ) { 5665 5666 if( button.element.disabled ){ 5667 return; 5668 } 5669 5670 if ( newState >= $.ButtonState.GROUP && 5671 button.currentState == $.ButtonState.REST ) { 5672 stopFading( button ); 5673 button.currentState = $.ButtonState.GROUP; 5674 } 5675 5676 if ( newState >= $.ButtonState.HOVER && 5677 button.currentState == $.ButtonState.GROUP ) { 5678 if( button.imgHover ){ 5679 button.imgHover.style.visibility = ""; 5680 } 5681 button.currentState = $.ButtonState.HOVER; 5682 } 5683 5684 if ( newState >= $.ButtonState.DOWN && 5685 button.currentState == $.ButtonState.HOVER ) { 5686 if( button.imgDown ){ 5687 button.imgDown.style.visibility = ""; 5688 } 5689 button.currentState = $.ButtonState.DOWN; 5690 } 5691 }; 5692 5693 5694 function outTo( button, newState ) { 5695 5696 if( button.element.disabled ){ 5697 return; 5698 } 5699 5700 if ( newState <= $.ButtonState.HOVER && 5701 button.currentState == $.ButtonState.DOWN ) { 5702 if( button.imgDown ){ 5703 button.imgDown.style.visibility = "hidden"; 5704 } 5705 button.currentState = $.ButtonState.HOVER; 5706 } 5707 5708 if ( newState <= $.ButtonState.GROUP && 5709 button.currentState == $.ButtonState.HOVER ) { 5710 if( button.imgHover ){ 5711 button.imgHover.style.visibility = "hidden"; 5712 } 5713 button.currentState = $.ButtonState.GROUP; 5714 } 5715 5716 if ( newState <= $.ButtonState.REST && 5717 button.currentState == $.ButtonState.GROUP ) { 5718 beginFading( button ); 5719 button.currentState = $.ButtonState.REST; 5720 } 5721 }; 5722 5723 5724 5725 }( OpenSeadragon )); 5726 5727 (function( $ ){ 5728 /** 5729 * Manages events on groups of buttons. 5730 * @class 5731 * @param {Object} options - a dictionary of settings applied against the entire 5732 * group of buttons 5733 * @param {Array} options.buttons Array of buttons 5734 * @param {Element} [options.group] Element to use as the container, 5735 * @param {Object} options.config Object with Viewer settings ( TODO: is 5736 * this actually used anywhere? ) 5737 * @param {Function} [options.enter] Function callback for when the mouse 5738 * enters group 5739 * @param {Function} [options.exit] Function callback for when mouse leaves 5740 * the group 5741 * @param {Function} [options.release] Function callback for when mouse is 5742 * released 5743 * @property {Array} buttons - An array containing the buttons themselves. 5744 * @property {Element} element - The shared container for the buttons. 5745 * @property {Object} config - Configurable settings for the group of buttons. 5746 * @property {OpenSeadragon.MouseTracker} tracker - Tracks mouse events accross 5747 * the group of buttons. 5748 **/ 5749 $.ButtonGroup = function( options ) { 5750 5751 $.extend( true, this, { 5752 buttons: [], 5753 clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, 5754 clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold, 5755 labelText: "" 5756 }, options ); 5757 5758 // copy the botton elements 5759 var buttons = this.buttons.concat([]), 5760 _this = this, 5761 i; 5762 5763 this.element = options.element || $.makeNeutralElement( "fieldgroup" ); 5764 5765 if( !options.group ){ 5766 this.label = $.makeNeutralElement( "label" ); 5767 //TODO: support labels for ButtonGroups 5768 //this.label.innerHTML = this.labelText; 5769 this.element.style.display = "inline-block"; 5770 this.element.appendChild( this.label ); 5771 for ( i = 0; i < buttons.length; i++ ) { 5772 this.element.appendChild( buttons[ i ].element ); 5773 } 5774 } 5775 5776 this.tracker = new $.MouseTracker({ 5777 element: this.element, 5778 clickTimeThreshold: this.clickTimeThreshold, 5779 clickDistThreshold: this.clickDistThreshold, 5780 enterHandler: function() { 5781 var i; 5782 for ( i = 0; i < _this.buttons.length; i++ ) { 5783 _this.buttons[ i ].notifyGroupEnter(); 5784 } 5785 }, 5786 exitHandler: function() { 5787 var i, 5788 buttonDownElement = arguments.length > 2 ? 5789 arguments[ 2 ] : 5790 null; 5791 if ( !buttonDownElement ) { 5792 for ( i = 0; i < _this.buttons.length; i++ ) { 5793 _this.buttons[ i ].notifyGroupExit(); 5794 } 5795 } 5796 }, 5797 releaseHandler: function() { 5798 var i, 5799 insideElementRelease = arguments.length > 3 ? 5800 arguments[ 3 ] : 5801 null; 5802 if ( !insideElementRelease ) { 5803 for ( i = 0; i < _this.buttons.length; i++ ) { 5804 _this.buttons[ i ].notifyGroupExit(); 5805 } 5806 } 5807 } 5808 }).setTracking( true ); 5809 }; 5810 5811 $.ButtonGroup.prototype = { 5812 5813 /** 5814 * TODO: Figure out why this is used on the public API and if a more useful 5815 * api can be created. 5816 * @function 5817 * @name OpenSeadragon.ButtonGroup.prototype.emulateEnter 5818 */ 5819 emulateEnter: function() { 5820 this.tracker.enterHandler(); 5821 }, 5822 5823 /** 5824 * TODO: Figure out why this is used on the public API and if a more useful 5825 * api can be created. 5826 * @function 5827 * @name OpenSeadragon.ButtonGroup.prototype.emulateExit 5828 */ 5829 emulateExit: function() { 5830 this.tracker.exitHandler(); 5831 } 5832 }; 5833 5834 5835 }( OpenSeadragon )); 5836 5837 (function( $ ){ 5838 5839 /** 5840 * A Rectangle really represents a 2x2 matrix where each row represents a 5841 * 2 dimensional vector component, the first is (x,y) and the second is 5842 * (width, height). The latter component implies the equation of a simple 5843 * plane. 5844 * 5845 * @class 5846 * @param {Number} x The vector component 'x'. 5847 * @param {Number} y The vector component 'y'. 5848 * @param {Number} width The vector component 'height'. 5849 * @param {Number} height The vector component 'width'. 5850 * @property {Number} x The vector component 'x'. 5851 * @property {Number} y The vector component 'y'. 5852 * @property {Number} width The vector component 'width'. 5853 * @property {Number} height The vector component 'height'. 5854 */ 5855 $.Rect = function( x, y, width, height ) { 5856 this.x = typeof ( x ) == "number" ? x : 0; 5857 this.y = typeof ( y ) == "number" ? y : 0; 5858 this.width = typeof ( width ) == "number" ? width : 0; 5859 this.height = typeof ( height ) == "number" ? height : 0; 5860 }; 5861 5862 $.Rect.prototype = { 5863 5864 /** 5865 * The aspect ratio is simply the ratio of width to height. 5866 * @function 5867 * @returns {Number} The ratio of width to height. 5868 */ 5869 getAspectRatio: function() { 5870 return this.width / this.height; 5871 }, 5872 5873 /** 5874 * Provides the coordinates of the upper-left corner of the rectanglea s a 5875 * point. 5876 * @function 5877 * @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of 5878 * the rectangle. 5879 */ 5880 getTopLeft: function() { 5881 return new $.Point( this.x, this.y ); 5882 }, 5883 5884 /** 5885 * Provides the coordinates of the bottom-right corner of the rectangle as a 5886 * point. 5887 * @function 5888 * @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of 5889 * the rectangle. 5890 */ 5891 getBottomRight: function() { 5892 return new $.Point( 5893 this.x + this.width, 5894 this.y + this.height 5895 ); 5896 }, 5897 5898 /** 5899 * Computes the center of the rectangle. 5900 * @function 5901 * @returns {OpenSeadragon.Point} The center of the rectangle as represnted 5902 * as represented by a 2-dimensional vector (x,y) 5903 */ 5904 getCenter: function() { 5905 return new $.Point( 5906 this.x + this.width / 2.0, 5907 this.y + this.height / 2.0 5908 ); 5909 }, 5910 5911 /** 5912 * Returns the width and height component as a vector OpenSeadragon.Point 5913 * @function 5914 * @returns {OpenSeadragon.Point} The 2 dimensional vector represnting the 5915 * the width and height of the rectangle. 5916 */ 5917 getSize: function() { 5918 return new $.Point( this.width, this.height ); 5919 }, 5920 5921 /** 5922 * Determines if two Rectanlges have equivalent components. 5923 * @function 5924 * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to. 5925 * @return {Boolean} 'true' if all components are equal, otherwise 'false'. 5926 */ 5927 equals: function( other ) { 5928 return ( other instanceof $.Rect ) && 5929 ( this.x === other.x ) && 5930 ( this.y === other.y ) && 5931 ( this.width === other.width ) && 5932 ( this.height === other.height ); 5933 }, 5934 5935 /** 5936 * Provides a string representation of the retangle which is useful for 5937 * debugging. 5938 * @function 5939 * @returns {String} A string representation of the rectangle. 5940 */ 5941 toString: function() { 5942 return "[" + 5943 this.x + "," + 5944 this.y + "," + 5945 this.width + "x" + 5946 this.height + 5947 "]"; 5948 } 5949 }; 5950 5951 5952 }( OpenSeadragon )); 5953 5954 (function( $ ){ 5955 5956 /** 5957 * A display rectanlge is very similar to the OpenSeadragon.Rect but adds two 5958 * fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels 5959 * for this rectangle. 5960 * @class 5961 * @extends OpenSeadragon.Rect 5962 * @param {Number} x The vector component 'x'. 5963 * @param {Number} y The vector component 'y'. 5964 * @param {Number} width The vector component 'height'. 5965 * @param {Number} height The vector component 'width'. 5966 * @param {Number} minLevel The lowest zoom level supported. 5967 * @param {Number} maxLevel The highest zoom level supported. 5968 * @property {Number} minLevel The lowest zoom level supported. 5969 * @property {Number} maxLevel The highest zoom level supported. 5970 */ 5971 $.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) { 5972 $.Rect.apply( this, [ x, y, width, height ] ); 5973 5974 this.minLevel = minLevel; 5975 this.maxLevel = maxLevel; 5976 } 5977 5978 $.extend( $.DisplayRect.prototype, $.Rect.prototype ); 5979 5980 }( OpenSeadragon )); 5981 5982 (function( $ ){ 5983 5984 /** 5985 * @class 5986 * @param {Object} options - Spring configuration settings. 5987 * @param {Number} options.initial - Initial value of spring, default to 0 so 5988 * spring is not in motion initally by default. 5989 * @param {Number} options.springStiffness - Spring stiffness. 5990 * @param {Number} options.animationTime - Animation duration per spring. 5991 * 5992 * @property {Number} initial - Initial value of spring, default to 0 so 5993 * spring is not in motion initally by default. 5994 * @property {Number} springStiffness - Spring stiffness. 5995 * @property {Number} animationTime - Animation duration per spring. 5996 * @property {Object} current 5997 * @property {Number} start 5998 * @property {Number} target 5999 */ 6000 $.Spring = function( options ) { 6001 var args = arguments; 6002 6003 if( typeof( options ) != 'object' ){ 6004 //allows backward compatible use of ( initialValue, config ) as 6005 //constructor parameters 6006 options = { 6007 initial: args.length && typeof ( args[ 0 ] ) == "number" ? 6008 args[ 0 ] : 6009 0, 6010 springStiffness: args.length > 1 ? 6011 args[ 1 ].springStiffness : 6012 5.0, 6013 animationTime: args.length > 1 ? 6014 args[ 1 ].animationTime : 6015 1.5 6016 }; 6017 } 6018 6019 $.extend( true, this, options); 6020 6021 6022 this.current = { 6023 value: typeof ( this.initial ) == "number" ? 6024 this.initial : 6025 0, 6026 time: new Date().getTime() // always work in milliseconds 6027 }; 6028 6029 this.start = { 6030 value: this.current.value, 6031 time: this.current.time 6032 }; 6033 6034 this.target = { 6035 value: this.current.value, 6036 time: this.current.time 6037 }; 6038 }; 6039 6040 $.Spring.prototype = { 6041 6042 /** 6043 * @function 6044 * @param {Number} target 6045 */ 6046 resetTo: function( target ) { 6047 this.target.value = target; 6048 this.target.time = this.current.time; 6049 this.start.value = this.target.value; 6050 this.start.time = this.target.time; 6051 }, 6052 6053 /** 6054 * @function 6055 * @param {Number} target 6056 */ 6057 springTo: function( target ) { 6058 this.start.value = this.current.value; 6059 this.start.time = this.current.time; 6060 this.target.value = target; 6061 this.target.time = this.start.time + 1000 * this.animationTime; 6062 }, 6063 6064 /** 6065 * @function 6066 * @param {Number} delta 6067 */ 6068 shiftBy: function( delta ) { 6069 this.start.value += delta; 6070 this.target.value += delta; 6071 }, 6072 6073 /** 6074 * @function 6075 */ 6076 update: function() { 6077 this.current.time = new Date().getTime(); 6078 this.current.value = (this.current.time >= this.target.time) ? 6079 this.target.value : 6080 this.start.value + 6081 ( this.target.value - this.start.value ) * 6082 transform( 6083 this.springStiffness, 6084 ( this.current.time - this.start.time ) / 6085 ( this.target.time - this.start.time ) 6086 ); 6087 } 6088 } 6089 6090 /** 6091 * @private 6092 */ 6093 function transform( stiffness, x ) { 6094 return ( 1.0 - Math.exp( stiffness * -x ) ) / 6095 ( 1.0 - Math.exp( -stiffness ) ); 6096 }; 6097 6098 }( OpenSeadragon )); 6099 6100 (function( $ ){ 6101 6102 /** 6103 * @class 6104 * @param {Number} level The zoom level this tile belongs to. 6105 * @param {Number} x The vector component 'x'. 6106 * @param {Number} y The vector component 'y'. 6107 * @param {OpenSeadragon.Point} bounds Where this tile fits, in normalized 6108 * coordinates. 6109 * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has 6110 * this tile failed to load? ) 6111 * @param {String} url The URL of this tile's image. 6112 * 6113 * @property {Number} level The zoom level this tile belongs to. 6114 * @property {Number} x The vector component 'x'. 6115 * @property {Number} y The vector component 'y'. 6116 * @property {OpenSeadragon.Point} bounds Where this tile fits, in normalized 6117 * coordinates 6118 * @property {Boolean} exists Is this tile a part of a sparse image? ( Also has 6119 * this tile failed to load? 6120 * @property {String} url The URL of this tile's image. 6121 * @property {Boolean} loaded Is this tile loaded? 6122 * @property {Boolean} loading Is this tile loading 6123 * @property {Element} element The HTML element for this tile 6124 * @property {Image} image The Image object for this tile 6125 * @property {String} style The alias of this.element.style. 6126 * @property {String} position This tile's position on screen, in pixels. 6127 * @property {String} size This tile's size on screen, in pixels 6128 * @property {String} blendStart The start time of this tile's blending 6129 * @property {String} opacity The current opacity this tile should be. 6130 * @property {String} distance The distance of this tile to the viewport center 6131 * @property {String} visibility The visibility score of this tile. 6132 * @property {Boolean} beingDrawn Whether this tile is currently being drawn 6133 * @property {Number} lastTouchTime Timestamp the tile was last touched. 6134 */ 6135 $.Tile = function(level, x, y, bounds, exists, url) { 6136 this.level = level; 6137 this.x = x; 6138 this.y = y; 6139 this.bounds = bounds; 6140 this.exists = exists; 6141 this.url = url; 6142 this.loaded = false; 6143 this.loading = false; 6144 6145 this.element = null; 6146 this.image = null; 6147 6148 this.style = null; 6149 this.position = null; 6150 this.size = null; 6151 this.blendStart = null; 6152 this.opacity = null; 6153 this.distance = null; 6154 this.visibility = null; 6155 6156 this.beingDrawn = false; 6157 this.lastTouchTime = 0; 6158 }; 6159 6160 $.Tile.prototype = { 6161 6162 /** 6163 * Provides a string representation of this tiles level and (x,y) 6164 * components. 6165 * @function 6166 * @returns {String} 6167 */ 6168 toString: function() { 6169 return this.level + "/" + this.x + "_" + this.y; 6170 }, 6171 6172 /** 6173 * Renders the tile in an html container. 6174 * @function 6175 * @param {Element} container 6176 */ 6177 drawHTML: function( container ) { 6178 6179 var position = this.position.apply( Math.floor ), 6180 size = this.size.apply( Math.ceil ); 6181 6182 if ( !this.loaded || !this.image ) { 6183 $.console.warn( 6184 "Attempting to draw tile %s when it's not yet loaded.", 6185 this.toString() 6186 ); 6187 return; 6188 } 6189 6190 if ( !this.element ) { 6191 this.element = $.makeNeutralElement("img"); 6192 this.element.src = this.url; 6193 this.style = this.element.style; 6194 6195 this.style.position = "absolute"; 6196 this.style.msInterpolationMode = "nearest-neighbor"; 6197 } 6198 6199 6200 if ( this.element.parentNode != container ) { 6201 container.appendChild( this.element ); 6202 } 6203 6204 this.element.style.left = position.x + "px"; 6205 this.element.style.top = position.y + "px"; 6206 this.element.style.width = size.x + "px"; 6207 this.element.style.height = size.y + "px"; 6208 6209 $.setElementOpacity( this.element, this.opacity ); 6210 6211 }, 6212 6213 /** 6214 * Renders the tile in a canvas-based context. 6215 * @function 6216 * @param {Canvas} context 6217 */ 6218 drawCanvas: function( context ) { 6219 6220 var position = this.position, 6221 size = this.size; 6222 6223 if ( !this.loaded || !this.image ) { 6224 $.console.warn( 6225 "Attempting to draw tile %s when it's not yet loaded.", 6226 this.toString() 6227 ); 6228 return; 6229 } 6230 context.globalAlpha = this.opacity; 6231 context.drawImage( this.image, position.x, position.y, size.x, size.y ); 6232 }, 6233 6234 /** 6235 * Removes tile from it's contianer. 6236 * @function 6237 */ 6238 unload: function() { 6239 if ( this.element && this.element.parentNode ) { 6240 this.element.parentNode.removeChild( this.element ); 6241 } 6242 6243 this.element = null; 6244 this.image = null; 6245 this.loaded = false; 6246 this.loading = false; 6247 } 6248 }; 6249 6250 }( OpenSeadragon )); 6251 6252 (function( $ ){ 6253 6254 /** 6255 * An enumeration of positions that an overlay may be assigned relative 6256 * to the viewport including CENTER, TOP_LEFT (default), TOP, TOP_RIGHT, 6257 * RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT, and LEFT. 6258 * @static 6259 */ 6260 $.OverlayPlacement = { 6261 CENTER: 0, 6262 TOP_LEFT: 1, 6263 TOP: 2, 6264 TOP_RIGHT: 3, 6265 RIGHT: 4, 6266 BOTTOM_RIGHT: 5, 6267 BOTTOM: 6, 6268 BOTTOM_LEFT: 7, 6269 LEFT: 8 6270 }; 6271 6272 /** 6273 * An Overlay provides a 6274 * @class 6275 */ 6276 $.Overlay = function( element, location, placement ) { 6277 this.element = element; 6278 this.scales = location instanceof $.Rect; 6279 this.bounds = new $.Rect( 6280 location.x, 6281 location.y, 6282 location.width, 6283 location.height 6284 ); 6285 this.position = new $.Point( 6286 location.x, 6287 location.y 6288 ); 6289 this.size = new $.Point( 6290 location.width, 6291 location.height 6292 ); 6293 this.style = element.style; 6294 // rects are always top-left 6295 this.placement = location instanceof $.Point ? 6296 placement : 6297 $.OverlayPlacement.TOP_LEFT; 6298 }; 6299 6300 $.Overlay.prototype = { 6301 6302 /** 6303 * @function 6304 * @param {OpenSeadragon.OverlayPlacement} position 6305 * @param {OpenSeadragon.Point} size 6306 */ 6307 adjust: function( position, size ) { 6308 switch ( this.placement ) { 6309 case $.OverlayPlacement.TOP_LEFT: 6310 break; 6311 case $.OverlayPlacement.TOP: 6312 position.x -= size.x / 2; 6313 break; 6314 case $.OverlayPlacement.TOP_RIGHT: 6315 position.x -= size.x; 6316 break; 6317 case $.OverlayPlacement.RIGHT: 6318 position.x -= size.x; 6319 position.y -= size.y / 2; 6320 break; 6321 case $.OverlayPlacement.BOTTOM_RIGHT: 6322 position.x -= size.x; 6323 position.y -= size.y; 6324 break; 6325 case $.OverlayPlacement.BOTTOM: 6326 position.x -= size.x / 2; 6327 position.y -= size.y; 6328 break; 6329 case $.OverlayPlacement.BOTTOM_LEFT: 6330 position.y -= size.y; 6331 break; 6332 case $.OverlayPlacement.LEFT: 6333 position.y -= size.y / 2; 6334 break; 6335 case $.OverlayPlacement.CENTER: 6336 default: 6337 position.x -= size.x / 2; 6338 position.y -= size.y / 2; 6339 break; 6340 } 6341 }, 6342 6343 /** 6344 * @function 6345 */ 6346 destroy: function() { 6347 var element = this.element, 6348 style = this.style; 6349 6350 if ( element.parentNode ) { 6351 element.parentNode.removeChild( element ); 6352 } 6353 6354 style.top = ""; 6355 style.left = ""; 6356 style.position = ""; 6357 6358 if ( this.scales ) { 6359 style.width = ""; 6360 style.height = ""; 6361 } 6362 }, 6363 6364 /** 6365 * @function 6366 * @param {Element} container 6367 */ 6368 drawHTML: function( container ) { 6369 var element = this.element, 6370 style = this.style, 6371 scales = this.scales, 6372 position, 6373 size; 6374 6375 if ( element.parentNode != container ) { 6376 container.appendChild( element ); 6377 } 6378 6379 if ( !scales ) { 6380 this.size = $.getElementSize( element ); 6381 } 6382 6383 position = this.position; 6384 size = this.size; 6385 6386 this.adjust( position, size ); 6387 6388 position = position.apply( Math.floor ); 6389 size = size.apply( Math.ceil ); 6390 6391 style.left = position.x + "px"; 6392 style.top = position.y + "px"; 6393 style.position = "absolute"; 6394 6395 if ( scales ) { 6396 style.width = size.x + "px"; 6397 style.height = size.y + "px"; 6398 } 6399 }, 6400 6401 /** 6402 * @function 6403 * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location 6404 * @param {OpenSeadragon.OverlayPlacement} position 6405 */ 6406 update: function( location, placement ) { 6407 this.scales = location instanceof $.Rect; 6408 this.bounds = new $.Rect( 6409 location.x, 6410 location.y, 6411 location.width, 6412 location.height 6413 ); 6414 // rects are always top-left 6415 this.placement = location instanceof $.Point ? 6416 placement : 6417 $.OverlayPlacement.TOP_LEFT; 6418 } 6419 6420 }; 6421 6422 }( OpenSeadragon )); 6423 6424 (function( $ ){ 6425 6426 var TIMEOUT = 5000, 6427 DEVICE_SCREEN = $.getWindowSize(), 6428 BROWSER = $.Browser.vendor, 6429 BROWSER_VERSION = $.Browser.version, 6430 6431 SUBPIXEL_RENDERING = ( 6432 ( BROWSER == $.BROWSERS.FIREFOX ) || 6433 ( BROWSER == $.BROWSERS.OPERA ) || 6434 ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) || 6435 ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) || 6436 ( BROWSER == $.BROWSERS.IE && BROWSER_VERSION >= 9 ) 6437 ), 6438 6439 USE_CANVAS = SUBPIXEL_RENDERING 6440 && !( DEVICE_SCREEN.x < 600 || DEVICE_SCREEN.y < 600 ) 6441 && !( navigator.appVersion.match( 'Mobile' ) ) 6442 && $.isFunction( document.createElement( "canvas" ).getContext ); 6443 6444 //console.error( 'USE_CANVAS ' + USE_CANVAS ); 6445 6446 /** 6447 * @class 6448 * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source. 6449 * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. 6450 * @param {Element} element - Reference to Viewer 'canvas'. 6451 * @property {OpenSeadragon.TileSource} source - Reference to Viewer tile source. 6452 * @property {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. 6453 * @property {Element} container - Reference to Viewer 'canvas'. 6454 * @property {Element|Canvas} canvas - TODO 6455 * @property {CanvasContext} context - TODO 6456 * @property {Object} config - Reference to Viewer config. 6457 * @property {Number} downloading - How many images are currently being loaded in parallel. 6458 * @property {Number} normHeight - Ratio of zoomable image height to width. 6459 * @property {Object} tilesMatrix - A '3d' dictionary [level][x][y] --> Tile. 6460 * @property {Array} tilesLoaded - An unordered list of Tiles with loaded images. 6461 * @property {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. 6462 * @property {Array} overlays - An unordered list of Overlays added. 6463 * @property {Array} lastDrawn - An unordered list of Tiles drawn last frame. 6464 * @property {Number} lastResetTime - Last time for which the drawer was reset. 6465 * @property {Boolean} midUpdate - Is the drawer currently updating the viewport? 6466 * @property {Boolean} updateAgain - Does the drawer need to update the viewort again? 6467 * @property {Element} element - DEPRECATED Alias for container. 6468 */ 6469 $.Drawer = function( options ) { 6470 6471 //backward compatibility for positional args while prefering more 6472 //idiomatic javascript options object as the only argument 6473 var args = arguments, 6474 i; 6475 6476 if( !$.isPlainObject( options ) ){ 6477 options = { 6478 source: args[ 0 ], 6479 viewport: args[ 1 ], 6480 element: args[ 2 ] 6481 }; 6482 } 6483 6484 $.extend( true, this, { 6485 6486 //internal state properties 6487 downloading: 0, 6488 tilesMatrix: {}, 6489 tilesLoaded: [], 6490 coverage: {}, 6491 lastDrawn: [], 6492 lastResetTime: 0, 6493 midUpdate: false, 6494 updateAgain: true, 6495 6496 //internal state / configurable settings 6497 overlays: [], 6498 6499 //configurable settings 6500 maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount, 6501 imageLoaderLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, 6502 minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, 6503 wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, 6504 wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, 6505 immediateRender: $.DEFAULT_SETTINGS.immediateRender, 6506 blendTime: $.DEFAULT_SETTINGS.blendTime, 6507 alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, 6508 minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio 6509 6510 }, options ); 6511 6512 this.container = $.getElement( this.element ); 6513 this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" ); 6514 this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null; 6515 this.normHeight = this.source.dimensions.y / this.source.dimensions.x; 6516 this.element = this.container; 6517 6518 6519 this.canvas.style.width = "100%"; 6520 this.canvas.style.height = "100%"; 6521 this.canvas.style.position = "absolute"; 6522 6523 // explicit left-align 6524 this.container.style.textAlign = "left"; 6525 this.container.appendChild( this.canvas ); 6526 6527 //create the correct type of overlay by convention if the overlays 6528 //are not already OpenSeadragon.Overlays 6529 for( i = 0; i < this.overlays.length; i++ ){ 6530 if( $.isPlainObject( this.overlays[ i ] ) ){ 6531 6532 (function( _this, overlay ){ 6533 6534 var link = document.createElement("a"), 6535 rect = new $.Rect( 6536 overlay.x, 6537 overlay.y, 6538 overlay.width, 6539 overlay.height 6540 ), 6541 id = Math.floor(Math.random()*10000000); 6542 6543 link.href = "#/overlay/"+id; 6544 link.id = id; 6545 link.className = overlay.className ? 6546 overlay.className : 6547 "openseadragon-overlay"; 6548 6549 _this.overlays[ i ] = new $.Overlay( link, rect ); 6550 6551 }( this, this.overlays[ i ] )); 6552 6553 } else if ( $.isFunction( this.overlays[ i ] ) ){ 6554 6555 } 6556 } 6557 6558 //this.profiler = new $.Profiler(); 6559 }; 6560 6561 $.Drawer.prototype = { 6562 6563 /** 6564 * Adds an html element as an overlay to the current viewport. Useful for 6565 * highlighting words or areas of interest on an image or other zoomable 6566 * interface. 6567 * @method 6568 * @param {Element|String} element - A reference to an element or an id for 6569 * the element which will overlayed. 6570 * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or 6571 * rectangle which will be overlayed. 6572 * @param {OpenSeadragon.OverlayPlacement} placement - The position of the 6573 * viewport which the location coordinates will be treated as relative 6574 * to. 6575 */ 6576 addOverlay: function( element, location, placement ) { 6577 element = $.getElement( element ); 6578 6579 if ( getOverlayIndex( this.overlays, element ) >= 0 ) { 6580 // they're trying to add a duplicate overlay 6581 return; 6582 } 6583 6584 this.overlays.push( new $.Overlay( element, location, placement ) ); 6585 this.updateAgain = true; 6586 }, 6587 6588 /** 6589 * Updates the overlay represented by the reference to the element or 6590 * element id moving it to the new location, relative to the new placement. 6591 * @method 6592 * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or 6593 * rectangle which will be overlayed. 6594 * @param {OpenSeadragon.OverlayPlacement} placement - The position of the 6595 * viewport which the location coordinates will be treated as relative 6596 * to. 6597 */ 6598 updateOverlay: function( element, location, placement ) { 6599 var i; 6600 6601 element = $.getElement( element ); 6602 i = getOverlayIndex( this.overlays, element ); 6603 6604 if ( i >= 0 ) { 6605 this.overlays[ i ].update( location, placement ); 6606 this.updateAgain = true; 6607 } 6608 }, 6609 6610 /** 6611 * Removes and overlay identified by the reference element or element id 6612 * and schedules and update. 6613 * @method 6614 * @param {Element|String} element - A reference to the element or an 6615 * element id which represent the ovelay content to be removed. 6616 */ 6617 removeOverlay: function( element ) { 6618 var i; 6619 6620 element = $.getElement( element ); 6621 i = getOverlayIndex( this.overlays, element ); 6622 6623 if ( i >= 0 ) { 6624 this.overlays[ i ].destroy(); 6625 this.overlays.splice( i, 1 ); 6626 this.updateAgain = true; 6627 } 6628 }, 6629 6630 /** 6631 * Removes all currently configured Overlays from this Drawer and schedules 6632 * and update. 6633 * @method 6634 */ 6635 clearOverlays: function() { 6636 while ( this.overlays.length > 0 ) { 6637 this.overlays.pop().destroy(); 6638 this.updateAgain = true; 6639 } 6640 }, 6641 6642 6643 /** 6644 * Returns whether the Drawer is scheduled for an update at the 6645 * soonest possible opportunity. 6646 * @method 6647 * @returns {Boolean} - Whether the Drawer is scheduled for an update at the 6648 * soonest possible opportunity. 6649 */ 6650 needsUpdate: function() { 6651 return this.updateAgain; 6652 }, 6653 6654 /** 6655 * Returns the total number of tiles that have been loaded by this Drawer. 6656 * @method 6657 * @returns {Number} - The total number of tiles that have been loaded by 6658 * this Drawer. 6659 */ 6660 numTilesLoaded: function() { 6661 return this.tilesLoaded.length; 6662 }, 6663 6664 /** 6665 * Clears all tiles and triggers an update on the next call to 6666 * Drawer.prototype.update(). 6667 * @method 6668 */ 6669 reset: function() { 6670 clearTiles( this ); 6671 this.lastResetTime = +new Date(); 6672 this.updateAgain = true; 6673 }, 6674 6675 /** 6676 * Forces the Drawer to update. 6677 * @method 6678 */ 6679 update: function() { 6680 //this.profiler.beginUpdate(); 6681 this.midUpdate = true; 6682 updateViewport( this ); 6683 this.midUpdate = false; 6684 //this.profiler.endUpdate(); 6685 }, 6686 6687 /** 6688 * Used internally to load images when required. May also be used to 6689 * preload a set of images so the browser will have them available in 6690 * the local cache to optimize user experience in certain cases. Because 6691 * the number of parallel image loads is configurable, if too many images 6692 * are currently being loaded, the request will be ignored. Since by 6693 * default drawer.imageLoaderLimit is 0, the native browser parallel 6694 * image loading policy will be used. 6695 * @method 6696 * @param {String} src - The url of the image to load. 6697 * @param {Function} callback - The function that will be called with the 6698 * Image object as the only parameter, whether on 'load' or on 'abort'. 6699 * For now this means the callback is expected to distinguish between 6700 * error and success conditions by inspecting the Image object. 6701 * @return {Boolean} loading - Wheter the request was submitted or ignored 6702 * based on OpenSeadragon.DEFAULT_SETTINGS.imageLoaderLimit. 6703 */ 6704 loadImage: function( src, callback ) { 6705 var _this = this, 6706 loading = false, 6707 image, 6708 jobid, 6709 complete; 6710 6711 if ( !this.imageLoaderLimit || 6712 this.downloading < this.imageLoaderLimit ) { 6713 6714 this.downloading++; 6715 6716 image = new Image(); 6717 6718 complete = function( imagesrc ){ 6719 _this.downloading--; 6720 if (typeof ( callback ) == "function") { 6721 try { 6722 callback( image ); 6723 } catch ( e ) { 6724 $.console.error( 6725 "%s while executing %s callback: %s", 6726 e.name, 6727 src, 6728 e.message, 6729 e 6730 ); 6731 } 6732 } 6733 }; 6734 6735 image.onload = function(){ 6736 finishLoadingImage( image, complete, true ); 6737 }; 6738 6739 image.onabort = image.onerror = function(){ 6740 finishLoadingImage( image, complete, false ); 6741 }; 6742 6743 jobid = window.setTimeout( function(){ 6744 finishLoadingImage( image, complete, false, jobid ); 6745 }, TIMEOUT ); 6746 6747 loading = true; 6748 image.src = src; 6749 } 6750 6751 return loading; 6752 } 6753 }; 6754 6755 /** 6756 * @private 6757 * @inner 6758 * Pretty much every other line in this needs to be documented so its clear 6759 * how each piece of this routine contributes to the drawing process. That's 6760 * why there are so many TODO's inside this function. 6761 */ 6762 function updateViewport( drawer ) { 6763 6764 drawer.updateAgain = false; 6765 6766 var tile, 6767 level, 6768 best = null, 6769 haveDrawn = false, 6770 currentTime = +new Date(), 6771 viewportSize = drawer.viewport.getContainerSize(), 6772 viewportBounds = drawer.viewport.getBounds( true ), 6773 viewportTL = viewportBounds.getTopLeft(), 6774 viewportBR = viewportBounds.getBottomRight(), 6775 zeroRatioC = drawer.viewport.deltaPixelsFromPoints( 6776 drawer.source.getPixelRatio( 0 ), 6777 true 6778 ).x, 6779 lowestLevel = Math.max( 6780 drawer.source.minLevel, 6781 Math.floor( 6782 Math.log( drawer.minZoomImageRatio ) / 6783 Math.log( 2 ) 6784 ) 6785 ), 6786 highestLevel = Math.min( 6787 drawer.source.maxLevel, 6788 Math.floor( 6789 Math.log( zeroRatioC / drawer.minPixelRatio ) / 6790 Math.log( 2 ) 6791 ) 6792 ), 6793 renderPixelRatioC, 6794 renderPixelRatioT, 6795 zeroRatioT, 6796 optimalRatio, 6797 levelOpacity, 6798 levelVisibility; 6799 6800 //TODO 6801 while ( drawer.lastDrawn.length > 0 ) { 6802 tile = drawer.lastDrawn.pop(); 6803 tile.beingDrawn = false; 6804 } 6805 6806 //TODO 6807 drawer.canvas.innerHTML = ""; 6808 if ( USE_CANVAS ) { 6809 drawer.canvas.width = viewportSize.x; 6810 drawer.canvas.height = viewportSize.y; 6811 drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y ); 6812 } 6813 6814 //TODO 6815 if ( !drawer.wrapHorizontal && 6816 ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { 6817 return; 6818 } else if 6819 ( !drawer.wrapVertical && 6820 ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) { 6821 return; 6822 } 6823 6824 //TODO 6825 if ( !drawer.wrapHorizontal ) { 6826 viewportTL.x = Math.max( viewportTL.x, 0 ); 6827 viewportBR.x = Math.min( viewportBR.x, 1 ); 6828 } 6829 if ( !drawer.wrapVertical ) { 6830 viewportTL.y = Math.max( viewportTL.y, 0 ); 6831 viewportBR.y = Math.min( viewportBR.y, drawer.normHeight ); 6832 } 6833 6834 //TODO 6835 lowestLevel = Math.min( lowestLevel, highestLevel ); 6836 6837 //TODO 6838 for ( level = highestLevel; level >= lowestLevel; level-- ) { 6839 6840 //Avoid calculations for draw if we have already drawn this 6841 renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints( 6842 drawer.source.getPixelRatio( level ), 6843 true 6844 ).x; 6845 6846 if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) || 6847 ( level == lowestLevel ) ) { 6848 drawLevel = true; 6849 haveDrawn = true; 6850 } else if ( !haveDrawn ) { 6851 continue; 6852 } 6853 6854 renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints( 6855 drawer.source.getPixelRatio( level ), 6856 false 6857 ).x; 6858 6859 zeroRatioT = drawer.viewport.deltaPixelsFromPoints( 6860 drawer.source.getPixelRatio( 0 ), 6861 false 6862 ).x; 6863 6864 optimalRatio = drawer.immediateRender ? 6865 1 : 6866 zeroRatioT; 6867 6868 levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); 6869 6870 levelVisibility = optimalRatio / Math.abs( 6871 optimalRatio - renderPixelRatioT 6872 ); 6873 6874 //TODO 6875 best = updateLevel( 6876 drawer, 6877 haveDrawn, 6878 level, 6879 levelOpacity, 6880 levelVisibility, 6881 viewportTL, 6882 viewportBR, 6883 currentTime, 6884 best 6885 ); 6886 6887 //TODO 6888 if ( providesCoverage( drawer.coverage, level ) ) { 6889 break; 6890 } 6891 } 6892 6893 //TODO 6894 drawTiles( drawer, drawer.lastDrawn ); 6895 drawOverlays( drawer.viewport, drawer.overlays, drawer.container ); 6896 6897 //TODO 6898 if ( best ) { 6899 loadTile( drawer, best, currentTime ); 6900 // because we haven't finished drawing, so 6901 drawer.updateAgain = true; 6902 } 6903 }; 6904 6905 6906 function updateLevel( drawer, haveDrawn, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ 6907 6908 var x, y, 6909 tileTL, 6910 tileBR, 6911 numberOfTiles, 6912 viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() ); 6913 6914 6915 //OK, a new drawing so do your calculations 6916 tileTL = drawer.source.getTileAtPoint( level, viewportTL ); 6917 tileBR = drawer.source.getTileAtPoint( level, viewportBR ); 6918 numberOfTiles = drawer.source.getNumTiles( level ); 6919 6920 resetCoverage( drawer.coverage, level ); 6921 6922 if ( !drawer.wrapHorizontal ) { 6923 tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); 6924 } 6925 if ( !drawer.wrapVertical ) { 6926 tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); 6927 } 6928 6929 for ( x = tileTL.x; x <= tileBR.x; x++ ) { 6930 for ( y = tileTL.y; y <= tileBR.y; y++ ) { 6931 6932 best = updateTile( 6933 drawer, 6934 drawLevel, 6935 haveDrawn, 6936 x, y, 6937 level, 6938 levelOpacity, 6939 levelVisibility, 6940 viewportCenter, 6941 numberOfTiles, 6942 currentTime, 6943 best 6944 ); 6945 6946 } 6947 } 6948 return best; 6949 }; 6950 6951 function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ 6952 6953 var tile = getTile( 6954 x, y, 6955 level, 6956 drawer.source, 6957 drawer.tilesMatrix, 6958 currentTime, 6959 numberOfTiles, 6960 drawer.normHeight 6961 ), 6962 drawTile = drawLevel, 6963 newbest; 6964 6965 setCoverage( drawer.coverage, level, x, y, false ); 6966 6967 if ( !tile.exists ) { 6968 return best; 6969 } 6970 6971 if ( haveDrawn && !drawTile ) { 6972 if ( isCovered( drawer.coverage, level, x, y ) ) { 6973 setCoverage( drawer.coverage, level, x, y, true ); 6974 } else { 6975 drawTile = true; 6976 } 6977 } 6978 6979 if ( !drawTile ) { 6980 return best; 6981 } 6982 6983 positionTile( 6984 tile, 6985 drawer.source.tileOverlap, 6986 drawer.viewport, 6987 viewportCenter, 6988 levelVisibility 6989 ); 6990 6991 if ( tile.loaded ) { 6992 6993 drawer.updateAgain = blendTile( 6994 drawer, 6995 tile, 6996 x, y, 6997 level, 6998 levelOpacity, 6999 currentTime 7000 ); 7001 } else if ( tile.loading ) { 7002 // the tile is already in the download queue 7003 // thanks josh1093 for finally translating this typo 7004 } else { 7005 best = compareTiles( best, tile ); 7006 } 7007 7008 return best; 7009 }; 7010 7011 function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) { 7012 var xMod, 7013 yMod, 7014 bounds, 7015 exists, 7016 url, 7017 tile; 7018 7019 if ( !tilesMatrix[ level ] ) { 7020 tilesMatrix[ level ] = {}; 7021 } 7022 if ( !tilesMatrix[ level ][ x ] ) { 7023 tilesMatrix[ level ][ x ] = {}; 7024 } 7025 7026 if ( !tilesMatrix[ level ][ x ][ y ] ) { 7027 xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; 7028 yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; 7029 bounds = tileSource.getTileBounds( level, xMod, yMod ); 7030 exists = tileSource.tileExists( level, xMod, yMod ); 7031 url = tileSource.getTileUrl( level, xMod, yMod ); 7032 7033 bounds.x += 1.0 * ( x - xMod ) / numTiles.x; 7034 bounds.y += normHeight * ( y - yMod ) / numTiles.y; 7035 7036 tilesMatrix[ level ][ x ][ y ] = new $.Tile( 7037 level, 7038 x, 7039 y, 7040 bounds, 7041 exists, 7042 url 7043 ); 7044 } 7045 7046 tile = tilesMatrix[ level ][ x ][ y ]; 7047 tile.lastTouchTime = time; 7048 7049 return tile; 7050 }; 7051 7052 7053 function loadTile( drawer, tile, time ) { 7054 tile.loading = drawer.loadImage( 7055 tile.url, 7056 function( image ){ 7057 onTileLoad( drawer, tile, time, image ); 7058 } 7059 ); 7060 }; 7061 7062 function onTileLoad( drawer, tile, time, image ) { 7063 var insertionIndex, 7064 cutoff, 7065 worstTile, 7066 worstTime, 7067 worstLevel, 7068 worstTileIndex, 7069 prevTile, 7070 prevTime, 7071 prevLevel, 7072 i; 7073 7074 tile.loading = false; 7075 7076 if ( drawer.midUpdate ) { 7077 $.console.warn( "Tile load callback in middle of drawing routine." ); 7078 return; 7079 } else if ( !image ) { 7080 $.console.log( "Tile %s failed to load: %s", tile, tile.url ); 7081 tile.exists = false; 7082 return; 7083 } else if ( time < drawer.lastResetTime ) { 7084 $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); 7085 return; 7086 } 7087 7088 tile.loaded = true; 7089 tile.image = image; 7090 7091 insertionIndex = drawer.tilesLoaded.length; 7092 7093 if ( drawer.tilesLoaded.length >= drawer.maxImageCacheCount ) { 7094 cutoff = Math.ceil( Math.log( drawer.source.tileSize ) / Math.log( 2 ) ); 7095 7096 worstTile = null; 7097 worstTileIndex = -1; 7098 7099 for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) { 7100 prevTile = drawer.tilesLoaded[ i ]; 7101 7102 if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) { 7103 continue; 7104 } else if ( !worstTile ) { 7105 worstTile = prevTile; 7106 worstTileIndex = i; 7107 continue; 7108 } 7109 7110 prevTime = prevTile.lastTouchTime; 7111 worstTime = worstTile.lastTouchTime; 7112 prevLevel = prevTile.level; 7113 worstLevel = worstTile.level; 7114 7115 if ( prevTime < worstTime || 7116 ( prevTime == worstTime && prevLevel > worstLevel ) ) { 7117 worstTile = prevTile; 7118 worstTileIndex = i; 7119 } 7120 } 7121 7122 if ( worstTile && worstTileIndex >= 0 ) { 7123 worstTile.unload(); 7124 insertionIndex = worstTileIndex; 7125 } 7126 } 7127 7128 drawer.tilesLoaded[ insertionIndex ] = tile; 7129 drawer.updateAgain = true; 7130 }; 7131 7132 7133 function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){ 7134 var boundsTL = tile.bounds.getTopLeft(), 7135 boundsSize = tile.bounds.getSize(), 7136 positionC = viewport.pixelFromPoint( boundsTL, true ), 7137 positionT = viewport.pixelFromPoint( boundsTL, false ), 7138 sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ), 7139 sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ), 7140 tileCenter = positionT.plus( sizeT.divide( 2 ) ), 7141 tileDistance = viewportCenter.distanceTo( tileCenter ); 7142 7143 if ( !overlap ) { 7144 sizeC = sizeC.plus( new $.Point( 1, 1 ) ); 7145 } 7146 7147 tile.position = positionC; 7148 tile.size = sizeC; 7149 tile.distance = tileDistance; 7150 tile.visibility = levelVisibility; 7151 }; 7152 7153 7154 function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ 7155 var blendTimeMillis = 1000 * drawer.blendTime, 7156 deltaTime, 7157 opacity; 7158 7159 if ( !tile.blendStart ) { 7160 tile.blendStart = currentTime; 7161 } 7162 7163 deltaTime = currentTime - tile.blendStart; 7164 opacity = Math.min( 1, deltaTime / blendTimeMillis ); 7165 7166 if ( drawer.alwaysBlend ) { 7167 opacity *= levelOpacity; 7168 } 7169 7170 tile.opacity = opacity; 7171 7172 drawer.lastDrawn.push( tile ); 7173 7174 if ( opacity == 1 ) { 7175 setCoverage( drawer.coverage, level, x, y, true ); 7176 } else if ( deltaTime < blendTimeMillis ) { 7177 return true; 7178 } 7179 7180 return false; 7181 }; 7182 7183 7184 function clearTiles( drawer ) { 7185 drawer.tilesMatrix = {}; 7186 drawer.tilesLoaded = []; 7187 }; 7188 7189 /** 7190 * @private 7191 * @inner 7192 * Returns true if the given tile provides coverage to lower-level tiles of 7193 * lower resolution representing the same content. If neither x nor y is 7194 * given, returns true if the entire visible level provides coverage. 7195 * 7196 * Note that out-of-bounds tiles provide coverage in this sense, since 7197 * there's no content that they would need to cover. Tiles at non-existent 7198 * levels that are within the image bounds, however, do not. 7199 */ 7200 function providesCoverage( coverage, level, x, y ) { 7201 var rows, 7202 cols, 7203 i, j; 7204 7205 if ( !coverage[ level ] ) { 7206 return false; 7207 } 7208 7209 if ( x === undefined || y === undefined ) { 7210 rows = coverage[ level ]; 7211 for ( i in rows ) { 7212 if ( rows.hasOwnProperty( i ) ) { 7213 cols = rows[ i ]; 7214 for ( j in cols ) { 7215 if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { 7216 return false; 7217 } 7218 } 7219 } 7220 } 7221 7222 return true; 7223 } 7224 7225 return ( 7226 coverage[ level ][ x] === undefined || 7227 coverage[ level ][ x ][ y ] === undefined || 7228 coverage[ level ][ x ][ y ] === true 7229 ); 7230 }; 7231 7232 /** 7233 * @private 7234 * @inner 7235 * Returns true if the given tile is completely covered by higher-level 7236 * tiles of higher resolution representing the same content. If neither x 7237 * nor y is given, returns true if the entire visible level is covered. 7238 */ 7239 function isCovered( coverage, level, x, y ) { 7240 if ( x === undefined || y === undefined ) { 7241 return providesCoverage( coverage, level + 1 ); 7242 } else { 7243 return ( 7244 providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && 7245 providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && 7246 providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && 7247 providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) 7248 ); 7249 } 7250 }; 7251 7252 /** 7253 * @private 7254 * @inner 7255 * Sets whether the given tile provides coverage or not. 7256 */ 7257 function setCoverage( coverage, level, x, y, covers ) { 7258 if ( !coverage[ level ] ) { 7259 $.console.warn( 7260 "Setting coverage for a tile before its level's coverage has been reset: %s", 7261 level 7262 ); 7263 return; 7264 } 7265 7266 if ( !coverage[ level ][ x ] ) { 7267 coverage[ level ][ x ] = {}; 7268 } 7269 7270 coverage[ level ][ x ][ y ] = covers; 7271 }; 7272 7273 /** 7274 * @private 7275 * @inner 7276 * Resets coverage information for the given level. This should be called 7277 * after every draw routine. Note that at the beginning of the next draw 7278 * routine, coverage for every visible tile should be explicitly set. 7279 */ 7280 function resetCoverage( coverage, level ) { 7281 coverage[ level ] = {}; 7282 }; 7283 7284 /** 7285 * @private 7286 * @inner 7287 * Determines the 'z-index' of the given overlay. Overlays are ordered in 7288 * a z-index based on the order they are added to the Drawer. 7289 */ 7290 function getOverlayIndex( overlays, element ) { 7291 var i; 7292 for ( i = overlays.length - 1; i >= 0; i-- ) { 7293 if ( overlays[ i ].element == element ) { 7294 return i; 7295 } 7296 } 7297 7298 return -1; 7299 }; 7300 7301 /** 7302 * @private 7303 * @inner 7304 * Determines whether the 'last best' tile for the area is better than the 7305 * tile in question. 7306 */ 7307 function compareTiles( previousBest, tile ) { 7308 if ( !previousBest ) { 7309 return tile; 7310 } 7311 7312 if ( tile.visibility > previousBest.visibility ) { 7313 return tile; 7314 } else if ( tile.visibility == previousBest.visibility ) { 7315 if ( tile.distance < previousBest.distance ) { 7316 return tile; 7317 } 7318 } 7319 7320 return previousBest; 7321 }; 7322 7323 function finishLoadingImage( image, callback, successful, jobid ){ 7324 7325 image.onload = null; 7326 image.onabort = null; 7327 image.onerror = null; 7328 7329 if ( jobid ) { 7330 window.clearTimeout( jobid ); 7331 } 7332 window.setTimeout( function() { 7333 callback( image.src, successful ? image : null); 7334 }, 1 ); 7335 7336 }; 7337 7338 7339 function drawOverlays( viewport, overlays, container ){ 7340 var i, 7341 length = overlays.length; 7342 for ( i = 0; i < length; i++ ) { 7343 drawOverlay( viewport, overlays[ i ], container ); 7344 } 7345 }; 7346 7347 function drawOverlay( viewport, overlay, container ){ 7348 7349 overlay.position = viewport.pixelFromPoint( 7350 overlay.bounds.getTopLeft(), 7351 true 7352 ); 7353 overlay.size = viewport.deltaPixelsFromPoints( 7354 overlay.bounds.getSize(), 7355 true 7356 ); 7357 overlay.drawHTML( container ); 7358 }; 7359 7360 function drawTiles( drawer, lastDrawn ){ 7361 var i, 7362 tile; 7363 7364 for ( i = lastDrawn.length - 1; i >= 0; i-- ) { 7365 tile = lastDrawn[ i ]; 7366 7367 //TODO: get rid of this if by determining the tile draw method once up 7368 // front and defining the appropriate 'draw' function 7369 if ( USE_CANVAS ) { 7370 tile.drawCanvas( drawer.context ); 7371 } else { 7372 tile.drawHTML( drawer.canvas ); 7373 } 7374 7375 tile.beingDrawn = true; 7376 } 7377 }; 7378 7379 }( OpenSeadragon )); 7380 7381 (function( $ ){ 7382 7383 7384 /** 7385 * @class 7386 */ 7387 $.Viewport = function( options ) { 7388 7389 //backward compatibility for positional args while prefering more 7390 //idiomatic javascript options object as the only argument 7391 var args = arguments; 7392 if( args.length && args[ 0 ] instanceof $.Point ){ 7393 options = { 7394 containerSize: args[ 0 ], 7395 contentSize: args[ 1 ], 7396 config: args[ 2 ] 7397 }; 7398 } 7399 7400 //options.config and the general config argument are deprecated 7401 //in favor of the more direct specification of optional settings 7402 //being pass directly on the options object 7403 if ( options.config ){ 7404 $.extend( true, options, options.config ); 7405 delete options.config; 7406 } 7407 7408 $.extend( true, this, { 7409 7410 //required settings 7411 containerSize: null, 7412 contentSize: null, 7413 7414 //internal state properties 7415 zoomPoint: null, 7416 7417 //configurable options 7418 springStiffness: $.DEFAULT_SETTINGS.springStiffness, 7419 animationTime: $.DEFAULT_SETTINGS.animationTime, 7420 minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, 7421 maxZoomPixelRatio: $.DEFAULT_SETTINGS.maxZoomPixelRatio, 7422 visibilityRatio: $.DEFAULT_SETTINGS.visibilityRatio, 7423 wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, 7424 wrapVertical: $.DEFAULT_SETTINGS.wrapVertical 7425 7426 }, options ); 7427 7428 this.centerSpringX = new $.Spring({ 7429 initial: 0, 7430 springStiffness: this.springStiffness, 7431 animationTime: this.animationTime 7432 }); 7433 this.centerSpringY = new $.Spring({ 7434 initial: 0, 7435 springStiffness: this.springStiffness, 7436 animationTime: this.animationTime 7437 }); 7438 this.zoomSpring = new $.Spring({ 7439 initial: 1, 7440 springStiffness: this.springStiffness, 7441 animationTime: this.animationTime 7442 }); 7443 7444 this.resetContentSize( this.contentSize ); 7445 this.goHome( true ); 7446 //this.fitHorizontally( true ); 7447 this.update(); 7448 }; 7449 7450 $.Viewport.prototype = { 7451 7452 resetContentSize: function( contentSize ){ 7453 this.contentSize = contentSize; 7454 this.contentAspectX = this.contentSize.x / this.contentSize.y; 7455 this.contentAspectY = this.contentSize.y / this.contentSize.x; 7456 this.homeBounds = new $.Rect( 7457 0, 7458 0, 7459 1, 7460 this.contentAspectY 7461 ); 7462 this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectX ); 7463 this.fitHeightBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); 7464 }, 7465 7466 /** 7467 * @function 7468 */ 7469 getHomeZoom: function() { 7470 7471 var aspectFactor = Math.min( 7472 this.contentAspectX, 7473 this.contentAspectY 7474 ) / this.getAspectRatio(); 7475 7476 return ( aspectFactor >= 1 ) ? 7477 1 : 7478 aspectFactor; 7479 }, 7480 7481 /** 7482 * @function 7483 */ 7484 getMinZoom: function() { 7485 var homeZoom = this.getHomeZoom() 7486 zoom = this.minZoomImageRatio * homeZoom; 7487 7488 return Math.min( zoom, homeZoom ); 7489 }, 7490 7491 /** 7492 * @function 7493 */ 7494 getMaxZoom: function() { 7495 var zoom = 7496 this.contentSize.x * 7497 this.maxZoomPixelRatio / 7498 this.containerSize.x; 7499 return Math.max( zoom, this.getHomeZoom() ); 7500 }, 7501 7502 /** 7503 * @function 7504 */ 7505 getAspectRatio: function() { 7506 return this.containerSize.x / this.containerSize.y; 7507 }, 7508 7509 /** 7510 * @function 7511 */ 7512 getContainerSize: function() { 7513 return new $.Point( 7514 this.containerSize.x, 7515 this.containerSize.y 7516 ); 7517 }, 7518 7519 /** 7520 * @function 7521 */ 7522 getBounds: function( current ) { 7523 var center = this.getCenter( current ), 7524 width = 1.0 / this.getZoom( current ), 7525 height = width / this.getAspectRatio(); 7526 7527 return new $.Rect( 7528 center.x - ( width / 2.0 ), 7529 center.y - ( height / 2.0 ), 7530 width, 7531 height 7532 ); 7533 }, 7534 7535 /** 7536 * @function 7537 */ 7538 getCenter: function( current ) { 7539 var centerCurrent = new $.Point( 7540 this.centerSpringX.current.value, 7541 this.centerSpringY.current.value 7542 ), 7543 centerTarget = new $.Point( 7544 this.centerSpringX.target.value, 7545 this.centerSpringY.target.value 7546 ), 7547 oldZoomPixel, 7548 zoom, 7549 width, 7550 height, 7551 bounds, 7552 newZoomPixel, 7553 deltaZoomPixels, 7554 deltaZoomPoints; 7555 7556 if ( current ) { 7557 return centerCurrent; 7558 } else if ( !this.zoomPoint ) { 7559 return centerTarget; 7560 } 7561 7562 oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true); 7563 7564 zoom = this.getZoom(); 7565 width = 1.0 / zoom; 7566 height = width / this.getAspectRatio(); 7567 bounds = new $.Rect( 7568 centerCurrent.x - width / 2.0, 7569 centerCurrent.y - height / 2.0, 7570 width, 7571 height 7572 ); 7573 7574 newZoomPixel = this.zoomPoint.minus( 7575 bounds.getTopLeft() 7576 ).times( 7577 this.containerSize.x / bounds.width 7578 ); 7579 deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); 7580 deltaZoomPoints = deltaZoomPixels.divide( this.containerSize.x * zoom ); 7581 7582 return centerTarget.plus( deltaZoomPoints ); 7583 }, 7584 7585 /** 7586 * @function 7587 */ 7588 getZoom: function( current ) { 7589 if ( current ) { 7590 return this.zoomSpring.current.value; 7591 } else { 7592 return this.zoomSpring.target.value; 7593 } 7594 }, 7595 7596 7597 /** 7598 * @function 7599 */ 7600 applyConstraints: function( immediately ) { 7601 var actualZoom = this.getZoom(), 7602 constrainedZoom = Math.max( 7603 Math.min( actualZoom, this.getMaxZoom() ), 7604 this.getMinZoom() 7605 ), 7606 bounds, 7607 horizontalThreshold, 7608 verticalThreshold, 7609 left, 7610 right, 7611 top, 7612 bottom, 7613 dx = 0, 7614 dy = 0; 7615 7616 if ( actualZoom != constrainedZoom ) { 7617 this.zoomTo( constrainedZoom, this.zoomPoint, immediately ); 7618 } 7619 7620 bounds = this.getBounds(); 7621 7622 horizontalThreshold = this.visibilityRatio * bounds.width; 7623 verticalThreshold = this.visibilityRatio * bounds.height; 7624 7625 left = bounds.x + bounds.width; 7626 right = 1 - bounds.x; 7627 top = bounds.y + bounds.height; 7628 bottom = this.contentAspectY - bounds.y; 7629 7630 if ( this.wrapHorizontal ) { 7631 //do nothing 7632 } else if ( left < horizontalThreshold ) { 7633 dx = horizontalThreshold - left; 7634 } else if ( right < horizontalThreshold ) { 7635 dx = right - horizontalThreshold; 7636 } 7637 7638 if ( this.wrapVertical ) { 7639 //do nothing 7640 } else if ( top < verticalThreshold ) { 7641 dy = verticalThreshold - top; 7642 } else if ( bottom < verticalThreshold ) { 7643 dy = bottom - verticalThreshold; 7644 } 7645 7646 if ( dx || dy ) { 7647 bounds.x += dx; 7648 bounds.y += dy; 7649 this.fitBounds( bounds, immediately ); 7650 } 7651 }, 7652 7653 /** 7654 * @function 7655 * @param {Boolean} immediately 7656 */ 7657 ensureVisible: function( immediately ) { 7658 this.applyConstraints( immediately ); 7659 }, 7660 7661 /** 7662 * @function 7663 * @param {OpenSeadragon.Rect} bounds 7664 * @param {Boolean} immediately 7665 */ 7666 fitBounds: function( bounds, immediately ) { 7667 var aspect = this.getAspectRatio(), 7668 center = bounds.getCenter(), 7669 newBounds = new $.Rect( 7670 bounds.x, 7671 bounds.y, 7672 bounds.width, 7673 bounds.height 7674 ), 7675 oldBounds, 7676 oldZoom, 7677 newZoom, 7678 referencePoint; 7679 7680 if ( newBounds.getAspectRatio() >= aspect ) { 7681 newBounds.height = bounds.width / aspect; 7682 newBounds.y = center.y - newBounds.height / 2; 7683 } else { 7684 newBounds.width = bounds.height * aspect; 7685 newBounds.x = center.x - newBounds.width / 2; 7686 } 7687 7688 this.panTo( this.getCenter( true ), true ); 7689 this.zoomTo( this.getZoom( true ), null, true ); 7690 7691 oldBounds = this.getBounds(); 7692 oldZoom = this.getZoom(); 7693 newZoom = 1.0 / newBounds.width; 7694 if ( newZoom == oldZoom || newBounds.width == oldBounds.width ) { 7695 this.panTo( center, immediately ); 7696 return; 7697 } 7698 7699 referencePoint = oldBounds.getTopLeft().times( 7700 this.containerSize.x / oldBounds.width 7701 ).minus( 7702 newBounds.getTopLeft().times( 7703 this.containerSize.x / newBounds.width 7704 ) 7705 ).divide( 7706 this.containerSize.x / oldBounds.width - 7707 this.containerSize.x / newBounds.width 7708 ); 7709 7710 this.zoomTo( newZoom, referencePoint, immediately ); 7711 }, 7712 7713 /** 7714 * @function 7715 * @param {Boolean} immediately 7716 */ 7717 goHome: function( immediately ) { 7718 return this.fitVertically( immediately ); 7719 }, 7720 7721 /** 7722 * @function 7723 * @param {Boolean} immediately 7724 */ 7725 fitVertically: function( immediately ) { 7726 var center = this.getCenter(); 7727 7728 if ( this.wrapHorizontal ) { 7729 center.x = ( 1 + ( center.x % 1 ) ) % 1; 7730 this.centerSpringX.resetTo( center.x ); 7731 this.centerSpringX.update(); 7732 } 7733 7734 if ( this.wrapVertical ) { 7735 center.y = ( 7736 this.contentAspectY + ( center.y % this.contentAspectY ) 7737 ) % this.contentAspectY; 7738 this.centerSpringY.resetTo( center.y ); 7739 this.centerSpringY.update(); 7740 } 7741 7742 this.fitBounds( this.homeBounds, immediately ); 7743 }, 7744 7745 /** 7746 * @function 7747 * @param {Boolean} immediately 7748 */ 7749 fitHorizontally: function( immediately ) { 7750 var center = this.getCenter(); 7751 7752 if ( this.wrapHorizontal ) { 7753 center.x = ( 7754 this.contentAspectX + ( center.x % this.contentAspectX ) 7755 ) % this.contentAspectX; 7756 this.centerSpringX.resetTo( center.x ); 7757 this.centerSpringX.update(); 7758 } 7759 7760 if ( this.wrapVertical ) { 7761 center.y = ( 1 + ( center.y % 1 ) ) % 1; 7762 this.centerSpringY.resetTo( center.y ); 7763 this.centerSpringY.update(); 7764 } 7765 7766 this.fitBounds( this.fitWidthBounds, immediately ); 7767 }, 7768 7769 7770 /** 7771 * @function 7772 * @param {OpenSeadragon.Point} delta 7773 * @param {Boolean} immediately 7774 */ 7775 panBy: function( delta, immediately ) { 7776 var center = new $.Point( 7777 this.centerSpringX.target.value, 7778 this.centerSpringY.target.value 7779 ); 7780 this.panTo( center.plus( delta ), immediately ); 7781 }, 7782 7783 /** 7784 * @function 7785 * @param {OpenSeadragon.Point} center 7786 * @param {Boolean} immediately 7787 */ 7788 panTo: function( center, immediately ) { 7789 if ( immediately ) { 7790 this.centerSpringX.resetTo( center.x ); 7791 this.centerSpringY.resetTo( center.y ); 7792 } else { 7793 this.centerSpringX.springTo( center.x ); 7794 this.centerSpringY.springTo( center.y ); 7795 } 7796 }, 7797 7798 /** 7799 * @function 7800 */ 7801 zoomBy: function( factor, refPoint, immediately ) { 7802 this.zoomTo( this.zoomSpring.target.value * factor, refPoint, immediately ); 7803 }, 7804 7805 /** 7806 * @function 7807 */ 7808 zoomTo: function( zoom, refPoint, immediately ) { 7809 7810 if ( immediately ) { 7811 this.zoomSpring.resetTo( zoom ); 7812 } else { 7813 this.zoomSpring.springTo( zoom ); 7814 } 7815 7816 this.zoomPoint = refPoint instanceof $.Point ? 7817 refPoint : 7818 null; 7819 }, 7820 7821 /** 7822 * @function 7823 */ 7824 resize: function( newContainerSize, maintain ) { 7825 var oldBounds = this.getBounds(), 7826 newBounds = oldBounds, 7827 widthDeltaFactor = newContainerSize.x / this.containerSize.x; 7828 7829 this.containerSize = new $.Point( 7830 newContainerSize.x, 7831 newContainerSize.y 7832 ); 7833 7834 if (maintain) { 7835 newBounds.width = oldBounds.width * widthDeltaFactor; 7836 newBounds.height = newBounds.width / this.getAspectRatio(); 7837 } 7838 7839 this.fitBounds( newBounds, true ); 7840 }, 7841 7842 /** 7843 * @function 7844 */ 7845 update: function() { 7846 var oldCenterX = this.centerSpringX.current.value, 7847 oldCenterY = this.centerSpringY.current.value, 7848 oldZoom = this.zoomSpring.current.value, 7849 oldZoomPixel, 7850 newZoomPixel, 7851 deltaZoomPixels, 7852 deltaZoomPoints; 7853 7854 if (this.zoomPoint) { 7855 oldZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); 7856 } 7857 7858 this.zoomSpring.update(); 7859 7860 if (this.zoomPoint && this.zoomSpring.current.value != oldZoom) { 7861 newZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); 7862 deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); 7863 deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true ); 7864 7865 this.centerSpringX.shiftBy( deltaZoomPoints.x ); 7866 this.centerSpringY.shiftBy( deltaZoomPoints.y ); 7867 } else { 7868 this.zoomPoint = null; 7869 } 7870 7871 this.centerSpringX.update(); 7872 this.centerSpringY.update(); 7873 7874 return this.centerSpringX.current.value != oldCenterX || 7875 this.centerSpringY.current.value != oldCenterY || 7876 this.zoomSpring.current.value != oldZoom; 7877 }, 7878 7879 7880 /** 7881 * @function 7882 */ 7883 deltaPixelsFromPoints: function( deltaPoints, current ) { 7884 return deltaPoints.times( 7885 this.containerSize.x * this.getZoom( current ) 7886 ); 7887 }, 7888 7889 /** 7890 * @function 7891 */ 7892 deltaPointsFromPixels: function( deltaPixels, current ) { 7893 return deltaPixels.divide( 7894 this.containerSize.x * this.getZoom( current ) 7895 ); 7896 }, 7897 7898 /** 7899 * @function 7900 */ 7901 pixelFromPoint: function( point, current ) { 7902 var bounds = this.getBounds( current ); 7903 return point.minus( 7904 bounds.getTopLeft() 7905 ).times( 7906 this.containerSize.x / bounds.width 7907 ); 7908 }, 7909 7910 /** 7911 * @function 7912 */ 7913 pointFromPixel: function( pixel, current ) { 7914 var bounds = this.getBounds( current ); 7915 return pixel.divide( 7916 this.containerSize.x / bounds.width 7917 ).plus( 7918 bounds.getTopLeft() 7919 ); 7920 } 7921 }; 7922 7923 }( OpenSeadragon )); 7924