PageSourceSearch

https://luneville.fr/wp-content/plugins/wpvr/public/lib/pannellum/src/js/pannellum.js?ver=8.5.76

js luneville.fr collected 2026-10-02 16:41:55 UTC 107,935 bytes, 3,225 lines download raw bytes

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

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