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