1/* 2 * Pannellum - An HTML5 based Panorama Viewer 3 * Copyright (c) 2011-2019 Matthew Petroff 4 * 5 * Permission is hereby granted, free of charge, to any person obtaining a copy 6 * of this software and associated documentation files (the "Software"), to deal 7 * in the Software without restriction, including without limitation the rights 8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 * copies of the Software, and to permit persons to whom the Software is 10 * furnished to do so, subject to the following conditions: 11 * 12 * The above copyright notice and this permission notice shall be included in 13 * all copies or substantial portions of the Software. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 * THE SOFTWARE. 22 */ 23 24window.pannellum = (function(window, document, undefined) { 25 26'use strict'; 27 28/** 29 * Creates a new panorama viewer. 30 * @constructor 31 * @param {HTMLElement|string} container - The container (div) element for the 32 * viewer, or its ID. 33 * @param {Object} initialConfig - Inital configuration for viewer. 34 */ 35function Viewer(container, initialConfig) { 36 37var _this = this; 38 39// Declare variables 40var config, 41 renderer, 42 preview, 43 isUserInteracting = false, 44 latestInteraction = Date.now(), 45 onPointerDownPointerX = 0, 46 onPointerDownPointerY = 0, 47 onPointerDownPointerDist = -1, 48 onPointerDownYaw = 0, 49 onPointerDownPitch = 0, 50 keysDown = new Array(10), 51 fullscreenActive = false, 52 loaded, 53 error = false, 54 isTimedOut = false, 55 listenersAdded = false, 56 panoImage, 57 prevTime, 58 speed = {'yaw': 0, 'pitch': 0, 'hfov': 0}, 59 animating = false, 60 orientation = false, 61 orientationYawOffset = 0, 62 autoRotateStart, 63 autoRotateSpeed = 0, 64 origHfov, 65 origPitch, 66 animatedMove = {}, 67 externalEventListeners = {}, 68 specifiedPhotoSphereExcludes = [], 69 update = false, // Should we update when still to render dynamic content 70 eps = 1e-6, 71 hotspotsCreated = false, 72 destroyed = false; 73 74var defaultConfig = { 75 hfov: 100, 76 minHfov: 50, 77 multiResMinHfov: false, 78 maxHfov: 120, 79 pitch: 0, 80 minPitch: undefined, 81 maxPitch: undefined, 82 yaw: 0, 83 minYaw: -180, 84 maxYaw: 180, 85 roll: 0, 86 haov: 360, 87 vaov: 180, 88 vOffset: 0, 89 autoRotate: false, 90 autoRotateInactivityDelay: -1, 91 autoRotateStopDelay: undefined, 92 type: 'equirectangular', 93 northOffset: 0, 94 showFullscreenCtrl: true, 95 dynamic: false, 96 dynamicUpdate: false, 97 doubleClickZoom: true, 98 keyboardZoom: true, 99 mouseZoom: true, 100 showZoomCtrl: true, 101 autoLoad: false, 102 showControls: true, 103 orientationOnByDefault: false, 104 hotSpotDebug: false, 105 backgroundColor: [0, 0, 0], 106 avoidShowingBackground: false, 107 animationTimingFunction: timingFunction, 108 draggable: true, 109 disableKeyboardCtrl: false, 110 crossOrigin: 'anonymous', 111 touchPanSpeedCoeffFactor: 1, 112 capturedKeyNumbers: [16, 17, 27, 37, 38, 39, 40, 61, 65, 68, 83, 87, 107, 109, 173, 187, 189], 113 friction: 0.15 114}; 115 116// Translatable / configurable strings 117// Some strings contain '%s', which is a placeholder for inserted values 118// When setting strings in external configuration, `\n` should be used instead of `<br>` to insert line breaks 119defaultConfig.strings = { 120 // Labels 121 loadButtonLabel: 'Click to<br>Load<br>Panorama', 122 loadingLabel: 'Loading...', 123 bylineLabel: 'by %s', // One substitution: author 124 125 // Errors 126 noPanoramaError: 'No panorama image was specified.', 127 fileAccessError: 'The file %s could not be accessed.', // One substitution: file URL 128 malformedURLError: 'There is something wrong with the panorama URL.', 129 iOS8WebGLError: "Due to iOS 8's broken WebGL implementation, only " + 130 "progressive encoded JPEGs work for your device (this " + 131 "panorama uses standard encoding).", 132 genericWebGLError: 'Your browser does not have the necessary WebGL support to display this panorama.', 133 textureSizeError: 'This panorama is too big for your device! It\'s ' + 134 '%spx wide, but your device only supports images up to ' + 135 '%spx wide. Try another device.' + 136 ' (If you\'re the author, try scaling down the image.)', // Two substitutions: image width, max image width 137 unknownError: 'Unknown error. Check developer console.', 138}; 139 140// Initialize container 141container = typeof container === 'string' ? document.getElementById(container) : container; 142container.classList.add('pnlm-container'); 143container.tabIndex = 0; 144 145// Create container for ui 146var uiContainer = document.createElement('div'); 147uiContainer.className = 'pnlm-ui'; 148container.appendChild(uiContainer); 149 150// Create container for renderer 151var renderContainer = document.createElement('div'); 152renderContainer.className = 'pnlm-render-container'; 153container.appendChild(renderContainer); 154var dragFix = document.createElement('div'); 155dragFix.className = 'pnlm-dragfix'; 156uiContainer.appendChild(dragFix); 157 158// Display about information on right click 159var aboutMsg = document.createElement('span'); 160aboutMsg.className = 'pnlm-about-msg'; 161aboutMsg.innerHTML = '<a href="https://pannellum.org/" target="_blank">Pannellum</a>'; 162uiContainer.appendChild(aboutMsg); 163dragFix.addEventListener('contextmenu', aboutMessage); 164 165// Create info display 166var infoDisplay = {}; 167 168// Hot spot debug indicator 169var hotSpotDebugIndicator = document.createElement('div'); 170hotSpotDebugIndicator.className = 'pnlm-sprite pnlm-hot-spot-debug-indicator'; 171uiContainer.appendChild(hotSpotDebugIndicator); 172 173// Panorama info 174infoDisplay.container = document.createElement('div'); 175infoDisplay.container.className = 'pnlm-panorama-info'; 176infoDisplay.title = document.createElement('div'); 177infoDisplay.title.className = 'pnlm-title-box'; 178infoDisplay.container.appendChild(infoDisplay.title); 179infoDisplay.author = document.createElement('div'); 180infoDisplay.author.className = 'pnlm-author-box'; 181infoDisplay.container.appendChild(infoDisplay.author); 182uiContainer.appendChild(infoDisplay.container); 183 184// Load box 185infoDisplay.load = {}; 186infoDisplay.load.box = document.createElement('div'); 187infoDisplay.load.box.className = 'pnlm-load-box'; 188infoDisplay.load.boxp = document.createElement('p'); 189infoDisplay.load.box.appendChild(infoDisplay.load.boxp); 190infoDisplay.load.lbox = document.createElement('div'); 191infoDisplay.load.lbox.className = 'pnlm-lbox'; 192infoDisplay.load.lbox.innerHTML = '<div class="pnlm-loading"></div>'; 193infoDisplay.load.box.appendChild(infoDisplay.load.lbox); 194infoDisplay.load.lbar = document.createElement('div'); 195infoDisplay.load.lbar.className = 'pnlm-lbar'; 196infoDisplay.load.lbarFill = document.createElement('div'); 197infoDisplay.load.lbarFill.className = 'pnlm-lbar-fill'; 198infoDisplay.load.lbar.appendChild(infoDisplay.load.lbarFill); 199infoDisplay.load.box.appendChild(infoDisplay.load.lbar); 200infoDisplay.load.msg = document.createElement('p'); 201infoDisplay.load.msg.className = 'pnlm-lmsg'; 202infoDisplay.load.box.appendChild(infoDisplay.load.msg); 203uiContainer.appendChild(infoDisplay.load.box); 204 205// Error message 206infoDisplay.errorMsg = document.createElement('div'); 207infoDisplay.errorMsg.className = 'pnlm-error-msg pnlm-info-box'; 208uiContainer.appendChild(infoDisplay.errorMsg); 209 210// Create controls 211var controls = {};
212controls.container = document.createElement('div'); 213controls.container.className = 'pnlm-controls-container'; 214uiContainer.appendChild(controls.container); 215 216// Load button 217controls.load = document.createElement('div'); 218controls.load.className = 'pnlm-load-button'; 219controls.load.addEventListener('click', function() { 220 processOptions(); 221 load(); 222}); 223uiContainer.appendChild(controls.load); 224 225// Zoom controls 226controls.zoom = document.createElement('div'); 227controls.zoom.className = 'pnlm-zoom-controls pnlm-controls'; 228controls.zoomIn = document.createElement('div'); 229controls.zoomIn.className = 'pnlm-zoom-in pnlm-sprite pnlm-control'; 230controls.zoomIn.addEventListener('click', zoomIn); 231controls.zoom.appendChild(controls.zoomIn); 232controls.zoomOut = document.createElement('div'); 233controls.zoomOut.className = 'pnlm-zoom-out pnlm-sprite pnlm-control'; 234controls.zoomOut.addEventListener('click', zoomOut); 235controls.zoom.appendChild(controls.zoomOut); 236controls.container.appendChild(controls.zoom); 237 238// Fullscreen toggle 239controls.fullscreen = document.createElement('div'); 240controls.fullscreen.addEventListener('click', toggleFullscreen); 241controls.fullscreen.className = 'pnlm-fullscreen-toggle-button pnlm-sprite pnlm-fullscreen-toggle-button-inactive pnlm-controls pnlm-control'; 242if (document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled || document.msFullscreenEnabled) 243 controls.container.appendChild(controls.fullscreen); 244 245// Device orientation toggle 246controls.orientation = document.createElement('div'); 247controls.orientation.addEventListener('click', function(e) { 248 if (orientation) 249 stopOrientation(); 250 else 251 startOrientation(); 252}); 253controls.orientation.addEventListener('mousedown', function(e) {e.stopPropagation();}); 254controls.orientation.addEventListener('touchstart', function(e) {e.stopPropagation();}); 255controls.orientation.addEventListener('pointerdown', function(e) {e.stopPropagation();}); 256controls.orientation.className = 'pnlm-orientation-button pnlm-orientation-button-inactive pnlm-sprite pnlm-controls pnlm-control'; 257var orientationSupport = false; 258if (window.DeviceOrientationEvent && location.protocol == 'https:' && 259 navigator.userAgent.toLowerCase().indexOf('mobi') >= 0) { 260 // This user agent check is here because there's no way to check if a 261 // device has an inertia measurement unit. We used to be able to check if a 262 // DeviceOrientationEvent had non-null values, but with iOS 13 requiring a 263 // permission prompt to access such events, this is no longer possible. 264 controls.container.appendChild(controls.orientation); 265 orientationSupport = true; 266} 267 268// Compass 269var compass = document.createElement('div'); 270compass.className = 'pnlm-compass pnlm-controls pnlm-control'; 271uiContainer.appendChild(compass); 272 273// Load and process configuration 274if (initialConfig.firstScene) { 275 // Activate first scene if specified in URL 276 mergeConfig(initialConfig.firstScene); 277} else if (initialConfig.default && initialConfig.default.firstScene) { 278 // Activate first scene if specified in file 279 mergeConfig(initialConfig.default.firstScene); 280} else { 281 mergeConfig(null); 282} 283processOptions(true); 284 285/** 286 * Initializes viewer. 287 * @private 288 */ 289function init() { 290 // Display an error for IE 9 as it doesn't work but also doesn't otherwise 291 // show an error (older versions don't work at all) 292 // Based on: http://stackoverflow.com/a/10965203 293 var div = document.createElement("div"); 294 div.innerHTML = "<!--[if lte IE 9]><i></i><![endif]-->"; 295 if (div.getElementsByTagName("i").length == 1) { 296 anError(); 297 return; 298 } 299 300 origHfov = config.hfov; 301 origPitch = config.pitch; 302 303 var i, p; 304 305 if (config.type == 'cubemap') { 306 panoImage = []; 307 for (i = 0; i < 6; i++) { 308 panoImage.push(new Image()); 309 panoImage[i].crossOrigin = config.crossOrigin; 310 } 311 infoDisplay.load.lbox.style.display = 'block'; 312 infoDisplay.load.lbar.style.display = 'none'; 313 } else if (config.type == 'multires') { 314 var c = JSON.parse(JSON.stringify(config.multiRes)); // Deep copy 315 // Avoid "undefined" in path, check (optional) multiRes.basePath, too 316 // Use only multiRes.basePath if it's an absolute URL 317 if (config.basePath && config.multiRes.basePath && 318 !(/^(?:[a-z]+:)?\/\//i.test(config.multiRes.basePath))) { 319 c.basePath = config.basePath + config.multiRes.basePath; 320 } else if (config.multiRes.basePath) { 321 c.basePath = config.multiRes.basePath; 322 } else if(config.basePath) { 323 c.basePath = config.basePath; 324 } 325 panoImage = c; 326 } else { 327 if (config.dynamic === true) { 328 panoImage = config.panorama; 329 } else { 330 if (config.panorama === undefined) { 331 anError(config.strings.noPanoramaError); 332 return; 333 } 334 panoImage = new Image(); 335 } 336 } 337 338 // Configure image loading 339 if (config.type == 'cubemap') { 340 // Quick loading counter for synchronous loading 341 var itemsToLoad = 6; 342 343 var onLoad = function() { 344 itemsToLoad--; 345 if (itemsToLoad === 0) { 346 onImageLoad(); 347 } 348 }; 349 350 var onError = function(e) { 351 var a = document.createElement('a'); 352 a.href = e.target.src; 353 a.textContent = a.href;
vendor: 4,118 bytes, lines 354-462
354 anError(config.strings.fileAccessError.replace('%s', a.outerHTML)); 355 }; 356 357 for (i = 0; i < panoImage.length; i++) { 358 p = config.cubeMap[i]; 359 if (p == "null") { // support partial cubemap image with explicitly empty faces 360 console.log('Will use background instead of missing cubemap face ' + i); 361 onLoad(); 362 } else { 363 if (config.basePath && !absoluteURL(p)) { 364 p = config.basePath + p; 365 } 366 panoImage[i].onload = onLoad; 367 panoImage[i].onerror = onError; 368 panoImage[i].src = sanitizeURL(p); 369 } 370 } 371 } else if (config.type == 'multires') { 372 onImageLoad(); 373 } else { 374 p = ''; 375 if (config.basePath) { 376 p = config.basePath; 377 } 378 379 if (config.dynamic !== true) { 380 // Still image 381 p = absoluteURL(config.panorama) ? config.panorama : p + config.panorama; 382 383 panoImage.onload = function() { 384 window.URL.revokeObjectURL(this.src); // Clean up 385 onImageLoad(); 386 }; 387 388 var xhr = new XMLHttpRequest(); 389 xhr.onloadend = function() { 390 if (xhr.status != 200) { 391 // Display error if image can't be loaded 392 var a = document.createElement('a'); 393 a.href = p; 394 a.textContent = a.href; 395 anError(config.strings.fileAccessError.replace('%s', a.outerHTML)); 396 } 397 var img = this.response; 398 parseGPanoXMP(img); 399 infoDisplay.load.msg.innerHTML = ''; 400 }; 401 xhr.onprogress = function(e) { 402 if (e.lengthComputable) { 403 // Display progress 404 var percent = e.loaded / e.total * 100; 405 infoDisplay.load.lbarFill.style.width = percent + '%'; 406 var unit, numerator, denominator; 407 if (e.total > 1e6) { 408 unit = 'MB'; 409 numerator = (e.loaded / 1e6).toFixed(2); 410 denominator = (e.total / 1e6).toFixed(2); 411 } else if (e.total > 1e3) { 412 unit = 'kB'; 413 numerator = (e.loaded / 1e3).toFixed(1); 414 denominator = (e.total / 1e3).toFixed(1); 415 } else { 416 unit = 'B'; 417 numerator = e.loaded; 418 denominator = e.total; 419 } 420 infoDisplay.load.msg.innerHTML = numerator + ' / ' + denominator + ' ' + unit; 421 } else { 422 // Display loading spinner 423 infoDisplay.load.lbox.style.display = 'block'; 424 infoDisplay.load.lbar.style.display = 'none'; 425 } 426 }; 427 try { 428 xhr.open('GET', p, true); 429 } catch (e) { 430 // Malformed URL 431 anError(config.strings.malformedURLError); 432 } 433 xhr.responseType = 'blob'; 434 xhr.setRequestHeader('Accept', 'image/*,*/*;q=0.9'); 435 xhr.withCredentials = config.crossOrigin === 'use-credentials'; 436 xhr.send(); 437 } 438 } 439 440 if (config.draggable) 441 uiContainer.classList.add('pnlm-grab'); 442 uiContainer.classList.remove('pnlm-grabbing'); 443 444 // Properly handle switching to dynamic scenes 445 update = config.dynamicUpdate === true; 446 if (config.dynamic && update) { 447 panoImage = config.panorama; 448 onImageLoad(); 449 } 450} 451 452/** 453 * Test if URL is absolute or relative. 454 * @private 455 * @param {string} url - URL to test 456 * @returns {boolean} True if absolute, else false 457 */ 458function absoluteURL(url) { 459 // From http://stackoverflow.com/a/19709846 460 return new RegExp('^(?:[a-z]+:)?//', 'i').test(url) || url[0] == '/' || url.slice(0, 5) == 'blob:'; 461} 462
463/** 464 * Create renderer and initialize event listeners once image is loaded. 465 * @private 466 */ 467function onImageLoad() { 468 if (!renderer) 469 renderer = new libpannellum.renderer(renderContainer); 470 471 // Only add event listeners once 472 if (!listenersAdded) { 473 listenersAdded = true; 474 dragFix.addEventListener('mousedown', onDocumentMouseDown, false); 475 document.addEventListener('mousemove', onDocumentMouseMove, false); 476 document.addEventListener('mouseup', onDocumentMouseUp, false); 477 if (config.mouseZoom) { 478 uiContainer.addEventListener('mousewheel', onDocumentMouseWheel, false); 479 uiContainer.addEventListener('DOMMouseScroll', onDocumentMouseWheel, false); 480 } 481 if (config.doubleClickZoom) { 482 dragFix.addEventListener('dblclick', onDocumentDoubleClick, false); 483 } 484 container.addEventListener('mozfullscreenchange', onFullScreenChange, false); 485 container.addEventListener('webkitfullscreenchange', onFullScreenChange, false); 486 container.addEventListener('msfullscreenchange', onFullScreenChange, false); 487 container.addEventListener('fullscreenchange', onFullScreenChange, false); 488 window.addEventListener('resize', onDocumentResize, false); 489 window.addEventListener('orientationchange', onDocumentResize, false); 490 if (!config.disableKeyboardCtrl) { 491 container.addEventListener('keydown', onDocumentKeyPress, false); 492 container.addEventListener('keyup', onDocumentKeyUp, false); 493 container.addEventListener('blur', clearKeys, false); 494 } 495 document.addEventListener('mouseleave', onDocumentMouseUp, false); 496 if (document.documentElement.style.pointerAction === '' && 497 document.documentElement.style.touchAction === '') { 498 dragFix.addEventListener('pointerdown', onDocumentPointerDown, false); 499 dragFix.addEventListener('pointermove', onDocumentPointerMove, false); 500 dragFix.addEventListener('pointerup', onDocumentPointerUp, false); 501 dragFix.addEventListener('pointerleave', onDocumentPointerUp, false); 502 } else { 503 dragFix.addEventListener('touchstart', onDocumentTouchStart, false); 504 dragFix.addEventListener('touchmove', onDocumentTouchMove, false); 505 dragFix.addEventListener('touchend', onDocumentTouchEnd, false); 506 } 507 508 // Deal with MS pointer events 509 if (window.navigator.pointerEnabled) 510 container.style.touchAction = 'none'; 511 } 512 513 renderInit(); 514 setHfov(config.hfov); // possibly adapt hfov after configuration and canvas is complete; prevents empty space on top or bottom by zomming out too much 515 setTimeout(function(){isTimedOut = true;}, 500); 516} 517 518/** 519 * Parses Google Photo Sphere XMP Metadata. 520 * https://developers.google.com/photo-sphere/metadata/ 521 * @private 522 * @param {Image} image - Image to read XMP metadata from. 523 */ 524function parseGPanoXMP(image) { 525 var reader = new FileReader(); 526 reader.addEventListener('loadend', function() { 527 var img = reader.result; 528 529 // This awful browser specific test exists because iOS 8 does not work 530 // with non-progressive encoded JPEGs. 531 if (navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/)) { 532 var flagIndex = img.indexOf('\xff\xc2'); 533 if (flagIndex < 0 || flagIndex > 65536) 534 anError(config.strings.iOS8WebGLError); 535 } 536 537 var start = img.indexOf('<x:xmpmeta'); 538 if (start > -1 && config.ignoreGPanoXMP !== true) { 539 var xmpData = img.substring(start, img.indexOf('</x:xmpmeta>') + 12); 540 541 // Extract the requested tag from the XMP data 542 var getTag = function(tag) { 543 var result; 544 if (xmpData.indexOf(tag + '="') >= 0) { 545 result = xmpData.substring(xmpData.indexOf(tag + '="') + tag.length + 2); 546 result = result.substring(0, result.indexOf('"')); 547 } else if (xmpData.indexOf(tag + '>') >= 0) { 548 result = xmpData.substring(xmpData.indexOf(tag + '>') + tag.length + 1); 549 result = result.substring(0, result.indexOf('<')); 550 } 551 if (result !== undefined) { 552 return Number(result); 553 } 554 return null; 555 }; 556 557 // Relevant XMP data 558 var xmp = { 559 fullWidth: getTag('GPano:FullPanoWidthPixels'), 560 croppedWidth: getTag('GPano:CroppedAreaImageWidthPixels'), 561 fullHeight: getTag('GPano:FullPanoHeightPixels'), 562 croppedHeight: getTag('GPano:CroppedAreaImageHeightPixels'), 563 topPixels: getTag('GPano:CroppedAreaTopPixels'), 564 heading: getTag('GPano:PoseHeadingDegrees'), 565 horizonPitch: getTag('GPano:PosePitchDegrees'), 566 horizonRoll: getTag('GPano:PoseRollDegrees') 567 }; 568 569 if (xmp.fullWidth !== null && xmp.croppedWidth !== null && 570 xmp.fullHeight !== null && xmp.croppedHeight !== null && 571 xmp.topPixels !== null) { 572 573 // Set up viewer using GPano XMP data 574 if (specifiedPhotoSphereExcludes.indexOf('haov') < 0) 575 config.haov = xmp.croppedWidth / xmp.fullWidth * 360; 576 if (specifiedPhotoSphereExcludes.indexOf('vaov') < 0) 577 config.vaov = xmp.croppedHeight / xmp.fullHeight * 180; 578 if (specifiedPhotoSphereExcludes.indexOf('vOffset') < 0) 579 config.vOffset = ((xmp.topPixels + xmp.croppedHeight / 2) / xmp.fullHeight - 0.5) * -180; 580 if (xmp.heading !== null && specifiedPhotoSphereExcludes.indexOf('northOffset') < 0) { 581 // TODO: make sure this works correctly for partial panoramas 582 config.northOffset = xmp.heading; 583 if (config.compass !== false) { 584 config.compass = true; 585 } 586 } 587 if (xmp.horizonPitch !== null && xmp.horizonRoll !== null) { 588 if (specifiedPhotoSphereExcludes.indexOf('horizonPitch') < 0) 589 config.horizonPitch = xmp.horizonPitch; 590 if (specifiedPhotoSphereExcludes.indexOf('horizonRoll') < 0) 591 config.horizonRoll = xmp.horizonRoll; 592 } 593 594 // TODO: add support for initial view settings 595 } 596 } 597 598 // Load panorama 599 panoImage.src = window.URL.createObjectURL(image); 600 }); 601 if (reader.readAsBinaryString !== undefined) 602 reader.readAsBinaryString(image); 603 else 604 reader.readAsText(image); 605} 606 607/** 608 * Displays an error message. 609 * @private 610 * @param {string} errorMsg - Error message to display. If not specified, a 611 * generic WebGL error is displayed. 612 */ 613function anError(errorMsg) { 614 if (errorMsg === undefined) 615 errorMsg = config.strings.genericWebGLError; 616 infoDisplay.errorMsg.innerHTML = '<p>' + errorMsg + '</p>'; 617 controls.load.style.display = 'none'; 618 infoDisplay.load.box.style.display = 'none'; 619 infoDisplay.errorMsg.style.display = 'table'; 620 error = true; 621 loaded = undefined; 622 renderContainer.style.display = 'none'; 623 fireEvent('error', errorMsg); 624} 625 626/** 627 * Hides error message display. 628 * @private 629 */ 630function clearError() { 631 if (error) { 632 infoDisplay.load.box.style.display = 'none'; 633 infoDisplay.errorMsg.style.display = 'none'; 634 error = false;
635 renderContainer.style.display = 'block'; 636 fireEvent('errorcleared'); 637 } 638} 639 640/** 641 * Displays about message. 642 * @private 643 * @param {MouseEvent} event - Right click location 644 */ 645function aboutMessage(event) { 646 var pos = mousePosition(event); 647 aboutMsg.style.left = pos.x + 'px'; 648 aboutMsg.style.top = pos.y + 'px'; 649 clearTimeout(aboutMessage.t1); 650 clearTimeout(aboutMessage.t2); 651 aboutMsg.style.display = 'block'; 652 aboutMsg.style.opacity = 1; 653 aboutMessage.t1 = setTimeout(function() {aboutMsg.style.opacity = 0;}, 2000); 654 aboutMessage.t2 = setTimeout(function() {aboutMsg.style.display = 'none';}, 2500); 655 event.preventDefault(); 656} 657 658/** 659 * Calculate mouse position relative to top left of viewer container. 660 * @private 661 * @param {MouseEvent} event - Mouse event to use in calculation 662 * @returns {Object} Calculated X and Y coordinates 663 */ 664function mousePosition(event) { 665 var bounds = container.getBoundingClientRect(); 666 var pos = {}; 667 // pageX / pageY needed for iOS 668 pos.x = (event.clientX || event.pageX) - bounds.left; 669 pos.y = (event.clientY || event.pageY) - bounds.top; 670 return pos; 671} 672 673/** 674 * Event handler for mouse clicks. Initializes panning. Prints center and click 675 * location coordinates when hot spot debugging is enabled. 676 * @private 677 * @param {MouseEvent} event - Document mouse down event. 678 */ 679function onDocumentMouseDown(event) { 680 // Override default action 681 event.preventDefault(); 682 // But not all of it 683 container.focus(); 684 685 // Only do something if the panorama is loaded 686 if (!loaded || !config.draggable) { 687 return; 688 } 689 690 // Calculate mouse position relative to top left of viewer container 691 var pos = mousePosition(event); 692 693 // Log pitch / yaw of mouse click when debugging / placing hot spots 694 if (config.hotSpotDebug) { 695 var coords = mouseEventToCoords(event); 696 console.log('Pitch: ' + coords[0] + ', Yaw: ' + coords[1] + ', Center Pitch: ' + 697 config.pitch + ', Center Yaw: ' + config.yaw + ', HFOV: ' + config.hfov); 698 } 699 700 // Turn off auto-rotation if enabled 701 stopAnimation(); 702 703 stopOrientation(); 704 config.roll = 0; 705 706 speed.hfov = 0; 707 708 isUserInteracting = true; 709 latestInteraction = Date.now(); 710 711 onPointerDownPointerX = pos.x; 712 onPointerDownPointerY = pos.y; 713 714 onPointerDownYaw = config.yaw; 715 onPointerDownPitch = config.pitch; 716 717 uiContainer.classList.add('pnlm-grabbing'); 718 uiContainer.classList.remove('pnlm-grab'); 719 720 fireEvent('mousedown', event); 721 animateInit(); 722} 723 724/** 725 * Event handler for double clicks. Zooms in at clicked location 726 * @private 727 * @param {MouseEvent} event - Document mouse down event. 728 */ 729function onDocumentDoubleClick(event) { 730 if (config.minHfov === config.hfov) { 731 _this.setHfov(origHfov, 1000); 732 } else { 733 var coords = mouseEventToCoords(event); 734 _this.lookAt(coords[0], coords[1], config.minHfov, 1000); 735 } 736} 737 738/** 739 * Calculate panorama pitch and yaw from location of mouse event. 740 * @private 741 * @param {MouseEvent} event - Document mouse down event. 742 * @returns {number[]} [pitch, yaw] 743 */ 744function mouseEventToCoords(event) { 745 var pos = mousePosition(event); 746 var canvas = renderer.getCanvas(); 747 var canvasWidth = canvas.clientWidth, 748 canvasHeight = canvas.clientHeight; 749 var x = pos.x / canvasWidth * 2 - 1; 750 var y = (1 - pos.y / canvasHeight * 2) * canvasHeight / canvasWidth; 751 var focal = 1 / Math.tan(config.hfov * Math.PI / 360); 752 var s = Math.sin(config.pitch * Math.PI / 180); 753 var c = Math.cos(config.pitch * Math.PI / 180); 754 var a = focal * c - y * s; 755 var root = Math.sqrt(x*x + a*a); 756 var pitch = Math.atan((y * c + focal * s) / root) * 180 / Math.PI; 757 var yaw = Math.atan2(x / root, a / root) * 180 / Math.PI + config.yaw; 758 if (yaw < -180) 759 yaw += 360; 760 if (yaw > 180) 761 yaw -= 360; 762 return [pitch, yaw]; 763} 764 765/** 766 * Event handler for mouse moves. Pans center of view. 767 * @private 768 * @param {MouseEvent} event - Document mouse move event. 769 */ 770function onDocumentMouseMove(event) { 771 if (isUserInteracting && loaded) { 772 latestInteraction = Date.now(); 773 var canvas = renderer.getCanvas(); 774 var canvasWidth = canvas.clientWidth, 775 canvasHeight = canvas.clientHeight; 776 var pos = mousePosition(event); 777 //TODO: This still isn't quite right 778 var yaw = ((Math.atan(onPointerDownPointerX / canvasWidth * 2 - 1) - Math.atan(pos.x / canvasWidth * 2 - 1)) * 180 / Math.PI * config.hfov / 90) + onPointerDownYaw; 779 speed.yaw = (yaw - config.yaw) % 360 * 0.2; 780 config.yaw = yaw; 781 782 var vfov = 2 * Math.atan(Math.tan(config.hfov/360*Math.PI) * canvasHeight / canvasWidth) * 180 / Math.PI; 783 784 var pitch = ((Math.atan(pos.y / canvasHeight * 2 - 1) - Math.atan(onPointerDownPointerY / canvasHeight * 2 - 1)) * 180 / Math.PI * vfov / 90) + onPointerDownPitch; 785 speed.pitch = (pitch - config.pitch) * 0.2; 786 config.pitch = pitch; 787 } 788} 789 790/** 791 * Event handler for mouse up events. Stops panning. 792 * @private 793 */ 794function onDocumentMouseUp(event) { 795 if (!isUserInteracting) { 796 return; 797 } 798 isUserInteracting = false;
799 if (Date.now() - latestInteraction > 15) { 800 // Prevents jump when user rapidly moves mouse, stops, and then 801 // releases the mouse button 802 speed.pitch = speed.yaw = 0; 803 } 804 uiContainer.classList.add('pnlm-grab'); 805 uiContainer.classList.remove('pnlm-grabbing'); 806 latestInteraction = Date.now(); 807 808 fireEvent('mouseup', event); 809} 810 811/** 812 * Event handler for touches. Initializes panning if one touch or zooming if 813 * two touches. 814 * @private 815 * @param {TouchEvent} event - Document touch start event. 816 */ 817function onDocumentTouchStart(event) { 818 // Only do something if the panorama is loaded 819 if (!loaded || !config.draggable) { 820 return; 821 } 822 823 // Turn off auto-rotation if enabled 824 stopAnimation(); 825 826 stopOrientation(); 827 config.roll = 0; 828 829 speed.hfov = 0; 830 831 // Calculate touch position relative to top left of viewer container 832 var pos0 = mousePosition(event.targetTouches[0]); 833 834 onPointerDownPointerX = pos0.x; 835 onPointerDownPointerY = pos0.y; 836 837 if (event.targetTouches.length == 2) { 838 // Down pointer is the center of the two fingers 839 var pos1 = mousePosition(event.targetTouches[1]); 840 onPointerDownPointerX += (pos1.x - pos0.x) * 0.5; 841 onPointerDownPointerY += (pos1.y - pos0.y) * 0.5; 842 onPointerDownPointerDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + 843 (pos0.y - pos1.y) * (pos0.y - pos1.y)); 844 } 845 isUserInteracting = true; 846 latestInteraction = Date.now(); 847 848 onPointerDownYaw = config.yaw; 849 onPointerDownPitch = config.pitch; 850 851 fireEvent('touchstart', event); 852 animateInit(); 853} 854 855/** 856 * Event handler for touch movements. Pans center of view if one touch or 857 * adjusts zoom if two touches. 858 * @private 859 * @param {TouchEvent} event - Document touch move event. 860 */ 861function onDocumentTouchMove(event) { 862 if (!config.draggable) { 863 return; 864 } 865 866 // Override default action 867 event.preventDefault(); 868 if (loaded) { 869 latestInteraction = Date.now(); 870 } 871 if (isUserInteracting && loaded) { 872 var pos0 = mousePosition(event.targetTouches[0]); 873 var clientX = pos0.x; 874 var clientY = pos0.y; 875 876 if (event.targetTouches.length == 2 && onPointerDownPointerDist != -1) { 877 var pos1 = mousePosition(event.targetTouches[1]); 878 clientX += (pos1.x - pos0.x) * 0.5; 879 clientY += (pos1.y - pos0.y) * 0.5; 880 var clientDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + 881 (pos0.y - pos1.y) * (pos0.y - pos1.y)); 882 setHfov(config.hfov + (onPointerDownPointerDist - clientDist) * 0.1); 883 onPointerDownPointerDist = clientDist; 884 } 885 886 // The smaller the config.hfov value (the more zoomed-in the user is), the faster 887 // yaw/pitch are perceived to change on one-finger touchmove (panning) events and vice versa. 888 // To improve usability at both small and large zoom levels (config.hfov values) 889 // we introduce a dynamic pan speed coefficient. 890 // 891 // Currently this seems to *roughly* keep initial drag/pan start position close to 892 // the user's finger while panning regardless of zoom level / config.hfov value. 893 var touchmovePanSpeedCoeff = (config.hfov / 360) * config.touchPanSpeedCoeffFactor; 894 895 var yaw = (onPointerDownPointerX - clientX) * touchmovePanSpeedCoeff + onPointerDownYaw; 896 speed.yaw = (yaw - config.yaw) % 360 * 0.2; 897 config.yaw = yaw; 898 899 var pitch = (clientY - onPointerDownPointerY) * touchmovePanSpeedCoeff + onPointerDownPitch; 900 speed.pitch = (pitch - config.pitch) * 0.2; 901 config.pitch = pitch; 902 } 903} 904 905/** 906 * Event handler for end of touches. Stops panning and/or zooming. 907 * @private 908 */ 909function onDocumentTouchEnd() { 910 isUserInteracting = false; 911 if (Date.now() - latestInteraction > 150) { 912 speed.pitch = speed.yaw = 0; 913 } 914 onPointerDownPointerDist = -1; 915 latestInteraction = Date.now(); 916 917 fireEvent('touchend', event); 918} 919 920var pointerIDs = [], 921 pointerCoordinates = []; 922/** 923 * Event handler for touch starts in IE / Edge. 924 * @private 925 * @param {PointerEvent} event - Document pointer down event. 926 */ 927function onDocumentPointerDown(event) { 928 if (event.pointerType == 'touch') { 929 // Only do something if the panorama is loaded 930 if (!loaded || !config.draggable) 931 return; 932 pointerIDs.push(event.pointerId); 933 pointerCoordinates.push({clientX: event.clientX, clientY: event.clientY}); 934 event.targetTouches = pointerCoordinates; 935 onDocumentTouchStart(event); 936 event.preventDefault(); 937 } 938} 939 940/** 941 * Event handler for touch moves in IE / Edge. 942 * @private 943 * @param {PointerEvent} event - Document pointer move event. 944 */ 945function onDocumentPointerMove(event) { 946 if (event.pointerType == 'touch') { 947 if (!config.draggable) 948 return; 949 for (var i = 0; i < pointerIDs.length; i++) { 950 if (event.pointerId == pointerIDs[i]) { 951 pointerCoordinates[i].clientX = event.clientX; 952 pointerCoordinates[i].clientY = event.clientY; 953 event.targetTouches = pointerCoordinates; 954 onDocumentTouchMove(event); 955 event.preventDefault(); 956 return; 957 } 958 } 959 } 960} 961 962/** 963 * Event handler for touch ends in IE / Edge. 964 * @private 965 * @param {PointerEvent} event - Document pointer up event. 966 */ 967function onDocumentPointerUp(event) { 968 if (event.pointerType == 'touch') { 969 var defined = false;
970 for (var i = 0; i < pointerIDs.length; i++) { 971 if (event.pointerId == pointerIDs[i]) 972 pointerIDs[i] = undefined; 973 if (pointerIDs[i]) 974 defined = true; 975 } 976 if (!defined) { 977 pointerIDs = []; 978 pointerCoordinates = []; 979 onDocumentTouchEnd(); 980 } 981 event.preventDefault(); 982 } 983} 984 985/** 986 * Event handler for mouse wheel. Changes zoom. 987 * @private 988 * @param {WheelEvent} event - Document mouse wheel event. 989 */ 990function onDocumentMouseWheel(event) { 991 // Only do something if the panorama is loaded and mouse wheel zoom is enabled 992 if (!loaded || (config.mouseZoom == 'fullscreenonly' && !fullscreenActive)) { 993 return; 994 } 995 996 event.preventDefault(); 997 998 // Turn off auto-rotation if enabled 999 stopAnimation(); 1000 latestInteraction = Date.now(); 1001 1002 if (event.wheelDeltaY) { 1003 // WebKit 1004 setHfov(config.hfov - event.wheelDeltaY * 0.05); 1005 speed.hfov = event.wheelDelta < 0 ? 1 : -1; 1006 } else if (event.wheelDelta) { 1007 // Opera / Explorer 9 1008 setHfov(config.hfov - event.wheelDelta * 0.05); 1009 speed.hfov = event.wheelDelta < 0 ? 1 : -1; 1010 } else if (event.detail) { 1011 // Firefox 1012 setHfov(config.hfov + event.detail * 1.5); 1013 speed.hfov = event.detail > 0 ? 1 : -1; 1014 } 1015 animateInit(); 1016} 1017 1018/** 1019 * Event handler for key presses. Updates list of currently pressed keys. 1020 * @private 1021 * @param {KeyboardEvent} event - Document key press event. 1022 */ 1023function onDocumentKeyPress(event) { 1024 // Turn off auto-rotation if enabled 1025 stopAnimation(); 1026 latestInteraction = Date.now(); 1027 1028 stopOrientation(); 1029 config.roll = 0; 1030 1031 // Record key pressed 1032 var keynumber = event.which || event.keycode; 1033 1034 // Override default action for keys that are used 1035 if (config.capturedKeyNumbers.indexOf(keynumber) < 0) 1036 return; 1037 event.preventDefault(); 1038 1039 // If escape key is pressed 1040 if (keynumber == 27) { 1041 // If in fullscreen mode 1042 if (fullscreenActive) { 1043 toggleFullscreen(); 1044 } 1045 } else { 1046 // Change key 1047 changeKey(keynumber, true); 1048 } 1049} 1050 1051/** 1052 * Clears list of currently pressed keys. 1053 * @private 1054 */ 1055function clearKeys() { 1056 for (var i = 0; i < 10; i++) { 1057 keysDown[i] = false; 1058 } 1059} 1060 1061/** 1062 * Event handler for key releases. Updates list of currently pressed keys. 1063 * @private 1064 * @param {KeyboardEvent} event - Document key up event. 1065 */ 1066function onDocumentKeyUp(event) { 1067 // Record key pressed 1068 var keynumber = event.which || event.keycode; 1069 1070 // Override default action for keys that are used 1071 if (config.capturedKeyNumbers.indexOf(keynumber) < 0) 1072 return; 1073 event.preventDefault(); 1074 1075 // Change key 1076 changeKey(keynumber, false); 1077} 1078 1079/** 1080 * Updates list of currently pressed keys. 1081 * @private 1082 * @param {number} keynumber - Key number. 1083 * @param {boolean} value - Whether or not key is pressed. 1084 */ 1085function changeKey(keynumber, value) { 1086 var keyChanged = false; 1087 switch(keynumber) { 1088 // If minus key is released 1089 case 109: case 189: case 17: case 173: 1090 if (keysDown[0] != value) { keyChanged = true; } 1091 keysDown[0] = value; break; 1092 1093 // If plus key is released 1094 case 107: case 187: case 16: case 61: 1095 if (keysDown[1] != value) { keyChanged = true; } 1096 keysDown[1] = value; break; 1097 1098 // If up arrow is released 1099 case 38: 1100 if (keysDown[2] != value) { keyChanged = true; } 1101 keysDown[2] = value; break; 1102 1103 // If "w" is released 1104 case 87: 1105 if (keysDown[6] != value) { keyChanged = true; } 1106 keysDown[6] = value; break; 1107 1108 // If down arrow is released 1109 case 40: 1110 if (keysDown[3] != value) { keyChanged = true; } 1111 keysDown[3] = value; break; 1112 1113 // If "s" is released 1114 case 83: 1115 if (keysDown[7] != value) { keyChanged = true; } 1116 keysDown[7] = value; break; 1117 1118 // If left arrow is released 1119 case 37: 1120 if (keysDown[4] != value) { keyChanged = true; } 1121 keysDown[4] = value; break; 1122 1123 // If "a" is released 1124 case 65: 1125 if (keysDown[8] != value) { keyChanged = true; } 1126 keysDown[8] = value; break; 1127 1128 // If right arrow is released 1129 case 39: 1130 if (keysDown[5] != value) { keyChanged = true; } 1131 keysDown[5] = value; break; 1132 1133 // If "d" is released 1134 case 68: 1135 if (keysDown[9] != value) { keyChanged = true; } 1136 keysDown[9] = value; 1137 } 1138 1139 if (keyChanged && value) { 1140 if (typeof performance !== 'undefined' && performance.now()) { 1141 prevTime = performance.now(); 1142 } else { 1143 prevTime = Date.now(); 1144 } 1145 animateInit(); 1146 } 1147} 1148 1149/** 1150 * Pans and/or zooms panorama based on currently pressed keys. Also handles 1151 * panorama "inertia" and auto rotation. 1152 * @private 1153 */ 1154function keyRepeat() { 1155 // Only do something if the panorama is loaded 1156 if (!loaded) { 1157 return; 1158 } 1159 1160 var isKeyDown = false;
1161 1162 var prevPitch = config.pitch; 1163 var prevYaw = config.yaw; 1164 var prevZoom = config.hfov; 1165 1166 var newTime; 1167 if (typeof performance !== 'undefined' && performance.now()) { 1168 newTime = performance.now(); 1169 } else { 1170 newTime = Date.now(); 1171 } 1172 if (prevTime === undefined) { 1173 prevTime = newTime; 1174 } 1175 var diff = (newTime - prevTime) * config.hfov / 1700; 1176 diff = Math.min(diff, 1.0); 1177 1178 // If minus key is down 1179 if (keysDown[0] && config.keyboardZoom === true) { 1180 setHfov(config.hfov + (speed.hfov * 0.8 + 0.5) * diff); 1181 isKeyDown = true; 1182 } 1183 1184 // If plus key is down 1185 if (keysDown[1] && config.keyboardZoom === true) { 1186 setHfov(config.hfov + (speed.hfov * 0.8 - 0.2) * diff); 1187 isKeyDown = true; 1188 } 1189 1190 // If up arrow or "w" is down 1191 if (keysDown[2] || keysDown[6]) { 1192 // Pan up 1193 config.pitch += (speed.pitch * 0.8 + 0.2) * diff; 1194 isKeyDown = true; 1195 } 1196 1197 // If down arrow or "s" is down 1198 if (keysDown[3] || keysDown[7]) { 1199 // Pan down 1200 config.pitch += (speed.pitch * 0.8 - 0.2) * diff; 1201 isKeyDown = true; 1202 } 1203 1204 // If left arrow or "a" is down 1205 if (keysDown[4] || keysDown[8]) { 1206 // Pan left 1207 config.yaw += (speed.yaw * 0.8 - 0.2) * diff; 1208 isKeyDown = true; 1209 } 1210 1211 // If right arrow or "d" is down 1212 if (keysDown[5] || keysDown[9]) { 1213 // Pan right 1214 config.yaw += (speed.yaw * 0.8 + 0.2) * diff; 1215 isKeyDown = true; 1216 } 1217 1218 if (isKeyDown) 1219 latestInteraction = Date.now(); 1220 1221 // If auto-rotate 1222 if (config.autoRotate) { 1223 // Pan 1224 if (newTime - prevTime > 0.001) { 1225 var timeDiff = (newTime - prevTime) / 1000; 1226 var yawDiff = (speed.yaw / timeDiff * diff - config.autoRotate * 0.2) * timeDiff; 1227 yawDiff = (-config.autoRotate > 0 ? 1 : -1) * Math.min(Math.abs(config.autoRotate * timeDiff), Math.abs(yawDiff)); 1228 config.yaw += yawDiff; 1229 } 1230 1231 // Deal with stopping auto rotation after a set delay 1232 if (config.autoRotateStopDelay) { 1233 config.autoRotateStopDelay -= newTime - prevTime; 1234 if (config.autoRotateStopDelay <= 0) { 1235 config.autoRotateStopDelay = false; 1236 autoRotateSpeed = config.autoRotate; 1237 config.autoRotate = 0; 1238 } 1239 } 1240 } 1241 1242 // Animated moves 1243 if (animatedMove.pitch) { 1244 animateMove('pitch'); 1245 prevPitch = config.pitch; 1246 } 1247 if (animatedMove.yaw) { 1248 animateMove('yaw'); 1249 prevYaw = config.yaw; 1250 } 1251 if (animatedMove.hfov) { 1252 animateMove('hfov'); 1253 prevZoom = config.hfov; 1254 } 1255 1256 // "Inertia" 1257 if (diff > 0 && !config.autoRotate) { 1258 // "Friction" 1259 var slowDownFactor = 1 - config.friction; 1260 1261 // Yaw 1262 if (!keysDown[4] && !keysDown[5] && !keysDown[8] && !keysDown[9] && !animatedMove.yaw) { 1263 config.yaw += speed.yaw * diff * slowDownFactor; 1264 } 1265 // Pitch 1266 if (!keysDown[2] && !keysDown[3] && !keysDown[6] && !keysDown[7] && !animatedMove.pitch) { 1267 config.pitch += speed.pitch * diff * slowDownFactor; 1268 } 1269 // Zoom 1270 if (!keysDown[0] && !keysDown[1] && !animatedMove.hfov) { 1271 setHfov(config.hfov + speed.hfov * diff * slowDownFactor); 1272 } 1273 } 1274 1275 prevTime = newTime; 1276 if (diff > 0) { 1277 speed.yaw = speed.yaw * 0.8 + (config.yaw - prevYaw) / diff * 0.2; 1278 speed.pitch = speed.pitch * 0.8 + (config.pitch - prevPitch) / diff * 0.2; 1279 speed.hfov = speed.hfov * 0.8 + (config.hfov - prevZoom) / diff * 0.2; 1280 1281 // Limit speed 1282 var maxSpeed = config.autoRotate ? Math.abs(config.autoRotate) : 5; 1283 speed.yaw = Math.min(maxSpeed, Math.max(speed.yaw, -maxSpeed)); 1284 speed.pitch = Math.min(maxSpeed, Math.max(speed.pitch, -maxSpeed)); 1285 speed.hfov = Math.min(maxSpeed, Math.max(speed.hfov, -maxSpeed)); 1286 } 1287 1288 // Stop movement if opposite controls are pressed 1289 if (keysDown[0] && keysDown[1]) { 1290 speed.hfov = 0; 1291 } 1292 if ((keysDown[2] || keysDown[6]) && (keysDown[3] || keysDown[7])) { 1293 speed.pitch = 0; 1294 } 1295 if ((keysDown[4] || keysDown[8]) && (keysDown[5] || keysDown[9])) { 1296 speed.yaw = 0; 1297 } 1298} 1299 1300/** 1301 * Animates moves. 1302 * @param {string} axis - Axis to animate 1303 * @private 1304 */ 1305function animateMove(axis) { 1306 var t = animatedMove[axis]; 1307 var normTime = Math.min(1, Math.max((Date.now() - t.startTime) / 1000 / (t.duration / 1000), 0)); 1308 var result = t.startPosition + config.animationTimingFunction(normTime) * (t.endPosition - t.startPosition); 1309 if ((t.endPosition > t.startPosition && result >= t.endPosition) || 1310 (t.endPosition < t.startPosition && result <= t.endPosition) || 1311 t.endPosition === t.startPosition) { 1312 result = t.endPosition; 1313 speed[axis] = 0; 1314 delete animatedMove[axis]; 1315 } 1316 config[axis] = result; 1317} 1318 1319/** 1320 * @param {number} t - Normalized time in animation 1321 * @return {number} Position in animation 1322 * @private 1323 */ 1324function timingFunction(t) { 1325 // easeInOutQuad from https://gist.github.com/gre/1650294 1326 return t < 0.5 ? 2*t*t : -1+(4-2*t)*t; 1327} 1328
1329/** 1330 * Event handler for document resizes. Updates viewer size and rerenders view. 1331 * @private 1332 */ 1333function onDocumentResize() { 1334 // Resize panorama renderer (moved to onFullScreenChange) 1335 //renderer.resize(); 1336 //animateInit(); 1337 1338 // Kludge to deal with WebKit regression: https://bugs.webkit.org/show_bug.cgi?id=93525 1339 onFullScreenChange('resize'); 1340} 1341 1342/** 1343 * Initializes animation. 1344 * @private 1345 */ 1346function animateInit() { 1347 if (animating) { 1348 return; 1349 } 1350 animating = true; 1351 animate(); 1352} 1353 1354/** 1355 * Animates view, using requestAnimationFrame to trigger rendering. 1356 * @private 1357 */ 1358function animate() { 1359 if (destroyed) { 1360 return; 1361 } 1362 1363 render(); 1364 if (autoRotateStart) 1365 clearTimeout(autoRotateStart); 1366 if (isUserInteracting || orientation === true) { 1367 requestAnimationFrame(animate); 1368 } else if (keysDown[0] || keysDown[1] || keysDown[2] || keysDown[3] || 1369 keysDown[4] || keysDown[5] || keysDown[6] || keysDown[7] || 1370 keysDown[8] || keysDown[9] || config.autoRotate || 1371 animatedMove.pitch || animatedMove.yaw || animatedMove.hfov || 1372 Math.abs(speed.yaw) > 0.01 || Math.abs(speed.pitch) > 0.01 || 1373 Math.abs(speed.hfov) > 0.01) { 1374 1375 keyRepeat(); 1376 if (config.autoRotateInactivityDelay >= 0 && autoRotateSpeed && 1377 Date.now() - latestInteraction > config.autoRotateInactivityDelay && 1378 !config.autoRotate) { 1379 config.autoRotate = autoRotateSpeed; 1380 _this.lookAt(origPitch, undefined, origHfov, 3000); 1381 } 1382 requestAnimationFrame(animate); 1383 } else if (renderer && (renderer.isLoading() || (config.dynamic === true && update))) { 1384 requestAnimationFrame(animate); 1385 } else { 1386 fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()}); 1387 animating = false; 1388 prevTime = undefined; 1389 var autoRotateStartTime = config.autoRotateInactivityDelay - 1390 (Date.now() - latestInteraction); 1391 if (autoRotateStartTime > 0) { 1392 autoRotateStart = setTimeout(function() { 1393 config.autoRotate = autoRotateSpeed; 1394 _this.lookAt(origPitch, undefined, origHfov, 3000); 1395 animateInit(); 1396 }, autoRotateStartTime); 1397 } else if (config.autoRotateInactivityDelay >= 0 && autoRotateSpeed) { 1398 config.autoRotate = autoRotateSpeed; 1399 _this.lookAt(origPitch, undefined, origHfov, 3000); 1400 animateInit(); 1401 } 1402 } 1403} 1404 1405/** 1406 * Renders panorama view. 1407 * @private 1408 */ 1409function render() { 1410 var tmpyaw; 1411 1412 if (loaded) { 1413 var canvas = renderer.getCanvas(); 1414 1415 if (config.autoRotate !== false) { 1416 // When auto-rotating this check needs to happen first (see issue #764) 1417 if (config.yaw > 360) { 1418 config.yaw -= 360; 1419 } else if (config.yaw < -360) { 1420 config.yaw += 360; 1421 } 1422 } 1423 1424 // Keep a tmp value of yaw for autoRotate comparison later 1425 tmpyaw = config.yaw; 1426 1427 // Optionally avoid showing background (empty space) on left or right by adapting min/max yaw 1428 var hoffcut = 0, 1429 voffcut = 0; 1430 if (config.avoidShowingBackground) { 1431 var hfov2 = config.hfov / 2, 1432 vfov2 = Math.atan2(Math.tan(hfov2 / 180 * Math.PI), (canvas.width / canvas.height)) * 180 / Math.PI, 1433 transposed = config.vaov > config.haov; 1434 if (transposed) { 1435 voffcut = vfov2 * (1 - Math.min(Math.cos((config.pitch - hfov2) / 180 * Math.PI), 1436 Math.cos((config.pitch + hfov2) / 180 * Math.PI))); 1437 } else { 1438 hoffcut = hfov2 * (1 - Math.min(Math.cos((config.pitch - vfov2) / 180 * Math.PI), 1439 Math.cos((config.pitch + vfov2) / 180 * Math.PI))); 1440 } 1441 } 1442 1443 // Ensure the yaw is within min and max allowed 1444 var yawRange = config.maxYaw - config.minYaw, 1445 minYaw = -180, 1446 maxYaw = 180; 1447 if (yawRange < 360) { 1448 minYaw = config.minYaw + config.hfov / 2 + hoffcut; 1449 maxYaw = config.maxYaw - config.hfov / 2 - hoffcut; 1450 if (yawRange < config.hfov) { 1451 // Lock yaw to average of min and max yaw when both can be seen at once 1452 minYaw = maxYaw = (minYaw + maxYaw) / 2; 1453 }
1454 config.yaw = Math.max(minYaw, Math.min(maxYaw, config.yaw)); 1455 } 1456 1457 if (!(config.autoRotate !== false)) { 1458 // When not auto-rotating, this check needs to happen after the 1459 // previous check (see issue #698) 1460 if (config.yaw > 360) { 1461 config.yaw -= 360; 1462 } else if (config.yaw < -360) { 1463 config.yaw += 360; 1464 } 1465 } 1466 1467 // Check if we autoRotate in a limited by min and max yaw 1468 // If so reverse direction 1469 if (config.autoRotate !== false && tmpyaw != config.yaw && 1470 prevTime !== undefined) { // this condition prevents changing the direction initially 1471 config.autoRotate *= -1; 1472 } 1473 1474 // Ensure the calculated pitch is within min and max allowed 1475 var vfov = 2 * Math.atan(Math.tan(config.hfov / 180 * Math.PI * 0.5) / 1476 (canvas.width / canvas.height)) / Math.PI * 180; 1477 var minPitch = config.minPitch + vfov / 2, 1478 maxPitch = config.maxPitch - vfov / 2; 1479 var pitchRange = config.maxPitch - config.minPitch; 1480 if (pitchRange < vfov) { 1481 // Lock pitch to average of min and max pitch when both can be seen at once 1482 minPitch = maxPitch = (minPitch + maxPitch) / 2; 1483 } 1484 if (isNaN(minPitch)) 1485 minPitch = -90; 1486 if (isNaN(maxPitch)) 1487 maxPitch = 90; 1488 config.pitch = Math.max(minPitch, Math.min(maxPitch, config.pitch)); 1489 1490 renderer.render(config.pitch * Math.PI / 180, config.yaw * Math.PI / 180, config.hfov * Math.PI / 180, {roll: config.roll * Math.PI / 180}); 1491 1492 renderHotSpots(); 1493 1494 // Update compass 1495 if (config.compass) { 1496 compass.style.transform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; 1497 compass.style.webkitTransform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; 1498 } 1499 } 1500} 1501 1502/** 1503 * Creates a new quaternion. 1504 * @private 1505 * @constructor 1506 * @param {Number} w - W value 1507 * @param {Number} x - X value 1508 * @param {Number} y - Y value 1509 * @param {Number} z - Z value 1510 */ 1511function Quaternion(w, x, y, z) { 1512 this.w = w; 1513 this.x = x; 1514 this.y = y; 1515 this.z = z; 1516} 1517 1518/** 1519 * Multiplies quaternions. 1520 * @private 1521 * @param {Quaternion} q - Quaternion to multiply 1522 * @returns {Quaternion} Result of multiplication 1523 */ 1524Quaternion.prototype.multiply = function(q) { 1525 return new Quaternion(this.w*q.w - this.x*q.x - this.y*q.y - this.z*q.z, 1526 this.x*q.w + this.w*q.x + this.y*q.z - this.z*q.y, 1527 this.y*q.w + this.w*q.y + this.z*q.x - this.x*q.z, 1528 this.z*q.w + this.w*q.z + this.x*q.y - this.y*q.x); 1529}; 1530 1531/** 1532 * Converts quaternion to Euler angles. 1533 * @private 1534 * @returns {Number[]} [phi angle, theta angle, psi angle] 1535 */ 1536Quaternion.prototype.toEulerAngles = function() { 1537 var phi = Math.atan2(2 * (this.w * this.x + this.y * this.z), 1538 1 - 2 * (this.x * this.x + this.y * this.y)), 1539 theta = Math.asin(2 * (this.w * this.y - this.z * this.x)), 1540 psi = Math.atan2(2 * (this.w * this.z + this.x * this.y), 1541 1 - 2 * (this.y * this.y + this.z * this.z)); 1542 return [phi, theta, psi]; 1543}; 1544 1545/** 1546 * Converts device orientation API Tait-Bryan angles to a quaternion. 1547 * @private 1548 * @param {Number} alpha - Alpha angle (in degrees) 1549 * @param {Number} beta - Beta angle (in degrees) 1550 * @param {Number} gamma - Gamma angle (in degrees) 1551 * @returns {Quaternion} Orientation quaternion 1552 */ 1553function taitBryanToQuaternion(alpha, beta, gamma) { 1554 var r = [beta ? beta * Math.PI / 180 / 2 : 0, 1555 gamma ? gamma * Math.PI / 180 / 2 : 0, 1556 alpha ? alpha * Math.PI / 180 / 2 : 0]; 1557 var c = [Math.cos(r[0]), Math.cos(r[1]), Math.cos(r[2])], 1558 s = [Math.sin(r[0]), Math.sin(r[1]), Math.sin(r[2])]; 1559 1560 return new Quaternion(c[0]*c[1]*c[2] - s[0]*s[1]*s[2], 1561 s[0]*c[1]*c[2] - c[0]*s[1]*s[2], 1562 c[0]*s[1]*c[2] + s[0]*c[1]*s[2], 1563 c[0]*c[1]*s[2] + s[0]*s[1]*c[2]); 1564} 1565 1566/** 1567 * Computes current device orientation quaternion from device orientation API 1568 * Tait-Bryan angles. 1569 * @private 1570 * @param {Number} alpha - Alpha angle (in degrees) 1571 * @param {Number} beta - Beta angle (in degrees) 1572 * @param {Number} gamma - Gamma angle (in degrees) 1573 * @returns {Quaternion} Orientation quaternion 1574 */ 1575function computeQuaternion(alpha, beta, gamma) { 1576 // Convert Tait-Bryan angles to quaternion 1577 var quaternion = taitBryanToQuaternion(alpha, beta, gamma); 1578 // Apply world transform 1579 quaternion = quaternion.multiply(new Quaternion(Math.sqrt(0.5), -Math.sqrt(0.5), 0, 0)); 1580 // Apply screen transform 1581 var angle = window.orientation ? -window.orientation * Math.PI / 180 / 2 : 0; 1582 return quaternion.multiply(new Quaternion(Math.cos(angle), 0, -Math.sin(angle), 0)); 1583} 1584 1585/** 1586 * Event handler for device orientation API. Controls pointing. 1587 * @private 1588 * @param {DeviceOrientationEvent} event - Device orientation event. 1589 */ 1590function orientationListener(e) { 1591 var q = computeQuaternion(e.alpha, e.beta, e.gamma).toEulerAngles(); 1592 if (typeof(orientation) == 'number' && orientation < 10) { 1593 // This kludge is necessary because iOS sometimes provides a few stale 1594 // device orientation events when the listener is removed and then 1595 // readded. Thus, we skip the first 10 events to prevent this from 1596 // causing problems. 1597 orientation += 1; 1598 } else if (orientation === 10) { 1599 // Record starting yaw to prevent jumping 1600 orientationYawOffset = q[2] / Math.PI * 180 + config.yaw; 1601 orientation = true; 1602 requestAnimationFrame(animate); 1603 } else { 1604 config.pitch = q[0] / Math.PI * 180; 1605 config.roll = -q[1] / Math.PI * 180; 1606 config.yaw = -q[2] / Math.PI * 180 + orientationYawOffset; 1607 } 1608} 1609 1610/** 1611 * Initializes renderer. 1612 * @private 1613 */ 1614function renderInit() { 1615 try { 1616 var params = {}; 1617 if (config.horizonPitch !== undefined) 1618 params.horizonPitch = config.horizonPitch * Math.PI / 180; 1619 if (config.horizonRoll !== undefined) 1620 params.horizonRoll = config.horizonRoll * Math.PI / 180; 1621 if (config.backgroundColor !== undefined) 1622 params.backgroundColor = config.backgroundColor; 1623 renderer.init(panoImage, config.type, config.dynamic, config.haov * Math.PI / 180, config.vaov * Math.PI / 180, config.vOffset * Math.PI / 180, renderInitCallback, params); 1624 if (config.dynamic !== true) { 1625 // Allow image to be garbage collected 1626 panoImage = undefined; 1627 } 1628 } catch(event) { 1629 // Panorama not loaded 1630 1631 // Display error if there is a bad texture 1632 if (event.type == 'webgl error' || event.type == 'no webgl') { 1633 anError(); 1634 } else if (event.type == 'webgl size error') { 1635 anError(config.strings.textureSizeError.replace('%s', event.width).replace('%s', event.maxWidth)); 1636 } else { 1637 anError(config.strings.unknownError); 1638 throw event; 1639 } 1640 } 1641} 1642 1643/** 1644 * Triggered when render initialization finishes. Handles fading between 1645 * scenes as well as showing the compass and hotspots and hiding the loading 1646 * display. 1647 * @private 1648 */ 1649function renderInitCallback() { 1650 // Fade if specified 1651 if (config.sceneFadeDuration && renderer.fadeImg !== undefined) { 1652 renderer.fadeImg.style.opacity = 0; 1653 // Remove image 1654 var fadeImg = renderer.fadeImg; 1655 delete renderer.fadeImg; 1656 setTimeout(function() { 1657 renderContainer.removeChild(fadeImg); 1658 fireEvent('scenechangefadedone'); 1659 }, config.sceneFadeDuration); 1660 } 1661 1662 // Show compass if applicable 1663 if (config.compass) { 1664 compass.style.display = 'inline'; 1665 } else { 1666 compass.style.display = 'none'; 1667 } 1668 1669 // Show hotspots 1670 createHotSpots(); 1671 1672 // Hide loading display 1673 infoDisplay.load.box.style.display = 'none'; 1674 if (preview !== undefined) { 1675 renderContainer.removeChild(preview); 1676 preview = undefined; 1677 } 1678 loaded = true; 1679 1680 fireEvent('load'); 1681 1682 animateInit(); 1683} 1684 1685/** 1686 * Creates hot spot element for the current scene. 1687 * @private 1688 * @param {Object} hs - The configuration for the hotspot 1689 */ 1690function createHotSpot(hs) { 1691 // Make sure hot spot pitch and yaw are numbers 1692 hs.pitch = Number(hs.pitch) || 0; 1693 hs.yaw = Number(hs.yaw) || 0; 1694 1695 var div = document.createElement('div'); 1696 div.className = 'pnlm-hotspot-base'; 1697 if (hs.cssClass) 1698 div.className += ' ' + hs.cssClass; 1699 else 1700 div.className += ' pnlm-hotspot pnlm-sprite pnlm-' + escapeHTML(hs.type); 1701 1702 var span = document.createElement('span'); 1703 if (hs.text) 1704 span.innerHTML = escapeHTML(hs.text); 1705 1706 var a; 1707 if (hs.video) { 1708 var video = document.createElement('video'), 1709 vidp = hs.video; 1710 if (config.basePath && !absoluteURL(vidp)) 1711 vidp = config.basePath + vidp; 1712 video.src = sanitizeURL(vidp); 1713 video.controls = true; 1714 video.style.width = hs.width + 'px'; 1715 renderContainer.appendChild(div); 1716 span.appendChild(video); 1717 } else if (hs.image) { 1718 var imgp = hs.image; 1719 if (config.basePath && !absoluteURL(imgp)) 1720 imgp = config.basePath + imgp; 1721 a = document.createElement('a'); 1722 a.href = sanitizeURL(hs.URL ? hs.URL : imgp, true); 1723 a.target = '_blank'; 1724 span.appendChild(a); 1725 var image = document.createElement('img'); 1726 image.src = sanitizeURL(imgp); 1727 image.style.width = hs.width + 'px'; 1728 image.style.paddingTop = '5px'; 1729 renderContainer.appendChild(div); 1730 a.appendChild(image); 1731 span.style.maxWidth = 'initial'; 1732 } else if (hs.URL) { 1733 a = document.createElement('a'); 1734 a.href = sanitizeURL(hs.URL, true); 1735 if (hs.attributes) { 1736 for (var key in hs.attributes) { 1737 a.setAttribute(key, hs.attributes[key]); 1738 } 1739 } else { 1740 a.target = '_blank'; 1741 } 1742 renderContainer.appendChild(a); 1743 div.className += ' pnlm-pointer'; 1744 span.className += ' pnlm-pointer'; 1745 a.appendChild(div); 1746 } else { 1747 if (hs.sceneId) { 1748 div.onclick = div.ontouchend = function() { 1749 if (!div.clicked) { 1750 div.clicked = true; 1751 loadScene(hs.sceneId, hs.targetPitch, hs.targetYaw, hs.targetHfov); 1752 } 1753 return false;
1754 }; 1755 div.className += ' pnlm-pointer'; 1756 span.className += ' pnlm-pointer'; 1757 } 1758 renderContainer.appendChild(div); 1759 } 1760 1761 if (hs.createTooltipFunc) { 1762 hs.createTooltipFunc(div, hs.createTooltipArgs); 1763 } else if (hs.text || hs.video || hs.image) { 1764 div.classList.add('pnlm-tooltip'); 1765 div.appendChild(span); 1766 span.style.width = span.scrollWidth - 20 + 'px'; 1767 span.style.marginLeft = -(span.scrollWidth - div.offsetWidth) / 2 + 'px'; 1768 span.style.marginTop = -span.scrollHeight - 12 + 'px'; 1769 } 1770 if (hs.clickHandlerFunc) { 1771 div.addEventListener('click', function(e) { 1772 hs.clickHandlerFunc(e, hs.clickHandlerArgs); 1773 }, 'false'); 1774 div.className += ' pnlm-pointer'; 1775 span.className += ' pnlm-pointer'; 1776 } 1777 hs.div = div; 1778} 1779 1780/** 1781 * Creates hot spot elements for the current scene. 1782 * @private 1783 */ 1784function createHotSpots() { 1785 if (hotspotsCreated) return; 1786 1787 if (!config.hotSpots) { 1788 config.hotSpots = []; 1789 } else { 1790 // Sort by pitch so tooltip is never obscured by another hot spot 1791 config.hotSpots = config.hotSpots.sort(function(a, b) { 1792 return a.pitch < b.pitch; 1793 }); 1794 config.hotSpots.forEach(createHotSpot); 1795 } 1796 hotspotsCreated = true; 1797 renderHotSpots(); 1798} 1799 1800/** 1801 * Destroys currently created hot spot elements. 1802 * @private 1803 */ 1804function destroyHotSpots() { 1805 var hs = config.hotSpots; 1806 hotspotsCreated = false; 1807 delete config.hotSpots; 1808 if (hs) { 1809 for (var i = 0; i < hs.length; i++) { 1810 var current = hs[i].div; 1811 if (current) { 1812 while (current.parentNode && current.parentNode != renderContainer) { 1813 current = current.parentNode; 1814 } 1815 renderContainer.removeChild(current); 1816 } 1817 delete hs[i].div; 1818 } 1819 } 1820} 1821 1822/** 1823 * Renders hot spot, updating its position and visibility. 1824 * @private 1825 */ 1826function renderHotSpot(hs) { 1827 var hsPitchSin = Math.sin(hs.pitch * Math.PI / 180), 1828 hsPitchCos = Math.cos(hs.pitch * Math.PI / 180), 1829 configPitchSin = Math.sin(config.pitch * Math.PI / 180), 1830 configPitchCos = Math.cos(config.pitch * Math.PI / 180), 1831 yawCos = Math.cos((-hs.yaw + config.yaw) * Math.PI / 180); 1832 var z = hsPitchSin * configPitchSin + hsPitchCos * yawCos * configPitchCos; 1833 if ((hs.yaw <= 90 && hs.yaw > -90 && z <= 0) || 1834 ((hs.yaw > 90 || hs.yaw <= -90) && z <= 0)) { 1835 hs.div.style.visibility = 'hidden'; 1836 } else { 1837 var yawSin = Math.sin((-hs.yaw + config.yaw) * Math.PI / 180), 1838 hfovTan = Math.tan(config.hfov * Math.PI / 360); 1839 hs.div.style.visibility = 'visible'; 1840 // Subpixel rendering doesn't work in Firefox 1841 // https://bugzilla.mozilla.org/show_bug.cgi?id=739176 1842 var canvas = renderer.getCanvas(), 1843 canvasWidth = canvas.clientWidth, 1844 canvasHeight = canvas.clientHeight; 1845 var coord = [-canvasWidth / hfovTan * yawSin * hsPitchCos / z / 2, 1846 -canvasWidth / hfovTan * (hsPitchSin * configPitchCos - 1847 hsPitchCos * yawCos * configPitchSin) / z / 2]; 1848 // Apply roll 1849 var rollSin = Math.sin(config.roll * Math.PI / 180), 1850 rollCos = Math.cos(config.roll * Math.PI / 180); 1851 coord = [coord[0] * rollCos - coord[1] * rollSin, 1852 coord[0] * rollSin + coord[1] * rollCos]; 1853 // Apply transform 1854 coord[0] += (canvasWidth - hs.div.offsetWidth) / 2; 1855 coord[1] += (canvasHeight - hs.div.offsetHeight) / 2; 1856 var transform = 'translate(' + coord[0] + 'px, ' + coord[1] + 1857 'px) translateZ(9999px) rotate(' + config.roll + 'deg)'; 1858 if (hs.scale) { 1859 transform += ' scale(' + (origHfov/config.hfov) / z + ')'; 1860 } 1861 hs.div.style.webkitTransform = transform; 1862 hs.div.style.MozTransform = transform; 1863 hs.div.style.transform = transform; 1864 } 1865} 1866 1867/** 1868 * Renders hot spots, updating their positions and visibility. 1869 * @private 1870 */ 1871function renderHotSpots() {
1872 config.hotSpots.forEach(renderHotSpot); 1873} 1874 1875/** 1876 * Merges a scene configuration into the current configuration. 1877 * @private 1878 * @param {string} sceneId - Identifier of scene configuration to merge in. 1879 */ 1880function mergeConfig(sceneId) { 1881 config = {}; 1882 var k, s; 1883 var photoSphereExcludes = ['haov', 'vaov', 'vOffset', 'northOffset', 'horizonPitch', 'horizonRoll']; 1884 specifiedPhotoSphereExcludes = []; 1885 1886 // Merge default config 1887 for (k in defaultConfig) { 1888 if (defaultConfig.hasOwnProperty(k)) { 1889 config[k] = defaultConfig[k]; 1890 } 1891 } 1892 1893 // Merge default scene config 1894 for (k in initialConfig.default) { 1895 if (initialConfig.default.hasOwnProperty(k)) { 1896 if (k == 'strings') { 1897 for (s in initialConfig.default.strings) { 1898 if (initialConfig.default.strings.hasOwnProperty(s)) { 1899 config.strings[s] = escapeHTML(initialConfig.default.strings[s]); 1900 } 1901 } 1902 } else { 1903 config[k] = initialConfig.default[k]; 1904 if (photoSphereExcludes.indexOf(k) >= 0) { 1905 specifiedPhotoSphereExcludes.push(k); 1906 } 1907 } 1908 } 1909 } 1910 1911 // Merge current scene config 1912 if ((sceneId !== null) && (sceneId !== '') && (initialConfig.scenes) && (initialConfig.scenes[sceneId])) { 1913 var scene = initialConfig.scenes[sceneId]; 1914 for (k in scene) { 1915 if (scene.hasOwnProperty(k)) { 1916 if (k == 'strings') { 1917 for (s in scene.strings) { 1918 if (scene.strings.hasOwnProperty(s)) { 1919 config.strings[s] = escapeHTML(scene.strings[s]); 1920 } 1921 } 1922 } else { 1923 config[k] = scene[k]; 1924 if (photoSphereExcludes.indexOf(k) >= 0) { 1925 specifiedPhotoSphereExcludes.push(k); 1926 } 1927 } 1928 } 1929 } 1930 config.scene = sceneId; 1931 } 1932 1933 // Merge initial config 1934 for (k in initialConfig) { 1935 if (initialConfig.hasOwnProperty(k)) { 1936 if (k == 'strings') { 1937 for (s in initialConfig.strings) { 1938 if (initialConfig.strings.hasOwnProperty(s)) { 1939 config.strings[s] = escapeHTML(initialConfig.strings[s]); 1940 } 1941 } 1942 } else { 1943 config[k] = initialConfig[k]; 1944 if (photoSphereExcludes.indexOf(k) >= 0) { 1945 specifiedPhotoSphereExcludes.push(k); 1946 } 1947 } 1948 } 1949 } 1950} 1951 1952/** 1953 * Processes configuration options. 1954 * @param {boolean} [isPreview] - Whether or not the preview is being displayed 1955 * @private 1956 */ 1957function processOptions(isPreview) { 1958 isPreview = isPreview ? isPreview : false; 1959 1960 // Process preview first so it always loads before the browser hits its 1961 // maximum number of connections to a server as can happen with cubic 1962 // panoramas 1963 if (isPreview && 'preview' in config) { 1964 var p = config.preview; 1965 if (config.basePath && !absoluteURL(p)) 1966 p = config.basePath + p; 1967 preview = document.createElement('div'); 1968 preview.className = 'pnlm-preview-img'; 1969 preview.style.backgroundImage = "url('" + sanitizeURLForCss(p) + "')"; 1970 renderContainer.appendChild(preview); 1971 } 1972 1973 // Handle different preview values 1974 var title = config.title, 1975 author = config.author; 1976 if (isPreview) { 1977 if ('previewTitle' in config) 1978 config.title = config.previewTitle; 1979 if ('previewAuthor' in config) 1980 config.author = config.previewAuthor; 1981 } 1982 1983 // Reset title / author display 1984 if (!config.hasOwnProperty('title')) 1985 infoDisplay.title.innerHTML = ''; 1986 if (!config.hasOwnProperty('author')) 1987 infoDisplay.author.innerHTML = ''; 1988 if (!config.hasOwnProperty('title') && !config.hasOwnProperty('author')) 1989 infoDisplay.container.style.display = 'none'; 1990 1991 // Fill in load button label and loading box text 1992 controls.load.innerHTML = '<p>' + config.strings.loadButtonLabel + '</p>'; 1993 infoDisplay.load.boxp.innerHTML = config.strings.loadingLabel; 1994 1995 // Process other options 1996 for (var key in config) { 1997 if (config.hasOwnProperty(key)) { 1998 switch(key) { 1999 case 'title': 2000 infoDisplay.title.innerHTML = escapeHTML(config[key]); 2001 infoDisplay.container.style.display = 'inline'; 2002 break; 2003 2004 case 'author': 2005 var authorText = escapeHTML(config[key]); 2006 if (config.authorURL) { 2007 var authorLink = document.createElement('a'); 2008 authorLink.href = sanitizeURL(config['authorURL'], true); 2009 authorLink.target = '_blank'; 2010 authorLink.innerHTML = escapeHTML(config[key]); 2011 authorText = authorLink.outerHTML; 2012 } 2013 infoDisplay.author.innerHTML = config.strings.bylineLabel.replace('%s', authorText); 2014 infoDisplay.container.style.display = 'inline'; 2015 break; 2016 2017 case 'fallback': 2018 var link = document.createElement('a'); 2019 link.href = sanitizeURL(config[key], true); 2020 link.target = '_blank'; 2021 link.textContent = 'Click here to view this panorama in an alternative viewer.'; 2022 var message = document.createElement('p'); 2023 message.textContent = 'Your browser does not support WebGL.'; 2024 message.appendChild(document.createElement('br')); 2025 message.appendChild(link); 2026 infoDisplay.errorMsg.innerHTML = ''; // Removes all children nodes 2027 infoDisplay.errorMsg.appendChild(message); 2028 break; 2029 2030 case 'hfov': 2031 setHfov(Number(config[key])); 2032 break; 2033 2034 case 'autoLoad': 2035 if (config[key] === true && renderer === undefined) { 2036 // Show loading box 2037 infoDisplay.load.box.style.display = 'inline'; 2038 // Hide load button 2039 controls.load.style.display = 'none'; 2040 // Initialize 2041 init(); 2042 } 2043 break; 2044 2045 case 'showZoomCtrl': 2046 if (config[key] && config.showControls != false) { 2047 // Show zoom controls 2048 controls.zoom.style.display = 'block'; 2049 } else { 2050 // Hide zoom controls 2051 controls.zoom.style.display = 'none'; 2052 } 2053 break; 2054 2055 case 'showFullscreenCtrl': 2056 if (config[key] && config.showControls != false && ('fullscreen' in document || 'mozFullScreen' in document || 2057 'webkitIsFullScreen' in document || 'msFullscreenElement' in document)) { 2058 2059 // Show fullscreen control 2060 controls.fullscreen.style.display = 'block'; 2061 } else { 2062 // Hide fullscreen control 2063 controls.fullscreen.style.display = 'none'; 2064 } 2065 break; 2066 2067 case 'hotSpotDebug': 2068 if (config[key]) 2069 hotSpotDebugIndicator.style.display = 'block'; 2070 else 2071 hotSpotDebugIndicator.style.display = 'none'; 2072 break; 2073 2074 case 'showControls': 2075 if (!config[key]) { 2076 controls.orientation.style.display = 'none'; 2077 controls.zoom.style.display = 'none'; 2078 controls.fullscreen.style.display = 'none'; 2079 } 2080 break; 2081 2082 case 'orientationOnByDefault': 2083 if (config[key]) 2084 startOrientation(); 2085 break; 2086 } 2087 } 2088 } 2089 2090 if (isPreview) { 2091 // Restore original values if changed for preview 2092 if (title) 2093 config.title = title; 2094 else 2095 delete config.title; 2096 if (author) 2097 config.author = author; 2098 else 2099 delete config.author; 2100 } 2101} 2102 2103/** 2104 * Toggles fullscreen mode. 2105 * @private 2106 */ 2107function toggleFullscreen() { 2108 if (loaded && !error) { 2109 if (!fullscreenActive) { 2110 try { 2111 if (container.requestFullscreen) { 2112 container.requestFullscreen(); 2113 } else if (container.mozRequestFullScreen) { 2114 container.mozRequestFullScreen(); 2115 } else if (container.msRequestFullscreen) { 2116 container.msRequestFullscreen(); 2117 } else { 2118 container.webkitRequestFullScreen(); 2119 } 2120 } catch(event) { 2121 // Fullscreen doesn't work 2122 } 2123 } else { 2124 if (document.exitFullscreen) { 2125 document.exitFullscreen(); 2126 } else if (document.mozCancelFullScreen) { 2127 document.mozCancelFullScreen(); 2128 } else if (document.webkitCancelFullScreen) { 2129 document.webkitCancelFullScreen(); 2130 } else if (document.msExitFullscreen) { 2131 document.msExitFullscreen(); 2132 } 2133 } 2134 } 2135} 2136 2137/** 2138 * Event handler for fullscreen changes. 2139 * @private 2140 */ 2141function onFullScreenChange(resize) { 2142 if (document.fullscreenElement || document.fullscreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement) { 2143 controls.fullscreen.classList.add('pnlm-fullscreen-toggle-button-active'); 2144 fullscreenActive = true; 2145 } else { 2146 controls.fullscreen.classList.remove('pnlm-fullscreen-toggle-button-active'); 2147 fullscreenActive = false;
2148 } 2149 if (resize !== 'resize') 2150 fireEvent('fullscreenchange', fullscreenActive); 2151 // Resize renderer (deal with browser quirks and fixes #155) 2152 renderer.resize(); 2153 setHfov(config.hfov); 2154 animateInit(); 2155} 2156 2157/** 2158 * Increases panorama zoom. For use with zoom button. 2159 * @private 2160 */ 2161function zoomIn() { 2162 if (loaded) { 2163 setHfov(config.hfov - 5); 2164 animateInit(); 2165 } 2166} 2167 2168/** 2169 * Decreases panorama zoom. For use with zoom button. 2170 * @private 2171 */ 2172function zoomOut() { 2173 if (loaded) { 2174 setHfov(config.hfov + 5); 2175 animateInit(); 2176 } 2177} 2178 2179/** 2180 * Clamps horzontal field of view to viewer's limits. 2181 * @private 2182 * @param {number} hfov - Input horizontal field of view (in degrees) 2183 * @return {number} - Clamped horizontal field of view (in degrees) 2184 */ 2185function constrainHfov(hfov) { 2186 // Keep field of view within bounds 2187 var minHfov = config.minHfov; 2188 if (config.type == 'multires' && renderer && !config.multiResMinHfov) { 2189 minHfov = Math.min(minHfov, renderer.getCanvas().width / (config.multiRes.cubeResolution / 90 * 0.9)); 2190 } 2191 if (minHfov > config.maxHfov) { 2192 // Don't change view if bounds don't make sense 2193 console.log('HFOV bounds do not make sense (minHfov > maxHfov).'); 2194 return config.hfov; 2195 } 2196 var newHfov = config.hfov; 2197 if (hfov < minHfov) { 2198 newHfov = minHfov; 2199 } else if (hfov > config.maxHfov) { 2200 newHfov = config.maxHfov; 2201 } else { 2202 newHfov = hfov; 2203 } 2204 // Optionally avoid showing background (empty space) on top or bottom by adapting newHfov 2205 if (config.avoidShowingBackground && renderer) { 2206 var canvas = renderer.getCanvas(); 2207 newHfov = Math.min(newHfov, 2208 Math.atan(Math.tan((config.maxPitch - config.minPitch) / 360 * Math.PI) / 2209 canvas.height * canvas.width) * 360 / Math.PI); 2210 } 2211 return newHfov; 2212} 2213 2214/** 2215 * Sets viewer's horizontal field of view. 2216 * @private 2217 * @param {number} hfov - Desired horizontal field of view in degrees. 2218 */ 2219function setHfov(hfov) { 2220 config.hfov = constrainHfov(hfov); 2221 fireEvent('zoomchange', config.hfov); 2222} 2223 2224/** 2225 * Stops auto rotation and animated moves. 2226 * @private 2227 */ 2228function stopAnimation() { 2229 animatedMove = {}; 2230 autoRotateSpeed = config.autoRotate ? config.autoRotate : autoRotateSpeed; 2231 config.autoRotate = false; 2232} 2233 2234/** 2235 * Loads panorama. 2236 * @private 2237 */ 2238function load() { 2239 // Since WebGL error handling is very general, first we clear any error box 2240 // since it is a new scene and the error from previous maybe because of lacking 2241 // memory etc and not because of a lack of WebGL support etc 2242 clearError(); 2243 loaded = false; 2244 2245 controls.load.style.display = 'none'; 2246 infoDisplay.load.box.style.display = 'inline'; 2247 init(); 2248} 2249 2250/** 2251 * Loads scene. 2252 * @private 2253 * @param {string} sceneId - Identifier of scene configuration to merge in. 2254 * @param {number} targetPitch - Pitch viewer should be centered on once scene loads. 2255 * @param {number} targetYaw - Yaw viewer should be centered on once scene loads. 2256 * @param {number} targetHfov - HFOV viewer should use once scene loads. 2257 * @param {boolean} [fadeDone] - If `true`, fade setup is skipped. 2258 */ 2259function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) { 2260 if (!loaded) 2261 fadeDone = true; // Don't try to fade when there isn't a scene loaded 2262 loaded = false; 2263 animatedMove = {}; 2264 2265 // Set up fade if specified 2266 var fadeImg, workingPitch, workingYaw, workingHfov; 2267 if (config.sceneFadeDuration && !fadeDone) { 2268 var data = renderer.render(config.pitch * Math.PI / 180, config.yaw * Math.PI / 180, config.hfov * Math.PI / 180, {returnImage: true}); 2269 if (data !== undefined) { 2270 fadeImg = new Image(); 2271 fadeImg.className = 'pnlm-fade-img'; 2272 fadeImg.style.transition = 'opacity ' + (config.sceneFadeDuration / 1000) + 's'; 2273 fadeImg.style.width = '100%'; 2274 fadeImg.style.height = '100%'; 2275 fadeImg.onload = function() { 2276 loadScene(sceneId, targetPitch, targetYaw, targetHfov, true); 2277 }; 2278 fadeImg.src = data; 2279 renderContainer.appendChild(fadeImg); 2280 renderer.fadeImg = fadeImg; 2281 return; 2282 } 2283 } 2284 2285 // Set new pointing 2286 if (targetPitch === 'same') { 2287 workingPitch = config.pitch; 2288 } else { 2289 workingPitch = targetPitch; 2290 } 2291 if (targetYaw === 'same') { 2292 workingYaw = config.yaw; 2293 } else if (targetYaw === 'sameAzimuth') { 2294 workingYaw = config.yaw + (config.northOffset || 0) - (initialConfig.scenes[sceneId].northOffset || 0); 2295 } else { 2296 workingYaw = targetYaw; 2297 } 2298 if (targetHfov === 'same') { 2299 workingHfov = config.hfov; 2300 } else { 2301 workingHfov = targetHfov; 2302 } 2303 2304 // Destroy hot spots from previous scene 2305 destroyHotSpots(); 2306 2307 // Create the new config for the scene 2308 mergeConfig(sceneId); 2309 2310 // Stop motion 2311 speed.yaw = speed.pitch = speed.hfov = 0; 2312 2313 // Reload scene 2314 processOptions(); 2315 if (workingPitch !== undefined) { 2316 config.pitch = workingPitch; 2317 } 2318 if (workingYaw !== undefined) { 2319 config.yaw = workingYaw; 2320 } 2321 if (workingHfov !== undefined) { 2322 config.hfov = workingHfov; 2323 } 2324 fireEvent('scenechange', sceneId); 2325 load(); 2326} 2327 2328/** 2329 * Stop using device orientation. 2330 * @private 2331 */ 2332function stopOrientation() { 2333 window.removeEventListener('deviceorientation', orientationListener); 2334 controls.orientation.classList.remove('pnlm-orientation-button-active'); 2335 orientation = false;
2336} 2337 2338/** 2339 * Start using device orientation. 2340 * @private 2341 */ 2342function startOrientation() { 2343 if (typeof DeviceMotionEvent.requestPermission === 'function') { 2344 DeviceOrientationEvent.requestPermission().then(function(response) { 2345 if (response == 'granted') { 2346 orientation = 1; 2347 window.addEventListener('deviceorientation', orientationListener); 2348 controls.orientation.classList.add('pnlm-orientation-button-active'); 2349 } 2350 }); 2351 } else { 2352 orientation = 1; 2353 window.addEventListener('deviceorientation', orientationListener); 2354 controls.orientation.classList.add('pnlm-orientation-button-active'); 2355 } 2356} 2357 2358/** 2359 * Escapes HTML string (to mitigate possible DOM XSS attacks). 2360 * @private 2361 * @param {string} s - String to escape 2362 * @returns {string} Escaped string 2363 */ 2364function escapeHTML(s) { 2365 if (!initialConfig.escapeHTML) 2366 return String(s).split('\n').join('<br>'); 2367 return String(s).split(/&/g).join('&') 2368 .split('"').join('"') 2369 .split("'").join(''') 2370 .split('<').join('<') 2371 .split('>').join('>') 2372 .split('/').join('/') 2373 .split('\n').join('<br>'); // Allow line breaks 2374} 2375 2376/** 2377 * Removes possibility of XSS attacks with URLs. 2378 * The URL cannot be of protocol 'javascript'. 2379 * @private 2380 * @param {string} url - URL to sanitize 2381 * @param {boolean} href - True if URL is for link (blocks data URIs) 2382 * @returns {string} Sanitized URL 2383 */ 2384function sanitizeURL(url, href) { 2385 try { 2386 var decoded_url = decodeURIComponent(unescape(url)).replace(/[^\w:]/g, '').toLowerCase(); 2387 } catch (e) { 2388 return 'about:blank'; 2389 } 2390 if (decoded_url.indexOf('javascript:') === 0 || 2391 decoded_url.indexOf('vbscript:') === 0) { 2392 console.log('Script URL removed.'); 2393 return 'about:blank'; 2394 } 2395 if (href && decoded_url.indexOf('data:') === 0) { 2396 console.log('Data URI removed from link.'); 2397 return 'about:blank'; 2398 } 2399 return url; 2400} 2401 2402/** 2403 * Unescapes HTML entities. 2404 * Copied from Marked.js 0.7.0. 2405 * @private 2406 * @param {string} url - URL to sanitize 2407 * @param {boolean} href - True if URL is for link (blocks data URIs) 2408 * @returns {string} Sanitized URL 2409 */ 2410function unescape(html) { 2411 // Explicitly match decimal, hex, and named HTML entities 2412 return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) { 2413 n = n.toLowerCase(); 2414 if (n === 'colon') return ':'; 2415 if (n.charAt(0) === '#') { 2416 return n.charAt(1) === 'x' 2417 ? String.fromCharCode(parseInt(n.substring(2), 16)) 2418 : String.fromCharCode(+n.substring(1)); 2419 } 2420 return ''; 2421 }); 2422} 2423 2424/** 2425 * Removes possibility of XSS atacks with URLs for CSS. 2426 * The URL will be sanitized with `sanitizeURL()` and single quotes 2427 * and double quotes escaped. 2428 * @private 2429 * @param {string} url - URL to sanitize 2430 * @returns {string} Sanitized URL 2431 */ 2432function sanitizeURLForCss(url) { 2433 return sanitizeURL(url) 2434 .replace(/"/g, '%22') 2435 .replace(/'/g, '%27'); 2436} 2437 2438/** 2439 * Checks whether or not a panorama is loaded. 2440 * @memberof Viewer 2441 * @instance 2442 * @returns {boolean} `true` if a panorama is loaded, else `false` 2443 */ 2444this.isLoaded = function() { 2445 return Boolean(loaded); 2446}; 2447 2448/** 2449 * Returns the pitch of the center of the view. 2450 * @memberof Viewer 2451 * @instance 2452 * @returns {number} Pitch in degrees 2453 */ 2454this.getPitch = function() { 2455 return config.pitch; 2456}; 2457 2458/** 2459 * Sets the pitch of the center of the view. 2460 * @memberof Viewer 2461 * @instance 2462 * @param {number} pitch - Pitch in degrees 2463 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2464 * @param {function} [callback] - Function to call when animation finishes 2465 * @param {object} [callbackArgs] - Arguments to pass to callback function 2466 * @returns {Viewer} `this` 2467 */ 2468this.setPitch = function(pitch, animated, callback, callbackArgs) { 2469 latestInteraction = Date.now(); 2470 if (Math.abs(pitch - config.pitch) <= eps) { 2471 if (typeof callback == 'function') 2472 callback(callbackArgs); 2473 return this; 2474 } 2475 animated = animated == undefined ? 1000: Number(animated); 2476 if (animated) { 2477 animatedMove.pitch = { 2478 'startTime': Date.now(), 2479 'startPosition': config.pitch, 2480 'endPosition': pitch, 2481 'duration': animated 2482 }; 2483 if (typeof callback == 'function') 2484 setTimeout(function(){callback(callbackArgs);}, animated); 2485 } else { 2486 config.pitch = pitch; 2487 } 2488 animateInit(); 2489 return this; 2490}; 2491 2492/** 2493 * Returns the minimum and maximum allowed pitches (in degrees). 2494 * @memberof Viewer 2495 * @instance 2496 * @returns {number[]} [minimum pitch, maximum pitch] 2497 */ 2498this.getPitchBounds = function() { 2499 return [config.minPitch, config.maxPitch]; 2500}; 2501 2502/** 2503 * Set the minimum and maximum allowed pitches (in degrees). 2504 * @memberof Viewer 2505 * @instance 2506 * @param {number[]} bounds - [minimum pitch, maximum pitch] 2507 * @returns {Viewer} `this` 2508 */
2509this.setPitchBounds = function(bounds) { 2510 config.minPitch = Math.max(-90, Math.min(bounds[0], 90)); 2511 config.maxPitch = Math.max(-90, Math.min(bounds[1], 90)); 2512 return this; 2513}; 2514 2515/** 2516 * Returns the yaw of the center of the view. 2517 * @memberof Viewer 2518 * @instance 2519 * @returns {number} Yaw in degrees 2520 */ 2521this.getYaw = function() { 2522 return (config.yaw + 540) % 360 - 180; 2523}; 2524 2525/** 2526 * Sets the yaw of the center of the view. 2527 * @memberof Viewer 2528 * @instance 2529 * @param {number} yaw - Yaw in degrees [-180, 180] 2530 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2531 * @param {function} [callback] - Function to call when animation finishes 2532 * @param {object} [callbackArgs] - Arguments to pass to callback function 2533 * @returns {Viewer} `this` 2534 */ 2535this.setYaw = function(yaw, animated, callback, callbackArgs) { 2536 latestInteraction = Date.now(); 2537 if (Math.abs(yaw - config.yaw) <= eps) { 2538 if (typeof callback == 'function') 2539 callback(callbackArgs); 2540 return this; 2541 } 2542 animated = animated == undefined ? 1000: Number(animated); 2543 yaw = ((yaw + 180) % 360) - 180; // Keep in bounds 2544 if (animated) { 2545 // Animate in shortest direction 2546 if (config.yaw - yaw > 180) 2547 yaw += 360; 2548 else if (yaw - config.yaw > 180) 2549 yaw -= 360; 2550 2551 animatedMove.yaw = { 2552 'startTime': Date.now(), 2553 'startPosition': config.yaw, 2554 'endPosition': yaw, 2555 'duration': animated 2556 }; 2557 if (typeof callback == 'function') 2558 setTimeout(function(){callback(callbackArgs);}, animated); 2559 } else { 2560 config.yaw = yaw; 2561 } 2562 animateInit(); 2563 return this; 2564}; 2565 2566/** 2567 * Returns the minimum and maximum allowed pitches (in degrees). 2568 * @memberof Viewer 2569 * @instance 2570 * @returns {number[]} [yaw pitch, maximum yaw] 2571 */ 2572this.getYawBounds = function() { 2573 return [config.minYaw, config.maxYaw]; 2574}; 2575 2576/** 2577 * Set the minimum and maximum allowed yaws (in degrees [-360, 360]). 2578 * @memberof Viewer 2579 * @instance 2580 * @param {number[]} bounds - [minimum yaw, maximum yaw] 2581 * @returns {Viewer} `this` 2582 */ 2583this.setYawBounds = function(bounds) { 2584 config.minYaw = Math.max(-360, Math.min(bounds[0], 360)); 2585 config.maxYaw = Math.max(-360, Math.min(bounds[1], 360)); 2586 return this; 2587}; 2588 2589/** 2590 * Returns the horizontal field of view. 2591 * @memberof Viewer 2592 * @instance 2593 * @returns {number} Horizontal field of view in degrees 2594 */ 2595this.getHfov = function() { 2596 return config.hfov; 2597}; 2598 2599/** 2600 * Sets the horizontal field of view. 2601 * @memberof Viewer 2602 * @instance 2603 * @param {number} hfov - Horizontal field of view in degrees 2604 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2605 * @param {function} [callback] - Function to call when animation finishes 2606 * @param {object} [callbackArgs] - Arguments to pass to callback function 2607 * @returns {Viewer} `this` 2608 */ 2609this.setHfov = function(hfov, animated, callback, callbackArgs) { 2610 latestInteraction = Date.now(); 2611 if (Math.abs(hfov - config.hfov) <= eps) { 2612 if (typeof callback == 'function') 2613 callback(callbackArgs); 2614 return this; 2615 } 2616 animated = animated == undefined ? 1000: Number(animated); 2617 if (animated) { 2618 animatedMove.hfov = { 2619 'startTime': Date.now(), 2620 'startPosition': config.hfov, 2621 'endPosition': constrainHfov(hfov), 2622 'duration': animated 2623 }; 2624 if (typeof callback == 'function') 2625 setTimeout(function(){callback(callbackArgs);}, animated); 2626 } else { 2627 setHfov(hfov); 2628 } 2629 animateInit(); 2630 return this; 2631}; 2632 2633/** 2634 * Returns the minimum and maximum allowed horizontal fields of view 2635 * (in degrees). 2636 * @memberof Viewer 2637 * @instance 2638 * @returns {number[]} [minimum hfov, maximum hfov] 2639 */ 2640this.getHfovBounds = function() { 2641 return [config.minHfov, config.maxHfov]; 2642}; 2643 2644/** 2645 * Set the minimum and maximum allowed horizontal fields of view (in degrees). 2646 * @memberof Viewer 2647 * @instance 2648 * @param {number[]} bounds - [minimum hfov, maximum hfov] 2649 * @returns {Viewer} `this` 2650 */
2651this.setHfovBounds = function(bounds) { 2652 config.minHfov = Math.max(0, bounds[0]); 2653 config.maxHfov = Math.max(0, bounds[1]); 2654 return this; 2655}; 2656 2657/** 2658 * Set a new view. Any parameters not specified remain the same. 2659 * @memberof Viewer 2660 * @instance 2661 * @param {number} [pitch] - Target pitch 2662 * @param {number} [yaw] - Target yaw 2663 * @param {number} [hfov] - Target hfov 2664 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2665 * @param {function} [callback] - Function to call when animation finishes 2666 * @param {object} [callbackArgs] - Arguments to pass to callback function 2667 * @returns {Viewer} `this` 2668 */ 2669this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) { 2670 animated = animated == undefined ? 1000: Number(animated); 2671 if (pitch !== undefined && Math.abs(pitch - config.pitch) > eps) { 2672 this.setPitch(pitch, animated, callback, callbackArgs); 2673 callback = undefined; 2674 } 2675 if (yaw !== undefined && Math.abs(yaw - config.yaw) > eps) { 2676 this.setYaw(yaw, animated, callback, callbackArgs); 2677 callback = undefined; 2678 } 2679 if (hfov !== undefined && Math.abs(hfov - config.hfov) > eps) { 2680 this.setHfov(hfov, animated, callback, callbackArgs); 2681 callback = undefined; 2682 } 2683 if (typeof callback == 'function') 2684 callback(callbackArgs); 2685 return this; 2686}; 2687 2688/** 2689 * Returns the panorama's north offset. 2690 * @memberof Viewer 2691 * @instance 2692 * @returns {number} North offset in degrees 2693 */ 2694this.getNorthOffset = function() { 2695 return config.northOffset; 2696}; 2697 2698/** 2699 * Sets the panorama's north offset. 2700 * @memberof Viewer 2701 * @instance 2702 * @param {number} heading - North offset in degrees 2703 * @returns {Viewer} `this` 2704 */ 2705this.setNorthOffset = function(heading) { 2706 config.northOffset = Math.min(360, Math.max(0, heading)); 2707 animateInit(); 2708 return this; 2709}; 2710 2711/** 2712 * Returns the panorama's horizon roll. 2713 * @memberof Viewer 2714 * @instance 2715 * @returns {number} Horizon roll in degrees 2716 */ 2717this.getHorizonRoll = function() { 2718 return config.horizonRoll; 2719}; 2720 2721/** 2722 * Sets the panorama's horizon roll. 2723 * @memberof Viewer 2724 * @instance 2725 * @param {number} roll - Horizon roll in degrees [-90, 90] 2726 * @returns {Viewer} `this` 2727 */ 2728this.setHorizonRoll = function(roll) { 2729 config.horizonRoll = Math.min(90, Math.max(-90, roll)); 2730 renderer.setPose(config.horizonPitch * Math.PI / 180, config.horizonRoll * Math.PI / 180); 2731 animateInit(); 2732 return this; 2733}; 2734 2735/** 2736 * Returns the panorama's horizon pitch. 2737 * @memberof Viewer 2738 * @instance 2739 * @returns {number} Horizon pitch in degrees 2740 */ 2741this.getHorizonPitch = function() { 2742 return config.horizonPitch; 2743}; 2744 2745/** 2746 * Sets the panorama's horizon pitch. 2747 * @memberof Viewer 2748 * @instance 2749 * @param {number} pitch - Horizon pitch in degrees [-90, 90] 2750 * @returns {Viewer} `this` 2751 */ 2752this.setHorizonPitch = function(pitch) { 2753 config.horizonPitch = Math.min(90, Math.max(-90, pitch)); 2754 renderer.setPose(config.horizonPitch * Math.PI / 180, config.horizonRoll * Math.PI / 180); 2755 animateInit(); 2756 return this; 2757}; 2758 2759/** 2760 * Start auto rotation. 2761 * 2762 * Before starting rotation, the viewer is panned to `pitch`. 2763 * @memberof Viewer 2764 * @instance 2765 * @param {number} [speed] - Auto rotation speed / direction. If not specified, previous value is used. 2766 * @param {number} [pitch] - The pitch to rotate at. If not specified, inital pitch is used. 2767 * @returns {Viewer} `this` 2768 */ 2769this.startAutoRotate = function(speed, pitch) { 2770 speed = speed || autoRotateSpeed || 1; 2771 pitch = pitch === undefined ? origPitch : pitch; 2772 config.autoRotate = speed; 2773 _this.lookAt(pitch, undefined, origHfov, 3000); 2774 animateInit(); 2775 return this; 2776}; 2777 2778/** 2779 * Stop auto rotation. 2780 * @memberof Viewer 2781 * @instance 2782 * @returns {Viewer} `this` 2783 */ 2784this.stopAutoRotate = function() { 2785 autoRotateSpeed = config.autoRotate ? config.autoRotate : autoRotateSpeed; 2786 config.autoRotate = false; 2787 config.autoRotateInactivityDelay = -1; 2788 return this; 2789}; 2790 2791/** 2792 * Stops all movement. 2793 * @memberof Viewer 2794 * @instance 2795 */ 2796this.stopMovement = function() { 2797 stopAnimation(); 2798 speed = {'yaw': 0, 'pitch': 0, 'hfov': 0}; 2799}; 2800 2801/** 2802 * Returns the panorama renderer. 2803 * @memberof Viewer 2804 * @instance 2805 * @returns {Renderer} 2806 */ 2807this.getRenderer = function() { 2808 return renderer; 2809}; 2810 2811/** 2812 * Sets update flag for dynamic content. 2813 * @memberof Viewer 2814 * @instance 2815 * @param {boolean} bool - Whether or not viewer should update even when still 2816 * @returns {Viewer} `this` 2817 */ 2818this.setUpdate = function(bool) { 2819 update = bool === true; 2820 if (renderer === undefined) 2821 onImageLoad(); 2822 else 2823 animateInit(); 2824 return this; 2825}; 2826 2827/** 2828 * Calculate panorama pitch and yaw from location of mouse event. 2829 * @memberof Viewer 2830 * @instance 2831 * @param {MouseEvent} event - Document mouse down event. 2832 * @returns {number[]} [pitch, yaw] 2833 */ 2834this.mouseEventToCoords = function(event) { 2835 return mouseEventToCoords(event); 2836}; 2837 2838/** 2839 * Change scene being viewed. 2840 * @memberof Viewer 2841 * @instance 2842 * @param {string}
2842 sceneId - Identifier of scene to switch to. 2843 * @param {number} [pitch] - Pitch to use with new scene 2844 * @param {number} [yaw] - Yaw to use with new scene 2845 * @param {number} [hfov] - HFOV to use with new scene 2846 * @returns {Viewer} `this` 2847 */ 2848this.loadScene = function(sceneId, pitch, yaw, hfov) { 2849 if (loaded !== false) 2850 loadScene(sceneId, pitch, yaw, hfov); 2851 return this; 2852}; 2853 2854/** 2855 * Get ID of current scene. 2856 * @memberof Viewer 2857 * @instance 2858 * @returns {string} ID of current scene 2859 */ 2860this.getScene = function() { 2861 return config.scene; 2862}; 2863 2864/** 2865 * Add a new scene. 2866 * @memberof Viewer 2867 * @instance 2868 * @param {string} sceneId - The ID of the new scene 2869 * @param {string} config - The configuration of the new scene 2870 * @returns {Viewer} `this` 2871 */ 2872this.addScene = function(sceneId, config) { 2873 initialConfig.scenes[sceneId] = config; 2874 return this; 2875}; 2876 2877/** 2878 * Remove a scene. 2879 * @memberof Viewer 2880 * @instance 2881 * @param {string} sceneId - The ID of the scene 2882 * @returns {boolean} False if the scene is the current scene or if the scene doesn't exists, else true 2883 */ 2884this.removeScene = function(sceneId) { 2885 if (config.scene === sceneId || !initialConfig.scenes.hasOwnProperty(sceneId)) 2886 return false; 2887 delete initialConfig.scenes[sceneId]; 2888 return true; 2889}; 2890 2891/** 2892 * Toggle fullscreen. 2893 * @memberof Viewer 2894 * @instance 2895 * @returns {Viewer} `this` 2896 */ 2897this.toggleFullscreen = function() { 2898 toggleFullscreen(); 2899 return this; 2900}; 2901 2902/** 2903 * Get configuration of current scene. 2904 * @memberof Viewer 2905 * @instance 2906 * @returns {Object} Configuration of current scene 2907 */ 2908this.getConfig = function() { 2909 return config; 2910}; 2911 2912/** 2913 * Get viewer's container element. 2914 * @memberof Viewer 2915 * @instance 2916 * @returns {HTMLElement} Container `div` element 2917 */ 2918this.getContainer = function() { 2919 return container; 2920}; 2921 2922/** 2923 * Add a new hot spot. 2924 * @memberof Viewer 2925 * @instance 2926 * @param {Object} hs - The configuration for the hot spot 2927 * @param {string} [sceneId] - Adds hot spot to specified scene if provided, else to current scene 2928 * @returns {Viewer} `this` 2929 * @throws Throws an error if the scene ID is provided but invalid 2930 */ 2931this.addHotSpot = function(hs, sceneId) { 2932 if (sceneId === undefined && config.scene === undefined) { 2933 // Not a tour 2934 config.hotSpots.push(hs); 2935 } else { 2936 // Tour 2937 var id = sceneId !== undefined ? sceneId : config.scene; 2938 if (initialConfig.scenes.hasOwnProperty(id)) { 2939 if (!initialConfig.scenes[id].hasOwnProperty('hotSpots')) { 2940 initialConfig.scenes[id].hotSpots = []; // Create hot spots array if needed 2941 if (id == config.scene) 2942 config.hotSpots = initialConfig.scenes[id].hotSpots; // Link to current config 2943 } 2944 initialConfig.scenes[id].hotSpots.push(hs); // Add hot spot to config 2945 } else { 2946 throw 'Invalid scene ID!'; 2947 } 2948 } 2949 if (sceneId === undefined || config.scene == sceneId) { 2950 // Add to current scene 2951 createHotSpot(hs); 2952 if (loaded) 2953 renderHotSpot(hs); 2954 } 2955 return this; 2956}; 2957 2958/** 2959 * Remove a hot spot. 2960 * @memberof Viewer 2961 * @instance 2962 * @param {string} hotSpotId - The ID of the hot spot 2963 * @param {string} [sceneId] - Removes hot spot from specified scene if provided, else from current scene 2964 * @returns {boolean} True if deletion is successful, else false 2965 */ 2966this.removeHotSpot = function(hotSpotId, sceneId) { 2967 if (sceneId === undefined || config.scene == sceneId) { 2968 if (!config.hotSpots) 2969 return false; 2970 for (var i = 0; i < config.hotSpots.length; i++) { 2971 if (config.hotSpots[i].hasOwnProperty('id') && 2972 config.hotSpots[i].id === hotSpotId) { 2973 // Delete hot spot DOM elements 2974 var current = config.hotSpots[i].div; 2975 while (current.parentNode != renderContainer) 2976 current = current.parentNode; 2977 renderContainer.removeChild(current); 2978 delete config.hotSpots[i].div; 2979 // Remove hot spot from configuration 2980 config.hotSpots.splice(i, 1); 2981 return true; 2982 } 2983 } 2984 } else { 2985 if (initialConfig.scenes.hasOwnProperty(sceneId)) { 2986 if (!initialConfig.scenes[sceneId].hasOwnProperty('hotSpots')) 2987 return false;
vendor: 5,046 bytes, lines 2988-3156
2988 for (var j = 0; j < initialConfig.scenes[sceneId].hotSpots.length; j++) { 2989 if (initialConfig.scenes[sceneId].hotSpots[j].hasOwnProperty('id') && 2990 initialConfig.scenes[sceneId].hotSpots[j].id === hotSpotId) { 2991 // Remove hot spot from configuration 2992 initialConfig.scenes[sceneId].hotSpots.splice(j, 1); 2993 return true; 2994 } 2995 } 2996 } else { 2997 return false; 2998 } 2999 } 3000}; 3001 3002/** 3003 * This method should be called if the viewer's container is resized. 3004 * @memberof Viewer 3005 * @instance 3006 */ 3007this.resize = function() { 3008 if (renderer) 3009 onDocumentResize(); 3010}; 3011 3012/** 3013 * Check if a panorama is loaded. 3014 * @memberof Viewer 3015 * @instance 3016 * @returns {boolean} True if a panorama is loaded, else false 3017 */ 3018this.isLoaded = function() { 3019 return loaded; 3020}; 3021 3022/** 3023 * Check if device orientation control is supported. 3024 * @memberof Viewer 3025 * @instance 3026 * @returns {boolean} True if supported, else false 3027 */ 3028this.isOrientationSupported = function() { 3029 return orientationSupport || false; 3030}; 3031 3032/** 3033 * Stop using device orientation. 3034 * @memberof Viewer 3035 * @instance 3036 */ 3037this.stopOrientation = function() { 3038 stopOrientation(); 3039}; 3040 3041/** 3042 * Start using device orientation (does nothing if not supported). 3043 * @memberof Viewer 3044 * @instance 3045 */ 3046this.startOrientation = function() { 3047 if (orientationSupport) 3048 startOrientation(); 3049}; 3050 3051/** 3052 * Check if device orientation control is currently activated. 3053 * @memberof Viewer 3054 * @instance 3055 * @returns {boolean} True if active, else false 3056 */ 3057this.isOrientationActive = function() { 3058 return Boolean(orientation); 3059}; 3060 3061/** 3062 * Subscribe listener to specified event. 3063 * @memberof Viewer 3064 * @instance 3065 * @param {string} type - Type of event to subscribe to. 3066 * @param {Function} listener - Listener function to subscribe to event. 3067 * @returns {Viewer} `this` 3068 */ 3069this.on = function(type, listener) { 3070 externalEventListeners[type] = externalEventListeners[type] || []; 3071 externalEventListeners[type].push(listener); 3072 return this; 3073}; 3074 3075/** 3076 * Remove an event listener (or listeners). 3077 * @memberof Viewer 3078 * @param {string} [type] - Type of event to remove listeners from. If not specified, all listeners are removed. 3079 * @param {Function} [listener] - Listener function to remove. If not specified, all listeners of specified type are removed. 3080 * @returns {Viewer} `this` 3081 */ 3082this.off = function(type, listener) { 3083 if (!type) { 3084 // Remove all listeners if type isn't specified 3085 externalEventListeners = {}; 3086 return this; 3087 } 3088 if (listener) { 3089 var i = externalEventListeners[type].indexOf(listener); 3090 if (i >= 0) { 3091 // Remove listener if found 3092 externalEventListeners[type].splice(i, 1); 3093 } 3094 if (externalEventListeners[type].length == 0) { 3095 // Remove category if empty 3096 delete externalEventListeners[type]; 3097 } 3098 } else { 3099 // Remove category of listeners if listener isn't specified 3100 delete externalEventListeners[type]; 3101 } 3102 return this; 3103}; 3104 3105/** 3106 * Fire listeners attached to specified event. 3107 * @private 3108 * @param {string} [type] - Type of event to fire listeners for. 3109 */ 3110function fireEvent(type) { 3111 if (type in externalEventListeners) { 3112 // Reverse iteration is useful, if event listener is removed inside its definition 3113 for (var i = externalEventListeners[type].length; i > 0; i--) { 3114 externalEventListeners[type][externalEventListeners[type].length - i].apply(null, [].slice.call(arguments, 1)); 3115 } 3116 } 3117} 3118 3119/** 3120 * Destructor. 3121 * @instance 3122 * @memberof Viewer 3123 */ 3124this.destroy = function() { 3125 destroyed = true; 3126 clearTimeout(autoRotateStart); 3127 3128 if (renderer) 3129 renderer.destroy(); 3130 if (listenersAdded) { 3131 document.removeEventListener('mousemove', onDocumentMouseMove, false); 3132 document.removeEventListener('mouseup', onDocumentMouseUp, false); 3133 container.removeEventListener('mozfullscreenchange', onFullScreenChange, false); 3134 container.removeEventListener('webkitfullscreenchange', onFullScreenChange, false); 3135 container.removeEventListener('msfullscreenchange', onFullScreenChange, false); 3136 container.removeEventListener('fullscreenchange', onFullScreenChange, false); 3137 window.removeEventListener('resize', onDocumentResize, false); 3138 window.removeEventListener('orientationchange', onDocumentResize, false); 3139 container.removeEventListener('keydown', onDocumentKeyPress, false); 3140 container.removeEventListener('keyup', onDocumentKeyUp, false); 3141 container.removeEventListener('blur', clearKeys, false); 3142 document.removeEventListener('mouseleave', onDocumentMouseUp, false); 3143 } 3144 container.innerHTML = ''; 3145 container.classList.remove('pnlm-container'); 3146}; 3147 3148} 3149 3150return { 3151 viewer: function(container, config) { 3152 return new Viewer(container, config); 3153 } 3154}; 3155 3156})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.