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() { return false; } 237 } 238 239 if (this.width == 0 && this.height == 0) { 240 this.width = this.viewer.offsetWidth; 241 this.height = this.viewer.offsetHeight; 242 } 243 244 this.calcScale(); 245 246 var fullSize = this.tileSize; 247 // explicit set of zoom level 248 if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) { 249 fullSize = this.tileSize * Math.pow(2, this.zoomLevel); 250 } 251 // calculate the zoom level based on what fits best in window 252 else { 253 this.zoomLevel = -1; 254 fullSize = this.tileSize / 2; 255 do { 256 this.zoomLevel += 1; 257 fullSize *= 2; 258 } while (fullSize < Math.max(this.width, this.height)); 259 // take into account picture smaller than window size 260 if (this.zoomLevel > this.maxZoomLevel) { 261 var diff = this.zoomLevel - this.maxZoomLevel; 262 this.zoomLevel = this.maxZoomLevel; 263 fullSize /= Math.pow(2, diff); 264 } 265 } 266 267 //fixme fullsize from above not used anymore? 268 269 270 // calculate the center 271 this.x = Math.floor(this.scaleSize.x * 0.5 - this.width * 0.5) * -1; 272 this.y = Math.floor(this.scaleSize.y * 0.5 - this.height * 0.5) * -1; 273 274 275 // offset of viewer in the window 276 for (var node = this.viewer; node; node = node.offsetParent) { 277 this.top += node.offsetTop; 278 this.left += node.offsetLeft; 279 } 280 281 for (var child = this.viewer.firstChild; child; child = child.nextSibling) { 282 if (child.className == PanoJS.SURFACE_STYLE_CLASS) { 283 this.surface = child; 284 child.backingBean = this; 285 // empty surface 286 child.innerHTML = ''; 287 } 288 else if (child.className == PanoJS.WELL_STYLE_CLASS) { 289 this.well = child; 290 child.backingBean = this; 291 } 292 else if (child.className == PanoJS.CONTROLS_STYLE_CLASS) { 293 child.style.display = ''; 294 for (var control = child.firstChild; control; control = control.nextSibling) { 295 if (control.className) { 296 control.onclick = PanoJS[control.className + 'Handler']; 297 } 298 } 299 } 300 } 301 302 this.viewer.backingBean = this; 303 this.surface.style.cursor = PanoJS.GRAB_MOUSE_CURSOR; 304 this.prepareTiles(); 305 this.initialized = true; 306 }, 307 308 calcScale : function(){ 309 var inv = Math.max(this.imgsize.x,this.imgsize.y) / 310 (this.tileSize * Math.pow(2, this.zoomLevel)); 311 this.scaleSize.x = this.imgsize.x / inv; 312 this.scaleSize.y = this.imgsize.y / inv; 313 }, 314 315 prepareTiles : function() { 316 var rows = Math.ceil(this.height / this.tileSize) + 1; 317 var cols = Math.ceil(this.width / this.tileSize) + 1; 318 319 for (var c = 0; c < cols; c++) { 320 var tileCol = []; 321 322 for (var r = 0; r < rows; r++) { 323 /** 324 * element is the DOM element associated with this tile 325 * posx/posy are the pixel offsets of the tile 326 * xIndex/yIndex are the index numbers of the tile segment 327 * qx/qy represents the quadrant location of the tile 328 */ 329 var tile = { 330 'element' : null, 331 'posx' : 0, 332 'posy' : 0, 333 'xIndex' : c, 334 'yIndex' : r, 335 'qx' : c, 336 'qy' : r 337 }; 338 339 tileCol.push(tile); 340 } 341 342 this.tiles.push(tileCol); 343 } 344 345 this.surface.onmousedown = PanoJS.mousePressedHandler; 346 this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler; 347 this.surface.ondblclick = PanoJS.doubleClickHandler; 348 if (PanoJS.USE_KEYBOARD) { 349 window.onkeypress = PanoJS.keyboardMoveHandler; 350 window.onkeydown = PanoJS.keyboardZoomHandler; 351 } 352 353 this.positionTiles(); 354 }, 355 356 /** 357 * Position the tiles based on the x, y coordinates of the 358 * viewer, taking into account the motion offsets, which 359 * are calculated by a motion event handler. 360 */ 361 positionTiles : function(motion, reset) { 362 // default to no motion, just setup tiles 363 if (typeof motion == 'undefined') { 364 motion = { 'x' : 0, 'y' : 0 }; 365 } 366 367 for (var c = 0; c < this.tiles.length; c++) { 368 for (var r = 0; r < this.tiles[c].length; r++) { 369 var tile = this.tiles[c][r]; 370 371 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 372 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 373 374 var visible = true; 375 376 if (tile.posx > this.width) { 377 // tile moved out of view to the right 378 // consider the tile coming into view from the left 379 do { 380 tile.xIndex -= this.tiles.length; 381 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 382 } while (tile.posx > this.width); 383 384 if (tile.posx + this.tileSize < 0) { 385 visible = false; 386 } 387 388 } else { 389 // tile may have moved out of view from the left 390 // if so, consider the tile coming into view from the right 391 while (tile.posx < -this.tileSize) { 392 tile.xIndex += this.tiles.length; 393 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 394 } 395 396 if (tile.posx > this.width) { 397 visible = false; 398 } 399 } 400 401 if (tile.posy > this.height) { 402 // tile moved out of view to the bottom 403 // consider the tile coming into view from the top 404 do { 405 tile.yIndex -= this.tiles[c].length; 406 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 407 } while (tile.posy > this.height); 408 409 if (tile.posy + this.tileSize < 0) { 410 visible = false; 411 } 412 413 } else { 414 // tile may have moved out of view to the top 415 // if so, consider the tile coming into view from the bottom 416 while (tile.posy < -this.tileSize) { 417 tile.yIndex += this.tiles[c].length; 418 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 419 } 420 421 if (tile.posy > this.height) { 422 visible = false; 423 } 424 } 425 426 // initialize the image object for this quadrant 427 if (!this.initialized) { 428 this.assignTileImage(tile, true); 429 tile.element.style.top = tile.posy + 'px'; 430 tile.element.style.left = tile.posx + 'px'; 431 } 432 433 // display the image if visible 434 if (visible) { 435 this.assignTileImage(tile); 436 } 437 438 // seems to need this no matter what 439 tile.element.style.top = tile.posy + 'px'; 440 tile.element.style.left = tile.posx + 'px'; 441 } 442 } 443 444 // reset the x, y coordinates of the viewer according to motion 445 if (reset) { 446 this.x += motion.x; 447 this.y += motion.y; 448 } 449 }, 450 451 /** 452 * Determine the source image of the specified tile based 453 * on the zoom level and position of the tile. If forceBlankImage 454 * is specified, the source should be automatically set to the 455 * null tile image. This method will also setup an onload 456 * routine, delaying the appearance of the tile until it is fully 457 * loaded, if configured to do so. 458 */ 459 assignTileImage : function(tile, forceBlankImage) { 460 var tileImgId, src; 461 var useBlankImage = (forceBlankImage ? true : false); 462 463 // check if image has been scrolled too far in any particular direction 464 // and if so, use the null tile image 465 if (!useBlankImage) { 466 var left = tile.xIndex < 0; 467 var high = tile.yIndex < 0; 468 var right = tile.xIndex >= Math.ceil(this.scaleSize.x / this.tileSize); 469 var low = tile.yIndex >= Math.ceil(this.scaleSize.y / this.tileSize); 470 if (high || left || low || right) { 471 useBlankImage = true; 472 } 473 } 474 475 if (useBlankImage) { 476 tileImgId = 'blank:' + tile.qx + ':' + tile.qy; 477 src = this.cache['blank'].src; 478 } 479 else { 480 tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel); 481 } 482 483 // only remove tile if identity is changing 484 if (tile.element != null && 485 tile.element.parentNode != null && 486 tile.element.relativeSrc != src) { 487 this.well.removeChild(tile.element); 488 } 489 490 var tileImg = this.cache[tileImgId]; 491 // create cache if not exist 492 if (tileImg == null) { 493 tileImg = this.cache[tileImgId] = this.createPrototype(src); 494 } 495 496 if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) { 497 tileImg.onload = function() {}; 498 if (tileImg.image) { 499 tileImg.image.onload = function() {}; 500 } 501 502 if (tileImg.parentNode == null) { 503 tile.element = this.well.appendChild(tileImg); 504 } 505 } 506 else { 507 var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy; 508 var loadingImg = this.cache[loadingImgId]; 509 if (loadingImg == null) { 510 loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src); 511 } 512 513 loadingImg.targetSrc = tileImgId; 514 515 var well = this.well; 516 tile.element = well.appendChild(loadingImg); 517 tileImg.onload = function() { 518 // make sure our destination is still present 519 if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) { 520 tileImg.style.top = loadingImg.style.top; 521 tileImg.style.left = loadingImg.style.left; 522 well.replaceChild(tileImg, loadingImg); 523 tile.element = tileImg; 524 } 525 526 tileImg.onload = function() {}; 527 return false; 528 }; 529 530 // konqueror only recognizes the onload event on an Image 531 // javascript object, so we must handle that case here 532 if (!PanoJS.DOM_ONLOAD) { 533 tileImg.image = new Image(); 534 tileImg.image.onload = tileImg.onload; 535 tileImg.image.src = tileImg.src; 536 } 537 } 538 }, 539 540 createPrototype : function(src) { 541 var img = document.createElement('img'); 542 img.src = src; 543 img.relativeSrc = src; 544 img.className = PanoJS.TILE_STYLE_CLASS; 545 img.style.width = this.tileSize + 'px'; 546 img.style.height = this.tileSize + 'px'; 547 return img; 548 }, 549 550 addViewerMovedListener : function(listener) { 551 this.viewerMovedListeners.push(listener); 552 }, 553 554 addViewerZoomedListener : function(listener) { 555 this.viewerZoomedListeners.push(listener); 556 }, 557 558 /** 559 * Notify listeners of a zoom event on the viewer. 560 */ 561 notifyViewerZoomed : function() { 562 var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1); 563 for (var i = 0; i < this.viewerZoomedListeners.length; i++) { 564 this.viewerZoomedListeners[i].viewerZoomed( 565 new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage) 566 ); 567 } 568 }, 569 570 /** 571 * Notify listeners of a move event on the viewer. 572 */ 573 notifyViewerMoved : function(coords) { 574 if (typeof coords == 'undefined') { 575 coords = { 'x' : 0, 'y' : 0 }; 576 } 577 578 for (var i = 0; i < this.viewerMovedListeners.length; i++) { 579 this.viewerMovedListeners[i].viewerMoved( 580 new PanoJS.MoveEvent( 581 this.x + (coords.x - this.mark.x), 582 this.y + (coords.y - this.mark.y) 583 ) 584 ); 585 } 586 }, 587 588 zoom : function(direction) { 589 // ensure we are not zooming out of range 590 if (this.zoomLevel + direction < 0) { 591 if (PanoJS.MSG_BEYOND_MIN_ZOOM) { 592 alert(PanoJS.MSG_BEYOND_MIN_ZOOM); 593 } 594 return; 595 } 596 else if (this.zoomLevel + direction > this.maxZoomLevel) { 597 if (PanoJS.MSG_BEYOND_MAX_ZOOM) { 598 alert(PanoJS.MSG_BEYOND_MAX_ZOOM); 599 } 600 return; 601 } 602 603 this.blank(); 604 605 var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) }; 606 607 var before = { 608 'x' : (coords.x - this.x), 609 'y' : (coords.y - this.y) 610 }; 611 612 var after = { 613 'x' : Math.floor(before.x * Math.pow(2, direction)), 614 'y' : Math.floor(before.y * Math.pow(2, direction)) 615 }; 616 617 this.x = coords.x - after.x; 618 this.y = coords.y - after.y; 619 this.zoomLevel += direction; 620 this.calcScale(); 621 this.positionTiles(); 622 623 this.notifyViewerZoomed(); 624 }, 625 626 /** 627 * Clear all the tiles from the well for a complete reinitialization of the 628 * viewer. At this point the viewer is not considered to be initialized. 629 */ 630 clear : function() { 631 this.blank(); 632 this.initialized = false; 633 this.tiles = []; 634 }, 635 636 /** 637 * Remove all tiles from the well, which effectively "hides" 638 * them for a repaint. 639 */ 640 blank : function() { 641 for (imgId in this.cache) { 642 var img = this.cache[imgId]; 643 img.onload = function() {}; 644 if (img.image) { 645 img.image.onload = function() {}; 646 } 647 648 if (img.parentNode != null) { 649 this.well.removeChild(img); 650 } 651 } 652 }, 653 654 /** 655 * Method specifically for handling a mouse move event. A direct 656 * movement of the viewer can be achieved by calling positionTiles() directly. 657 */ 658 moveViewer : function(coords) { 659 this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) }); 660 this.notifyViewerMoved(coords); 661 }, 662 663 /** 664 * Make the specified coords the new center of the image placement. 665 * This method is typically triggered as the result of a double-click 666 * event. The calculation considers the distance between the center 667 * of the viewable area and the specified (viewer-relative) coordinates. 668 * If absolute is specified, treat the point as relative to the entire 669 * image, rather than only the viewable portion. 670 */ 671 recenter : function(coords, absolute) { 672 if (absolute) { 673 coords.x += this.x; 674 coords.y += this.y; 675 } 676 677 var motion = { 678 'x' : Math.floor((this.width / 2) - coords.x), 679 'y' : Math.floor((this.height / 2) - coords.y) 680 }; 681 682 if (motion.x == 0 && motion.y == 0) { 683 return; 684 } 685 686 if (PanoJS.USE_SLIDE) { 687 var target = motion; 688 var x, y; 689 // handle special case of vertical movement 690 if (target.x == 0) { 691 x = 0; 692 y = this.slideAcceleration; 693 } 694 else { 695 var slope = Math.abs(target.y / target.x); 696 x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5)); 697 y = Math.round(slope * x); 698 } 699 700 motion = { 701 'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1), 702 'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1) 703 }; 704 } 705 706 this.positionTiles(motion, true); 707 this.notifyViewerMoved(); 708 709 if (!PanoJS.USE_SLIDE) { 710 return; 711 } 712 713 var newcoords = { 714 'x' : coords.x + motion.x, 715 'y' : coords.y + motion.y 716 }; 717 718 var self = this; 719 // TODO: use an exponential growth rather than linear (should also depend on how far we are going) 720 // FIXME: this could be optimized by calling positionTiles directly perhaps 721 this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR; 722 this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY ); 723 }, 724 725 resize : function() { 726 // IE fires a premature resize event 727 if (!this.initialized) { 728 return; 729 } 730 731 var newWidth = this.viewer.offsetWidth; 732 var newHeight = this.viewer.offsetHeight; 733 734 this.viewer.style.display = 'none'; 735 this.clear(); 736 737 var before = { 738 'x' : Math.floor(this.width / 2), 739 'y' : Math.floor(this.height / 2) 740 }; 741 742 if (this.border >= 0) { 743 this.fitToWindow(this.border); 744 } 745 else { 746 this.width = newWidth; 747 this.height = newHeight; 748 } 749 750 this.prepareTiles(); 751 752 var after = { 753 'x' : Math.floor(this.width / 2), 754 'y' : Math.floor(this.height / 2) 755 }; 756 757 if (this.border >= 0) { 758 this.x += (after.x - before.x); 759 this.y += (after.y - before.y); 760 } 761 this.positionTiles(); 762 this.viewer.style.display = ''; 763 this.initialized = true; 764 this.notifyViewerMoved(); 765 }, 766 767 /** 768 * Resolve the coordinates from this mouse event by subtracting the 769 * offset of the viewer in the browser window (or frame). This does 770 * take into account the scroll offset of the page. 771 */ 772 resolveCoordinates : function(e) { 773 return { 774 'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left, 775 'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top 776 }; 777 }, 778 779 press : function(coords) { 780 this.activate(true); 781 this.mark = coords; 782 }, 783 784 release : function(coords) { 785 this.activate(false); 786 var motion = { 787 'x' : (coords.x - this.mark.x), 788 'y' : (coords.y - this.mark.y) 789 }; 790 791 this.x += motion.x; 792 this.y += motion.y; 793 this.mark = { 'x' : 0, 'y' : 0 }; 794 }, 795 796 /** 797 * Activate the viewer into motion depending on whether the mouse is pressed or 798 * not pressed. This method localizes the changes that must be made to the 799 * layers. 800 */ 801 activate : function(pressed) { 802 this.pressed = pressed; 803 this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR); 804 this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function() {}); 805 }, 806 807 /** 808 * Check whether the specified point exceeds the boundaries of 809 * the viewer's primary image. 810 */ 811 pointExceedsBoundaries : function(coords) { 812 return (coords.x < this.x || 813 coords.y < this.y || 814 coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) || 815 coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y)); 816 }, 817 818 // QUESTION: where is the best place for this method to be invoked? 819 resetSlideMotion : function() { 820 // QUESTION: should this be > 0 ? 821 if (this.slideMonitor != 0) { 822 clearTimeout(this.slideMonitor); 823 this.slideMonitor = 0; 824 } 825 826 this.slideAcceleration = 0; 827 } 828}; 829 830PanoJS.TileUrlProvider = function(baseUri, image) { 831 this.baseUri = baseUri; 832 this.image = image; 833}; 834 835PanoJS.TileUrlProvider.prototype = { 836 assembleUrl: function(xIndex, yIndex, zoom) { 837 return this.baseUri + '?tile=' + 838 zoom + '-' + xIndex + '-' + yIndex + 839 '&image=' + encodeURIComponent(this.image); 840 } 841}; 842 843PanoJS.mousePressedHandler = function(e) { 844 e = e ? e : window.event; 845 // only grab on left-click 846 if (e.button < 2) { 847 var self = this.backingBean; 848 var coords = self.resolveCoordinates(e); 849 if (self.pointExceedsBoundaries(coords)) { 850 e.cancelBubble = true; 851 } 852 else { 853 self.press(coords); 854 } 855 } 856 857 // NOTE: MANDATORY! must return false so event does not propagate to well! 858 return false; 859}; 860 861PanoJS.mouseReleasedHandler = function(e) { 862 e = e ? e : window.event; 863 var self = this.backingBean; 864 if (self.pressed) { 865 // OPTION: could decide to move viewer only on release, right here 866 self.release(self.resolveCoordinates(e)); 867 } 868}; 869 870PanoJS.mouseMovedHandler = function(e) { 871 e = e ? e : window.event; 872 var self = this.backingBean; 873 self.moveCount++; 874 if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) { 875 self.moveViewer(self.resolveCoordinates(e)); 876 } 877}; 878 879PanoJS.zoomInHandler = function(e) { 880 e = e ? e : window.event; 881 var self = this.parentNode.parentNode.backingBean; 882 self.zoom(1); 883 return false; 884}; 885 886PanoJS.zoomOutHandler = function(e) { 887 e = e ? e : window.event; 888 var self = this.parentNode.parentNode.backingBean; 889 self.zoom(-1); 890 return false; 891}; 892 893PanoJS.doubleClickHandler = function(e) { 894 e = e ? e : window.event; 895 var self = this.backingBean; 896 coords = self.resolveCoordinates(e); 897 if (!self.pointExceedsBoundaries(coords)) { 898 self.resetSlideMotion(); 899 self.recenter(coords); 900 } 901}; 902 903PanoJS.keyboardMoveHandler = 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 == 38) 908 viewer.positionTiles({'x': 0,'y': -PanoJS.MOVE_THROTTLE}, true); 909 if (e.keyCode == 39) 910 viewer.positionTiles({'x': -PanoJS.MOVE_THROTTLE,'y': 0}, true); 911 if (e.keyCode == 40) 912 viewer.positionTiles({'x': 0,'y': PanoJS.MOVE_THROTTLE}, true); 913 if (e.keyCode == 37) 914 viewer.positionTiles({'x': PanoJS.MOVE_THROTTLE,'y': 0}, true); 915 } 916}; 917 918PanoJS.keyboardZoomHandler = function(e) { 919 e = e ? e : window.event; 920 for (var i = 0; i < PanoJS.VIEWERS.length; i++) { 921 var viewer = PanoJS.VIEWERS[i]; 922 if (e.keyCode == 109) 923 viewer.zoom(-1); 924 if (e.keyCode == 107) 925 viewer.zoom(1); 926 } 927}; 928 929PanoJS.MoveEvent = function(x, y) { 930 this.x = x; 931 this.y = y; 932}; 933 934PanoJS.ZoomEvent = function(x, y, level, percentage) { 935 this.x = x; 936 this.y = y; 937 this.percentage = percentage; 938 this.level = level; 939}; 940 941addInitEvent(function(){ 942 var panos = getElementsByClass('panoview_plugin',document,'div'); 943 for(var i=0; i<panos.length; i++){ 944 var pano = panos[i]; 945 var opts = getElementsByClass('options',pano,'div')[0]; 946 var conf; 947 eval('conf ='+opts.innerHTML); //JSON 948 949 var viewerBean = new PanoJS(pano, conf); 950 viewerBean.init(); 951 } 952}); 953