PageSourceSearch

https://hewi-karriere.de/wp-content/plugins/wpvr-pro/public/lib/pannellum/src/js/pannellum.js?ver=1

js hewi-karriere.de collected 2026-10-04 08:00:11 UTC 131,763 bytes, 3,238 lines download raw bytes

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