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