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