PageSourceSearch

https://print-n-apply.netlify.app/wp-content/plugins/wpvr-pro/lib/pannellum/src/js/pannellum.js

js print-n-apply.netlify.app collected 2026-10-03 11:17:42 UTC 107,407 bytes, 3,202 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.