vendor: 19,949 bytes, lines 1-511
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-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;
vendor: 21,612 bytes, lines 666-1371
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 var mirrorData = { 820 pitch: pitch, 821 yaw: yaw 822 } 823 fireEvent('mousemove', mirrorData); 824 825 } 826} 827 828/** 829 * Event handler for mouse up events. Stops panning. 830 * @private 831 */ 832function onDocumentMouseUp(event) { 833 if (!isUserInteracting) { 834 return; 835 } 836 isUserInteracting = false; 837 if (Date.now() - latestInteraction > 15) { 838 // Prevents jump when user rapidly moves mouse, stops, and then 839 // releases the mouse button 840 speed.pitch = speed.yaw = 0; 841 } 842 uiContainer.classList.add('pnlm-grab'); 843 uiContainer.classList.remove('pnlm-grabbing'); 844 latestInteraction = Date.now(); 845 846 fireEvent('mouseup', event); 847} 848 849/** 850 * Event handler for touches. Initializes panning if one touch or zooming if 851 * two touches. 852 * @private 853 * @param {TouchEvent} event - Document touch start event. 854 */ 855function onDocumentTouchStart(event) { 856 // Only do something if the panorama is loaded 857 if (!loaded || !config.draggable) { 858 return; 859 } 860 861 // Turn off auto-rotation if enabled 862 stopAnimation(); 863 864 stopOrientation(); 865 config.roll = 0; 866 867 speed.hfov = 0; 868 869 // Calculate touch position relative to top left of viewer container 870 var pos0 = mousePosition(event.targetTouches[0]); 871 872 onPointerDownPointerX = pos0.x; 873 onPointerDownPointerY = pos0.y; 874 875 if (event.targetTouches.length == 2) { 876 // Down pointer is the center of the two fingers 877 var pos1 = mousePosition(event.targetTouches[1]); 878 onPointerDownPointerX += (pos1.x - pos0.x) * 0.5; 879 onPointerDownPointerY += (pos1.y - pos0.y) * 0.5; 880 onPointerDownPointerDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + 881 (pos0.y - pos1.y) * (pos0.y - pos1.y)); 882 } 883 isUserInteracting = true; 884 latestInteraction = Date.now(); 885 886 onPointerDownYaw = config.yaw; 887 onPointerDownPitch = config.pitch; 888 889 fireEvent('touchstart', event); 890 animateInit(); 891} 892 893/** 894 * Event handler for touch movements. Pans center of view if one touch or 895 * adjusts zoom if two touches. 896 * @private 897 * @param {TouchEvent} event - Document touch move event. 898 */ 899function onDocumentTouchMove(event) { 900 if (!config.draggable) { 901 return; 902 } 903 904 // Override default action 905 event.preventDefault(); 906 if (loaded) { 907 latestInteraction = Date.now(); 908 } 909 if (isUserInteracting && loaded) { 910 var pos0 = mousePosition(event.targetTouches[0]); 911 var clientX = pos0.x; 912 var clientY = pos0.y; 913 914 if (event.targetTouches.length == 2 && onPointerDownPointerDist != -1) { 915 var pos1 = mousePosition(event.targetTouches[1]); 916 clientX += (pos1.x - pos0.x) * 0.5; 917 clientY += (pos1.y - pos0.y) * 0.5; 918 var clientDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + 919 (pos0.y - pos1.y) * (pos0.y - pos1.y)); 920 setHfov(config.hfov + (onPointerDownPointerDist - clientDist) * 0.1); 921 onPointerDownPointerDist = clientDist; 922 } 923 924 // The smaller the config.hfov value (the more zoomed-in the user is), the faster 925 // yaw/pitch are perceived to change on one-finger touchmove (panning) events and vice versa. 926 // To improve usability at both small and large zoom levels (config.hfov values) 927 // we introduce a dynamic pan speed coefficient. 928 // 929 // Currently this seems to *roughly* keep initial drag/pan start position close to 930 // the user's finger while panning regardless of zoom level / config.hfov value. 931 var touchmovePanSpeedCoeff = (config.hfov / 360) * config.touchPanSpeedCoeffFactor; 932 933 var yaw = (onPointerDownPointerX - clientX) * touchmovePanSpeedCoeff + onPointerDownYaw; 934 speed.yaw = (yaw - config.yaw) % 360 * 0.2; 935 config.yaw = yaw; 936 937 var pitch = (clientY - onPointerDownPointerY) * touchmovePanSpeedCoeff + onPointerDownPitch; 938 speed.pitch = (pitch - config.pitch) * 0.2; 939 config.pitch = pitch; 940 var mirrorData = { 941 pitch: pitch, 942 yaw: yaw 943 } 944 fireEvent('touchmove', mirrorData); 945 } 946} 947 948/** 949 * Event handler for end of touches. Stops panning and/or zooming. 950 * @private 951 */ 952function onDocumentTouchEnd() { 953 isUserInteracting = false; 954 if (Date.now() - latestInteraction > 150) { 955 speed.pitch = speed.yaw = 0; 956 } 957 onPointerDownPointerDist = -1; 958 latestInteraction = Date.now(); 959 960 fireEvent('touchend', event); 961} 962 963var pointerIDs = [], 964 pointerCoordinates = []; 965/** 966 * Event handler for touch starts in IE / Edge. 967 * @private 968 * @param {PointerEvent} event - Document pointer down event. 969 */ 970function onDocumentPointerDown(event) { 971 if (event.pointerType == 'touch') { 972 // Only do something if the panorama is loaded 973 if (!loaded || !config.draggable) 974 return; 975 pointerIDs.push(event.pointerId); 976 pointerCoordinates.push({clientX: event.clientX, clientY: event.clientY}); 977 event.targetTouches = pointerCoordinates; 978 onDocumentTouchStart(event); 979 event.preventDefault(); 980 } 981} 982 983/** 984 * Event handler for touch moves in IE / Edge. 985 * @private 986 * @param {PointerEvent} event - Document pointer move event. 987 */ 988function onDocumentPointerMove(event) { 989 if (event.pointerType == 'touch') { 990 if (!config.draggable) 991 return; 992 for (var i = 0; i < pointerIDs.length; i++) { 993 if (event.pointerId == pointerIDs[i]) { 994 pointerCoordinates[i].clientX = event.clientX; 995 pointerCoordinates[i].clientY = event.clientY; 996 event.targetTouches = pointerCoordinates; 997 onDocumentTouchMove(event); 998 event.preventDefault(); 999 return; 1000 } 1001 } 1002 } 1003} 1004 1005/** 1006 * Event handler for touch ends in IE / Edge. 1007 * @private 1008 * @param {PointerEvent} event - Document pointer up event. 1009 */ 1010function onDocumentPointerUp(event) { 1011 if (event.pointerType == 'touch') { 1012 var defined = false; 1013 for (var i = 0; i < pointerIDs.length; i++) { 1014 if (event.pointerId == pointerIDs[i]) 1015 pointerIDs[i] = undefined; 1016 if (pointerIDs[i]) 1017 defined = true; 1018 } 1019 if (!defined) { 1020 pointerIDs = []; 1021 pointerCoordinates = []; 1022 onDocumentTouchEnd(); 1023 } 1024 event.preventDefault(); 1025 } 1026} 1027 1028/** 1029 * Event handler for mouse wheel. Changes zoom. 1030 * @private 1031 * @param {WheelEvent} event - Document mouse wheel event. 1032 */ 1033function onDocumentMouseWheel(event) { 1034 // Only do something if the panorama is loaded and mouse wheel zoom is enabled 1035 if (!loaded || (config.mouseZoom == 'fullscreenonly' && !fullscreenActive)) { 1036 return; 1037 } 1038 1039 event.preventDefault(); 1040 1041 // Turn off auto-rotation if enabled 1042 stopAnimation(); 1043 latestInteraction = Date.now(); 1044 1045 if (event.wheelDeltaY) { 1046 // WebKit 1047 setHfov(config.hfov - event.wheelDeltaY * 0.05); 1048 speed.hfov = event.wheelDelta < 0 ? 1 : -1; 1049 } else if (event.wheelDelta) { 1050 // Opera / Explorer 9 1051 setHfov(config.hfov - event.wheelDelta * 0.05); 1052 speed.hfov = event.wheelDelta < 0 ? 1 : -1; 1053 } else if (event.detail) { 1054 // Firefox 1055 setHfov(config.hfov + event.detail * 1.5); 1056 speed.hfov = event.detail > 0 ? 1 : -1; 1057 } 1058 animateInit(); 1059} 1060 1061/** 1062 * Event handler for key presses. Updates list of currently pressed keys. 1063 * @private 1064 * @param {KeyboardEvent} event - Document key press event. 1065 */ 1066function onDocumentKeyPress(event) { 1067 // Turn off auto-rotation if enabled 1068 stopAnimation(); 1069 latestInteraction = Date.now(); 1070 1071 stopOrientation(); 1072 config.roll = 0; 1073 1074 // Record key pressed 1075 var keynumber = event.which || event.keycode; 1076 1077 // Override default action for keys that are used 1078 if (config.capturedKeyNumbers.indexOf(keynumber) < 0) 1079 return; 1080 event.preventDefault(); 1081 1082 // If escape key is pressed 1083 if (keynumber == 27) { 1084 // If in fullscreen mode 1085 if (fullscreenActive) { 1086 toggleFullscreen(); 1087 } 1088 } else { 1089 // Change key 1090 changeKey(keynumber, true); 1091 } 1092} 1093 1094/** 1095 * Clears list of currently pressed keys. 1096 * @private 1097 */ 1098function clearKeys() { 1099 for (var i = 0; i < 10; i++) { 1100 keysDown[i] = false; 1101 } 1102} 1103 1104/** 1105 * Event handler for key releases. Updates list of currently pressed keys. 1106 * @private 1107 * @param {KeyboardEvent} event - Document key up event. 1108 */ 1109function onDocumentKeyUp(event) { 1110 // Record key pressed 1111 var keynumber = event.which || event.keycode; 1112 1113 // Override default action for keys that are used 1114 if (config.capturedKeyNumbers.indexOf(keynumber) < 0) 1115 return; 1116 event.preventDefault(); 1117 1118 // Change key 1119 changeKey(keynumber, false); 1120} 1121 1122/** 1123 * Updates list of currently pressed keys. 1124 * @private 1125 * @param {number} keynumber - Key number. 1126 * @param {boolean} value - Whether or not key is pressed. 1127 */ 1128function changeKey(keynumber, value) { 1129 var keyChanged = false; 1130 switch(keynumber) { 1131 // If minus key is released 1132 case 109: case 189: case 17: case 173: 1133 if (keysDown[0] != value) { keyChanged = true; } 1134 keysDown[0] = value; break; 1135 1136 // If plus key is released 1137 case 107: case 187: case 16: case 61: 1138 if (keysDown[1] != value) { keyChanged = true; } 1139 keysDown[1] = value; break; 1140 1141 // If up arrow is released 1142 case 38: 1143 if (keysDown[2] != value) { keyChanged = true; } 1144 keysDown[2] = value; break; 1145 1146 // If "w" is released 1147 case 87: 1148 if (keysDown[6] != value) { keyChanged = true; } 1149 keysDown[6] = value; break; 1150 1151 // If down arrow is released 1152 case 40: 1153 if (keysDown[3] != value) { keyChanged = true; } 1154 keysDown[3] = value; break; 1155 1156 // If "s" is released 1157 case 83: 1158 if (keysDown[7] != value) { keyChanged = true; } 1159 keysDown[7] = value; break; 1160 1161 // If left arrow is released 1162 case 37: 1163 if (keysDown[4] != value) { keyChanged = true; } 1164 keysDown[4] = value; break; 1165 1166 // If "a" is released 1167 case 65: 1168 if (keysDown[8] != value) { keyChanged = true; } 1169 keysDown[8] = value; break; 1170 1171 // If right arrow is released 1172 case 39: 1173 if (keysDown[5] != value) { keyChanged = true; } 1174 keysDown[5] = value; break; 1175 1176 // If "d" is released 1177 case 68: 1178 if (keysDown[9] != value) { keyChanged = true; } 1179 keysDown[9] = value; 1180 } 1181 1182 if (keyChanged && value) { 1183 if (typeof performance !== 'undefined' && performance.now()) { 1184 prevTime = performance.now(); 1185 } else { 1186 prevTime = Date.now(); 1187 } 1188 animateInit(); 1189 } 1190} 1191 1192/** 1193 * Pans and/or zooms panorama based on currently pressed keys. Also handles 1194 * panorama "inertia" and auto rotation. 1195 * @private 1196 */ 1197function keyRepeat() { 1198 // Only do something if the panorama is loaded 1199 if (!loaded) { 1200 return; 1201 } 1202 1203 var isKeyDown = false; 1204 1205 var prevPitch = config.pitch; 1206 var prevYaw = config.yaw; 1207 var prevZoom = config.hfov; 1208 1209 var newTime; 1210 if (typeof performance !== 'undefined' && performance.now()) { 1211 newTime = performance.now(); 1212 } else { 1213 newTime = Date.now(); 1214 } 1215 if (prevTime === undefined) { 1216 prevTime = newTime; 1217 } 1218 var diff = (newTime - prevTime) * config.hfov / 1700; 1219 diff = Math.min(diff, 1.0); 1220 1221 // If minus key is down 1222 if (keysDown[0] && config.keyboardZoom === true) { 1223 setHfov(config.hfov + (speed.hfov * 0.8 + 0.5) * diff); 1224 isKeyDown = true; 1225 } 1226 1227 // If plus key is down 1228 if (keysDown[1] && config.keyboardZoom === true) { 1229 setHfov(config.hfov + (speed.hfov * 0.8 - 0.2) * diff); 1230 isKeyDown = true; 1231 } 1232 1233 // If up arrow or "w" is down 1234 if (keysDown[2] || keysDown[6]) { 1235 // Pan up 1236 config.pitch += (speed.pitch * 0.8 + 0.2) * diff; 1237 isKeyDown = true; 1238 } 1239 1240 // If down arrow or "s" is down 1241 if (keysDown[3] || keysDown[7]) { 1242 // Pan down 1243 config.pitch += (speed.pitch * 0.8 - 0.2) * diff; 1244 isKeyDown = true; 1245 } 1246 1247 // If left arrow or "a" is down 1248 if (keysDown[4] || keysDown[8]) { 1249 // Pan left 1250 config.yaw += (speed.yaw * 0.8 - 0.2) * diff; 1251 isKeyDown = true; 1252 } 1253 1254 // If right arrow or "d" is down 1255 if (keysDown[5] || keysDown[9]) { 1256 // Pan right 1257 config.yaw += (speed.yaw * 0.8 + 0.2) * diff; 1258 isKeyDown = true; 1259 } 1260 1261 if (isKeyDown) 1262 latestInteraction = Date.now(); 1263 1264 // If auto-rotate 1265 if (config.autoRotate) { 1266 // Pan 1267 if (newTime - prevTime > 0.001) { 1268 var timeDiff = (newTime - prevTime) / 1000; 1269 var yawDiff = (speed.yaw / timeDiff * diff - config.autoRotate * 0.2) * timeDiff; 1270 yawDiff = (-config.autoRotate > 0 ? 1 : -1) * Math.min(Math.abs(config.autoRotate * timeDiff), Math.abs(yawDiff)); 1271 config.yaw += yawDiff; 1272 } 1273 1274 // Deal with stopping auto rotation after a set delay 1275 if (config.autoRotateStopDelay) { 1276 config.autoRotateStopDelay -= newTime - prevTime; 1277 if (config.autoRotateStopDelay <= 0) { 1278 config.autoRotateStopDelay = false; 1279 autoRotateSpeed = config.autoRotate; 1280 config.autoRotate = 0; 1281 } 1282 } 1283 } 1284 1285 // Animated moves 1286 if (animatedMove.pitch) { 1287 animateMove('pitch'); 1288 prevPitch = config.pitch; 1289 } 1290 if (animatedMove.yaw) { 1291 animateMove('yaw'); 1292 prevYaw = config.yaw; 1293 } 1294 if (animatedMove.hfov) { 1295 animateMove('hfov'); 1296 prevZoom = config.hfov; 1297 } 1298 1299 // "Inertia" 1300 if (diff > 0 && !config.autoRotate) { 1301 // "Friction" 1302 var slowDownFactor = 1 - config.friction; 1303 1304 // Yaw 1305 if (!keysDown[4] && !keysDown[5] && !keysDown[8] && !keysDown[9] && !animatedMove.yaw) { 1306 config.yaw += speed.yaw * diff * slowDownFactor; 1307 } 1308 // Pitch 1309 if (!keysDown[2] && !keysDown[3] && !keysDown[6] && !keysDown[7] && !animatedMove.pitch) { 1310 config.pitch += speed.pitch * diff * slowDownFactor; 1311 } 1312 // Zoom 1313 if (!keysDown[0] && !keysDown[1] && !animatedMove.hfov) { 1314 setHfov(config.hfov + speed.hfov * diff * slowDownFactor); 1315 } 1316 } 1317 1318 prevTime = newTime; 1319 if (diff > 0) { 1320 speed.yaw = speed.yaw * 0.8 + (config.yaw - prevYaw) / diff * 0.2; 1321 speed.pitch = speed.pitch * 0.8 + (config.pitch - prevPitch) / diff * 0.2; 1322 speed.hfov = speed.hfov * 0.8 + (config.hfov - prevZoom) / diff * 0.2; 1323 1324 // Limit speed 1325 var maxSpeed = config.autoRotate ? Math.abs(config.autoRotate) : 5; 1326 speed.yaw = Math.min(maxSpeed, Math.max(speed.yaw, -maxSpeed)); 1327 speed.pitch = Math.min(maxSpeed, Math.max(speed.pitch, -maxSpeed)); 1328 speed.hfov = Math.min(maxSpeed, Math.max(speed.hfov, -maxSpeed)); 1329 } 1330 1331 // Stop movement if opposite controls are pressed 1332 if (keysDown[0] && keysDown[1]) { 1333 speed.hfov = 0; 1334 } 1335 if ((keysDown[2] || keysDown[6]) && (keysDown[3] || keysDown[7])) { 1336 speed.pitch = 0; 1337 } 1338 if ((keysDown[4] || keysDown[8]) && (keysDown[5] || keysDown[9])) { 1339 speed.yaw = 0; 1340 } 1341} 1342 1343/** 1344 * Animates moves. 1345 * @param {string} axis - Axis to animate 1346 * @private 1347 */ 1348function animateMove(axis) { 1349 var t = animatedMove[axis]; 1350 var normTime = Math.min(1, Math.max((Date.now() - t.startTime) / 1000 / (t.duration / 1000), 0)); 1351 var result = t.startPosition + config.animationTimingFunction(normTime) * (t.endPosition - t.startPosition); 1352 if ((t.endPosition > t.startPosition && result >= t.endPosition) || 1353 (t.endPosition < t.startPosition && result <= t.endPosition) || 1354 t.endPosition === t.startPosition) { 1355 result = t.endPosition; 1356 speed[axis] = 0; 1357 delete animatedMove[axis]; 1358 } 1359 config[axis] = result; 1360} 1361 1362/** 1363 * @param {number} t - Normalized time in animation 1364 * @return {number} Position in animation 1365 * @private 1366 */ 1367function timingFunction(t) { 1368 // easeInOutQuad from https://gist.github.com/gre/1650294 1369 return t < 0.5 ? 2*t*t : -1+(4-2*t)*t; 1370} 1371
1372/** 1373 * Event handler for document resizes. Updates viewer size and rerenders view. 1374 * @private 1375 */ 1376function onDocumentResize() { 1377 // Resize panorama renderer (moved to onFullScreenChange) 1378 //renderer.resize(); 1379 //animateInit(); 1380 1381 // Kludge to deal with WebKit regression: https://bugs.webkit.org/show_bug.cgi?id=93525 1382 onFullScreenChange('resize'); 1383} 1384 1385/** 1386 * Initializes animation. 1387 * @private 1388 */ 1389function animateInit() { 1390 if (animating) { 1391 return; 1392 } 1393 animating = true; 1394 animate(); 1395} 1396 1397/** 1398 * Animates view, using requestAnimationFrame to trigger rendering. 1399 * @private 1400 */ 1401function animate() { 1402 if (destroyed) { 1403 return; 1404 } 1405 1406 render(); 1407 if (autoRotateStart) 1408 clearTimeout(autoRotateStart); 1409 if (isUserInteracting || orientation === true) { 1410 requestAnimationFrame(animate); 1411 } else if (keysDown[0] || keysDown[1] || keysDown[2] || keysDown[3] || 1412 keysDown[4] || keysDown[5] || keysDown[6] || keysDown[7] || 1413 keysDown[8] || keysDown[9] || config.autoRotate || 1414 animatedMove.pitch || animatedMove.yaw || animatedMove.hfov || 1415 Math.abs(speed.yaw) > 0.01 || Math.abs(speed.pitch) > 0.01 || 1416 Math.abs(speed.hfov) > 0.01) { 1417 1418 keyRepeat(); 1419 if (config.autoRotateInactivityDelay >= 0 && autoRotateSpeed && 1420 Date.now() - latestInteraction > config.autoRotateInactivityDelay && 1421 !config.autoRotate) { 1422 config.autoRotate = autoRotateSpeed; 1423 _this.lookAt(origPitch, undefined, origHfov, 3000); 1424 } 1425 requestAnimationFrame(animate); 1426 } else if (renderer && (renderer.isLoading() || (config.dynamic === true && update))) { 1427 requestAnimationFrame(animate); 1428 } else { 1429 fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()}); 1430 animating = false; 1431 prevTime = undefined; 1432 var autoRotateStartTime = config.autoRotateInactivityDelay - 1433 (Date.now() - latestInteraction); 1434 if (autoRotateStartTime > 0) { 1435 autoRotateStart = setTimeout(function() { 1436 config.autoRotate = autoRotateSpeed; 1437 _this.lookAt(origPitch, undefined, origHfov, 3000); 1438 animateInit(); 1439 }, autoRotateStartTime); 1440 } else if (config.autoRotateInactivityDelay >= 0 && autoRotateSpeed) { 1441 config.autoRotate = autoRotateSpeed; 1442 _this.lookAt(origPitch, undefined, origHfov, 3000); 1443 animateInit(); 1444 } 1445 } 1446} 1447 1448/** 1449 * Renders panorama view. 1450 * @private 1451 */ 1452function render() { 1453 var tmpyaw; 1454 1455 if (loaded) { 1456 var canvas = renderer.getCanvas(); 1457 1458 if (config.autoRotate !== false) { 1459 // When auto-rotating this check needs to happen first (see issue #764) 1460 if (config.yaw > 360) { 1461 config.yaw -= 360; 1462 } else if (config.yaw < -360) { 1463 config.yaw += 360; 1464 } 1465 } 1466 1467 // Keep a tmp value of yaw for autoRotate comparison later 1468 tmpyaw = config.yaw; 1469 1470 // Optionally avoid showing background (empty space) on left or right by adapting min/max yaw 1471 var hoffcut = 0, 1472 voffcut = 0; 1473 if (config.avoidShowingBackground) { 1474 var hfov2 = config.hfov / 2, 1475 vfov2 = Math.atan2(Math.tan(hfov2 / 180 * Math.PI), (canvas.width / canvas.height)) * 180 / Math.PI, 1476 transposed = config.vaov > config.haov; 1477 if (transposed) { 1478 voffcut = vfov2 * (1 - Math.min(Math.cos((config.pitch - hfov2) / 180 * Math.PI), 1479 Math.cos((config.pitch + hfov2) / 180 * Math.PI))); 1480 } else { 1481 hoffcut = hfov2 * (1 - Math.min(Math.cos((config.pitch - vfov2) / 180 * Math.PI), 1482 Math.cos((config.pitch + vfov2) / 180 * Math.PI))); 1483 } 1484 } 1485 1486 // Ensure the yaw is within min and max allowed 1487 var yawRange = config.maxYaw - config.minYaw, 1488 minYaw = -180, 1489 maxYaw = 180; 1490 if (yawRange < 360) { 1491 minYaw = config.minYaw + config.hfov / 2 + hoffcut; 1492 maxYaw = config.maxYaw - config.hfov / 2 - hoffcut; 1493 if (yawRange < config.hfov) { 1494 // Lock yaw to average of min and max yaw when both can be seen at once 1495 minYaw = maxYaw = (minYaw + maxYaw) / 2; 1496 }
vendor: 10,904 bytes, lines 1497-1802
1497 config.yaw = Math.max(minYaw, Math.min(maxYaw, config.yaw)); 1498 } 1499 1500 if (!(config.autoRotate !== false)) { 1501 // When not auto-rotating, this check needs to happen after the 1502 // previous check (see issue #698) 1503 if (config.yaw > 360) { 1504 config.yaw -= 360; 1505 } else if (config.yaw < -360) { 1506 config.yaw += 360; 1507 } 1508 } 1509 1510 // Check if we autoRotate in a limited by min and max yaw 1511 // If so reverse direction 1512 if (config.autoRotate !== false && tmpyaw != config.yaw && 1513 prevTime !== undefined) { // this condition prevents changing the direction initially 1514 config.autoRotate *= -1; 1515 } 1516 1517 // Ensure the calculated pitch is within min and max allowed 1518 var vfov = 2 * Math.atan(Math.tan(config.hfov / 180 * Math.PI * 0.5) / 1519 (canvas.width / canvas.height)) / Math.PI * 180; 1520 var minPitch = config.minPitch + vfov / 2, 1521 maxPitch = config.maxPitch - vfov / 2; 1522 var pitchRange = config.maxPitch - config.minPitch; 1523 if (pitchRange < vfov) { 1524 // Lock pitch to average of min and max pitch when both can be seen at once 1525 minPitch = maxPitch = (minPitch + maxPitch) / 2; 1526 } 1527 if (isNaN(minPitch)) 1528 minPitch = -90; 1529 if (isNaN(maxPitch)) 1530 maxPitch = 90; 1531 config.pitch = Math.max(minPitch, Math.min(maxPitch, config.pitch)); 1532 1533 renderer.render(config.pitch * Math.PI / 180, config.yaw * Math.PI / 180, config.hfov * Math.PI / 180, {roll: config.roll * Math.PI / 180}); 1534 1535 renderHotSpots(); 1536 1537 // Update compass 1538 if (config.compass) { 1539 compass.style.transform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; 1540 compass.style.webkitTransform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; 1541 } 1542 } 1543} 1544 1545/** 1546 * Creates a new quaternion. 1547 * @private 1548 * @constructor 1549 * @param {Number} w - W value 1550 * @param {Number} x - X value 1551 * @param {Number} y - Y value 1552 * @param {Number} z - Z value 1553 */ 1554function Quaternion(w, x, y, z) { 1555 this.w = w; 1556 this.x = x; 1557 this.y = y; 1558 this.z = z; 1559} 1560 1561/** 1562 * Multiplies quaternions. 1563 * @private 1564 * @param {Quaternion} q - Quaternion to multiply 1565 * @returns {Quaternion} Result of multiplication 1566 */ 1567Quaternion.prototype.multiply = function(q) { 1568 return new Quaternion(this.w*q.w - this.x*q.x - this.y*q.y - this.z*q.z, 1569 this.x*q.w + this.w*q.x + this.y*q.z - this.z*q.y, 1570 this.y*q.w + this.w*q.y + this.z*q.x - this.x*q.z, 1571 this.z*q.w + this.w*q.z + this.x*q.y - this.y*q.x); 1572}; 1573 1574/** 1575 * Converts quaternion to Euler angles. 1576 * @private 1577 * @returns {Number[]} [phi angle, theta angle, psi angle] 1578 */ 1579Quaternion.prototype.toEulerAngles = function() { 1580 var phi = Math.atan2(2 * (this.w * this.x + this.y * this.z), 1581 1 - 2 * (this.x * this.x + this.y * this.y)), 1582 theta = Math.asin(2 * (this.w * this.y - this.z * this.x)), 1583 psi = Math.atan2(2 * (this.w * this.z + this.x * this.y), 1584 1 - 2 * (this.y * this.y + this.z * this.z)); 1585 return [phi, theta, psi]; 1586}; 1587 1588/** 1589 * Converts device orientation API Tait-Bryan angles to a quaternion. 1590 * @private 1591 * @param {Number} alpha - Alpha angle (in degrees) 1592 * @param {Number} beta - Beta angle (in degrees) 1593 * @param {Number} gamma - Gamma angle (in degrees) 1594 * @returns {Quaternion} Orientation quaternion 1595 */ 1596function taitBryanToQuaternion(alpha, beta, gamma) { 1597 var r = [beta ? beta * Math.PI / 180 / 2 : 0, 1598 gamma ? gamma * Math.PI / 180 / 2 : 0, 1599 alpha ? alpha * Math.PI / 180 / 2 : 0]; 1600 var c = [Math.cos(r[0]), Math.cos(r[1]), Math.cos(r[2])], 1601 s = [Math.sin(r[0]), Math.sin(r[1]), Math.sin(r[2])]; 1602 1603 return new Quaternion(c[0]*c[1]*c[2] - s[0]*s[1]*s[2], 1604 s[0]*c[1]*c[2] - c[0]*s[1]*s[2], 1605 c[0]*s[1]*c[2] + s[0]*c[1]*s[2], 1606 c[0]*c[1]*s[2] + s[0]*s[1]*c[2]); 1607} 1608 1609/** 1610 * Computes current device orientation quaternion from device orientation API 1611 * Tait-Bryan angles. 1612 * @private 1613 * @param {Number} alpha - Alpha angle (in degrees) 1614 * @param {Number} beta - Beta angle (in degrees) 1615 * @param {Number} gamma - Gamma angle (in degrees) 1616 * @returns {Quaternion} Orientation quaternion 1617 */ 1618function computeQuaternion(alpha, beta, gamma) { 1619 // Convert Tait-Bryan angles to quaternion 1620 var quaternion = taitBryanToQuaternion(alpha, beta, gamma); 1621 // Apply world transform 1622 quaternion = quaternion.multiply(new Quaternion(Math.sqrt(0.5), -Math.sqrt(0.5), 0, 0)); 1623 // Apply screen transform 1624 var angle = window.orientation ? -window.orientation * Math.PI / 180 / 2 : 0; 1625 return quaternion.multiply(new Quaternion(Math.cos(angle), 0, -Math.sin(angle), 0)); 1626} 1627 1628/** 1629 * Event handler for device orientation API. Controls pointing. 1630 * @private 1631 * @param {DeviceOrientationEvent} event - Device orientation event. 1632 */ 1633function orientationListener(e) { 1634 var q = computeQuaternion(e.alpha, e.beta, e.gamma).toEulerAngles(); 1635 if (typeof(orientation) == 'number' && orientation < 10) { 1636 // This kludge is necessary because iOS sometimes provides a few stale 1637 // device orientation events when the listener is removed and then 1638 // readded. Thus, we skip the first 10 events to prevent this from 1639 // causing problems. 1640 orientation += 1; 1641 } else if (orientation === 10) { 1642 // Record starting yaw to prevent jumping 1643 orientationYawOffset = q[2] / Math.PI * 180 + config.yaw; 1644 orientation = true; 1645 requestAnimationFrame(animate); 1646 } else { 1647 config.pitch = q[0] / Math.PI * 180; 1648 config.roll = -q[1] / Math.PI * 180; 1649 config.yaw = -q[2] / Math.PI * 180 + orientationYawOffset; 1650 } 1651} 1652 1653/** 1654 * Initializes renderer. 1655 * @private 1656 */ 1657function renderInit() { 1658 try { 1659 var params = {}; 1660 if (config.horizonPitch !== undefined) 1661 params.horizonPitch = config.horizonPitch * Math.PI / 180; 1662 if (config.horizonRoll !== undefined) 1663 params.horizonRoll = config.horizonRoll * Math.PI / 180; 1664 if (config.backgroundColor !== undefined) 1665 params.backgroundColor = config.backgroundColor; 1666 renderer.init(panoImage, config.type, config.dynamic, config.haov * Math.PI / 180, config.vaov * Math.PI / 180, config.vOffset * Math.PI / 180, renderInitCallback, params); 1667 if (config.dynamic !== true) { 1668 // Allow image to be garbage collected 1669 panoImage = undefined; 1670 } 1671 } catch(event) { 1672 // Panorama not loaded 1673 1674 // Display error if there is a bad texture 1675 if (event.type == 'webgl error' || event.type == 'no webgl') { 1676 anError(); 1677 } else if (event.type == 'webgl size error') { 1678 anError(config.strings.textureSizeError.replace('%s', event.width).replace('%s', event.maxWidth)); 1679 } else { 1680 anError(config.strings.unknownError); 1681 throw event; 1682 } 1683 } 1684} 1685 1686/** 1687 * Triggered when render initialization finishes. Handles fading between 1688 * scenes as well as showing the compass and hotspots and hiding the loading 1689 * display. 1690 * @private 1691 */ 1692function renderInitCallback() { 1693 // Fade if specified 1694 if (config.sceneFadeDuration && renderer.fadeImg !== undefined) { 1695 renderer.fadeImg.style.opacity = 0; 1696 // Remove image 1697 var fadeImg = renderer.fadeImg; 1698 delete renderer.fadeImg; 1699 setTimeout(function() { 1700 renderContainer.removeChild(fadeImg); 1701 fireEvent('scenechangefadedone'); 1702 }, config.sceneFadeDuration); 1703 } 1704 1705 // Show compass if applicable 1706 if (config.compass) { 1707 compass.style.display = 'inline'; 1708 } else { 1709 compass.style.display = 'none'; 1710 } 1711 1712 // Show hotspots 1713 createHotSpots(); 1714 1715 // Hide loading display 1716 infoDisplay.load.box.style.display = 'none'; 1717 if (preview !== undefined) { 1718 renderContainer.removeChild(preview); 1719 preview = undefined; 1720 } 1721 loaded = true; 1722 1723 animateInit(); 1724 1725 fireEvent('load'); 1726} 1727 1728/** 1729 * Creates hot spot element for the current scene. 1730 * @private 1731 * @param {Object} hs - The configuration for the hotspot 1732 */ 1733function createHotSpot(hs) { 1734 // Make sure hot spot pitch and yaw are numbers 1735 hs.pitch = Number(hs.pitch) || 0; 1736 hs.yaw = Number(hs.yaw) || 0; 1737 1738 var div = document.createElement('div'); 1739 div.className = 'pnlm-hotspot-base'; 1740 if (hs.cssClass) 1741 div.className += ' ' + hs.cssClass; 1742 else 1743 div.className += ' pnlm-hotspot pnlm-sprite pnlm-' + escapeHTML(hs.type); 1744 1745 var span = document.createElement('span'); 1746 if (hs.text) 1747 span.innerHTML = escapeHTML(hs.text); 1748 1749 var a; 1750 if (hs.video) { 1751 var video = document.createElement('video'), 1752 vidp = hs.video; 1753 if (config.basePath && !absoluteURL(vidp)) 1754 vidp = config.basePath + vidp; 1755 video.src = sanitizeURL(vidp); 1756 video.controls = true; 1757 video.style.width = hs.width + 'px'; 1758 renderContainer.appendChild(div); 1759 span.appendChild(video); 1760 } else if (hs.image) { 1761 var imgp = hs.image; 1762 if (config.basePath && !absoluteURL(imgp)) 1763 imgp = config.basePath + imgp; 1764 a = document.createElement('a'); 1765 a.href = sanitizeURL(hs.URL ? hs.URL : imgp, true); 1766 a.target = '_blank'; 1767 span.appendChild(a); 1768 var image = document.createElement('img'); 1769 image.src = sanitizeURL(imgp); 1770 image.style.width = hs.width + 'px'; 1771 image.style.paddingTop = '5px'; 1772 renderContainer.appendChild(div); 1773 a.appendChild(image); 1774 span.style.maxWidth = 'initial'; 1775 } else if (hs.URL) { 1776 a = document.createElement('a'); 1777 a.href = sanitizeURL(hs.URL, true); 1778 if (hs.attributes) { 1779 for (var key in hs.attributes) { 1780 a.setAttribute(key, hs.attributes[key]); 1781 } 1782 } else { 1783 //wpvr edit by sakib for in tab url open// -- handling from here 1784 if (hs.wpvr_url_open == 'on') { 1785 a.target = '_self'; 1786 } 1787 else { 1788 a.target = '_blank'; 1789 } 1790 } 1791 renderContainer.appendChild(a); 1792 div.className += ' pnlm-pointer'; 1793 span.className += ' pnlm-pointer'; 1794 a.appendChild(div); 1795 } else { 1796 if (hs.sceneId) { 1797 div.onclick = div.ontouchend = function() { 1798 if (!div.clicked) { 1799 div.clicked = true; 1800 loadScene(hs.sceneId, hs.targetPitch, hs.targetYaw, hs.targetHfov); 1801 } 1802 return false;
1803 }; 1804 div.className += ' pnlm-pointer'; 1805 span.className += ' pnlm-pointer'; 1806 } 1807 // renderContainer.appendChild("<span>title</span>"); 1808 renderContainer.appendChild(div); 1809 } 1810 1811 if (hs.createTooltipFunc) { 1812 hs.createTooltipFunc(div, hs.createTooltipArgs); 1813 } else if (hs.text || hs.video || hs.image) { 1814 div.classList.add('pnlm-tooltip'); 1815 div.appendChild(span); 1816 span.style.width = span.scrollWidth - 20 + 'px'; 1817 span.style.marginLeft = -(span.scrollWidth - div.offsetWidth) / 2 + 'px'; 1818 span.style.marginTop = -span.scrollHeight - 12 + 'px'; 1819 } 1820 if (hs.clickHandlerFunc) { 1821 div.addEventListener('click', function(e) { 1822 hs.clickHandlerFunc(e, hs.clickHandlerArgs); 1823 }, 'false'); 1824 div.className += ' pnlm-pointer'; 1825 span.className += ' pnlm-pointer'; 1826 } 1827 hs.div = div; 1828} 1829 1830/** 1831 * Creates hot spot elements for the current scene. 1832 * @private 1833 */ 1834function createHotSpots() { 1835 if (hotspotsCreated) return; 1836 1837 if (!config.hotSpots) { 1838 config.hotSpots = []; 1839 } else { 1840 // Sort by pitch so tooltip is never obscured by another hot spot 1841 config.hotSpots = config.hotSpots.sort(function(a, b) { 1842 return a.pitch < b.pitch; 1843 }); 1844 config.hotSpots.forEach(createHotSpot); 1845 } 1846 hotspotsCreated = true; 1847 renderHotSpots(); 1848} 1849 1850/** 1851 * Destroys currently created hot spot elements. 1852 * @private 1853 */ 1854function destroyHotSpots() { 1855 var hs = config.hotSpots; 1856 hotspotsCreated = false; 1857 delete config.hotSpots; 1858 if (hs) { 1859 for (var i = 0; i < hs.length; i++) { 1860 var current = hs[i].div; 1861 if (current) { 1862 while (current.parentNode && current.parentNode != renderContainer) { 1863 current = current.parentNode; 1864 } 1865 renderContainer.removeChild(current); 1866 } 1867 delete hs[i].div; 1868 } 1869 } 1870} 1871 1872/** 1873 * Renders hot spot, updating its position and visibility. 1874 * @private 1875 */ 1876function renderHotSpot(hs) { 1877 var hsPitchSin = Math.sin(hs.pitch * Math.PI / 180), 1878 hsPitchCos = Math.cos(hs.pitch * Math.PI / 180), 1879 configPitchSin = Math.sin(config.pitch * Math.PI / 180), 1880 configPitchCos = Math.cos(config.pitch * Math.PI / 180), 1881 yawCos = Math.cos((-hs.yaw + config.yaw) * Math.PI / 180); 1882 var z = hsPitchSin * configPitchSin + hsPitchCos * yawCos * configPitchCos; 1883 if ((hs.yaw <= 90 && hs.yaw > -90 && z <= 0) || 1884 ((hs.yaw > 90 || hs.yaw <= -90) && z <= 0)) { 1885 hs.div.style.visibility = 'hidden'; 1886 } else { 1887 var yawSin = Math.sin((-hs.yaw + config.yaw) * Math.PI / 180), 1888 hfovTan = Math.tan(config.hfov * Math.PI / 360); 1889 hs.div.style.visibility = 'visible'; 1890 // Subpixel rendering doesn't work in Firefox 1891 // https://bugzilla.mozilla.org/show_bug.cgi?id=739176 1892 var canvas = renderer.getCanvas(), 1893 canvasWidth = canvas.clientWidth, 1894 canvasHeight = canvas.clientHeight; 1895 var coord = [-canvasWidth / hfovTan * yawSin * hsPitchCos / z / 2, 1896 -canvasWidth / hfovTan * (hsPitchSin * configPitchCos - 1897 hsPitchCos * yawCos * configPitchSin) / z / 2]; 1898 // Apply roll 1899 var rollSin = Math.sin(config.roll * Math.PI / 180), 1900 rollCos = Math.cos(config.roll * Math.PI / 180); 1901 coord = [coord[0] * rollCos - coord[1] * rollSin, 1902 coord[0] * rollSin + coord[1] * rollCos]; 1903 // Apply transform 1904 coord[0] += (canvasWidth - hs.div.offsetWidth) / 2; 1905 coord[1] += (canvasHeight - hs.div.offsetHeight) / 2; 1906 var transform = 'translate(' + coord[0] + 'px, ' + coord[1] + 1907 'px) translateZ(9999px) rotate(' + config.roll + 'deg)'; 1908 if (hs.scale) { 1909 transform += ' scale(' + (origHfov/config.hfov) / z + ')'; 1910 } 1911 hs.div.style.webkitTransform = transform; 1912 hs.div.style.MozTransform = transform; 1913 hs.div.style.transform = transform; 1914 } 1915} 1916 1917/** 1918 * Renders hot spots, updating their positions and visibility. 1919 * @private 1920 */ 1921function renderHotSpots() {
1922 config.hotSpots.forEach(renderHotSpot); 1923} 1924 1925/** 1926 * Merges a scene configuration into the current configuration. 1927 * @private 1928 * @param {string} sceneId - Identifier of scene configuration to merge in. 1929 */ 1930function mergeConfig(sceneId) { 1931 config = {}; 1932 var k, s; 1933 var photoSphereExcludes = ['haov', 'vaov', 'vOffset', 'northOffset', 'horizonPitch', 'horizonRoll']; 1934 specifiedPhotoSphereExcludes = []; 1935 1936 // Merge default config 1937 for (k in defaultConfig) { 1938 if (defaultConfig.hasOwnProperty(k)) { 1939 config[k] = defaultConfig[k]; 1940 } 1941 } 1942 1943 // Merge default scene config 1944 for (k in initialConfig.default) { 1945 if (initialConfig.default.hasOwnProperty(k)) { 1946 if (k == 'strings') { 1947 for (s in initialConfig.default.strings) { 1948 if (initialConfig.default.strings.hasOwnProperty(s)) { 1949 config.strings[s] = escapeHTML(initialConfig.default.strings[s]); 1950 } 1951 } 1952 } else { 1953 config[k] = initialConfig.default[k]; 1954 if (photoSphereExcludes.indexOf(k) >= 0) { 1955 specifiedPhotoSphereExcludes.push(k); 1956 } 1957 } 1958 } 1959 } 1960 1961 // Merge current scene config 1962 if ((sceneId !== null) && (sceneId !== '') && (initialConfig.scenes) && (initialConfig.scenes[sceneId])) { 1963 var scene = initialConfig.scenes[sceneId]; 1964 for (k in scene) { 1965 if (scene.hasOwnProperty(k)) { 1966 if (k == 'strings') { 1967 for (s in scene.strings) { 1968 if (scene.strings.hasOwnProperty(s)) { 1969 config.strings[s] = escapeHTML(scene.strings[s]); 1970 } 1971 } 1972 } else { 1973 config[k] = scene[k]; 1974 if (photoSphereExcludes.indexOf(k) >= 0) { 1975 specifiedPhotoSphereExcludes.push(k); 1976 } 1977 } 1978 } 1979 } 1980 config.scene = sceneId; 1981 } 1982 1983 // Merge initial config 1984 for (k in initialConfig) { 1985 if (initialConfig.hasOwnProperty(k)) { 1986 if (k == 'strings') { 1987 for (s in initialConfig.strings) { 1988 if (initialConfig.strings.hasOwnProperty(s)) { 1989 config.strings[s] = escapeHTML(initialConfig.strings[s]); 1990 } 1991 } 1992 } else { 1993 config[k] = initialConfig[k]; 1994 if (photoSphereExcludes.indexOf(k) >= 0) { 1995 specifiedPhotoSphereExcludes.push(k); 1996 } 1997 } 1998 } 1999 } 2000} 2001 2002/** 2003 * Processes configuration options. 2004 * @param {boolean} [isPreview] - Whether or not the preview is being displayed 2005 * @private 2006 */ 2007function processOptions(isPreview) { 2008 isPreview = isPreview ? isPreview : false; 2009 2010 // Process preview first so it always loads before the browser hits its 2011 // maximum number of connections to a server as can happen with cubic 2012 // panoramas 2013 if (isPreview && 'preview' in config) { 2014 var p = config.preview; 2015 if (config.basePath && !absoluteURL(p)) 2016 p = config.basePath + p; 2017 preview = document.createElement('div'); 2018 preview.className = 'pnlm-preview-img'; 2019 preview.style.backgroundImage = "url('" + sanitizeURLForCss(p) + "')"; 2020 renderContainer.appendChild(preview); 2021 } 2022 2023 // Handle different preview values 2024 var title = config.title, 2025 author = config.author; 2026 if (isPreview) { 2027 if ('previewTitle' in config) 2028 config.title = config.previewTitle; 2029 if ('previewAuthor' in config) 2030 config.author = config.previewAuthor; 2031 } 2032 2033 // Reset title / author display 2034 if (!config.hasOwnProperty('title')) 2035 infoDisplay.title.innerHTML = ''; 2036 if (!config.hasOwnProperty('author')) 2037 infoDisplay.author.innerHTML = ''; 2038 if (!config.hasOwnProperty('title') && !config.hasOwnProperty('author')) 2039 infoDisplay.container.style.display = 'none'; 2040 2041 // Fill in load button label and loading box text 2042 controls.load.innerHTML = '<p>' + config.strings.loadButtonLabel + '</p>'; 2043 infoDisplay.load.boxp.innerHTML = config.strings.loadingLabel; 2044 2045 // Process other options 2046 for (var key in config) { 2047 if (config.hasOwnProperty(key)) { 2048 switch(key) { 2049 case 'title': 2050 infoDisplay.title.innerHTML = escapeHTML(config[key]); 2051 infoDisplay.container.style.display = 'inline'; 2052 break; 2053 2054 case 'author': 2055 var authorText = escapeHTML(config[key]); 2056 if (config.authorURL) { 2057 var authorLink = document.createElement('a'); 2058 authorLink.href = sanitizeURL(config['authorURL'], true); 2059 authorLink.target = '_blank'; 2060 authorLink.innerHTML = escapeHTML(config[key]); 2061 authorText = authorLink.outerHTML; 2062 } 2063 infoDisplay.author.innerHTML = config.strings.bylineLabel.replace('%s', authorText); 2064 infoDisplay.container.style.display = 'inline'; 2065 break; 2066 2067 case 'fallback': 2068 var link = document.createElement('a'); 2069 link.href = sanitizeURL(config[key], true); 2070 link.target = '_blank'; 2071 link.textContent = 'Click here to view this panorama in an alternative viewer.'; 2072 var message = document.createElement('p'); 2073 message.textContent = 'Your browser does not support WebGL.'; 2074 message.appendChild(document.createElement('br')); 2075 message.appendChild(link); 2076 infoDisplay.errorMsg.innerHTML = ''; // Removes all children nodes 2077 infoDisplay.errorMsg.appendChild(message); 2078 break; 2079 2080 case 'hfov': 2081 setHfov(Number(config[key])); 2082 break; 2083 2084 case 'autoLoad': 2085 if (config[key] === true && renderer === undefined) { 2086 // Show loading box 2087 infoDisplay.load.box.style.display = 'inline'; 2088 // Hide load button 2089 controls.load.style.display = 'none'; 2090 // Initialize 2091 init(); 2092 } 2093 break; 2094 2095 case 'showZoomCtrl': 2096 if (config[key] && config.showControls != false) { 2097 // Show zoom controls 2098 controls.zoom.style.display = 'block'; 2099 } else { 2100 // Hide zoom controls 2101 controls.zoom.style.display = 'none'; 2102 } 2103 break; 2104 2105 case 'showFullscreenCtrl': 2106 if (config[key] && config.showControls != false && ('fullscreen' in document || 'mozFullScreen' in document || 2107 'webkitIsFullScreen' in document || 'msFullscreenElement' in document)) { 2108 2109 // Show fullscreen control 2110 controls.fullscreen.style.display = 'block'; 2111 } else { 2112 // Hide fullscreen control 2113 controls.fullscreen.style.display = 'none'; 2114 } 2115 break; 2116 2117 case 'hotSpotDebug': 2118 if (config[key]) 2119 hotSpotDebugIndicator.style.display = 'block'; 2120 else 2121 hotSpotDebugIndicator.style.display = 'none'; 2122 break; 2123 2124 case 'showControls': 2125 if (!config[key]) { 2126 controls.orientation.style.display = 'none'; 2127 controls.zoom.style.display = 'none'; 2128 controls.fullscreen.style.display = 'none'; 2129 } 2130 break; 2131 2132 case 'orientationOnByDefault': 2133 if (config[key]) 2134 startOrientation(); 2135 break; 2136 } 2137 } 2138 } 2139 2140 if (isPreview) { 2141 // Restore original values if changed for preview 2142 if (title) 2143 config.title = title; 2144 else 2145 delete config.title; 2146 if (author) 2147 config.author = author; 2148 else 2149 delete config.author; 2150 } 2151} 2152 2153/** 2154 * Toggles fullscreen mode. 2155 * @private 2156 */ 2157function toggleFullscreen() { 2158 if (loaded && !error) { 2159 if (!fullscreenActive) { 2160 try { 2161 if (container.requestFullscreen) { 2162 container.requestFullscreen(); 2163 } else if (container.mozRequestFullScreen) { 2164 container.mozRequestFullScreen(); 2165 } else if (container.msRequestFullscreen) { 2166 container.msRequestFullscreen(); 2167 } else { 2168 container.webkitRequestFullScreen(); 2169 } 2170 } catch(event) { 2171 // Fullscreen doesn't work 2172 } 2173 } else { 2174 if (document.exitFullscreen) { 2175 document.exitFullscreen(); 2176 } else if (document.mozCancelFullScreen) { 2177 document.mozCancelFullScreen(); 2178 } else if (document.webkitCancelFullScreen) { 2179 document.webkitCancelFullScreen(); 2180 } else if (document.msExitFullscreen) { 2181 document.msExitFullscreen(); 2182 } 2183 } 2184 } 2185} 2186 2187/** 2188 * Event handler for fullscreen changes. 2189 * @private 2190 */ 2191function onFullScreenChange(resize) { 2192 if (document.fullscreenElement || document.fullscreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement) { 2193 controls.fullscreen.classList.add('pnlm-fullscreen-toggle-button-active'); 2194 fullscreenActive = true; 2195 } else { 2196 controls.fullscreen.classList.remove('pnlm-fullscreen-toggle-button-active'); 2197 fullscreenActive = false;
vendor: 5,689 bytes, lines 2198-2385
2198 } 2199 if (resize !== 'resize') 2200 fireEvent('fullscreenchange', fullscreenActive); 2201 // Resize renderer (deal with browser quirks and fixes #155) 2202 renderer.resize(); 2203 setHfov(config.hfov); 2204 animateInit(); 2205} 2206 2207/** 2208 * Increases panorama zoom. For use with zoom button. 2209 * @private 2210 */ 2211function zoomIn() { 2212 if (loaded) { 2213 setHfov(config.hfov - 5); 2214 animateInit(); 2215 } 2216} 2217 2218/** 2219 * Decreases panorama zoom. For use with zoom button. 2220 * @private 2221 */ 2222function zoomOut() { 2223 if (loaded) { 2224 setHfov(config.hfov + 5); 2225 animateInit(); 2226 } 2227} 2228 2229/** 2230 * Clamps horzontal field of view to viewer's limits. 2231 * @private 2232 * @param {number} hfov - Input horizontal field of view (in degrees) 2233 * @return {number} - Clamped horizontal field of view (in degrees) 2234 */ 2235function constrainHfov(hfov) { 2236 // Keep field of view within bounds 2237 var minHfov = config.minHfov; 2238 if (config.type == 'multires' && renderer && !config.multiResMinHfov) { 2239 minHfov = Math.min(minHfov, renderer.getCanvas().width / (config.multiRes.cubeResolution / 90 * 0.9)); 2240 } 2241 if (minHfov > config.maxHfov) { 2242 // Don't change view if bounds don't make sense 2243 console.log('HFOV bounds do not make sense (minHfov > maxHfov).'); 2244 return config.hfov; 2245 } 2246 var newHfov = config.hfov; 2247 if (hfov < minHfov) { 2248 newHfov = minHfov; 2249 } else if (hfov > config.maxHfov) { 2250 newHfov = config.maxHfov; 2251 } else { 2252 newHfov = hfov; 2253 } 2254 // Optionally avoid showing background (empty space) on top or bottom by adapting newHfov 2255 if (config.avoidShowingBackground && renderer) { 2256 var canvas = renderer.getCanvas(); 2257 newHfov = Math.min(newHfov, 2258 Math.atan(Math.tan((config.maxPitch - config.minPitch) / 360 * Math.PI) / 2259 canvas.height * canvas.width) * 360 / Math.PI); 2260 } 2261 return newHfov; 2262} 2263 2264/** 2265 * Sets viewer's horizontal field of view. 2266 * @private 2267 * @param {number} hfov - Desired horizontal field of view in degrees. 2268 */ 2269function setHfov(hfov) { 2270 config.hfov = constrainHfov(hfov); 2271 fireEvent('zoomchange', config.hfov); 2272} 2273 2274/** 2275 * Stops auto rotation and animated moves. 2276 * @private 2277 */ 2278function stopAnimation() { 2279 animatedMove = {}; 2280 autoRotateSpeed = config.autoRotate ? config.autoRotate : autoRotateSpeed; 2281 config.autoRotate = false; 2282} 2283 2284/** 2285 * Loads panorama. 2286 * @private 2287 */ 2288function load() { 2289 // Since WebGL error handling is very general, first we clear any error box 2290 // since it is a new scene and the error from previous maybe because of lacking 2291 // memory etc and not because of a lack of WebGL support etc 2292 clearError(); 2293 loaded = false; 2294 2295 controls.load.style.display = 'none'; 2296 infoDisplay.load.box.style.display = 'inline'; 2297 init(); 2298} 2299 2300/** 2301 * Loads scene. 2302 * @private 2303 * @param {string} sceneId - Identifier of scene configuration to merge in. 2304 * @param {number} targetPitch - Pitch viewer should be centered on once scene loads. 2305 * @param {number} targetYaw - Yaw viewer should be centered on once scene loads. 2306 * @param {number} targetHfov - HFOV viewer should use once scene loads. 2307 * @param {boolean} [fadeDone] - If `true`, fade setup is skipped. 2308 */ 2309function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) { 2310 if (!loaded) 2311 fadeDone = true; // Don't try to fade when there isn't a scene loaded 2312 loaded = false; 2313 animatedMove = {}; 2314 2315 // Set up fade if specified 2316 var fadeImg, workingPitch, workingYaw, workingHfov; 2317 if (config.sceneFadeDuration && !fadeDone) { 2318 var data = renderer.render(config.pitch * Math.PI / 180, config.yaw * Math.PI / 180, config.hfov * Math.PI / 180, {returnImage: true}); 2319 if (data !== undefined) { 2320 fadeImg = new Image(); 2321 fadeImg.className = 'pnlm-fade-img'; 2322 fadeImg.style.transition = 'opacity ' + (config.sceneFadeDuration / 1000) + 's'; 2323 fadeImg.style.width = '100%'; 2324 fadeImg.style.height = '100%'; 2325 fadeImg.onload = function() { 2326 loadScene(sceneId, targetPitch, targetYaw, targetHfov, true); 2327 }; 2328 fadeImg.src = data; 2329 renderContainer.appendChild(fadeImg); 2330 renderer.fadeImg = fadeImg; 2331 return; 2332 } 2333 } 2334 2335 // Set new pointing 2336 if (targetPitch === 'same') { 2337 workingPitch = config.pitch; 2338 } else { 2339 workingPitch = targetPitch; 2340 } 2341 if (targetYaw === 'same') { 2342 workingYaw = config.yaw; 2343 } else if (targetYaw === 'sameAzimuth') { 2344 workingYaw = config.yaw + (config.northOffset || 0) - (initialConfig.scenes[sceneId].northOffset || 0); 2345 } else { 2346 workingYaw = targetYaw; 2347 } 2348 if (targetHfov === 'same') { 2349 workingHfov = config.hfov; 2350 } else { 2351 workingHfov = targetHfov; 2352 } 2353 2354 // Destroy hot spots from previous scene 2355 destroyHotSpots(); 2356 2357 // Create the new config for the scene 2358 mergeConfig(sceneId); 2359 2360 // Stop motion 2361 speed.yaw = speed.pitch = speed.hfov = 0; 2362 2363 // Reload scene 2364 processOptions(); 2365 if (workingPitch !== undefined) { 2366 config.pitch = workingPitch; 2367 } 2368 if (workingYaw !== undefined) { 2369 config.yaw = workingYaw; 2370 } 2371 if (workingHfov !== undefined) { 2372 config.hfov = workingHfov; 2373 } 2374 fireEvent('scenechange', sceneId); 2375 load(); 2376} 2377 2378/** 2379 * Stop using device orientation. 2380 * @private 2381 */ 2382function stopOrientation() { 2383 window.removeEventListener('deviceorientation', orientationListener); 2384 controls.orientation.classList.remove('pnlm-orientation-button-active'); 2385 orientation = false;
2386} 2387 2388/** 2389 * Start using device orientation. 2390 * @private 2391 */ 2392function startOrientation() { 2393 if (!orientationSupport) 2394 return; 2395 if (typeof DeviceMotionEvent !== 'undefined' && 2396 typeof DeviceMotionEvent.requestPermission === 'function') { 2397 DeviceOrientationEvent.requestPermission().then(function(response) { 2398 if (response == 'granted') { 2399 orientation = 1; 2400 window.addEventListener('deviceorientation', orientationListener); 2401 controls.orientation.classList.add('pnlm-orientation-button-active'); 2402 } 2403 }); 2404 } else { 2405 orientation = 1; 2406 window.addEventListener('deviceorientation', orientationListener); 2407 controls.orientation.classList.add('pnlm-orientation-button-active'); 2408 } 2409} 2410 2411/** 2412 * Escapes HTML string (to mitigate possible DOM XSS attacks). 2413 * @private 2414 * @param {string} s - String to escape 2415 * @returns {string} Escaped string 2416 */ 2417function escapeHTML(s) { 2418 if (!initialConfig.escapeHTML) 2419 return String(s).split('\n').join('<br>'); 2420 return String(s).split(/&/g).join('&') 2421 .split('"').join('"') 2422 .split("'").join(''') 2423 .split('<').join('<') 2424 .split('>').join('>') 2425 .split('/').join('/') 2426 .split('\n').join('<br>'); // Allow line breaks 2427} 2428 2429/** 2430 * Removes possibility of XSS attacks with URLs. 2431 * The URL cannot be of protocol 'javascript'. 2432 * @private 2433 * @param {string} url - URL to sanitize 2434 * @param {boolean} href - True if URL is for link (blocks data URIs) 2435 * @returns {string} Sanitized URL 2436 */ 2437function sanitizeURL(url, href) { 2438 try { 2439 var decoded_url = decodeURIComponent(unescape(url)).replace(/[^\w:]/g, '').toLowerCase(); 2440 } catch (e) { 2441 return 'about:blank'; 2442 } 2443 if (decoded_url.indexOf('javascript:') === 0 || 2444 decoded_url.indexOf('vbscript:') === 0) { 2445 console.log('Script URL removed.'); 2446 return 'about:blank'; 2447 } 2448 if (href && decoded_url.indexOf('data:') === 0) { 2449 console.log('Data URI removed from link.'); 2450 return 'about:blank'; 2451 } 2452 return url; 2453} 2454 2455/** 2456 * Unescapes HTML entities. 2457 * Copied from Marked.js 0.7.0. 2458 * @private 2459 * @param {string} url - URL to sanitize 2460 * @param {boolean} href - True if URL is for link (blocks data URIs) 2461 * @returns {string} Sanitized URL 2462 */ 2463function unescape(html) { 2464 // Explicitly match decimal, hex, and named HTML entities 2465 return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) { 2466 n = n.toLowerCase(); 2467 if (n === 'colon') return ':'; 2468 if (n.charAt(0) === '#') { 2469 return n.charAt(1) === 'x' 2470 ? String.fromCharCode(parseInt(n.substring(2), 16)) 2471 : String.fromCharCode(+n.substring(1)); 2472 } 2473 return ''; 2474 }); 2475} 2476 2477/** 2478 * Removes possibility of XSS atacks with URLs for CSS. 2479 * The URL will be sanitized with `sanitizeURL()` and single quotes 2480 * and double quotes escaped. 2481 * @private 2482 * @param {string} url - URL to sanitize 2483 * @returns {string} Sanitized URL 2484 */ 2485function sanitizeURLForCss(url) { 2486 return sanitizeURL(url) 2487 .replace(/"/g, '%22') 2488 .replace(/'/g, '%27'); 2489} 2490 2491/** 2492 * Checks whether or not a panorama is loaded. 2493 * @memberof Viewer 2494 * @instance 2495 * @returns {boolean} `true` if a panorama is loaded, else `false` 2496 */ 2497this.isLoaded = function() { 2498 return Boolean(loaded); 2499}; 2500 2501/** 2502 * Returns the pitch of the center of the view. 2503 * @memberof Viewer 2504 * @instance 2505 * @returns {number} Pitch in degrees 2506 */ 2507this.getPitch = function() { 2508 return config.pitch; 2509}; 2510 2511/** 2512 * Sets the pitch of the center of the view. 2513 * @memberof Viewer 2514 * @instance 2515 * @param {number} pitch - Pitch in degrees 2516 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2517 * @param {function} [callback] - Function to call when animation finishes 2518 * @param {object} [callbackArgs] - Arguments to pass to callback function 2519 * @returns {Viewer} `this` 2520 */ 2521this.setPitch = function(pitch, animated, callback, callbackArgs) { 2522 latestInteraction = Date.now(); 2523 if (Math.abs(pitch - config.pitch) <= eps) { 2524 if (typeof callback == 'function') 2525 callback(callbackArgs); 2526 return this; 2527 } 2528 animated = animated == undefined ? 1000: Number(animated); 2529 if (animated) { 2530 animatedMove.pitch = { 2531 'startTime': Date.now(), 2532 'startPosition': config.pitch, 2533 'endPosition': pitch, 2534 'duration': animated 2535 }; 2536 if (typeof callback == 'function') 2537 setTimeout(function(){callback(callbackArgs);}, animated); 2538 } else { 2539 config.pitch = pitch; 2540 } 2541 animateInit(); 2542 return this; 2543}; 2544 2545/** 2546 * Returns the minimum and maximum allowed pitches (in degrees). 2547 * @memberof Viewer 2548 * @instance 2549 * @returns {number[]} [minimum pitch, maximum pitch] 2550 */ 2551this.getPitchBounds = function() { 2552 return [config.minPitch, config.maxPitch]; 2553}; 2554 2555/** 2556 * Set the minimum and maximum allowed pitches (in degrees). 2557 * @memberof Viewer 2558 * @instance 2559 * @param {number[]} bounds - [minimum pitch, maximum pitch] 2560 * @returns {Viewer} `this` 2561 */
2562this.setPitchBounds = function(bounds) { 2563 config.minPitch = Math.max(-90, Math.min(bounds[0], 90)); 2564 config.maxPitch = Math.max(-90, Math.min(bounds[1], 90)); 2565 return this; 2566}; 2567 2568/** 2569 * Returns the yaw of the center of the view. 2570 * @memberof Viewer 2571 * @instance 2572 * @returns {number} Yaw in degrees 2573 */ 2574this.getYaw = function() { 2575 return (config.yaw + 540) % 360 - 180; 2576}; 2577 2578/** 2579 * Sets the yaw of the center of the view. 2580 * @memberof Viewer 2581 * @instance 2582 * @param {number} yaw - Yaw in degrees [-180, 180] 2583 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2584 * @param {function} [callback] - Function to call when animation finishes 2585 * @param {object} [callbackArgs] - Arguments to pass to callback function 2586 * @returns {Viewer} `this` 2587 */ 2588this.setYaw = function(yaw, animated, callback, callbackArgs) { 2589 latestInteraction = Date.now(); 2590 if (Math.abs(yaw - config.yaw) <= eps) { 2591 if (typeof callback == 'function') 2592 callback(callbackArgs); 2593 return this; 2594 } 2595 animated = animated == undefined ? 1000: Number(animated); 2596 yaw = ((yaw + 180) % 360) - 180; // Keep in bounds 2597 if (animated) { 2598 // Animate in shortest direction 2599 if (config.yaw - yaw > 180) 2600 yaw += 360; 2601 else if (yaw - config.yaw > 180) 2602 yaw -= 360; 2603 2604 animatedMove.yaw = { 2605 'startTime': Date.now(), 2606 'startPosition': config.yaw, 2607 'endPosition': yaw, 2608 'duration': animated 2609 }; 2610 if (typeof callback == 'function') 2611 setTimeout(function(){callback(callbackArgs);}, animated); 2612 } else { 2613 config.yaw = yaw; 2614 } 2615 animateInit(); 2616 return this; 2617}; 2618 2619/** 2620 * Returns the minimum and maximum allowed pitches (in degrees). 2621 * @memberof Viewer 2622 * @instance 2623 * @returns {number[]} [yaw pitch, maximum yaw] 2624 */ 2625this.getYawBounds = function() { 2626 return [config.minYaw, config.maxYaw]; 2627}; 2628 2629/** 2630 * Set the minimum and maximum allowed yaws (in degrees [-360, 360]). 2631 * @memberof Viewer 2632 * @instance 2633 * @param {number[]} bounds - [minimum yaw, maximum yaw] 2634 * @returns {Viewer} `this` 2635 */ 2636this.setYawBounds = function(bounds) { 2637 config.minYaw = Math.max(-360, Math.min(bounds[0], 360)); 2638 config.maxYaw = Math.max(-360, Math.min(bounds[1], 360)); 2639 return this; 2640}; 2641 2642/** 2643 * Returns the horizontal field of view. 2644 * @memberof Viewer 2645 * @instance 2646 * @returns {number} Horizontal field of view in degrees 2647 */ 2648this.getHfov = function() { 2649 return config.hfov; 2650}; 2651 2652/** 2653 * Sets the horizontal field of view. 2654 * @memberof Viewer 2655 * @instance 2656 * @param {number} hfov - Horizontal field of view in degrees 2657 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2658 * @param {function} [callback] - Function to call when animation finishes 2659 * @param {object} [callbackArgs] - Arguments to pass to callback function 2660 * @returns {Viewer} `this` 2661 */ 2662this.setHfov = function(hfov, animated, callback, callbackArgs) { 2663 latestInteraction = Date.now(); 2664 if (Math.abs(hfov - config.hfov) <= eps) { 2665 if (typeof callback == 'function') 2666 callback(callbackArgs); 2667 return this; 2668 } 2669 animated = animated == undefined ? 1000: Number(animated); 2670 if (animated) { 2671 animatedMove.hfov = { 2672 'startTime': Date.now(), 2673 'startPosition': config.hfov, 2674 'endPosition': constrainHfov(hfov), 2675 'duration': animated 2676 }; 2677 if (typeof callback == 'function') 2678 setTimeout(function(){callback(callbackArgs);}, animated); 2679 } else { 2680 setHfov(hfov); 2681 } 2682 animateInit(); 2683 return this; 2684}; 2685 2686/** 2687 * Returns the minimum and maximum allowed horizontal fields of view 2688 * (in degrees). 2689 * @memberof Viewer 2690 * @instance 2691 * @returns {number[]} [minimum hfov, maximum hfov] 2692 */ 2693this.getHfovBounds = function() { 2694 return [config.minHfov, config.maxHfov]; 2695}; 2696 2697/** 2698 * Set the minimum and maximum allowed horizontal fields of view (in degrees). 2699 * @memberof Viewer 2700 * @instance 2701 * @param {number[]} bounds - [minimum hfov, maximum hfov] 2702 * @returns {Viewer} `this` 2703 */
2704this.setHfovBounds = function(bounds) { 2705 config.minHfov = Math.max(0, bounds[0]); 2706 config.maxHfov = Math.max(0, bounds[1]); 2707 return this; 2708}; 2709 2710/** 2711 * Set a new view. Any parameters not specified remain the same. 2712 * @memberof Viewer 2713 * @instance 2714 * @param {number} [pitch] - Target pitch 2715 * @param {number} [yaw] - Target yaw 2716 * @param {number} [hfov] - Target hfov 2717 * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation 2718 * @param {function} [callback] - Function to call when animation finishes 2719 * @param {object} [callbackArgs] - Arguments to pass to callback function 2720 * @returns {Viewer} `this` 2721 */ 2722this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) { 2723 animated = animated == undefined ? 1000: Number(animated); 2724 if (pitch !== undefined && Math.abs(pitch - config.pitch) > eps) { 2725 this.setPitch(pitch, animated, callback, callbackArgs); 2726 callback = undefined; 2727 } 2728 if (yaw !== undefined && Math.abs(yaw - config.yaw) > eps) { 2729 this.setYaw(yaw, animated, callback, callbackArgs); 2730 callback = undefined; 2731 } 2732 if (hfov !== undefined && Math.abs(hfov - config.hfov) > eps) { 2733 this.setHfov(hfov, animated, callback, callbackArgs); 2734 callback = undefined; 2735 } 2736 if (typeof callback == 'function') 2737 callback(callbackArgs); 2738 return this; 2739}; 2740 2741/** 2742 * Returns the panorama's north offset. 2743 * @memberof Viewer 2744 * @instance 2745 * @returns {number} North offset in degrees 2746 */ 2747this.getNorthOffset = function() { 2748 return config.northOffset; 2749}; 2750 2751/** 2752 * Sets the panorama's north offset. 2753 * @memberof Viewer 2754 * @instance 2755 * @param {number} heading - North offset in degrees 2756 * @returns {Viewer} `this` 2757 */ 2758this.setNorthOffset = function(heading) { 2759 config.northOffset = Math.min(360, Math.max(0, heading)); 2760 animateInit(); 2761 return this; 2762}; 2763 2764/** 2765 * Returns the panorama's horizon roll. 2766 * @memberof Viewer 2767 * @instance 2768 * @returns {number} Horizon roll in degrees 2769 */ 2770this.getHorizonRoll = function() { 2771 return config.horizonRoll; 2772}; 2773 2774/** 2775 * Sets the panorama's horizon roll. 2776 * @memberof Viewer 2777 * @instance 2778 * @param {number} roll - Horizon roll in degrees [-90, 90] 2779 * @returns {Viewer} `this` 2780 */ 2781this.setHorizonRoll = function(roll) { 2782 config.horizonRoll = Math.min(90, Math.max(-90, roll)); 2783 renderer.setPose(config.horizonPitch * Math.PI / 180, config.horizonRoll * Math.PI / 180); 2784 animateInit(); 2785 return this; 2786}; 2787 2788/** 2789 * Returns the panorama's horizon pitch. 2790 * @memberof Viewer 2791 * @instance 2792 * @returns {number} Horizon pitch in degrees 2793 */ 2794this.getHorizonPitch = function() { 2795 return config.horizonPitch; 2796}; 2797 2798/** 2799 * Sets the panorama's horizon pitch. 2800 * @memberof Viewer 2801 * @instance 2802 * @param {number} pitch - Horizon pitch in degrees [-90, 90] 2803 * @returns {Viewer} `this` 2804 */ 2805this.setHorizonPitch = function(pitch) { 2806 config.horizonPitch = Math.min(90, Math.max(-90, pitch)); 2807 renderer.setPose(config.horizonPitch * Math.PI / 180, config.horizonRoll * Math.PI / 180); 2808 animateInit(); 2809 return this; 2810}; 2811 2812/** 2813 * Start auto rotation. 2814 * 2815 * Before starting rotation, the viewer is panned to `pitch`. 2816 * @memberof Viewer 2817 * @instance 2818 * @param {number} [speed] - Auto rotation speed / direction. If not specified, previous value is used. 2819 * @param {number} [pitch] - The pitch to rotate at. If not specified, inital pitch is used. 2820 * @returns {Viewer} `this` 2821 */ 2822this.startAutoRotate = function(speed, pitch) { 2823 speed = speed || autoRotateSpeed || 1; 2824 pitch = pitch === undefined ? origPitch : pitch; 2825 config.autoRotate = speed; 2826 _this.lookAt(pitch, undefined, origHfov, 3000); 2827 animateInit(); 2828 return this; 2829}; 2830 2831/** 2832 * Stop auto rotation. 2833 * @memberof Viewer 2834 * @instance 2835 * @returns {Viewer} `this` 2836 */ 2837this.stopAutoRotate = function() { 2838 autoRotateSpeed = config.autoRotate ? config.autoRotate : autoRotateSpeed; 2839 config.autoRotate = false; 2840 config.autoRotateInactivityDelay = -1; 2841 return this; 2842}; 2843 2844/** 2845 * Stops all movement. 2846 * @memberof Viewer 2847 * @instance 2848 */ 2849this.stopMovement = function() { 2850 stopAnimation(); 2851 speed = {'yaw': 0, 'pitch': 0, 'hfov': 0}; 2852}; 2853 2854/** 2855 * Returns the panorama renderer. 2856 * @memberof Viewer 2857 * @instance 2858 * @returns {Renderer} 2859 */ 2860this.getRenderer = function() { 2861 return renderer; 2862}; 2863 2864/** 2865 * Sets update flag for dynamic content. 2866 * @memberof Viewer 2867 * @instance 2868 * @param {boolean} bool - Whether or not viewer should update even when still 2869 * @returns {Viewer} `this` 2870 */ 2871this.setUpdate = function(bool) { 2872 update = bool === true; 2873 if (renderer === undefined) 2874 onImageLoad(); 2875 else 2876 animateInit(); 2877 return this; 2878}; 2879 2880/** 2881 * Calculate panorama pitch and yaw from location of mouse event. 2882 * @memberof Viewer 2883 * @instance 2884 * @param {MouseEvent} event - Document mouse down event. 2885 * @returns {number[]} [pitch, yaw] 2886 */ 2887this.mouseEventToCoords = function(event) { 2888 return mouseEventToCoords(event); 2889}; 2890 2891/** 2892 * Change scene being viewed. 2893 * @memberof Viewer 2894 * @instance 2895 * @param {string}
2895 sceneId - Identifier of scene to switch to. 2896 * @param {number} [pitch] - Pitch to use with new scene 2897 * @param {number} [yaw] - Yaw to use with new scene 2898 * @param {number} [hfov] - HFOV to use with new scene 2899 * @returns {Viewer} `this` 2900 */ 2901this.loadScene = function(sceneId, pitch, yaw, hfov) { 2902 if (loaded !== false) 2903 loadScene(sceneId, pitch, yaw, hfov); 2904 return this; 2905}; 2906 2907/** 2908 * Get ID of current scene. 2909 * @memberof Viewer 2910 * @instance 2911 * @returns {string} ID of current scene 2912 */ 2913this.getScene = function() { 2914 return config.scene; 2915}; 2916 2917/** 2918 * Add a new scene. 2919 * @memberof Viewer 2920 * @instance 2921 * @param {string} sceneId - The ID of the new scene 2922 * @param {string} config - The configuration of the new scene 2923 * @returns {Viewer} `this` 2924 */ 2925this.addScene = function(sceneId, config) { 2926 initialConfig.scenes[sceneId] = config; 2927 return this; 2928}; 2929 2930/** 2931 * Remove a scene. 2932 * @memberof Viewer 2933 * @instance 2934 * @param {string} sceneId - The ID of the scene 2935 * @returns {boolean} False if the scene is the current scene or if the scene doesn't exists, else true 2936 */ 2937this.removeScene = function(sceneId) { 2938 if (config.scene === sceneId || !initialConfig.scenes.hasOwnProperty(sceneId)) 2939 return false; 2940 delete initialConfig.scenes[sceneId]; 2941 return true; 2942}; 2943 2944/** 2945 * Toggle fullscreen. 2946 * @memberof Viewer 2947 * @instance 2948 * @returns {Viewer} `this` 2949 */ 2950this.toggleFullscreen = function() { 2951 toggleFullscreen(); 2952 return this; 2953}; 2954 2955/** 2956 * Get configuration of current scene. 2957 * @memberof Viewer 2958 * @instance 2959 * @returns {Object} Configuration of current scene 2960 */ 2961this.getConfig = function() { 2962 return config; 2963}; 2964 2965/** 2966 * Get viewer's container element. 2967 * @memberof Viewer 2968 * @instance 2969 * @returns {HTMLElement} Container `div` element 2970 */ 2971this.getContainer = function() { 2972 return container; 2973}; 2974 2975/** 2976 * Add a new hot spot. 2977 * @memberof Viewer 2978 * @instance 2979 * @param {Object} hs - The configuration for the hot spot 2980 * @param {string} [sceneId] - Adds hot spot to specified scene if provided, else to current scene 2981 * @returns {Viewer} `this` 2982 * @throws Throws an error if the scene ID is provided but invalid 2983 */ 2984this.addHotSpot = function(hs, sceneId) { 2985 if (sceneId === undefined && config.scene === undefined) { 2986 // Not a tour 2987 config.hotSpots.push(hs); 2988 } else { 2989 // Tour 2990 var id = sceneId !== undefined ? sceneId : config.scene; 2991 if (initialConfig.scenes.hasOwnProperty(id)) { 2992 if (!initialConfig.scenes[id].hasOwnProperty('hotSpots')) { 2993 initialConfig.scenes[id].hotSpots = []; // Create hot spots array if needed 2994 if (id == config.scene) 2995 config.hotSpots = initialConfig.scenes[id].hotSpots; // Link to current config 2996 } 2997 initialConfig.scenes[id].hotSpots.push(hs); // Add hot spot to config 2998 } else { 2999 throw 'Invalid scene ID!'; 3000 } 3001 } 3002 if (sceneId === undefined || config.scene == sceneId) { 3003 // Add to current scene 3004 createHotSpot(hs); 3005 if (loaded) 3006 renderHotSpot(hs); 3007 } 3008 return this; 3009}; 3010 3011/** 3012 * Remove a hot spot. 3013 * @memberof Viewer 3014 * @instance 3015 * @param {string} hotSpotId - The ID of the hot spot 3016 * @param {string} [sceneId] - Removes hot spot from specified scene if provided, else from current scene 3017 * @returns {boolean} True if deletion is successful, else false 3018 */ 3019this.removeHotSpot = function(hotSpotId, sceneId) { 3020 if (sceneId === undefined || config.scene == sceneId) { 3021 if (!config.hotSpots) 3022 return false; 3023 for (var i = 0; i < config.hotSpots.length; i++) { 3024 if (config.hotSpots[i].hasOwnProperty('id') && 3025 config.hotSpots[i].id === hotSpotId) { 3026 // Delete hot spot DOM elements 3027 var current = config.hotSpots[i].div; 3028 while (current.parentNode != renderContainer) 3029 current = current.parentNode; 3030 renderContainer.removeChild(current); 3031 delete config.hotSpots[i].div; 3032 // Remove hot spot from configuration 3033 config.hotSpots.splice(i, 1); 3034 return true; 3035 } 3036 } 3037 } else { 3038 if (initialConfig.scenes.hasOwnProperty(sceneId)) { 3039 if (!initialConfig.scenes[sceneId].hasOwnProperty('hotSpots')) 3040 return false;
vendor: 5,046 bytes, lines 3041-3209
3041 for (var j = 0; j < initialConfig.scenes[sceneId].hotSpots.length; j++) { 3042 if (initialConfig.scenes[sceneId].hotSpots[j].hasOwnProperty('id') && 3043 initialConfig.scenes[sceneId].hotSpots[j].id === hotSpotId) { 3044 // Remove hot spot from configuration 3045 initialConfig.scenes[sceneId].hotSpots.splice(j, 1); 3046 return true; 3047 } 3048 } 3049 } else { 3050 return false; 3051 } 3052 } 3053}; 3054 3055/** 3056 * This method should be called if the viewer's container is resized. 3057 * @memberof Viewer 3058 * @instance 3059 */ 3060this.resize = function() { 3061 if (renderer) 3062 onDocumentResize(); 3063}; 3064 3065/** 3066 * Check if a panorama is loaded. 3067 * @memberof Viewer 3068 * @instance 3069 * @returns {boolean} True if a panorama is loaded, else false 3070 */ 3071this.isLoaded = function() { 3072 return loaded; 3073}; 3074 3075/** 3076 * Check if device orientation control is supported. 3077 * @memberof Viewer 3078 * @instance 3079 * @returns {boolean} True if supported, else false 3080 */ 3081this.isOrientationSupported = function() { 3082 return orientationSupport || false; 3083}; 3084 3085/** 3086 * Stop using device orientation. 3087 * @memberof Viewer 3088 * @instance 3089 */ 3090this.stopOrientation = function() { 3091 stopOrientation(); 3092}; 3093 3094/** 3095 * Start using device orientation (does nothing if not supported). 3096 * @memberof Viewer 3097 * @instance 3098 */ 3099this.startOrientation = function() { 3100 if (orientationSupport) 3101 startOrientation(); 3102}; 3103 3104/** 3105 * Check if device orientation control is currently activated. 3106 * @memberof Viewer 3107 * @instance 3108 * @returns {boolean} True if active, else false 3109 */ 3110this.isOrientationActive = function() { 3111 return Boolean(orientation); 3112}; 3113 3114/** 3115 * Subscribe listener to specified event. 3116 * @memberof Viewer 3117 * @instance 3118 * @param {string} type - Type of event to subscribe to. 3119 * @param {Function} listener - Listener function to subscribe to event. 3120 * @returns {Viewer} `this` 3121 */ 3122this.on = function(type, listener) { 3123 externalEventListeners[type] = externalEventListeners[type] || []; 3124 externalEventListeners[type].push(listener); 3125 return this; 3126}; 3127 3128/** 3129 * Remove an event listener (or listeners). 3130 * @memberof Viewer 3131 * @param {string} [type] - Type of event to remove listeners from. If not specified, all listeners are removed. 3132 * @param {Function} [listener] - Listener function to remove. If not specified, all listeners of specified type are removed. 3133 * @returns {Viewer} `this` 3134 */ 3135this.off = function(type, listener) { 3136 if (!type) { 3137 // Remove all listeners if type isn't specified 3138 externalEventListeners = {}; 3139 return this; 3140 } 3141 if (listener) { 3142 var i = externalEventListeners[type].indexOf(listener); 3143 if (i >= 0) { 3144 // Remove listener if found 3145 externalEventListeners[type].splice(i, 1); 3146 } 3147 if (externalEventListeners[type].length == 0) { 3148 // Remove category if empty 3149 delete externalEventListeners[type]; 3150 } 3151 } else { 3152 // Remove category of listeners if listener isn't specified 3153 delete externalEventListeners[type]; 3154 } 3155 return this; 3156}; 3157 3158/** 3159 * Fire listeners attached to specified event. 3160 * @private 3161 * @param {string} [type] - Type of event to fire listeners for. 3162 */ 3163function fireEvent(type) { 3164 if (type in externalEventListeners) { 3165 // Reverse iteration is useful, if event listener is removed inside its definition 3166 for (var i = externalEventListeners[type].length; i > 0; i--) { 3167 externalEventListeners[type][externalEventListeners[type].length - i].apply(null, [].slice.call(arguments, 1)); 3168 } 3169 } 3170} 3171 3172/** 3173 * Destructor. 3174 * @instance 3175 * @memberof Viewer 3176 */ 3177this.destroy = function() { 3178 destroyed = true; 3179 clearTimeout(autoRotateStart); 3180 3181 if (renderer) 3182 renderer.destroy(); 3183 if (listenersAdded) { 3184 document.removeEventListener('mousemove', onDocumentMouseMove, false); 3185 document.removeEventListener('mouseup', onDocumentMouseUp, false); 3186 container.removeEventListener('mozfullscreenchange', onFullScreenChange, false); 3187 container.removeEventListener('webkitfullscreenchange', onFullScreenChange, false); 3188 container.removeEventListener('msfullscreenchange', onFullScreenChange, false); 3189 container.removeEventListener('fullscreenchange', onFullScreenChange, false); 3190 window.removeEventListener('resize', onDocumentResize, false); 3191 window.removeEventListener('orientationchange', onDocumentResize, false); 3192 container.removeEventListener('keydown', onDocumentKeyPress, false); 3193 container.removeEventListener('keyup', onDocumentKeyUp, false); 3194 container.removeEventListener('blur', clearKeys, false); 3195 document.removeEventListener('mouseleave', onDocumentMouseUp, false); 3196 } 3197 container.innerHTML = ''; 3198 container.classList.remove('pnlm-container'); 3199}; 3200 3201} 3202 3203return { 3204 viewer: function(container, config) { 3205 return new Viewer(container, config); 3206 } 3207}; 3208 3209})(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.