/src/PanoJS.js
JavaScript | 924 lines | 627 code | 122 blank | 175 comment | 153 complexity | 4911dd31d1d9f8c352c877899154ea34 MD5 | raw file
1/** 2 * Panoramic JavaScript Image Viewer (PanoJS) 1.0.3-SVN 3 * 4 * Generates a draggable and zoomable viewer for images that would 5 * be otherwise too large for a browser window. Examples would include 6 * maps or high resolution document scans. 7 * 8 * Images must be precut into tiles, such as by the accompanying tilemaker.py 9 * python library. 10 * 11 * <div class="viewer"> 12 * <div class="well"><!-- --></div> 13 * <div class="surface"><!-- --></div> 14 * <div class="controls"> 15 * <a href="#" class="zoomIn">+</a> 16 * <a href="#" class="zoomOut">-</a> 17 * </div> 18 * </div> 19 * 20 * The "well" node is where generated IMG elements are appended. It 21 * should have the CSS rule "overflow: hidden", to occlude image tiles 22 * that have scrolled out of view. 23 * 24 * The "surface" node is the transparent mouse-responsive layer of the 25 * image viewer, and should match the well in size. 26 * 27 * var viewerBean = new PanoJS(element, 'tiles', 256, 3, 1); 28 * 29 * To disable the image toolbar in IE, be sure to add the following: 30 * <meta http-equiv="imagetoolbar" content="no" /> 31 * 32 * Copyright (c) 2005 Michal Migurski <mike-gsv@teczno.com> 33 * Dan Allen <dan.allen@mojavelinux.com> 34 * 35 * Redistribution and use in source form, with or without modification, 36 * are permitted provided that the following conditions are met: 37 * 1. Redistributions of source code must retain the above copyright 38 * notice, this list of conditions and the following disclaimer. 39 * 2. The name of the author may not be used to endorse or promote products 40 * derived from this software without specific prior written permission. 41 * 42 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 43 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 44 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 45 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 46 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 47 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 48 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 49 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 50 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 51 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 52 * 53 * @author Michal Migurski <mike-gsv@teczno.com> 54 * @author Dan Allen <dan.allen@mojavelinux.com> 55 * 56 * NOTE: if artifacts are appearing, then positions include half-pixels 57 * TODO: additional jsdoc and package jsmin 58 * TODO: Tile could be an object 59 */ 60function PanoJS(viewer, options) { 61 62 // listeners that are notified on a move (pan) event 63 this.viewerMovedListeners = []; 64 // listeners that are notified on a zoom event 65 this.viewerZoomedListeners = []; 66 67 if (typeof viewer == 'string') { 68 this.viewer = document.getElementById(viewer); 69 } 70 else { 71 this.viewer = viewer; 72 } 73 74 if (typeof options == 'undefined') { 75 options = {}; 76 } 77 78 if (typeof options.tileUrlProvider != 'undefined' && 79 PanoJS.isInstance(options.tileUrlProvider, PanoJS.TileUrlProvider)) { 80 this.tileUrlProvider = options.tileUrlProvider; 81 } 82 else { 83 this.tileUrlProvider = new PanoJS.TileUrlProvider( 84 options.tileBaseUri ? options.tileBaseUri : PanoJS.TILE_BASE_URI, 85 options.tilePrefix ? options.tilePrefix : PanoJS.TILE_PREFIX, 86 options.tileExtension ? options.tileExtension : PanoJS.TILE_EXTENSION 87 ); 88 } 89 90 this.tileSize = (options.tileSize ? options.tileSize : PanoJS.TILE_SIZE); 91 92 // assign and do some validation on the zoom levels to ensure sanity 93 this.zoomLevel = (typeof options.initialZoom == 'undefined' ? -1 : parseInt(options.initialZoom)); 94 this.maxZoomLevel = (typeof options.maxZoom == 'undefined' ? 0 : Math.abs(parseInt(options.maxZoom))); 95 if (this.zoomLevel > this.maxZoomLevel) { 96 this.zoomLevel = this.maxZoomLevel; 97 } 98 99 this.initialPan = (options.initialPan ? options.initialPan : PanoJS.INITIAL_PAN); 100 101 this.initialized = false; 102 this.surface = null; 103 this.well = null; 104 this.width = 0; 105 this.height = 0; 106 this.top = 0; 107 this.left = 0; 108 this.x = 0; 109 this.y = 0; 110 this.border = -1; 111 this.mark = { 'x' : 0, 'y' : 0 }; 112 this.pressed = false; 113 this.tiles = []; 114 this.cache = {}; 115 var blankTile = options.blankTile ? options.blankTile : PanoJS.BLANK_TILE_IMAGE; 116 var loadingTile = options.loadingTile ? options.loadingTile : PanoJS.LOADING_TILE_IMAGE; 117 this.cache['blank'] = new Image(); 118 this.cache['blank'].src = blankTile; 119 if (blankTile != loadingTile) { 120 this.cache['loading'] = new Image(); 121 this.cache['loading'].src = loadingTile; 122 } 123 else { 124 this.cache['loading'] = this.cache['blank']; 125 } 126 127 // employed to throttle the number of redraws that 128 // happen while the mouse is moving 129 this.moveCount = 0; 130 this.slideMonitor = 0; 131 this.slideAcceleration = 0; 132 133 // add to viewer registry 134 PanoJS.VIEWERS[PanoJS.VIEWERS.length] = this; 135} 136 137// project specific variables 138PanoJS.PROJECT_NAME = 'PanoJS'; 139PanoJS.PROJECT_VERSION = '1.0.0'; 140PanoJS.REVISION_FLAG = ''; 141 142// CSS definition settings 143PanoJS.SURFACE_STYLE_CLASS = 'surface'; 144PanoJS.WELL_STYLE_CLASS = 'well'; 145PanoJS.CONTROLS_STYLE_CLASS = 'controls' 146PanoJS.TILE_STYLE_CLASS = 'tile'; 147 148// language settings 149PanoJS.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.'; 150PanoJS.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.'; 151 152// defaults if not provided as constructor options 153PanoJS.TILE_BASE_URI = 'tiles'; 154PanoJS.TILE_PREFIX = 'tile-'; 155PanoJS.TILE_EXTENSION = 'jpg'; 156PanoJS.TILE_SIZE = 256; 157PanoJS.BLANK_TILE_IMAGE = 'blank.gif'; 158PanoJS.LOADING_TILE_IMAGE = 'blank.gif'; 159PanoJS.INITIAL_PAN = { 'x' : .5, 'y' : .5 }; 160PanoJS.USE_LOADER_IMAGE = true; 161PanoJS.USE_SLIDE = true; 162PanoJS.USE_KEYBOARD = true; 163 164// performance tuning variables 165PanoJS.MOVE_THROTTLE = 3; 166PanoJS.SLIDE_DELAY = 40; 167PanoJS.SLIDE_ACCELERATION_FACTOR = 5; 168 169// the following are calculated settings 170PanoJS.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true); 171PanoJS.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(grab.cur)' : '-moz-grab')); 172PanoJS.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(grabbing.cur)' : '-moz-grabbing')); 173 174// registry of all known viewers 175PanoJS.VIEWERS = []; 176 177// utility functions 178PanoJS.isInstance = function(object, clazz) { 179 // FIXME: can this just be replaced with instanceof operator? It has been reported that __proto__ is specific to Netscape 180 while (object != null) { 181 if (object == clazz.prototype) { 182 return true; 183 } 184 185 object = object.__proto__; 186 } 187 188 return false; 189} 190 191PanoJS.prototype = { 192 193 /** 194 * Resize the viewer to fit snug inside the browser window (or frame), 195 * spacing it from the edges by the specified border. 196 * 197 * This method should be called prior to init() 198 * FIXME: option to hide viewer to prevent scrollbar interference 199 */ 200 fitToWindow : function(border) { 201 if (typeof border != 'number' || border < 0) { 202 border = 0; 203 } 204 205 this.border = border; 206 var calcWidth = 0; 207 var calcHeight = 0; 208 if (window.innerWidth) { 209 calcWidth = window.innerWidth; 210 calcHeight = window.innerHeight; 211 } 212 else { 213 calcWidth = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth); 214 calcHeight = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight); 215 } 216 217 calcWidth = Math.max(calcWidth - 2 * border, 0); 218 calcHeight = Math.max(calcHeight - 2 * border, 0); 219 if (calcWidth % 2) { 220 calcWidth--; 221 } 222 223 if (calcHeight % 2) { 224 calcHeight--; 225 } 226 227 this.width = calcWidth; 228 this.height = calcHeight; 229 this.viewer.style.width = this.width + 'px'; 230 this.viewer.style.height = this.height + 'px'; 231 this.viewer.style.top = border + 'px'; 232 this.viewer.style.left = border + 'px'; 233 }, 234 235 init : function() { 236 if (document.attachEvent) { 237 document.body.ondragstart = function() { return false; } 238 } 239 240 if (this.width == 0 && this.height == 0) { 241 this.width = this.viewer.offsetWidth; 242 this.height = this.viewer.offsetHeight; 243 } 244 245 var fullSize = this.tileSize; 246 // explicit set of zoom level 247 if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) { 248 fullSize = this.tileSize * Math.pow(2, this.zoomLevel); 249 } 250 // calculate the zoom level based on what fits best in window 251 else { 252 this.zoomLevel = -1; 253 fullSize = this.tileSize / 2; 254 do { 255 this.zoomLevel += 1; 256 fullSize *= 2; 257 } while (fullSize < Math.max(this.width, this.height)); 258 // take into account picture smaller than window size 259 if (this.zoomLevel > this.maxZoomLevel) { 260 var diff = this.zoomLevel - this.maxZoomLevel; 261 this.zoomLevel = this.maxZoomLevel; 262 fullSize /= Math.pow(2, diff); 263 } 264 } 265 266 // move top level up and to the left so that the image is centered 267 this.x = Math.floor((fullSize - this.width) * -this.initialPan.x); 268 this.y = Math.floor((fullSize - this.height) * -this.initialPan.y); 269 270 // offset of viewer in the window 271 for (var node = this.viewer; node; node = node.offsetParent) { 272 this.top += node.offsetTop; 273 this.left += node.offsetLeft; 274 } 275 276 for (var child = this.viewer.firstChild; child; child = child.nextSibling) { 277 if (child.className == PanoJS.SURFACE_STYLE_CLASS) { 278 this.surface = child; 279 child.backingBean = this; 280 } 281 else if (child.className == PanoJS.WELL_STYLE_CLASS) { 282 this.well = child; 283 child.backingBean = this; 284 } 285 else if (child.className == PanoJS.CONTROLS_STYLE_CLASS) { 286 for (var control = child.firstChild; control; control = control.nextSibling) { 287 if (control.className) { 288 control.onclick = PanoJS[control.className + 'Handler']; 289 } 290 } 291 } 292 } 293 294 this.viewer.backingBean = this; 295 this.surface.style.cursor = PanoJS.GRAB_MOUSE_CURSOR; 296 this.prepareTiles(); 297 this.initialized = true; 298 }, 299 300 prepareTiles : function() { 301 var rows = Math.ceil(this.height / this.tileSize) + 1; 302 var cols = Math.ceil(this.width / this.tileSize) + 1; 303 304 for (var c = 0; c < cols; c++) { 305 var tileCol = []; 306 307 for (var r = 0; r < rows; r++) { 308 /** 309 * element is the DOM element associated with this tile 310 * posx/posy are the pixel offsets of the tile 311 * xIndex/yIndex are the index numbers of the tile segment 312 * qx/qy represents the quadrant location of the tile 313 */ 314 var tile = { 315 'element' : null, 316 'posx' : 0, 317 'posy' : 0, 318 'xIndex' : c, 319 'yIndex' : r, 320 'qx' : c, 321 'qy' : r 322 }; 323 324 tileCol.push(tile); 325 } 326 327 this.tiles.push(tileCol); 328 } 329 330 this.surface.onmousedown = PanoJS.mousePressedHandler; 331 this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler; 332 this.surface.ondblclick = PanoJS.doubleClickHandler; 333 if (PanoJS.USE_KEYBOARD) { 334 window.onkeypress = PanoJS.keyboardMoveHandler; 335 window.onkeydown = PanoJS.keyboardZoomHandler; 336 } 337 338 this.positionTiles(); 339 }, 340 341 /** 342 * Position the tiles based on the x, y coordinates of the 343 * viewer, taking into account the motion offsets, which 344 * are calculated by a motion event handler. 345 */ 346 positionTiles : function(motion, reset) { 347 // default to no motion, just setup tiles 348 if (typeof motion == 'undefined') { 349 motion = { 'x' : 0, 'y' : 0 }; 350 } 351 352 for (var c = 0; c < this.tiles.length; c++) { 353 for (var r = 0; r < this.tiles[c].length; r++) { 354 var tile = this.tiles[c][r]; 355 356 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 357 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 358 359 var visible = true; 360 361 if (tile.posx > this.width) { 362 // tile moved out of view to the right 363 // consider the tile coming into view from the left 364 do { 365 tile.xIndex -= this.tiles.length; 366 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 367 } while (tile.posx > this.width); 368 369 if (tile.posx + this.tileSize < 0) { 370 visible = false; 371 } 372 373 } else { 374 // tile may have moved out of view from the left 375 // if so, consider the tile coming into view from the right 376 while (tile.posx < -this.tileSize) { 377 tile.xIndex += this.tiles.length; 378 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 379 } 380 381 if (tile.posx > this.width) { 382 visible = false; 383 } 384 } 385 386 if (tile.posy > this.height) { 387 // tile moved out of view to the bottom 388 // consider the tile coming into view from the top 389 do { 390 tile.yIndex -= this.tiles[c].length; 391 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 392 } while (tile.posy > this.height); 393 394 if (tile.posy + this.tileSize < 0) { 395 visible = false; 396 } 397 398 } else { 399 // tile may have moved out of view to the top 400 // if so, consider the tile coming into view from the bottom 401 while (tile.posy < -this.tileSize) { 402 tile.yIndex += this.tiles[c].length; 403 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 404 } 405 406 if (tile.posy > this.height) { 407 visible = false; 408 } 409 } 410 411 // initialize the image object for this quadrant 412 if (!this.initialized) { 413 this.assignTileImage(tile, true); 414 tile.element.style.top = tile.posy + 'px'; 415 tile.element.style.left = tile.posx + 'px'; 416 } 417 418 // display the image if visible 419 if (visible) { 420 this.assignTileImage(tile); 421 } 422 423 // seems to need this no matter what 424 tile.element.style.top = tile.posy + 'px'; 425 tile.element.style.left = tile.posx + 'px'; 426 } 427 } 428 429 // reset the x, y coordinates of the viewer according to motion 430 if (reset) { 431 this.x += motion.x; 432 this.y += motion.y; 433 } 434 }, 435 436 /** 437 * Determine the source image of the specified tile based 438 * on the zoom level and position of the tile. If forceBlankImage 439 * is specified, the source should be automatically set to the 440 * null tile image. This method will also setup an onload 441 * routine, delaying the appearance of the tile until it is fully 442 * loaded, if configured to do so. 443 */ 444 assignTileImage : function(tile, forceBlankImage) { 445 var tileImgId, src; 446 var useBlankImage = (forceBlankImage ? true : false); 447 448 // check if image has been scrolled too far in any particular direction 449 // and if so, use the null tile image 450 if (!useBlankImage) { 451 var left = tile.xIndex < 0; 452 var high = tile.yIndex < 0; 453 var right = tile.xIndex >= Math.pow(2, this.zoomLevel); 454 var low = tile.yIndex >= Math.pow(2, this.zoomLevel); 455 if (high || left || low || right) { 456 useBlankImage = true; 457 } 458 } 459 460 if (useBlankImage) { 461 tileImgId = 'blank:' + tile.qx + ':' + tile.qy; 462 src = this.cache['blank'].src; 463 } 464 else { 465 tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel); 466 } 467 468 // only remove tile if identity is changing 469 if (tile.element != null && 470 tile.element.parentNode != null && 471 tile.element.relativeSrc != src) { 472 this.well.removeChild(tile.element); 473 } 474 475 var tileImg = this.cache[tileImgId]; 476 // create cache if not exist 477 if (tileImg == null) { 478 tileImg = this.cache[tileImgId] = this.createPrototype(src); 479 } 480 481 if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) { 482 tileImg.onload = function() {}; 483 if (tileImg.image) { 484 tileImg.image.onload = function() {}; 485 } 486 487 if (tileImg.parentNode == null) { 488 tile.element = this.well.appendChild(tileImg); 489 } 490 } 491 else { 492 var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy; 493 var loadingImg = this.cache[loadingImgId]; 494 if (loadingImg == null) { 495 loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src); 496 } 497 498 loadingImg.targetSrc = tileImgId; 499 500 var well = this.well; 501 tile.element = well.appendChild(loadingImg); 502 tileImg.onload = function() { 503 // make sure our destination is still present 504 if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) { 505 tileImg.style.top = loadingImg.style.top; 506 tileImg.style.left = loadingImg.style.left; 507 well.replaceChild(tileImg, loadingImg); 508 tile.element = tileImg; 509 } 510 511 tileImg.onload = function() {}; 512 return false; 513 } 514 515 // konqueror only recognizes the onload event on an Image 516 // javascript object, so we must handle that case here 517 if (!PanoJS.DOM_ONLOAD) { 518 tileImg.image = new Image(); 519 tileImg.image.onload = tileImg.onload; 520 tileImg.image.src = tileImg.src; 521 } 522 } 523 }, 524 525 createPrototype : function(src) { 526 var img = document.createElement('img'); 527 img.src = src; 528 img.relativeSrc = src; 529 img.className = PanoJS.TILE_STYLE_CLASS; 530 img.style.width = this.tileSize + 'px'; 531 img.style.height = this.tileSize + 'px'; 532 return img; 533 }, 534 535 addViewerMovedListener : function(listener) { 536 this.viewerMovedListeners.push(listener); 537 }, 538 539 addViewerZoomedListener : function(listener) { 540 this.viewerZoomedListeners.push(listener); 541 }, 542 543 /** 544 * Notify listeners of a zoom event on the viewer. 545 */ 546 notifyViewerZoomed : function() { 547 var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1); 548 for (var i = 0; i < this.viewerZoomedListeners.length; i++) { 549 this.viewerZoomedListeners[i].viewerZoomed( 550 new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage) 551 ); 552 } 553 }, 554 555 /** 556 * Notify listeners of a move event on the viewer. 557 */ 558 notifyViewerMoved : function(coords) { 559 if (typeof coords == 'undefined') { 560 coords = { 'x' : 0, 'y' : 0 }; 561 } 562 563 for (var i = 0; i < this.viewerMovedListeners.length; i++) { 564 this.viewerMovedListeners[i].viewerMoved( 565 new PanoJS.MoveEvent( 566 this.x + (coords.x - this.mark.x), 567 this.y + (coords.y - this.mark.y) 568 ) 569 ); 570 } 571 }, 572 573 zoom : function(direction) { 574 // ensure we are not zooming out of range 575 if (this.zoomLevel + direction < 0) { 576 if (PanoJS.MSG_BEYOND_MIN_ZOOM) { 577 alert(PanoJS.MSG_BEYOND_MIN_ZOOM); 578 } 579 return; 580 } 581 else if (this.zoomLevel + direction > this.maxZoomLevel) { 582 if (PanoJS.MSG_BEYOND_MAX_ZOOM) { 583 alert(PanoJS.MSG_BEYOND_MAX_ZOOM); 584 } 585 return; 586 } 587 588 this.blank(); 589 590 var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) }; 591 592 var before = { 593 'x' : (coords.x - this.x), 594 'y' : (coords.y - this.y) 595 }; 596 597 var after = { 598 'x' : Math.floor(before.x * Math.pow(2, direction)), 599 'y' : Math.floor(before.y * Math.pow(2, direction)) 600 }; 601 602 this.x = coords.x - after.x; 603 this.y = coords.y - after.y; 604 this.zoomLevel += direction; 605 this.positionTiles(); 606 607 this.notifyViewerZoomed(); 608 }, 609 610 /** 611 * Clear all the tiles from the well for a complete reinitialization of the 612 * viewer. At this point the viewer is not considered to be initialized. 613 */ 614 clear : function() { 615 this.blank(); 616 this.initialized = false; 617 this.tiles = []; 618 }, 619 620 /** 621 * Remove all tiles from the well, which effectively "hides" 622 * them for a repaint. 623 */ 624 blank : function() { 625 for (imgId in this.cache) { 626 var img = this.cache[imgId]; 627 img.onload = function() {}; 628 if (img.image) { 629 img.image.onload = function() {}; 630 } 631 632 if (img.parentNode != null) { 633 this.well.removeChild(img); 634 } 635 } 636 }, 637 638 /** 639 * Method specifically for handling a mouse move event. A direct 640 * movement of the viewer can be achieved by calling positionTiles() directly. 641 */ 642 moveViewer : function(coords) { 643 this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) }); 644 this.notifyViewerMoved(coords); 645 }, 646 647 /** 648 * Make the specified coords the new center of the image placement. 649 * This method is typically triggered as the result of a double-click 650 * event. The calculation considers the distance between the center 651 * of the viewable area and the specified (viewer-relative) coordinates. 652 * If absolute is specified, treat the point as relative to the entire 653 * image, rather than only the viewable portion. 654 */ 655 recenter : function(coords, absolute) { 656 if (absolute) { 657 coords.x += this.x; 658 coords.y += this.y; 659 } 660 661 var motion = { 662 'x' : Math.floor((this.width / 2) - coords.x), 663 'y' : Math.floor((this.height / 2) - coords.y) 664 }; 665 666 if (motion.x == 0 && motion.y == 0) { 667 return; 668 } 669 670 if (PanoJS.USE_SLIDE) { 671 var target = motion; 672 var x, y; 673 // handle special case of vertical movement 674 if (target.x == 0) { 675 x = 0; 676 y = this.slideAcceleration; 677 } 678 else { 679 var slope = Math.abs(target.y / target.x); 680 x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5)); 681 y = Math.round(slope * x); 682 } 683 684 motion = { 685 'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1), 686 'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1) 687 } 688 } 689 690 this.positionTiles(motion, true); 691 this.notifyViewerMoved(); 692 693 if (!PanoJS.USE_SLIDE) { 694 return; 695 } 696 697 var newcoords = { 698 'x' : coords.x + motion.x, 699 'y' : coords.y + motion.y 700 }; 701 702 var self = this; 703 // TODO: use an exponential growth rather than linear (should also depend on how far we are going) 704 // FIXME: this could be optimized by calling positionTiles directly perhaps 705 this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR; 706 this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY ); 707 }, 708 709 resize : function() { 710 // IE fires a premature resize event 711 if (!this.initialized) { 712 return; 713 } 714 715 var newWidth = this.viewer.offsetWidth; 716 var newHeight = this.viewer.offsetHeight; 717 718 this.viewer.style.display = 'none'; 719 this.clear(); 720 721 var before = { 722 'x' : Math.floor(this.width / 2), 723 'y' : Math.floor(this.height / 2) 724 }; 725 726 if (this.border >= 0) { 727 this.fitToWindow(this.border); 728 } 729 else { 730 this.width = newWidth; 731 this.height = newHeight; 732 } 733 734 this.prepareTiles(); 735 736 var after = { 737 'x' : Math.floor(this.width / 2), 738 'y' : Math.floor(this.height / 2) 739 }; 740 741 if (this.border >= 0) { 742 this.x += (after.x - before.x); 743 this.y += (after.y - before.y); 744 } 745 this.positionTiles(); 746 this.viewer.style.display = ''; 747 this.initialized = true; 748 this.notifyViewerMoved(); 749 }, 750 751 /** 752 * Resolve the coordinates from this mouse event by subtracting the 753 * offset of the viewer in the browser window (or frame). This does 754 * take into account the scroll offset of the page. 755 */ 756 resolveCoordinates : function(e) { 757 return { 758 'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left, 759 'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top 760 } 761 }, 762 763 press : function(coords) { 764 this.activate(true); 765 this.mark = coords; 766 }, 767 768 release : function(coords) { 769 this.activate(false); 770 var motion = { 771 'x' : (coords.x - this.mark.x), 772 'y' : (coords.y - this.mark.y) 773 }; 774 775 this.x += motion.x; 776 this.y += motion.y; 777 this.mark = { 'x' : 0, 'y' : 0 }; 778 }, 779 780 /** 781 * Activate the viewer into motion depending on whether the mouse is pressed or 782 * not pressed. This method localizes the changes that must be made to the 783 * layers. 784 */ 785 activate : function(pressed) { 786 this.pressed = pressed; 787 this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR); 788 this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function() {}); 789 }, 790 791 /** 792 * Check whether the specified point exceeds the boundaries of 793 * the viewer's primary image. 794 */ 795 pointExceedsBoundaries : function(coords) { 796 return (coords.x < this.x || 797 coords.y < this.y || 798 coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) || 799 coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y)); 800 }, 801 802 // QUESTION: where is the best place for this method to be invoked? 803 resetSlideMotion : function() { 804 // QUESTION: should this be > 0 ? 805 if (this.slideMonitor != 0) { 806 clearTimeout(this.slideMonitor); 807 this.slideMonitor = 0; 808 } 809 810 this.slideAcceleration = 0; 811 } 812}; 813 814PanoJS.TileUrlProvider = function(baseUri, prefix, extension) { 815 this.baseUri = baseUri; 816 this.prefix = prefix; 817 this.extension = extension; 818} 819 820PanoJS.TileUrlProvider.prototype = { 821 assembleUrl: function(xIndex, yIndex, zoom) { 822 return this.baseUri + '/' + 823 this.prefix + zoom + '-' + xIndex + '-' + yIndex + '.' + this.extension + 824 (PanoJS.REVISION_FLAG ? '?r=' + PanoJS.REVISION_FLAG : ''); 825 } 826} 827 828PanoJS.mousePressedHandler = function(e) { 829 e = e ? e : window.event; 830 // only grab on left-click 831 if (e.button < 2) { 832 var self = this.backingBean; 833 var coords = self.resolveCoordinates(e); 834 if (self.pointExceedsBoundaries(coords)) { 835 e.cancelBubble = true; 836 } 837 else { 838 self.press(coords); 839 } 840 } 841 842 // NOTE: MANDATORY! must return false so event does not propagate to well! 843 return false; 844}; 845 846PanoJS.mouseReleasedHandler = function(e) { 847 e = e ? e : window.event; 848 var self = this.backingBean; 849 if (self.pressed) { 850 // OPTION: could decide to move viewer only on release, right here 851 self.release(self.resolveCoordinates(e)); 852 } 853}; 854 855PanoJS.mouseMovedHandler = function(e) { 856 e = e ? e : window.event; 857 var self = this.backingBean; 858 self.moveCount++; 859 if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) { 860 self.moveViewer(self.resolveCoordinates(e)); 861 } 862}; 863 864PanoJS.zoomInHandler = function(e) { 865 e = e ? e : window.event; 866 var self = this.parentNode.parentNode.backingBean; 867 self.zoom(1); 868 return false; 869}; 870 871PanoJS.zoomOutHandler = function(e) { 872 e = e ? e : window.event; 873 var self = this.parentNode.parentNode.backingBean; 874 self.zoom(-1); 875 return false; 876}; 877 878PanoJS.doubleClickHandler = function(e) { 879 e = e ? e : window.event; 880 var self = this.backingBean; 881 coords = self.resolveCoordinates(e); 882 if (!self.pointExceedsBoundaries(coords)) { 883 self.resetSlideMotion(); 884 self.recenter(coords); 885 } 886}; 887 888PanoJS.keyboardMoveHandler = function(e) { 889 e = e ? e : window.event; 890 for (var i = 0; i < PanoJS.VIEWERS.length; i++) { 891 var viewer = PanoJS.VIEWERS[i]; 892 if (e.keyCode == 38) 893 viewer.positionTiles({'x': 0,'y': -PanoJS.MOVE_THROTTLE}, true); 894 if (e.keyCode == 39) 895 viewer.positionTiles({'x': -PanoJS.MOVE_THROTTLE,'y': 0}, true); 896 if (e.keyCode == 40) 897 viewer.positionTiles({'x': 0,'y': PanoJS.MOVE_THROTTLE}, true); 898 if (e.keyCode == 37) 899 viewer.positionTiles({'x': PanoJS.MOVE_THROTTLE,'y': 0}, true); 900 } 901} 902 903PanoJS.keyboardZoomHandler = function(e) { 904 e = e ? e : window.event; 905 for (var i = 0; i < PanoJS.VIEWERS.length; i++) { 906 var viewer = PanoJS.VIEWERS[i]; 907 if (e.keyCode == 109) 908 viewer.zoom(-1); 909 if (e.keyCode == 107) 910 viewer.zoom(1); 911 } 912} 913 914PanoJS.MoveEvent = function(x, y) { 915 this.x = x; 916 this.y = y; 917}; 918 919PanoJS.ZoomEvent = function(x, y, level, percentage) { 920 this.x = x; 921 this.y = y; 922 this.percentage = percentage; 923 this.level = level; 924};