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