1/** 2 * Panoramic JavaScript Image Viewer (PanoJS) 1.0.2 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 this.tileUrlProvider = new PanoJS.TileUrlProvider(options.tileBaseUri, options.image); 79 80 this.tileSize = (options.tileSize ? options.tileSize : PanoJS.TILE_SIZE); 81 82 // assign and do some validation on the zoom levels to ensure sanity 83 this.zoomLevel = (typeof options.initialZoom == 'undefined' ? -1 : parseInt(options.initialZoom)); 84 this.maxZoomLevel = (typeof options.maxZoom == 'undefined' ? 0 : Math.abs(parseInt(options.maxZoom))); 85 if (this.zoomLevel > this.maxZoomLevel) { 86 this.zoomLevel = this.maxZoomLevel; 87 } 88 89 this.imgsize = { 'x' : 0, 'y' : 0 }; 90 this.imgsize.x = (typeof options.imageWidth == 'undefined' ? -1 : parseInt(options.imageWidth)); 91 this.imgsize.y = (typeof options.imageHeight == 'undefined' ? -1 : parseInt(options.imageHeight)); 92 93 this.initialized = false; 94 this.surface = null; 95 this.well = null; 96 this.width = 0; 97 this.height = 0; 98 this.top = 0; 99 this.left = 0; 100 this.x = 0; 101 this.y = 0; 102 this.border = -1; 103 this.mark = { 'x' : 0, 'y' : 0 }; 104 this.pressed = false; 105 this.tiles = []; 106 this.cache = {}; 107 var blankTile = options.blankTile ? options.blankTile : PanoJS.BLANK_TILE_IMAGE; 108 var loadingTile = options.loadingTile ? options.loadingTile : PanoJS.LOADING_TILE_IMAGE; 109 this.cache['blank'] = new Image(); 110 this.cache['blank'].src = blankTile; 111 if (blankTile != loadingTile) { 112 this.cache['loading'] = new Image(); 113 this.cache['loading'].src = loadingTile; 114 } 115 else { 116 this.cache['loading'] = this.cache['blank']; 117 } 118 119 // employed to throttle the number of redraws that 120 // happen while the mouse is moving 121 this.moveCount = 0; 122 this.slideMonitor = 0; 123 this.slideAcceleration = 0; 124 125 // add to viewer registry 126 PanoJS.VIEWERS[PanoJS.VIEWERS.length] = this; 127} 128 129// project specific variables 130PanoJS.PROJECT_NAME = 'PanoJS'; 131PanoJS.PROJECT_VERSION = '1.0.0'; 132PanoJS.REVISION_FLAG = ''; 133 134// CSS definition settings 135PanoJS.SURFACE_STYLE_CLASS = 'surface'; 136PanoJS.WELL_STYLE_CLASS = 'well'; 137PanoJS.CONTROLS_STYLE_CLASS = 'controls' 138PanoJS.TILE_STYLE_CLASS = 'tile'; 139 140// language settings 141PanoJS.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.'; 142PanoJS.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.'; 143 144// defaults if not provided as constructor options 145PanoJS.TILE_BASE_URI = 'tiles'; 146PanoJS.TILE_PREFIX = 'tile-'; 147PanoJS.TILE_EXTENSION = 'jpg'; 148PanoJS.TILE_SIZE = 256; 149PanoJS.BLANK_TILE_IMAGE = 'blank.gif'; 150PanoJS.LOADING_TILE_IMAGE = 'blank.gif'; 151PanoJS.INITIAL_PAN = { 'x' : .5, 'y' : .5 }; 152PanoJS.USE_LOADER_IMAGE = true; 153PanoJS.USE_SLIDE = true; 154PanoJS.USE_KEYBOARD = true; 155 156// performance tuning variables 157PanoJS.MOVE_THROTTLE = 3; 158PanoJS.SLIDE_DELAY = 40; 159PanoJS.SLIDE_ACCELERATION_FACTOR = 5; 160 161// the following are calculated settings 162PanoJS.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true); 163PanoJS.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(grab.cur)' : '-moz-grab')); 164PanoJS.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(grabbing.cur)' : '-moz-grabbing')); 165 166// registry of all known viewers 167PanoJS.VIEWERS = []; 168 169// utility functions 170PanoJS.isInstance = function(object, clazz) { 171 // FIXME: can this just be replaced with instanceof operator? It has been reported that __proto__ is specific to Netscape 172 while (object != null) { 173 if (object == clazz.prototype) { 174 return true; 175 } 176 177 object = object.__proto__; 178 } 179 180 return false; 181} 182 183PanoJS.prototype = { 184 185 /** 186 * Resize the viewer to fit snug inside the browser window (or frame), 187 * spacing it from the edges by the specified border. 188 * 189 * This method should be called prior to init() 190 * FIXME: option to hide viewer to prevent scrollbar interference 191 */ 192 fitToWindow : function(border) { 193 if (typeof border != 'number' || border < 0) { 194 border = 0; 195 } 196 197 this.border = border; 198 var calcWidth = 0; 199 var calcHeight = 0; 200 if (window.innerWidth) { 201 calcWidth = window.innerWidth; 202 calcHeight = window.innerHeight; 203 } 204 else { 205 calcWidth = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth); 206 calcHeight = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight); 207 } 208 209 calcWidth = Math.max(calcWidth - 2 * border, 0); 210 calcHeight = Math.max(calcHeight - 2 * border, 0); 211 if (calcWidth % 2) { 212 calcWidth--; 213 } 214 215 if (calcHeight % 2) { 216 calcHeight--; 217 } 218 219 this.width = calcWidth; 220 this.height = calcHeight; 221 this.viewer.style.width = this.width + 'px'; 222 this.viewer.style.height = this.height + 'px'; 223 this.viewer.style.top = border + 'px'; 224 this.viewer.style.left = border + 'px'; 225 }, 226 227 init : function() { 228 if (document.attachEvent) { 229 document.body.ondragstart = function() { return false; } 230 } 231 232 if (this.width == 0 && this.height == 0) { 233 this.width = this.viewer.offsetWidth; 234 this.height = this.viewer.offsetHeight; 235 } 236 237 var fullSize = this.tileSize; 238 // explicit set of zoom level 239 if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) { 240 fullSize = this.tileSize * Math.pow(2, this.zoomLevel); 241 } 242 // calculate the zoom level based on what fits best in window 243 else { 244 this.zoomLevel = -1; 245 fullSize = this.tileSize / 2; 246 do { 247 this.zoomLevel += 1; 248 fullSize *= 2; 249 } while (fullSize < Math.max(this.width, this.height)); 250 // take into account picture smaller than window size 251 if (this.zoomLevel > this.maxZoomLevel) { 252 var diff = this.zoomLevel - this.maxZoomLevel; 253 this.zoomLevel = this.maxZoomLevel; 254 fullSize /= Math.pow(2, diff); 255 } 256 } 257 258//fixme fullsize not used anymore? 259 260 // calculate the center 261 this.x = 0; 262 this.y = 0; 263 var inv = Math.max(this.imgsize.x,this.imgsize.y) / 264 (this.tileSize * Math.pow(2, this.zoomLevel)); 265 if(this.imgsize.x){ 266 var xx = (this.imgsize.x / inv)*0.5; 267 this.x = Math.floor(xx - this.width * 0.5) * -1; 268 } 269 if(this.imgsize.y){ 270 var yy = (this.imgsize.y / inv)*0.5; 271 this.y = Math.floor(yy - this.height * 0.5) * -1; 272 } 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 } 286 else if (child.className == PanoJS.WELL_STYLE_CLASS) { 287 this.well = child; 288 child.backingBean = this; 289 // empty well 290 child.innerHTML = ''; 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 prepareTiles : function() { 309 var rows = Math.ceil(this.height / this.tileSize) + 1; 310 var cols = Math.ceil(this.width / this.tileSize) + 1; 311 312 for (var c = 0; c < cols; c++) { 313 var tileCol = []; 314 315 for (var r = 0; r < rows; r++) { 316 /** 317 * element is the DOM element associated with this tile 318 * posx/posy are the pixel offsets of the tile 319 * xIndex/yIndex are the index numbers of the tile segment 320 * qx/qy represents the quadrant location of the tile 321 */ 322 var tile = { 323 'element' : null, 324 'posx' : 0, 325 'posy' : 0, 326 'xIndex' : c, 327 'yIndex' : r, 328 'qx' : c, 329 'qy' : r 330 }; 331 332 tileCol.push(tile); 333 } 334 335 this.tiles.push(tileCol); 336 } 337 338 this.surface.onmousedown = PanoJS.mousePressedHandler; 339 this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler; 340 this.surface.ondblclick = PanoJS.doubleClickHandler; 341 if (PanoJS.USE_KEYBOARD) { 342 window.onkeypress = PanoJS.keyboardMoveHandler; 343 window.onkeydown = PanoJS.keyboardZoomHandler; 344 } 345 346 this.positionTiles(); 347 }, 348 349 /** 350 * Position the tiles based on the x, y coordinates of the 351 * viewer, taking into account the motion offsets, which 352 * are calculated by a motion event handler. 353 */ 354 positionTiles : function(motion, reset) { 355 // default to no motion, just setup tiles 356 if (typeof motion == 'undefined') { 357 motion = { 'x' : 0, 'y' : 0 }; 358 } 359 360 for (var c = 0; c < this.tiles.length; c++) { 361 for (var r = 0; r < this.tiles[c].length; r++) { 362 var tile = this.tiles[c][r]; 363 364 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 365 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 366 367 var visible = true; 368 369 if (tile.posx > this.width) { 370 // tile moved out of view to the right 371 // consider the tile coming into view from the left 372 do { 373 tile.xIndex -= this.tiles.length; 374 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 375 } while (tile.posx > this.width); 376 377 if (tile.posx + this.tileSize < 0) { 378 visible = false; 379 } 380 381 } else { 382 // tile may have moved out of view from the left 383 // if so, consider the tile coming into view from the right 384 while (tile.posx < -this.tileSize) { 385 tile.xIndex += this.tiles.length; 386 tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x; 387 } 388 389 if (tile.posx > this.width) { 390 visible = false; 391 } 392 } 393 394 if (tile.posy > this.height) { 395 // tile moved out of view to the bottom 396 // consider the tile coming into view from the top 397 do { 398 tile.yIndex -= this.tiles[c].length; 399 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 400 } while (tile.posy > this.height); 401 402 if (tile.posy + this.tileSize < 0) { 403 visible = false; 404 } 405 406 } else { 407 // tile may have moved out of view to the top 408 // if so, consider the tile coming into view from the bottom 409 while (tile.posy < -this.tileSize) { 410 tile.yIndex += this.tiles[c].length; 411 tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y; 412 } 413 414 if (tile.posy > this.height) { 415 visible = false; 416 } 417 } 418 419 // initialize the image object for this quadrant 420 if (!this.initialized) { 421 this.assignTileImage(tile, true); 422 tile.element.style.top = tile.posy + 'px'; 423 tile.element.style.left = tile.posx + 'px'; 424 } 425 426 // display the image if visible 427 if (visible) { 428 this.assignTileImage(tile); 429 } 430 431 // seems to need this no matter what 432 tile.element.style.top = tile.posy + 'px'; 433 tile.element.style.left = tile.posx + 'px'; 434 } 435 } 436 437 // reset the x, y coordinates of the viewer according to motion 438 if (reset) { 439 this.x += motion.x; 440 this.y += motion.y; 441 } 442 }, 443 444 /** 445 * Determine the source image of the specified tile based 446 * on the zoom level and position of the tile. If forceBlankImage 447 * is specified, the source should be automatically set to the 448 * null tile image. This method will also setup an onload 449 * routine, delaying the appearance of the tile until it is fully 450 * loaded, if configured to do so. 451 */ 452 assignTileImage : function(tile, forceBlankImage) { 453 var tileImgId, src; 454 var useBlankImage = (forceBlankImage ? true : false); 455 456 // check if image has been scrolled too far in any particular direction 457 // and if so, use the null tile image 458 if (!useBlankImage) { 459 var left = tile.xIndex < 0; 460 var high = tile.yIndex < 0; 461 var right = tile.xIndex >= Math.pow(2, this.zoomLevel); 462 var low = tile.yIndex >= Math.pow(2, this.zoomLevel); 463 if (high || left || low || right) { 464 useBlankImage = true; 465 } 466 } 467 468 if (useBlankImage) { 469 tileImgId = 'blank:' + tile.qx + ':' + tile.qy; 470 src = this.cache['blank'].src; 471 } 472 else { 473 tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel); 474 } 475 476 // only remove tile if identity is changing 477 if (tile.element != null && 478 tile.element.parentNode != null && 479 tile.element.relativeSrc != src) { 480 this.well.removeChild(tile.element); 481 } 482 483 var tileImg = this.cache[tileImgId]; 484 // create cache if not exist 485 if (tileImg == null) { 486 tileImg = this.cache[tileImgId] = this.createPrototype(src); 487 } 488 489 if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) { 490 tileImg.onload = function() {}; 491 if (tileImg.image) { 492 tileImg.image.onload = function() {}; 493 } 494 495 if (tileImg.parentNode == null) { 496 tile.element = this.well.appendChild(tileImg); 497 } 498 } 499 else { 500 var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy; 501 var loadingImg = this.cache[loadingImgId]; 502 if (loadingImg == null) { 503 loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src); 504 } 505 506 loadingImg.targetSrc = tileImgId; 507 508 var well = this.well; 509 tile.element = well.appendChild(loadingImg); 510 tileImg.onload = function() { 511 // make sure our destination is still present 512 if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) { 513 tileImg.style.top = loadingImg.style.top; 514 tileImg.style.left = loadingImg.style.left; 515 well.replaceChild(tileImg, loadingImg); 516 tile.element = tileImg; 517 } 518 519 tileImg.onload = function() {}; 520 return false; 521 } 522 523 // konqueror only recognizes the onload event on an Image 524 // javascript object, so we must handle that case here 525 if (!PanoJS.DOM_ONLOAD) { 526 tileImg.image = new Image(); 527 tileImg.image.onload = tileImg.onload; 528 tileImg.image.src = tileImg.src; 529 } 530 } 531 }, 532 533 createPrototype : function(src) { 534 var img = document.createElement('img'); 535 img.src = src; 536 img.relativeSrc = src; 537 img.className = PanoJS.TILE_STYLE_CLASS; 538 img.style.width = this.tileSize + 'px'; 539 img.style.height = this.tileSize + 'px'; 540 return img; 541 }, 542 543 addViewerMovedListener : function(listener) { 544 this.viewerMovedListeners.push(listener); 545 }, 546 547 addViewerZoomedListener : function(listener) { 548 this.viewerZoomedListeners.push(listener); 549 }, 550 551 /** 552 * Notify listeners of a zoom event on the viewer. 553 */ 554 notifyViewerZoomed : function() { 555 var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1); 556 for (var i = 0; i < this.viewerZoomedListeners.length; i++) { 557 this.viewerZoomedListeners[i].viewerZoomed( 558 new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage) 559 ); 560 } 561 }, 562 563 /** 564 * Notify listeners of a move event on the viewer. 565 */ 566 notifyViewerMoved : function(coords) { 567 if (typeof coords == 'undefined') { 568 coords = { 'x' : 0, 'y' : 0 }; 569 } 570 571 for (var i = 0; i < this.viewerMovedListeners.length; i++) { 572 this.viewerMovedListeners[i].viewerMoved( 573 new PanoJS.MoveEvent( 574 this.x + (coords.x - this.mark.x), 575 this.y + (coords.y - this.mark.y) 576 ) 577 ); 578 } 579 }, 580 581 zoom : function(direction) { 582 // ensure we are not zooming out of range 583 if (this.zoomLevel + direction < 0) { 584 if (PanoJS.MSG_BEYOND_MIN_ZOOM) { 585 alert(PanoJS.MSG_BEYOND_MIN_ZOOM); 586 } 587 return; 588 } 589 else if (this.zoomLevel + direction > this.maxZoomLevel) { 590 if (PanoJS.MSG_BEYOND_MAX_ZOOM) { 591 alert(PanoJS.MSG_BEYOND_MAX_ZOOM); 592 } 593 return; 594 } 595 596 this.blank(); 597 598 var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) }; 599 600 var before = { 601 'x' : (coords.x - this.x), 602 'y' : (coords.y - this.y) 603 }; 604 605 var after = { 606 'x' : Math.floor(before.x * Math.pow(2, direction)), 607 'y' : Math.floor(before.y * Math.pow(2, direction)) 608 }; 609 610 this.x = coords.x - after.x; 611 this.y = coords.y - after.y; 612 this.zoomLevel += direction; 613 this.positionTiles(); 614 615 this.notifyViewerZoomed(); 616 }, 617 618 /** 619 * Clear all the tiles from the well for a complete reinitialization of the 620 * viewer. At this point the viewer is not considered to be initialized. 621 */ 622 clear : function() { 623 this.blank(); 624 this.initialized = false; 625 this.tiles = []; 626 }, 627 628 /** 629 * Remove all tiles from the well, which effectively "hides" 630 * them for a repaint. 631 */ 632 blank : function() { 633 for (imgId in this.cache) { 634 var img = this.cache[imgId]; 635 img.onload = function() {}; 636 if (img.image) { 637 img.image.onload = function() {}; 638 } 639 640 if (img.parentNode != null) { 641 this.well.removeChild(img); 642 } 643 } 644 }, 645 646 /** 647 * Method specifically for handling a mouse move event. A direct 648 * movement of the viewer can be achieved by calling positionTiles() directly. 649 */ 650 moveViewer : function(coords) { 651 this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) }); 652 this.notifyViewerMoved(coords); 653 }, 654 655 /** 656 * Make the specified coords the new center of the image placement. 657 * This method is typically triggered as the result of a double-click 658 * event. The calculation considers the distance between the center 659 * of the viewable area and the specified (viewer-relative) coordinates. 660 * If absolute is specified, treat the point as relative to the entire 661 * image, rather than only the viewable portion. 662 */ 663 recenter : function(coords, absolute) { 664 if (absolute) { 665 coords.x += this.x; 666 coords.y += this.y; 667 } 668 669 var motion = { 670 'x' : Math.floor((this.width / 2) - coords.x), 671 'y' : Math.floor((this.height / 2) - coords.y) 672 }; 673 674 if (motion.x == 0 && motion.y == 0) { 675 return; 676 } 677 678 if (PanoJS.USE_SLIDE) { 679 var target = motion; 680 var x, y; 681 // handle special case of vertical movement 682 if (target.x == 0) { 683 x = 0; 684 y = this.slideAcceleration; 685 } 686 else { 687 var slope = Math.abs(target.y / target.x); 688 x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5)); 689 y = Math.round(slope * x); 690 } 691 692 motion = { 693 'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1), 694 'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1) 695 } 696 } 697 698 this.positionTiles(motion, true); 699 this.notifyViewerMoved(); 700 701 if (!PanoJS.USE_SLIDE) { 702 return; 703 } 704 705 var newcoords = { 706 'x' : coords.x + motion.x, 707 'y' : coords.y + motion.y 708 }; 709 710 var self = this; 711 // TODO: use an exponential growth rather than linear (should also depend on how far we are going) 712 // FIXME: this could be optimized by calling positionTiles directly perhaps 713 this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR; 714 this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY ); 715 }, 716 717 resize : function() { 718 // IE fires a premature resize event 719 if (!this.initialized) { 720 return; 721 } 722 723 var newWidth = this.viewer.offsetWidth; 724 var newHeight = this.viewer.offsetHeight; 725 726 this.viewer.style.display = 'none'; 727 this.clear(); 728 729 var before = { 730 'x' : Math.floor(this.width / 2), 731 'y' : Math.floor(this.height / 2) 732 }; 733 734 if (this.border >= 0) { 735 this.fitToWindow(this.border); 736 } 737 else { 738 this.width = newWidth; 739 this.height = newHeight; 740 } 741 742 this.prepareTiles(); 743 744 var after = { 745 'x' : Math.floor(this.width / 2), 746 'y' : Math.floor(this.height / 2) 747 }; 748 749 if (this.border >= 0) { 750 this.x += (after.x - before.x); 751 this.y += (after.y - before.y); 752 } 753 this.positionTiles(); 754 this.viewer.style.display = ''; 755 this.initialized = true; 756 this.notifyViewerMoved(); 757 }, 758 759 /** 760 * Resolve the coordinates from this mouse event by subtracting the 761 * offset of the viewer in the browser window (or frame). This does 762 * take into account the scroll offset of the page. 763 */ 764 resolveCoordinates : function(e) { 765 return { 766 'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left, 767 'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top 768 } 769 }, 770 771 press : function(coords) { 772 this.activate(true); 773 this.mark = coords; 774 }, 775 776 release : function(coords) { 777 this.activate(false); 778 var motion = { 779 'x' : (coords.x - this.mark.x), 780 'y' : (coords.y - this.mark.y) 781 }; 782 783 this.x += motion.x; 784 this.y += motion.y; 785 this.mark = { 'x' : 0, 'y' : 0 }; 786 }, 787 788 /** 789 * Activate the viewer into motion depending on whether the mouse is pressed or 790 * not pressed. This method localizes the changes that must be made to the 791 * layers. 792 */ 793 activate : function(pressed) { 794 this.pressed = pressed; 795 this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR); 796 this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function() {}); 797 }, 798 799 /** 800 * Check whether the specified point exceeds the boundaries of 801 * the viewer's primary image. 802 */ 803 pointExceedsBoundaries : function(coords) { 804 return (coords.x < this.x || 805 coords.y < this.y || 806 coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) || 807 coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y)); 808 }, 809 810 // QUESTION: where is the best place for this method to be invoked? 811 resetSlideMotion : function() { 812 // QUESTION: should this be > 0 ? 813 if (this.slideMonitor != 0) { 814 clearTimeout(this.slideMonitor); 815 this.slideMonitor = 0; 816 } 817 818 this.slideAcceleration = 0; 819 } 820}; 821 822PanoJS.TileUrlProvider = function(baseUri, image) { 823 this.baseUri = baseUri; 824 this.image = image; 825} 826 827PanoJS.TileUrlProvider.prototype = { 828 assembleUrl: function(xIndex, yIndex, zoom) { 829 return this.baseUri + '?tile=' + 830 zoom + '-' + xIndex + '-' + yIndex + 831 '&image=' + encodeURIComponent(this.image); 832 } 833} 834 835PanoJS.mousePressedHandler = function(e) { 836 e = e ? e : window.event; 837 // only grab on left-click 838 if (e.button < 2) { 839 var self = this.backingBean; 840 var coords = self.resolveCoordinates(e); 841 if (self.pointExceedsBoundaries(coords)) { 842 e.cancelBubble = true; 843 } 844 else { 845 self.press(coords); 846 } 847 } 848 849 // NOTE: MANDATORY! must return false so event does not propagate to well! 850 return false; 851}; 852 853PanoJS.mouseReleasedHandler = function(e) { 854 e = e ? e : window.event; 855 var self = this.backingBean; 856 if (self.pressed) { 857 // OPTION: could decide to move viewer only on release, right here 858 self.release(self.resolveCoordinates(e)); 859 } 860}; 861 862PanoJS.mouseMovedHandler = function(e) { 863 e = e ? e : window.event; 864 var self = this.backingBean; 865 self.moveCount++; 866 if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) { 867 self.moveViewer(self.resolveCoordinates(e)); 868 } 869}; 870 871PanoJS.zoomInHandler = 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.zoomOutHandler = function(e) { 879 e = e ? e : window.event; 880 var self = this.parentNode.parentNode.backingBean; 881 self.zoom(-1); 882 return false; 883}; 884 885PanoJS.doubleClickHandler = function(e) { 886 e = e ? e : window.event; 887 var self = this.backingBean; 888 coords = self.resolveCoordinates(e); 889 if (!self.pointExceedsBoundaries(coords)) { 890 self.resetSlideMotion(); 891 self.recenter(coords); 892 } 893}; 894 895PanoJS.keyboardMoveHandler = function(e) { 896 e = e ? e : window.event; 897 for (var i = 0; i < PanoJS.VIEWERS.length; i++) { 898 var viewer = PanoJS.VIEWERS[i]; 899 if (e.keyCode == 38) 900 viewer.positionTiles({'x': 0,'y': -PanoJS.MOVE_THROTTLE}, true); 901 if (e.keyCode == 39) 902 viewer.positionTiles({'x': -PanoJS.MOVE_THROTTLE,'y': 0}, true); 903 if (e.keyCode == 40) 904 viewer.positionTiles({'x': 0,'y': PanoJS.MOVE_THROTTLE}, true); 905 if (e.keyCode == 37) 906 viewer.positionTiles({'x': PanoJS.MOVE_THROTTLE,'y': 0}, true); 907 } 908} 909 910PanoJS.keyboardZoomHandler = function(e) { 911 e = e ? e : window.event; 912 for (var i = 0; i < PanoJS.VIEWERS.length; i++) { 913 var viewer = PanoJS.VIEWERS[i]; 914 if (e.keyCode == 109) 915 viewer.zoom(-1); 916 if (e.keyCode == 107) 917 viewer.zoom(1); 918 } 919} 920 921PanoJS.MoveEvent = function(x, y) { 922 this.x = x; 923 this.y = y; 924}; 925 926PanoJS.ZoomEvent = function(x, y, level, percentage) { 927 this.x = x; 928 this.y = y; 929 this.percentage = percentage; 930 this.level = level; 931}; 932 933addInitEvent(function(){ 934 var panos = getElementsByClass('panoview_plugin',document,'div'); 935 for(var i=0; i<panos.length; i++){ 936 var pano = panos[i]; 937 var opts = getElementsByClass('options',pano,'div')[0]; 938 var conf; 939 eval('conf ='+opts.innerHTML); //JSON 940 941 var viewerBean = new PanoJS(pano, conf); 942 viewerBean.init(); 943 } 944}); 945