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