{"translation-revision-date":"2025-02-12 13:17+0100","generator":"WP-CLI\/2.12.0","source":"src\/Assist\/tasks\/setup-woocommerce-germanized-store.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"ca","plural-forms":"nplurals=2; plural=n != 1;"},"Set up":["Nastavi\u0165"],"Revisit":["Znova skontrolova\u0165"],"Set up WooCommerce Germanized plugin":["Nastavte plugin WooCommerce Germanized"],"Ensure your eCommerce store complies with local regulations.":["Zabezpe\u010dte, aby v\u00e1\u0161 e-commerce obchod sp\u013a\u0148al miestne predpisy."]}}}.wp-block-social-links { box-sizing: border-box; padding-left: 0; padding-right: 0; text-indent: 0; margin-left: 0; background: none; } .wp-block-social-links .wp-social-link a, .wp-block-social-links .wp-social-link a:hover { text-decoration: none; border-bottom: 0; box-shadow: none; } .wp-block-social-links .wp-social-link svg { width: 1em; height: 1em; } .wp-block-social-links .wp-social-link span:not(.screen-reader-text) { margin-left: 0.5em; margin-right: 0.5em; font-size: 0.65em; } .wp-block-social-links.has-small-icon-size { font-size: 16px; } .wp-block-social-links, .wp-block-social-links.has-normal-icon-size { font-size: 24px; } .wp-block-social-links.has-large-icon-size { font-size: 36px; } .wp-block-social-links.has-huge-icon-size { font-size: 48px; } .wp-block-social-links.aligncenter { justify-content: center; display: flex; } .wp-block-social-links.alignright { justify-content: flex-end; } .wp-block-social-link { display: block; border-radius: 9999px; } @media not (prefers-reduced-motion) { .wp-block-social-link { transition: transform 0.1s ease; } } .wp-block-social-link { height: auto; } .wp-block-social-link a { align-items: center; display: flex; line-height: 0; } .wp-block-social-link:hover { transform: scale(1.1); } .wp-block-social-links .wp-block-social-link.wp-social-link { display: inline-block; margin: 0; padding: 0; } .wp-block-social-links .wp-block-social-link.wp-social-link .wp-block-social-link-anchor, .wp-block-social-links .wp-block-social-link.wp-social-link .wp-block-social-link-anchor:hover, .wp-block-social-links .wp-block-social-link.wp-social-link .wp-block-social-link-anchor:active, .wp-block-social-links .wp-block-social-link.wp-social-link .wp-block-social-link-anchor:visited, .wp-block-social-links .wp-block-social-link.wp-social-link .wp-block-social-link-anchor svg { color: currentColor; fill: currentColor; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link { background-color: #f0f0f0; color: #444; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-amazon { background-color: #f90; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-bandcamp { background-color: #1ea0c3; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-behance { background-color: #0757fe; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-bluesky { background-color: #0a7aff; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-codepen { background-color: #1e1f26; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-deviantart { background-color: #02e49b; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-discord { background-color: #5865f2; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-dribbble { background-color: #e94c89; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-dropbox { background-color: #4280ff; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-etsy { background-color: #f45800; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-facebook { background-color: #0866ff; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-fivehundredpx { background-color: #000; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-flickr { background-color: #0461dd; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-foursquare { background-color: #e65678; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-github { background-color: #24292d; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-goodreads { background-color: #eceadd; color: #382110; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-google { background-color: #ea4434; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-gravatar { background-color: #1d4fc4; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-instagram { background-color: #f00075; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-lastfm { background-color: #e21b24; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-linkedin { background-color: #0d66c2; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-mastodon { background-color: #3288d4; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-medium { background-color: #000; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-meetup { background-color: #f6405f; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-patreon { background-color: #000; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-pinterest { background-color: #e60122; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-pocket { background-color: #ef4155; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-reddit { background-color: #ff4500; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-skype { background-color: #0478d7; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-snapchat { background-color: #fefc00; color: #fff; stroke: #000; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-soundcloud { background-color: #ff5600; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-spotify { background-color: #1bd760; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-telegram { background-color: #2aabee; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-threads { background-color: #000; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-tiktok { background-color: #000; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-tumblr { background-color: #011835; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-twitch { background-color: #6440a4; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-twitter { background-color: #1da1f2; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-vimeo { background-color: #1eb7ea; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-vk { background-color: #4680c2; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-wordpress { background-color: #3499cd; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-whatsapp { background-color: #25d366; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-x { background-color: #000; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-yelp { background-color: #d32422; color: #fff; } :where(.wp-block-social-links:not(.is-style-logos-only)) .wp-social-link-youtube { background-color: #f00; color: #fff; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link { background: none; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link svg { width: 1.25em; height: 1.25em; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-amazon { color: #f90; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-bandcamp { color: #1ea0c3; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-behance { color: #0757fe; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-bluesky { color: #0a7aff; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-codepen { color: #1e1f26; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-deviantart { color: #02e49b; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-discord { color: #5865f2; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-dribbble { color: #e94c89; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-dropbox { color: #4280ff; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-etsy { color: #f45800; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-facebook { color: #0866ff; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-fivehundredpx { color: #000; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-flickr { color: #0461dd; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-foursquare { color: #e65678; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-github { color: #24292d; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-goodreads { color: #382110; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-google { color: #ea4434; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-gravatar { color: #1d4fc4; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-instagram { color: #f00075; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-lastfm { color: #e21b24; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-linkedin { color: #0d66c2; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-mastodon { color: #3288d4; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-medium { color: #000; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-meetup { color: #f6405f; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-patreon { color: #000; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-pinterest { color: #e60122; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-pocket { color: #ef4155; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-reddit { color: #ff4500; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-skype { color: #0478d7; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-snapchat { color: #fff; stroke: #000; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-soundcloud { color: #ff5600; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-spotify { color: #1bd760; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-telegram { color: #2aabee; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-threads { color: #000; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-tiktok { color: #000; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-tumblr { color: #011835; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-twitch { color: #6440a4; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-twitter { color: #1da1f2; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-vimeo { color: #1eb7ea; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-vk { color: #4680c2; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-whatsapp { color: #25d366; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-wordpress { color: #3499cd; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-x { color: #000; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-yelp { color: #d32422; } :where(.wp-block-social-links.is-style-logos-only) .wp-social-link-youtube { color: #f00; } .wp-block-social-links.is-style-pill-shape .wp-social-link { width: auto; } :root :where(.wp-block-social-links .wp-social-link a) { padding: 0.25em; } :root :where(.wp-block-social-links.is-style-logos-only .wp-social-link a) { padding: 0; } :root :where(.wp-block-social-links.is-style-pill-shape .wp-social-link a) { padding-left: 0.6666666667em; padding-right: 0.6666666667em; } .wp-block-social-links:not(.has-icon-color):not(.has-icon-background-color) .wp-social-link-snapchat .wp-block-social-link-label { color: #000; }/* * Pannellum - An HTML5 based Panorama Viewer * Copyright (c) 2011-2019 Matthew Petroff * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ window.pannellum = (function(window, document, undefined) { 'use strict'; /** * Creates a new panorama viewer. * @constructor * @param {HTMLElement|string} container - The container (div) element for the * viewer, or its ID. * @param {Object} initialConfig - Inital configuration for viewer. */ function Viewer(container, initialConfig) { var _this = this; // Declare variables var config, renderer, preview, isUserInteracting = false, latestInteraction = Date.now(), onPointerDownPointerX = 0, onPointerDownPointerY = 0, onPointerDownPointerDist = -1, onPointerDownYaw = 0, onPointerDownPitch = 0, keysDown = new Array(10), fullscreenActive = false, loaded, error = false, isTimedOut = false, listenersAdded = false, panoImage, prevTime, speed = {'yaw': 0, 'pitch': 0, 'hfov': 0}, animating = false, orientation = false, orientationYawOffset = 0, autoRotateStart, autoRotateSpeed = 0, origHfov, origPitch, animatedMove = {}, externalEventListeners = {}, specifiedPhotoSphereExcludes = [], update = false, // Should we update when still to render dynamic content eps = 1e-6, hotspotsCreated = false, destroyed = false; var defaultConfig = { hfov: 100, minHfov: 50, multiResMinHfov: false, maxHfov: 120, pitch: 0, minPitch: undefined, maxPitch: undefined, yaw: 0, minYaw: -180, maxYaw: 180, roll: 0, haov: 360, vaov: 180, vOffset: 0, autoRotate: false, autoRotateInactivityDelay: -1, autoRotateStopDelay: undefined, type: 'equirectangular', northOffset: 0, showFullscreenCtrl: true, dynamic: false, dynamicUpdate: false, doubleClickZoom: true, keyboardZoom: true, mouseZoom: true, showZoomCtrl: true, autoLoad: false, showControls: true, orientationOnByDefault: false, hotSpotDebug: false, backgroundColor: [0, 0, 0], avoidShowingBackground: false, animationTimingFunction: timingFunction, draggable: true, disableKeyboardCtrl: false, crossOrigin: 'anonymous', touchPanSpeedCoeffFactor: 1, capturedKeyNumbers: [16, 17, 27, 37, 38, 39, 40, 61, 65, 68, 83, 87, 107, 109, 173, 187, 189], friction: 0.15 }; // Translatable / configurable strings // Some strings contain '%s', which is a placeholder for inserted values // When setting strings in external configuration, `\n` should be used instead of `
` to insert line breaks defaultConfig.strings = { // Labels loadButtonLabel: 'Click to
Load
Panorama', loadingLabel: 'Loading...', bylineLabel: ' %s', // One substitution: author // Errors noPanoramaError: 'No panorama image was specified.', fileAccessError: 'The file %s could not be accessed.', // One substitution: file URL malformedURLError: 'There is something wrong with the panorama URL.', iOS8WebGLError: "Due to iOS 8's broken WebGL implementation, only " + "progressive encoded JPEGs work for your device (this " + "panorama uses standard encoding).", genericWebGLError: 'Your browser does not have the necessary WebGL support to display this panorama.', textureSizeError: 'This panorama is too big for your device! It\'s ' + '%spx wide, but your device only supports images up to ' + '%spx wide. Try another device.' + ' (If you\'re the author, try scaling down the image.)', // Two substitutions: image width, max image width unknownError: 'Unknown error. Check developer console.', }; // Initialize container container = typeof container === 'string' ? document.getElementById(container) : container; container.classList.add('pnlm-container'); container.tabIndex = 0; // Create container for ui var uiContainer = document.createElement('div'); uiContainer.className = 'pnlm-ui'; container.appendChild(uiContainer); // Create container for renderer var renderContainer = document.createElement('div'); renderContainer.className = 'pnlm-render-container'; container.appendChild(renderContainer); var dragFix = document.createElement('div'); dragFix.className = 'pnlm-dragfix'; uiContainer.appendChild(dragFix); // Display about information on right click var aboutMsg = document.createElement('span'); aboutMsg.className = 'pnlm-about-msg'; //==wpvr custom rextheme link==// aboutMsg.innerHTML = 'Rextheme'; //==wpvr custom rextheme link end==// uiContainer.appendChild(aboutMsg); dragFix.addEventListener('contextmenu', aboutMessage); // Create info display var infoDisplay = {}; // Hot spot debug indicator var hotSpotDebugIndicator = document.createElement('div'); hotSpotDebugIndicator.className = 'pnlm-sprite pnlm-hot-spot-debug-indicator'; uiContainer.appendChild(hotSpotDebugIndicator); // Panorama info infoDisplay.container = document.createElement('div'); infoDisplay.container.className = 'pnlm-panorama-info'; infoDisplay.title = document.createElement('div'); infoDisplay.title.className = 'pnlm-title-box'; infoDisplay.container.appendChild(infoDisplay.title); infoDisplay.author = document.createElement('div'); infoDisplay.author.className = 'pnlm-author-box'; infoDisplay.container.appendChild(infoDisplay.author); uiContainer.appendChild(infoDisplay.container); // Load box infoDisplay.load = {}; infoDisplay.load.box = document.createElement('div'); infoDisplay.load.box.className = 'pnlm-load-box'; infoDisplay.load.boxp = document.createElement('p'); infoDisplay.load.box.appendChild(infoDisplay.load.boxp); infoDisplay.load.lbox = document.createElement('div'); infoDisplay.load.lbox.className = 'pnlm-lbox'; infoDisplay.load.lbox.innerHTML = '
'; infoDisplay.load.box.appendChild(infoDisplay.load.lbox); infoDisplay.load.lbar = document.createElement('div'); infoDisplay.load.lbar.className = 'pnlm-lbar'; infoDisplay.load.lbarFill = document.createElement('div'); infoDisplay.load.lbarFill.className = 'pnlm-lbar-fill'; infoDisplay.load.lbar.appendChild(infoDisplay.load.lbarFill); infoDisplay.load.box.appendChild(infoDisplay.load.lbar); infoDisplay.load.msg = document.createElement('p'); infoDisplay.load.msg.className = 'pnlm-lmsg'; infoDisplay.load.box.appendChild(infoDisplay.load.msg); uiContainer.appendChild(infoDisplay.load.box); // Error message infoDisplay.errorMsg = document.createElement('div'); infoDisplay.errorMsg.className = 'pnlm-error-msg pnlm-info-box'; uiContainer.appendChild(infoDisplay.errorMsg); // Create controls var controls = {}; controls.container = document.createElement('div'); controls.container.className = 'pnlm-controls-container'; uiContainer.appendChild(controls.container); // Load button controls.load = document.createElement('div'); controls.load.className = 'pnlm-load-button'; controls.load.addEventListener('click', function() { processOptions(); load(); }); uiContainer.appendChild(controls.load); // Zoom controls controls.zoom = document.createElement('div'); controls.zoom.className = 'pnlm-zoom-controls pnlm-controls'; controls.zoomIn = document.createElement('div'); controls.zoomIn.className = 'pnlm-zoom-in pnlm-sprite pnlm-control'; controls.zoomIn.addEventListener('click', zoomIn); controls.zoom.appendChild(controls.zoomIn); controls.zoomOut = document.createElement('div'); controls.zoomOut.className = 'pnlm-zoom-out pnlm-sprite pnlm-control'; controls.zoomOut.addEventListener('click', zoomOut); controls.zoom.appendChild(controls.zoomOut); controls.container.appendChild(controls.zoom); // Fullscreen toggle controls.fullscreen = document.createElement('div'); controls.fullscreen.addEventListener('click', toggleFullscreen); controls.fullscreen.className = 'pnlm-fullscreen-toggle-button pnlm-sprite pnlm-fullscreen-toggle-button-inactive pnlm-controls pnlm-control'; if (document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled || document.msFullscreenEnabled) controls.container.appendChild(controls.fullscreen); // Device orientation toggle controls.orientation = document.createElement('div'); controls.orientation.addEventListener('click', function(e) { if (orientation) stopOrientation(); else startOrientation(); }); controls.orientation.addEventListener('mousedown', function(e) {e.stopPropagation();}); controls.orientation.addEventListener('touchstart', function(e) {e.stopPropagation();}); controls.orientation.addEventListener('pointerdown', function(e) {e.stopPropagation();}); controls.orientation.className = 'pnlm-orientation-button pnlm-orientation-button-inactive pnlm-sprite pnlm-controls pnlm-control'; var orientationSupport = false; if (window.DeviceOrientationEvent && location.protocol == 'https:' && navigator.userAgent.toLowerCase().indexOf('mobi') >= 0) { // This user agent check is here because there's no way to check if a // device has an inertia measurement unit. We used to be able to check if a // DeviceOrientationEvent had non-null values, but with iOS 13 requiring a // permission prompt to access such events, this is no longer possible. controls.container.appendChild(controls.orientation); orientationSupport = true; } // Compass var compass = document.createElement('div'); compass.className = 'pnlm-compass pnlm-controls pnlm-control'; uiContainer.appendChild(compass); // Load and process configuration if (initialConfig.firstScene) { // Activate first scene if specified in URL mergeConfig(initialConfig.firstScene); } else if (initialConfig.default && initialConfig.default.firstScene) { // Activate first scene if specified in file mergeConfig(initialConfig.default.firstScene); } else { mergeConfig(null); } processOptions(true); /** * Initializes viewer. * @private */ function init() { // Display an error for IE 9 as it doesn't work but also doesn't otherwise // show an error (older versions don't work at all) // Based on: http://stackoverflow.com/a/10965203 var div = document.createElement("div"); div.innerHTML = ""; if (div.getElementsByTagName("i").length == 1) { anError(); return; } origHfov = config.hfov; origPitch = config.pitch; var i, p; if (config.type == 'cubemap') { panoImage = []; for (i = 0; i < 6; i++) { panoImage.push(new Image()); panoImage[i].crossOrigin = config.crossOrigin; } infoDisplay.load.lbox.style.display = 'block'; infoDisplay.load.lbar.style.display = 'none'; } else if (config.type == 'multires') { var c = JSON.parse(JSON.stringify(config.multiRes)); // Deep copy // Avoid "undefined" in path, check (optional) multiRes.basePath, too // Use only multiRes.basePath if it's an absolute URL if (config.basePath && config.multiRes.basePath && !(/^(?:[a-z]+:)?\/\//i.test(config.multiRes.basePath))) { c.basePath = config.basePath + config.multiRes.basePath; } else if (config.multiRes.basePath) { c.basePath = config.multiRes.basePath; } else if(config.basePath) { c.basePath = config.basePath; } panoImage = c; } else { if (config.dynamic === true) { panoImage = config.panorama; } else { if (config.panorama === undefined) { anError(config.strings.noPanoramaError); return; } panoImage = new Image(); } } // Configure image loading if (config.type == 'cubemap') { // Quick loading counter for synchronous loading var itemsToLoad = 6; var onLoad = function() { itemsToLoad--; if (itemsToLoad === 0) { onImageLoad(); } }; var onError = function(e) { var a = document.createElement('a'); a.href = e.target.src; a.textContent = a.href; anError(config.strings.fileAccessError.replace('%s', a.outerHTML)); }; for (i = 0; i < panoImage.length; i++) { p = config.cubeMap[i]; if (p == "null") { // support partial cubemap image with explicitly empty faces console.log('Will use background instead of missing cubemap face ' + i); onLoad(); } else { if (config.basePath && !absoluteURL(p)) { p = config.basePath + p; } panoImage[i].onload = onLoad; panoImage[i].onerror = onError; panoImage[i].src = sanitizeURL(p); } } } else if (config.type == 'multires') { onImageLoad(); } else { p = ''; if (config.basePath) { p = config.basePath; } if (config.dynamic !== true) { // Still image p = absoluteURL(config.panorama) ? config.panorama : p + config.panorama; panoImage.onload = function() { window.URL.revokeObjectURL(this.src); // Clean up onImageLoad(); }; var xhr = new XMLHttpRequest(); xhr.onloadend = function() { if (xhr.status != 200) { // Display error if image can't be loaded var a = document.createElement('a'); a.href = p; a.textContent = a.href; anError(config.strings.fileAccessError.replace('%s', a.outerHTML)); } var img = this.response; parseGPanoXMP(img, p); infoDisplay.load.msg.innerHTML = ''; }; xhr.onprogress = function(e) { if (e.lengthComputable) { // Display progress var percent = e.loaded / e.total * 100; infoDisplay.load.lbarFill.style.width = percent + '%'; var unit, numerator, denominator; if (e.total > 1e6) { unit = 'MB'; numerator = (e.loaded / 1e6).toFixed(2); denominator = (e.total / 1e6).toFixed(2); } else if (e.total > 1e3) { unit = 'kB'; numerator = (e.loaded / 1e3).toFixed(1); denominator = (e.total / 1e3).toFixed(1); } else { unit = 'B'; numerator = e.loaded; denominator = e.total; } infoDisplay.load.msg.innerHTML = numerator + ' / ' + denominator + ' ' + unit; } else { // Display loading spinner infoDisplay.load.lbox.style.display = 'block'; infoDisplay.load.lbar.style.display = 'none'; } }; try { xhr.open('GET', p, true); } catch (e) { // Malformed URL anError(config.strings.malformedURLError); } xhr.responseType = 'blob'; xhr.setRequestHeader('Accept', 'image/*,*/*;q=0.9'); xhr.withCredentials = config.crossOrigin === 'use-credentials'; xhr.send(); } } if (config.draggable) uiContainer.classList.add('pnlm-grab'); uiContainer.classList.remove('pnlm-grabbing'); // Properly handle switching to dynamic scenes update = config.dynamicUpdate === true; if (config.dynamic && update) { panoImage = config.panorama; onImageLoad(); } } /** * Test if URL is absolute or relative. * @private * @param {string} url - URL to test * @returns {boolean} True if absolute, else false */ function absoluteURL(url) { // From http://stackoverflow.com/a/19709846 return new RegExp('^(?:[a-z]+:)?//', 'i').test(url) || url[0] == '/' || url.slice(0, 5) == 'blob:'; } /** * Create renderer and initialize event listeners once image is loaded. * @private */ function onImageLoad() { if (!renderer) renderer = new libpannellum.renderer(renderContainer); // Only add event listeners once if (!listenersAdded) { listenersAdded = true; dragFix.addEventListener('mousedown', onDocumentMouseDown, false); document.addEventListener('mousemove', onDocumentMouseMove, false); document.addEventListener('mouseup', onDocumentMouseUp, false); if (config.mouseZoom) { uiContainer.addEventListener('mousewheel', onDocumentMouseWheel, false); uiContainer.addEventListener('DOMMouseScroll', onDocumentMouseWheel, false); } if (config.doubleClickZoom) { dragFix.addEventListener('dblclick', onDocumentDoubleClick, false); } container.addEventListener('mozfullscreenchange', onFullScreenChange, false); container.addEventListener('webkitfullscreenchange', onFullScreenChange, false); container.addEventListener('msfullscreenchange', onFullScreenChange, false); container.addEventListener('fullscreenchange', onFullScreenChange, false); window.addEventListener('resize', onDocumentResize, false); window.addEventListener('orientationchange', onDocumentResize, false); if (!config.disableKeyboardCtrl) { container.addEventListener('keydown', onDocumentKeyPress, false); container.addEventListener('keyup', onDocumentKeyUp, false); container.addEventListener('blur', clearKeys, false); } document.addEventListener('mouseleave', onDocumentMouseUp, false); if (document.documentElement.style.pointerAction === '' && document.documentElement.style.touchAction === '') { dragFix.addEventListener('pointerdown', onDocumentPointerDown, false); dragFix.addEventListener('pointermove', onDocumentPointerMove, false); dragFix.addEventListener('pointerup', onDocumentPointerUp, false); dragFix.addEventListener('pointerleave', onDocumentPointerUp, false); } else { dragFix.addEventListener('touchstart', onDocumentTouchStart, false); dragFix.addEventListener('touchmove', onDocumentTouchMove, false); dragFix.addEventListener('touchend', onDocumentTouchEnd, false); } // Deal with MS pointer events if (window.navigator.pointerEnabled) container.style.touchAction = 'none'; } renderInit(); setHfov(config.hfov); // possibly adapt hfov after configuration and canvas is complete; prevents empty space on top or bottom by zomming out too much setTimeout(function(){isTimedOut = true;}, 500); } /** * Parses Google Photo Sphere XMP Metadata. * https://developers.google.com/photo-sphere/metadata/ * @private * @param {Image} image - Image to read XMP metadata from. */ function parseGPanoXMP(image, url) { var reader = new FileReader(); reader.addEventListener('loadend', function() { var img = reader.result; // This awful browser specific test exists because iOS 8 does not work // with non-progressive encoded JPEGs. if (navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/)) { var flagIndex = img.indexOf('\xff\xc2'); if (flagIndex < 0 || flagIndex > 65536) anError(config.strings.iOS8WebGLError); } var start = img.indexOf(' -1 && config.ignoreGPanoXMP !== true) { var xmpData = img.substring(start, img.indexOf('') + 12); // Extract the requested tag from the XMP data var getTag = function(tag) { var result; if (xmpData.indexOf(tag + '="') >= 0) { result = xmpData.substring(xmpData.indexOf(tag + '="') + tag.length + 2); result = result.substring(0, result.indexOf('"')); } else if (xmpData.indexOf(tag + '>') >= 0) { result = xmpData.substring(xmpData.indexOf(tag + '>') + tag.length + 1); result = result.substring(0, result.indexOf('<')); } if (result !== undefined) { return Number(result); } return null; }; // Relevant XMP data var xmp = { fullWidth: getTag('GPano:FullPanoWidthPixels'), croppedWidth: getTag('GPano:CroppedAreaImageWidthPixels'), fullHeight: getTag('GPano:FullPanoHeightPixels'), croppedHeight: getTag('GPano:CroppedAreaImageHeightPixels'), topPixels: getTag('GPano:CroppedAreaTopPixels'), heading: getTag('GPano:PoseHeadingDegrees'), horizonPitch: getTag('GPano:PosePitchDegrees'), horizonRoll: getTag('GPano:PoseRollDegrees') }; if (xmp.fullWidth !== null && xmp.croppedWidth !== null && xmp.fullHeight !== null && xmp.croppedHeight !== null && xmp.topPixels !== null) { // Set up viewer using GPano XMP data if (specifiedPhotoSphereExcludes.indexOf('haov') < 0) config.haov = xmp.croppedWidth / xmp.fullWidth * 360; if (specifiedPhotoSphereExcludes.indexOf('vaov') < 0) config.vaov = xmp.croppedHeight / xmp.fullHeight * 180; if (specifiedPhotoSphereExcludes.indexOf('vOffset') < 0) config.vOffset = ((xmp.topPixels + xmp.croppedHeight / 2) / xmp.fullHeight - 0.5) * -180; if (xmp.heading !== null && specifiedPhotoSphereExcludes.indexOf('northOffset') < 0) { // TODO: make sure this works correctly for partial panoramas config.northOffset = xmp.heading; if (config.compass !== false) { config.compass = true; } } if (xmp.horizonPitch !== null && xmp.horizonRoll !== null) { if (specifiedPhotoSphereExcludes.indexOf('horizonPitch') < 0) config.horizonPitch = xmp.horizonPitch; if (specifiedPhotoSphereExcludes.indexOf('horizonRoll') < 0) config.horizonRoll = xmp.horizonRoll; } // TODO: add support for initial view settings } } // Load panorama panoImage.src = window.URL.createObjectURL(image); panoImage.onerror = function() { // If the image fails to load, we check the Content Security Policy // headers and see if they block loading images as blobs. If they // do, we load the image directly from the URL. While this should // allow the image to load, it does prevent parsing of XMP data. function getCspHeaders() { if (!window.fetch) return null; return window.fetch(document.location.href) .then(function(resp){ return resp.headers.get('Content-Security-Policy'); }); } getCspHeaders().then(function(cspHeaders) { if (cspHeaders) { var invalidImgSource = cspHeaders.split(";").find(function(p) { var matchstring = p.match(/img-src(.*)/); if (matchstring) { return !matchstring[1].includes("blob"); } }); if (invalidImgSource) { console.log('CSP blocks blobs; reverting to URL.'); panoImage.crossOrigin = config.crossOrigin; panoImage.src = url; } } }); } }); if (reader.readAsBinaryString !== undefined) reader.readAsBinaryString(image); else reader.readAsText(image); } /** * Displays an error message. * @private * @param {string} errorMsg - Error message to display. If not specified, a * generic WebGL error is displayed. */ function anError(errorMsg) { if (errorMsg === undefined) errorMsg = config.strings.genericWebGLError; infoDisplay.errorMsg.innerHTML = '

' + errorMsg + '

'; controls.load.style.display = 'none'; infoDisplay.load.box.style.display = 'none'; infoDisplay.errorMsg.style.display = 'table'; error = true; loaded = undefined; renderContainer.style.display = 'none'; fireEvent('error', errorMsg); } /** * Hides error message display. * @private */ function clearError() { if (error) { infoDisplay.load.box.style.display = 'none'; infoDisplay.errorMsg.style.display = 'none'; error = false; renderContainer.style.display = 'block'; fireEvent('errorcleared'); } } /** * Displays about message. * @private * @param {MouseEvent} event - Right click location */ function aboutMessage(event) { var pos = mousePosition(event); aboutMsg.style.left = pos.x + 'px'; aboutMsg.style.top = pos.y + 'px'; clearTimeout(aboutMessage.t1); clearTimeout(aboutMessage.t2); aboutMsg.style.display = 'block'; aboutMsg.style.opacity = 1; aboutMessage.t1 = setTimeout(function() {aboutMsg.style.opacity = 0;}, 2000); aboutMessage.t2 = setTimeout(function() {aboutMsg.style.display = 'none';}, 2500); event.preventDefault(); } /** * Calculate mouse position relative to top left of viewer container. * @private * @param {MouseEvent} event - Mouse event to use in calculation * @returns {Object} Calculated X and Y coordinates */ function mousePosition(event) { var bounds = container.getBoundingClientRect(); var pos = {}; // pageX / pageY needed for iOS pos.x = (event.clientX || event.pageX) - bounds.left; pos.y = (event.clientY || event.pageY) - bounds.top; return pos; } /** * Event handler for mouse clicks. Initializes panning. Prints center and click * location coordinates when hot spot debugging is enabled. * @private * @param {MouseEvent} event - Document mouse down event. */ function onDocumentMouseDown(event) { // Override default action event.preventDefault(); // But not all of it container.focus(); // Only do something if the panorama is loaded if (!loaded || !config.draggable) { return; } // Calculate mouse position relative to top left of viewer container var pos = mousePosition(event); // Log pitch / yaw of mouse click when debugging / placing hot spots if (config.hotSpotDebug) { var coords = mouseEventToCoords(event); console.log('Pitch: ' + coords[0] + ', Yaw: ' + coords[1] + ', Center Pitch: ' + config.pitch + ', Center Yaw: ' + config.yaw + ', HFOV: ' + config.hfov); } // Turn off auto-rotation if enabled stopAnimation(); stopOrientation(); config.roll = 0; speed.hfov = 0; isUserInteracting = true; latestInteraction = Date.now(); onPointerDownPointerX = pos.x; onPointerDownPointerY = pos.y; onPointerDownYaw = config.yaw; onPointerDownPitch = config.pitch; uiContainer.classList.add('pnlm-grabbing'); uiContainer.classList.remove('pnlm-grab'); fireEvent('mousedown', event); animateInit(); } /** * Event handler for double clicks. Zooms in at clicked location * @private * @param {MouseEvent} event - Document mouse down event. */ function onDocumentDoubleClick(event) { if (config.minHfov === config.hfov) { _this.setHfov(origHfov, 1000); } else { var coords = mouseEventToCoords(event); _this.lookAt(coords[0], coords[1], config.minHfov, 1000); } } /** * Calculate panorama pitch and yaw from location of mouse event. * @private * @param {MouseEvent} event - Document mouse down event. * @returns {number[]} [pitch, yaw] */ function mouseEventToCoords(event) { var pos = mousePosition(event); var canvas = renderer.getCanvas(); var canvasWidth = canvas.clientWidth, canvasHeight = canvas.clientHeight; var x = pos.x / canvasWidth * 2 - 1; var y = (1 - pos.y / canvasHeight * 2) * canvasHeight / canvasWidth; var focal = 1 / Math.tan(config.hfov * Math.PI / 360); var s = Math.sin(config.pitch * Math.PI / 180); var c = Math.cos(config.pitch * Math.PI / 180); var a = focal * c - y * s; var root = Math.sqrt(x*x + a*a); var pitch = Math.atan((y * c + focal * s) / root) * 180 / Math.PI; var yaw = Math.atan2(x / root, a / root) * 180 / Math.PI + config.yaw; if (yaw < -180) yaw += 360; if (yaw > 180) yaw -= 360; return [pitch, yaw]; } /** * Event handler for mouse moves. Pans center of view. * @private * @param {MouseEvent} event - Document mouse move event. */ function onDocumentMouseMove(event) { if (isUserInteracting && loaded) { latestInteraction = Date.now(); var canvas = renderer.getCanvas(); var canvasWidth = canvas.clientWidth, canvasHeight = canvas.clientHeight; var pos = mousePosition(event); //TODO: This still isn't quite right var yaw = ((Math.atan(onPointerDownPointerX / canvasWidth * 2 - 1) - Math.atan(pos.x / canvasWidth * 2 - 1)) * 180 / Math.PI * config.hfov / 90) + onPointerDownYaw; speed.yaw = (yaw - config.yaw) % 360 * 0.2; config.yaw = yaw; var vfov = 2 * Math.atan(Math.tan(config.hfov/360*Math.PI) * canvasHeight / canvasWidth) * 180 / Math.PI; var pitch = ((Math.atan(pos.y / canvasHeight * 2 - 1) - Math.atan(onPointerDownPointerY / canvasHeight * 2 - 1)) * 180 / Math.PI * vfov / 90) + onPointerDownPitch; speed.pitch = (pitch - config.pitch) * 0.2; config.pitch = pitch; var mirrorData = { pitch: pitch, yaw: yaw } fireEvent('mousemove', mirrorData); } } /** * Event handler for mouse up events. Stops panning. * @private */ function onDocumentMouseUp(event) { if (!isUserInteracting) { return; } isUserInteracting = false; if (Date.now() - latestInteraction > 15) { // Prevents jump when user rapidly moves mouse, stops, and then // releases the mouse button speed.pitch = speed.yaw = 0; } uiContainer.classList.add('pnlm-grab'); uiContainer.classList.remove('pnlm-grabbing'); latestInteraction = Date.now(); fireEvent('mouseup', event); } /** * Event handler for touches. Initializes panning if one touch or zooming if * two touches. * @private * @param {TouchEvent} event - Document touch start event. */ function onDocumentTouchStart(event) { // Only do something if the panorama is loaded if (!loaded || !config.draggable) { return; } // Turn off auto-rotation if enabled stopAnimation(); stopOrientation(); config.roll = 0; speed.hfov = 0; // Calculate touch position relative to top left of viewer container var pos0 = mousePosition(event.targetTouches[0]); onPointerDownPointerX = pos0.x; onPointerDownPointerY = pos0.y; if (event.targetTouches.length == 2) { // Down pointer is the center of the two fingers var pos1 = mousePosition(event.targetTouches[1]); onPointerDownPointerX += (pos1.x - pos0.x) * 0.5; onPointerDownPointerY += (pos1.y - pos0.y) * 0.5; onPointerDownPointerDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + (pos0.y - pos1.y) * (pos0.y - pos1.y)); } isUserInteracting = true; latestInteraction = Date.now(); onPointerDownYaw = config.yaw; onPointerDownPitch = config.pitch; fireEvent('touchstart', event); animateInit(); } /** * Event handler for touch movements. Pans center of view if one touch or * adjusts zoom if two touches. * @private * @param {TouchEvent} event - Document touch move event. */ function onDocumentTouchMove(event) { if (!config.draggable) { return; } // Override default action event.preventDefault(); if (loaded) { latestInteraction = Date.now(); } if (isUserInteracting && loaded) { var pos0 = mousePosition(event.targetTouches[0]); var clientX = pos0.x; var clientY = pos0.y; if (event.targetTouches.length == 2 && onPointerDownPointerDist != -1) { var pos1 = mousePosition(event.targetTouches[1]); clientX += (pos1.x - pos0.x) * 0.5; clientY += (pos1.y - pos0.y) * 0.5; var clientDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + (pos0.y - pos1.y) * (pos0.y - pos1.y)); setHfov(config.hfov + (onPointerDownPointerDist - clientDist) * 0.1); onPointerDownPointerDist = clientDist; } // The smaller the config.hfov value (the more zoomed-in the user is), the faster // yaw/pitch are perceived to change on one-finger touchmove (panning) events and vice versa. // To improve usability at both small and large zoom levels (config.hfov values) // we introduce a dynamic pan speed coefficient. // // Currently this seems to *roughly* keep initial drag/pan start position close to // the user's finger while panning regardless of zoom level / config.hfov value. var touchmovePanSpeedCoeff = (config.hfov / 360) * config.touchPanSpeedCoeffFactor; var yaw = (onPointerDownPointerX - clientX) * touchmovePanSpeedCoeff + onPointerDownYaw; speed.yaw = (yaw - config.yaw) % 360 * 0.2; config.yaw = yaw; var pitch = (clientY - onPointerDownPointerY) * touchmovePanSpeedCoeff + onPointerDownPitch; speed.pitch = (pitch - config.pitch) * 0.2; config.pitch = pitch; var mirrorData = { pitch: pitch, yaw: yaw } fireEvent('touchmove', mirrorData); } } /** * Event handler for end of touches. Stops panning and/or zooming. * @private */ function onDocumentTouchEnd() { isUserInteracting = false; if (Date.now() - latestInteraction > 150) { speed.pitch = speed.yaw = 0; } onPointerDownPointerDist = -1; latestInteraction = Date.now(); fireEvent('touchend', event); } var pointerIDs = [], pointerCoordinates = []; /** * Event handler for touch starts in IE / Edge. * @private * @param {PointerEvent} event - Document pointer down event. */ function onDocumentPointerDown(event) { if (event.pointerType == 'touch') { // Only do something if the panorama is loaded if (!loaded || !config.draggable) return; pointerIDs.push(event.pointerId); pointerCoordinates.push({clientX: event.clientX, clientY: event.clientY}); event.targetTouches = pointerCoordinates; onDocumentTouchStart(event); event.preventDefault(); } } /** * Event handler for touch moves in IE / Edge. * @private * @param {PointerEvent} event - Document pointer move event. */ function onDocumentPointerMove(event) { if (event.pointerType == 'touch') { if (!config.draggable) return; for (var i = 0; i < pointerIDs.length; i++) { if (event.pointerId == pointerIDs[i]) { pointerCoordinates[i].clientX = event.clientX; pointerCoordinates[i].clientY = event.clientY; event.targetTouches = pointerCoordinates; onDocumentTouchMove(event); event.preventDefault(); return; } } } } /** * Event handler for touch ends in IE / Edge. * @private * @param {PointerEvent} event - Document pointer up event. */ function onDocumentPointerUp(event) { if (event.pointerType == 'touch') { var defined = false; for (var i = 0; i < pointerIDs.length; i++) { if (event.pointerId == pointerIDs[i]) pointerIDs[i] = undefined; if (pointerIDs[i]) defined = true; } if (!defined) { pointerIDs = []; pointerCoordinates = []; onDocumentTouchEnd(); } event.preventDefault(); } } /** * Event handler for mouse wheel. Changes zoom. * @private * @param {WheelEvent} event - Document mouse wheel event. */ function onDocumentMouseWheel(event) { // Only do something if the panorama is loaded and mouse wheel zoom is enabled if (!loaded || (config.mouseZoom == 'fullscreenonly' && !fullscreenActive)) { return; } event.preventDefault(); // Turn off auto-rotation if enabled stopAnimation(); latestInteraction = Date.now(); if (event.wheelDeltaY) { // WebKit setHfov(config.hfov - event.wheelDeltaY * 0.05); speed.hfov = event.wheelDelta < 0 ? 1 : -1; } else if (event.wheelDelta) { // Opera / Explorer 9 setHfov(config.hfov - event.wheelDelta * 0.05); speed.hfov = event.wheelDelta < 0 ? 1 : -1; } else if (event.detail) { // Firefox setHfov(config.hfov + event.detail * 1.5); speed.hfov = event.detail > 0 ? 1 : -1; } animateInit(); } /** * Event handler for key presses. Updates list of currently pressed keys. * @private * @param {KeyboardEvent} event - Document key press event. */ function onDocumentKeyPress(event) { // Turn off auto-rotation if enabled stopAnimation(); latestInteraction = Date.now(); stopOrientation(); config.roll = 0; // Record key pressed var keynumber = event.which || event.keycode; // Override default action for keys that are used if (config.capturedKeyNumbers.indexOf(keynumber) < 0) return; event.preventDefault(); // If escape key is pressed if (keynumber == 27) { // If in fullscreen mode if (fullscreenActive) { toggleFullscreen(); } } else { // Change key changeKey(keynumber, true); } } /** * Clears list of currently pressed keys. * @private */ function clearKeys() { for (var i = 0; i < 10; i++) { keysDown[i] = false; } } /** * Event handler for key releases. Updates list of currently pressed keys. * @private * @param {KeyboardEvent} event - Document key up event. */ function onDocumentKeyUp(event) { // Record key pressed var keynumber = event.which || event.keycode; // Override default action for keys that are used if (config.capturedKeyNumbers.indexOf(keynumber) < 0) return; event.preventDefault(); // Change key changeKey(keynumber, false); } /** * Updates list of currently pressed keys. * @private * @param {number} keynumber - Key number. * @param {boolean} value - Whether or not key is pressed. */ function changeKey(keynumber, value) { var keyChanged = false; switch(keynumber) { // If minus key is released case 109: case 189: case 17: case 173: if (keysDown[0] != value) { keyChanged = true; } keysDown[0] = value; break; // If plus key is released case 107: case 187: case 16: case 61: if (keysDown[1] != value) { keyChanged = true; } keysDown[1] = value; break; // If up arrow is released case 38: if (keysDown[2] != value) { keyChanged = true; } keysDown[2] = value; break; // If "w" is released case 87: if (keysDown[6] != value) { keyChanged = true; } keysDown[6] = value; break; // If down arrow is released case 40: if (keysDown[3] != value) { keyChanged = true; } keysDown[3] = value; break; // If "s" is released case 83: if (keysDown[7] != value) { keyChanged = true; } keysDown[7] = value; break; // If left arrow is released case 37: if (keysDown[4] != value) { keyChanged = true; } keysDown[4] = value; break; // If "a" is released case 65: if (keysDown[8] != value) { keyChanged = true; } keysDown[8] = value; break; // If right arrow is released case 39: if (keysDown[5] != value) { keyChanged = true; } keysDown[5] = value; break; // If "d" is released case 68: if (keysDown[9] != value) { keyChanged = true; } keysDown[9] = value; } if (keyChanged && value) { if (typeof performance !== 'undefined' && performance.now()) { prevTime = performance.now(); } else { prevTime = Date.now(); } animateInit(); } } /** * Pans and/or zooms panorama based on currently pressed keys. Also handles * panorama "inertia" and auto rotation. * @private */ function keyRepeat() { // Only do something if the panorama is loaded if (!loaded) { return; } var isKeyDown = false; var prevPitch = config.pitch; var prevYaw = config.yaw; var prevZoom = config.hfov; var newTime; if (typeof performance !== 'undefined' && performance.now()) { newTime = performance.now(); } else { newTime = Date.now(); } if (prevTime === undefined) { prevTime = newTime; } var diff = (newTime - prevTime) * config.hfov / 1700; diff = Math.min(diff, 1.0); // If minus key is down if (keysDown[0] && config.keyboardZoom === true) { setHfov(config.hfov + (speed.hfov * 0.8 + 0.5) * diff); isKeyDown = true; } // If plus key is down if (keysDown[1] && config.keyboardZoom === true) { setHfov(config.hfov + (speed.hfov * 0.8 - 0.2) * diff); isKeyDown = true; } // If up arrow or "w" is down if (keysDown[2] || keysDown[6]) { // Pan up config.pitch += (speed.pitch * 0.8 + 0.2) * diff; isKeyDown = true; } // If down arrow or "s" is down if (keysDown[3] || keysDown[7]) { // Pan down config.pitch += (speed.pitch * 0.8 - 0.2) * diff; isKeyDown = true; } // If left arrow or "a" is down if (keysDown[4] || keysDown[8]) { // Pan left config.yaw += (speed.yaw * 0.8 - 0.2) * diff; isKeyDown = true; } // If right arrow or "d" is down if (keysDown[5] || keysDown[9]) { // Pan right config.yaw += (speed.yaw * 0.8 + 0.2) * diff; isKeyDown = true; } if (isKeyDown) latestInteraction = Date.now(); // If auto-rotate if (config.autoRotate) { // Pan if (newTime - prevTime > 0.001) { var timeDiff = (newTime - prevTime) / 1000; var yawDiff = (speed.yaw / timeDiff * diff - config.autoRotate * 0.2) * timeDiff; yawDiff = (-config.autoRotate > 0 ? 1 : -1) * Math.min(Math.abs(config.autoRotate * timeDiff), Math.abs(yawDiff)); config.yaw += yawDiff; } // Deal with stopping auto rotation after a set delay if (config.autoRotateStopDelay) { config.autoRotateStopDelay -= newTime - prevTime; if (config.autoRotateStopDelay <= 0) { config.autoRotateStopDelay = false; autoRotateSpeed = config.autoRotate; config.autoRotate = 0; } } } // Animated moves if (animatedMove.pitch) { animateMove('pitch'); prevPitch = config.pitch; } if (animatedMove.yaw) { animateMove('yaw'); prevYaw = config.yaw; } if (animatedMove.hfov) { animateMove('hfov'); prevZoom = config.hfov; } // "Inertia" if (diff > 0 && !config.autoRotate) { // "Friction" var slowDownFactor = 1 - config.friction; // Yaw if (!keysDown[4] && !keysDown[5] && !keysDown[8] && !keysDown[9] && !animatedMove.yaw) { config.yaw += speed.yaw * diff * slowDownFactor; } // Pitch if (!keysDown[2] && !keysDown[3] && !keysDown[6] && !keysDown[7] && !animatedMove.pitch) { config.pitch += speed.pitch * diff * slowDownFactor; } // Zoom if (!keysDown[0] && !keysDown[1] && !animatedMove.hfov) { setHfov(config.hfov + speed.hfov * diff * slowDownFactor); } } prevTime = newTime; if (diff > 0) { speed.yaw = speed.yaw * 0.8 + (config.yaw - prevYaw) / diff * 0.2; speed.pitch = speed.pitch * 0.8 + (config.pitch - prevPitch) / diff * 0.2; speed.hfov = speed.hfov * 0.8 + (config.hfov - prevZoom) / diff * 0.2; // Limit speed var maxSpeed = config.autoRotate ? Math.abs(config.autoRotate) : 5; speed.yaw = Math.min(maxSpeed, Math.max(speed.yaw, -maxSpeed)); speed.pitch = Math.min(maxSpeed, Math.max(speed.pitch, -maxSpeed)); speed.hfov = Math.min(maxSpeed, Math.max(speed.hfov, -maxSpeed)); } // Stop movement if opposite controls are pressed if (keysDown[0] && keysDown[1]) { speed.hfov = 0; } if ((keysDown[2] || keysDown[6]) && (keysDown[3] || keysDown[7])) { speed.pitch = 0; } if ((keysDown[4] || keysDown[8]) && (keysDown[5] || keysDown[9])) { speed.yaw = 0; } } /** * Animates moves. * @param {string} axis - Axis to animate * @private */ function animateMove(axis) { var t = animatedMove[axis]; var normTime = Math.min(1, Math.max((Date.now() - t.startTime) / 1000 / (t.duration / 1000), 0)); var result = t.startPosition + config.animationTimingFunction(normTime) * (t.endPosition - t.startPosition); if ((t.endPosition > t.startPosition && result >= t.endPosition) || (t.endPosition < t.startPosition && result <= t.endPosition) || t.endPosition === t.startPosition) { result = t.endPosition; speed[axis] = 0; delete animatedMove[axis]; } config[axis] = result; } /** * @param {number} t - Normalized time in animation * @return {number} Position in animation * @private */ function timingFunction(t) { // easeInOutQuad from https://gist.github.com/gre/1650294 return t < 0.5 ? 2*t*t : -1+(4-2*t)*t; } /** * Event handler for document resizes. Updates viewer size and rerenders view. * @private */ function onDocumentResize() { // Resize panorama renderer (moved to onFullScreenChange) //renderer.resize(); //animateInit(); // Kludge to deal with WebKit regression: https://bugs.webkit.org/show_bug.cgi?id=93525 onFullScreenChange('resize'); } /** * Initializes animation. * @private */ function animateInit() { if (animating) { return; } animating = true; animate(); } /** * Animates view, using requestAnimationFrame to trigger rendering. * @private */ function animate() { if (destroyed) { return; } render(); if (autoRotateStart) clearTimeout(autoRotateStart); if (isUserInteracting || orientation === true) { requestAnimationFrame(animate); } else if (keysDown[0] || keysDown[1] || keysDown[2] || keysDown[3] || keysDown[4] || keysDown[5] || keysDown[6] || keysDown[7] || keysDown[8] || keysDown[9] || config.autoRotate || animatedMove.pitch || animatedMove.yaw || animatedMove.hfov || Math.abs(speed.yaw) > 0.01 || Math.abs(speed.pitch) > 0.01 || Math.abs(speed.hfov) > 0.01) { keyRepeat(); if (config.autoRotateInactivityDelay >= 0 && autoRotateSpeed && Date.now() - latestInteraction > config.autoRotateInactivityDelay && !config.autoRotate) { config.autoRotate = autoRotateSpeed; _this.lookAt(origPitch, undefined, origHfov, 3000); } requestAnimationFrame(animate); } else if (renderer && (renderer.isLoading() || (config.dynamic === true && update))) { requestAnimationFrame(animate); } else { fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()}); animating = false; prevTime = undefined; var autoRotateStartTime = config.autoRotateInactivityDelay - (Date.now() - latestInteraction); if (autoRotateStartTime > 0) { autoRotateStart = setTimeout(function() { config.autoRotate = autoRotateSpeed; _this.lookAt(origPitch, undefined, origHfov, 3000); animateInit(); }, autoRotateStartTime); } else if (config.autoRotateInactivityDelay >= 0 && autoRotateSpeed) { config.autoRotate = autoRotateSpeed; _this.lookAt(origPitch, undefined, origHfov, 3000); animateInit(); } } } /** * Renders panorama view. * @private */ function render() { var tmpyaw; if (loaded) { var canvas = renderer.getCanvas(); if (config.autoRotate !== false) { // When auto-rotating this check needs to happen first (see issue #764) if (config.yaw > 360) { config.yaw -= 360; } else if (config.yaw < -360) { config.yaw += 360; } } // Keep a tmp value of yaw for autoRotate comparison later tmpyaw = config.yaw; // Optionally avoid showing background (empty space) on left or right by adapting min/max yaw var hoffcut = 0, voffcut = 0; if (config.avoidShowingBackground) { var hfov2 = config.hfov / 2, vfov2 = Math.atan2(Math.tan(hfov2 / 180 * Math.PI), (canvas.width / canvas.height)) * 180 / Math.PI, transposed = config.vaov > config.haov; if (transposed) { voffcut = vfov2 * (1 - Math.min(Math.cos((config.pitch - hfov2) / 180 * Math.PI), Math.cos((config.pitch + hfov2) / 180 * Math.PI))); } else { hoffcut = hfov2 * (1 - Math.min(Math.cos((config.pitch - vfov2) / 180 * Math.PI), Math.cos((config.pitch + vfov2) / 180 * Math.PI))); } } // Ensure the yaw is within min and max allowed var yawRange = config.maxYaw - config.minYaw, minYaw = -180, maxYaw = 180; if (yawRange < 360) { minYaw = config.minYaw + config.hfov / 2 + hoffcut; maxYaw = config.maxYaw - config.hfov / 2 - hoffcut; if (yawRange < config.hfov) { // Lock yaw to average of min and max yaw when both can be seen at once minYaw = maxYaw = (minYaw + maxYaw) / 2; } config.yaw = Math.max(minYaw, Math.min(maxYaw, config.yaw)); } if (!(config.autoRotate !== false)) { // When not auto-rotating, this check needs to happen after the // previous check (see issue #698) if (config.yaw > 360) { config.yaw -= 360; } else if (config.yaw < -360) { config.yaw += 360; } } // Check if we autoRotate in a limited by min and max yaw // If so reverse direction if (config.autoRotate !== false && tmpyaw != config.yaw && prevTime !== undefined) { // this condition prevents changing the direction initially config.autoRotate *= -1; } // Ensure the calculated pitch is within min and max allowed var vfov = 2 * Math.atan(Math.tan(config.hfov / 180 * Math.PI * 0.5) / (canvas.width / canvas.height)) / Math.PI * 180; var minPitch = config.minPitch + vfov / 2, maxPitch = config.maxPitch - vfov / 2; var pitchRange = config.maxPitch - config.minPitch; if (pitchRange < vfov) { // Lock pitch to average of min and max pitch when both can be seen at once minPitch = maxPitch = (minPitch + maxPitch) / 2; } if (isNaN(minPitch)) minPitch = -90; if (isNaN(maxPitch)) maxPitch = 90; config.pitch = Math.max(minPitch, Math.min(maxPitch, config.pitch)); renderer.render(config.pitch * Math.PI / 180, config.yaw * Math.PI / 180, config.hfov * Math.PI / 180, {roll: config.roll * Math.PI / 180}); renderHotSpots(); // Update compass if (config.compass) { compass.style.transform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; compass.style.webkitTransform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; } } } /** * Creates a new quaternion. * @private * @constructor * @param {Number} w - W value * @param {Number} x - X value * @param {Number} y - Y value * @param {Number} z - Z value */ function Quaternion(w, x, y, z) { this.w = w; this.x = x; this.y = y; this.z = z; } /** * Multiplies quaternions. * @private * @param {Quaternion} q - Quaternion to multiply * @returns {Quaternion} Result of multiplication */ Quaternion.prototype.multiply = function(q) { return new Quaternion(this.w*q.w - this.x*q.x - this.y*q.y - this.z*q.z, this.x*q.w + this.w*q.x + this.y*q.z - this.z*q.y, this.y*q.w + this.w*q.y + this.z*q.x - this.x*q.z, this.z*q.w + this.w*q.z + this.x*q.y - this.y*q.x); }; /** * Converts quaternion to Euler angles. * @private * @returns {Number[]} [phi angle, theta angle, psi angle] */ Quaternion.prototype.toEulerAngles = function() { var phi = Math.atan2(2 * (this.w * this.x + this.y * this.z), 1 - 2 * (this.x * this.x + this.y * this.y)), theta = Math.asin(2 * (this.w * this.y - this.z * this.x)), psi = Math.atan2(2 * (this.w * this.z + this.x * this.y), 1 - 2 * (this.y * this.y + this.z * this.z)); return [phi, theta, psi]; }; /** * Converts device orientation API Tait-Bryan angles to a quaternion. * @private * @param {Number} alpha - Alpha angle (in degrees) * @param {Number} beta - Beta angle (in degrees) * @param {Number} gamma - Gamma angle (in degrees) * @returns {Quaternion} Orientation quaternion */ function taitBryanToQuaternion(alpha, beta, gamma) { var r = [beta ? beta * Math.PI / 180 / 2 : 0, gamma ? gamma * Math.PI / 180 / 2 : 0, alpha ? alpha * Math.PI / 180 / 2 : 0]; var c = [Math.cos(r[0]), Math.cos(r[1]), Math.cos(r[2])], s = [Math.sin(r[0]), Math.sin(r[1]), Math.sin(r[2])]; return new Quaternion(c[0]*c[1]*c[2] - s[0]*s[1]*s[2], s[0]*c[1]*c[2] - c[0]*s[1]*s[2], c[0]*s[1]*c[2] + s[0]*c[1]*s[2], c[0]*c[1]*s[2] + s[0]*s[1]*c[2]); } /** * Computes current device orientation quaternion from device orientation API * Tait-Bryan angles. * @private * @param {Number} alpha - Alpha angle (in degrees) * @param {Number} beta - Beta angle (in degrees) * @param {Number} gamma - Gamma angle (in degrees) * @returns {Quaternion} Orientation quaternion */ function computeQuaternion(alpha, beta, gamma) { // Convert Tait-Bryan angles to quaternion var quaternion = taitBryanToQuaternion(alpha, beta, gamma); // Apply world transform quaternion = quaternion.multiply(new Quaternion(Math.sqrt(0.5), -Math.sqrt(0.5), 0, 0)); // Apply screen transform var angle = window.orientation ? -window.orientation * Math.PI / 180 / 2 : 0; return quaternion.multiply(new Quaternion(Math.cos(angle), 0, -Math.sin(angle), 0)); } /** * Event handler for device orientation API. Controls pointing. * @private * @param {DeviceOrientationEvent} event - Device orientation event. */ function orientationListener(e) { var q = computeQuaternion(e.alpha, e.beta, e.gamma).toEulerAngles(); if (typeof(orientation) == 'number' && orientation < 10) { // This kludge is necessary because iOS sometimes provides a few stale // device orientation events when the listener is removed and then // readded. Thus, we skip the first 10 events to prevent this from // causing problems. orientation += 1; } else if (orientation === 10) { // Record starting yaw to prevent jumping orientationYawOffset = q[2] / Math.PI * 180 + config.yaw; orientation = true; requestAnimationFrame(animate); } else { config.pitch = q[0] / Math.PI * 180; config.roll = -q[1] / Math.PI * 180; config.yaw = -q[2] / Math.PI * 180 + orientationYawOffset; } } /** * Initializes renderer. * @private */ function renderInit() { try { var params = {}; if (config.horizonPitch !== undefined) params.horizonPitch = config.horizonPitch * Math.PI / 180; if (config.horizonRoll !== undefined) params.horizonRoll = config.horizonRoll * Math.PI / 180; if (config.backgroundColor !== undefined) params.backgroundColor = config.backgroundColor; renderer.init(panoImage, config.type, config.dynamic, config.haov * Math.PI / 180, config.vaov * Math.PI / 180, config.vOffset * Math.PI / 180, renderInitCallback, params); if (config.dynamic !== true) { // Allow image to be garbage collected panoImage = undefined; } } catch(event) { // Panorama not loaded // Display error if there is a bad texture if (event.type == 'webgl error' || event.type == 'no webgl') { anError(); } else if (event.type == 'webgl size error') { anError(config.strings.textureSizeError.replace('%s', event.width).replace('%s', event.maxWidth)); } else { anError(config.strings.unknownError); throw event; } } } /** * Triggered when render initialization finishes. Handles fading between * scenes as well as showing the compass and hotspots and hiding the loading * display. * @private */ function renderInitCallback() { // Fade if specified if (config.sceneFadeDuration && renderer.fadeImg !== undefined) { renderer.fadeImg.style.opacity = 0; // Remove image var fadeImg = renderer.fadeImg; delete renderer.fadeImg; setTimeout(function() { renderContainer.removeChild(fadeImg); fireEvent('scenechangefadedone'); }, config.sceneFadeDuration); } // Show compass if applicable if (config.compass) { compass.style.display = 'inline'; } else { compass.style.display = 'none'; } // Show hotspots createHotSpots(); // Hide loading display infoDisplay.load.box.style.display = 'none'; if (preview !== undefined) { renderContainer.removeChild(preview); preview = undefined; } loaded = true; animateInit(); fireEvent('load'); } /** * Creates hot spot element for the current scene. * @private * @param {Object} hs - The configuration for the hotspot */ function createHotSpot(hs) { // Make sure hot spot pitch and yaw are numbers hs.pitch = Number(hs.pitch) || 0; hs.yaw = Number(hs.yaw) || 0; var div = document.createElement('div'); div.className = 'pnlm-hotspot-base'; if (hs.cssClass) div.className += ' ' + hs.cssClass; else div.className += ' pnlm-hotspot pnlm-sprite pnlm-' + escapeHTML(hs.type); var span = document.createElement('span'); if (hs.text) span.innerHTML = escapeHTML(hs.text); var a; if (hs.video) { var video = document.createElement('video'), vidp = hs.video; if (config.basePath && !absoluteURL(vidp)) vidp = config.basePath + vidp; video.src = sanitizeURL(vidp); video.controls = true; video.style.width = hs.width + 'px'; renderContainer.appendChild(div); span.appendChild(video); } else if (hs.image) { var imgp = hs.image; if (config.basePath && !absoluteURL(imgp)) imgp = config.basePath + imgp; a = document.createElement('a'); a.href = sanitizeURL(hs.URL ? hs.URL : imgp, true); a.target = '_blank'; span.appendChild(a); var image = document.createElement('img'); image.src = sanitizeURL(imgp); image.style.width = hs.width + 'px'; image.style.paddingTop = '5px'; renderContainer.appendChild(div); a.appendChild(image); span.style.maxWidth = 'initial'; } else if (hs.URL) { a = document.createElement('a'); a.href = sanitizeURL(hs.URL, true); if (hs.attributes) { for (var key in hs.attributes) { a.setAttribute(key, hs.attributes[key]); } } else { //wpvr edit by sakib for in tab url open// -- handling from here if (hs.wpvr_url_open == 'on') { a.target = '_self'; } else { a.target = '_blank'; } } renderContainer.appendChild(a); div.className += ' pnlm-pointer'; span.className += ' pnlm-pointer'; a.appendChild(div); } else { if (hs.sceneId) { div.onclick = div.ontouchend = function() { if (!div.clicked) { div.clicked = true; loadScene(hs.sceneId, hs.targetPitch, hs.targetYaw, hs.targetHfov); } return false; }; div.className += ' pnlm-pointer'; span.className += ' pnlm-pointer'; } // renderContainer.appendChild("title"); renderContainer.appendChild(div); } if (hs.createTooltipFunc) { hs.createTooltipFunc(div, hs.createTooltipArgs); } else if (hs.text || hs.video || hs.image) { div.classList.add('pnlm-tooltip'); div.appendChild(span); span.style.width = span.scrollWidth - 20 + 'px'; span.style.marginLeft = -(span.scrollWidth - div.offsetWidth) / 2 + 'px'; span.style.marginTop = -span.scrollHeight - 12 + 'px'; } if (hs.clickHandlerFunc) { div.addEventListener('click', function(e) { hs.clickHandlerFunc(e, hs.clickHandlerArgs); }, 'false'); div.className += ' pnlm-pointer'; span.className += ' pnlm-pointer'; } hs.div = div; } /** * Creates hot spot elements for the current scene. * @private */ function createHotSpots() { if (hotspotsCreated) return; if (!config.hotSpots) { config.hotSpots = []; } else { // Sort by pitch so tooltip is never obscured by another hot spot config.hotSpots = config.hotSpots.sort(function(a, b) { return a.pitch < b.pitch; }); config.hotSpots.forEach(createHotSpot); } hotspotsCreated = true; renderHotSpots(); } /** * Destroys currently created hot spot elements. * @private */ function destroyHotSpots() { var hs = config.hotSpots; hotspotsCreated = false; delete config.hotSpots; if (hs) { for (var i = 0; i < hs.length; i++) { var current = hs[i].div; if (current) { while (current.parentNode && current.parentNode != renderContainer) { current = current.parentNode; } renderContainer.removeChild(current); } delete hs[i].div; } } } /** * Renders hot spot, updating its position and visibility. * @private */ function renderHotSpot(hs) { var hsPitchSin = Math.sin(hs.pitch * Math.PI / 180), hsPitchCos = Math.cos(hs.pitch * Math.PI / 180), configPitchSin = Math.sin(config.pitch * Math.PI / 180), configPitchCos = Math.cos(config.pitch * Math.PI / 180), yawCos = Math.cos((-hs.yaw + config.yaw) * Math.PI / 180); var z = hsPitchSin * configPitchSin + hsPitchCos * yawCos * configPitchCos; if ((hs.yaw <= 90 && hs.yaw > -90 && z <= 0) || ((hs.yaw > 90 || hs.yaw <= -90) && z <= 0)) { hs.div.style.visibility = 'hidden'; } else { var yawSin = Math.sin((-hs.yaw + config.yaw) * Math.PI / 180), hfovTan = Math.tan(config.hfov * Math.PI / 360); hs.div.style.visibility = 'visible'; // Subpixel rendering doesn't work in Firefox // https://bugzilla.mozilla.org/show_bug.cgi?id=739176 var canvas = renderer.getCanvas(), canvasWidth = canvas.clientWidth, canvasHeight = canvas.clientHeight; var coord = [-canvasWidth / hfovTan * yawSin * hsPitchCos / z / 2, -canvasWidth / hfovTan * (hsPitchSin * configPitchCos - hsPitchCos * yawCos * configPitchSin) / z / 2]; // Apply roll var rollSin = Math.sin(config.roll * Math.PI / 180), rollCos = Math.cos(config.roll * Math.PI / 180); coord = [coord[0] * rollCos - coord[1] * rollSin, coord[0] * rollSin + coord[1] * rollCos]; // Apply transform coord[0] += (canvasWidth - hs.div.offsetWidth) / 2; coord[1] += (canvasHeight - hs.div.offsetHeight) / 2; var transform = 'translate(' + coord[0] + 'px, ' + coord[1] + 'px) translateZ(9999px) rotate(' + config.roll + 'deg)'; if (hs.scale) { transform += ' scale(' + (origHfov/config.hfov) / z + ')'; } hs.div.style.webkitTransform = transform; hs.div.style.MozTransform = transform; hs.div.style.transform = transform; } } /** * Renders hot spots, updating their positions and visibility. * @private */ function renderHotSpots() { config.hotSpots.forEach(renderHotSpot); } /** * Merges a scene configuration into the current configuration. * @private * @param {string} sceneId - Identifier of scene configuration to merge in. */ function mergeConfig(sceneId) { config = {}; var k, s; var photoSphereExcludes = ['haov', 'vaov', 'vOffset', 'northOffset', 'horizonPitch', 'horizonRoll']; specifiedPhotoSphereExcludes = []; // Merge default config for (k in defaultConfig) { if (defaultConfig.hasOwnProperty(k)) { config[k] = defaultConfig[k]; } } // Merge default scene config for (k in initialConfig.default) { if (initialConfig.default.hasOwnProperty(k)) { if (k == 'strings') { for (s in initialConfig.default.strings) { if (initialConfig.default.strings.hasOwnProperty(s)) { config.strings[s] = escapeHTML(initialConfig.default.strings[s]); } } } else { config[k] = initialConfig.default[k]; if (photoSphereExcludes.indexOf(k) >= 0) { specifiedPhotoSphereExcludes.push(k); } } } } // Merge current scene config if ((sceneId !== null) && (sceneId !== '') && (initialConfig.scenes) && (initialConfig.scenes[sceneId])) { var scene = initialConfig.scenes[sceneId]; for (k in scene) { if (scene.hasOwnProperty(k)) { if (k == 'strings') { for (s in scene.strings) { if (scene.strings.hasOwnProperty(s)) { config.strings[s] = escapeHTML(scene.strings[s]); } } } else { config[k] = scene[k]; if (photoSphereExcludes.indexOf(k) >= 0) { specifiedPhotoSphereExcludes.push(k); } } } } config.scene = sceneId; } // Merge initial config for (k in initialConfig) { if (initialConfig.hasOwnProperty(k)) { if (k == 'strings') { for (s in initialConfig.strings) { if (initialConfig.strings.hasOwnProperty(s)) { config.strings[s] = escapeHTML(initialConfig.strings[s]); } } } else { config[k] = initialConfig[k]; if (photoSphereExcludes.indexOf(k) >= 0) { specifiedPhotoSphereExcludes.push(k); } } } } } /** * Processes configuration options. * @param {boolean} [isPreview] - Whether or not the preview is being displayed * @private */ function processOptions(isPreview) { isPreview = isPreview ? isPreview : false; // Process preview first so it always loads before the browser hits its // maximum number of connections to a server as can happen with cubic // panoramas if (isPreview && 'preview' in config) { var p = config.preview; if (config.basePath && !absoluteURL(p)) p = config.basePath + p; preview = document.createElement('div'); preview.className = 'pnlm-preview-img'; preview.style.backgroundImage = "url('" + sanitizeURLForCss(p) + "')"; renderContainer.appendChild(preview); } // Handle different preview values var title = config.title, author = config.author; if (isPreview) { if ('previewTitle' in config) config.title = config.previewTitle; if ('previewAuthor' in config) config.author = config.previewAuthor; } // Reset title / author display if (!config.hasOwnProperty('title')) infoDisplay.title.innerHTML = ''; if (!config.hasOwnProperty('author')) infoDisplay.author.innerHTML = ''; if (!config.hasOwnProperty('title') && !config.hasOwnProperty('author')) infoDisplay.container.style.display = 'none'; // Fill in load button label and loading box text controls.load.innerHTML = '

' + config.strings.loadButtonLabel + '

'; infoDisplay.load.boxp.innerHTML = config.strings.loadingLabel; // Process other options for (var key in config) { if (config.hasOwnProperty(key)) { switch(key) { case 'title': infoDisplay.title.innerHTML = escapeHTML(config[key]); infoDisplay.container.style.display = 'inline'; break; case 'author': var authorText = escapeHTML(config[key]); if (config.authorURL) { var authorLink = document.createElement('a'); authorLink.href = sanitizeURL(config['authorURL'], true); authorLink.target = '_blank'; authorLink.innerHTML = escapeHTML(config[key]); authorText = authorLink.outerHTML; } infoDisplay.author.innerHTML = config.strings.bylineLabel.replace('%s', authorText); infoDisplay.container.style.display = 'inline'; break; case 'fallback': var link = document.createElement('a'); link.href = sanitizeURL(config[key], true); link.target = '_blank'; link.textContent = 'Click here to view this panorama in an alternative viewer.'; var message = document.createElement('p'); message.textContent = 'Your browser does not support WebGL.'; message.appendChild(document.createElement('br')); message.appendChild(link); infoDisplay.errorMsg.innerHTML = ''; // Removes all children nodes infoDisplay.errorMsg.appendChild(message); break; case 'hfov': setHfov(Number(config[key])); break; case 'autoLoad': if (config[key] === true && renderer === undefined) { // Show loading box infoDisplay.load.box.style.display = 'inline'; // Hide load button controls.load.style.display = 'none'; // Initialize init(); } break; case 'showZoomCtrl': if (config[key] && config.showControls != false) { // Show zoom controls controls.zoom.style.display = 'block'; } else { // Hide zoom controls controls.zoom.style.display = 'none'; } break; case 'showFullscreenCtrl': if (config[key] && config.showControls != false && ('fullscreen' in document || 'mozFullScreen' in document || 'webkitIsFullScreen' in document || 'msFullscreenElement' in document)) { // Show fullscreen control controls.fullscreen.style.display = 'block'; } else { // Hide fullscreen control controls.fullscreen.style.display = 'none'; } break; case 'hotSpotDebug': if (config[key]) hotSpotDebugIndicator.style.display = 'block'; else hotSpotDebugIndicator.style.display = 'none'; break; case 'showControls': if (!config[key]) { controls.orientation.style.display = 'none'; controls.zoom.style.display = 'none'; controls.fullscreen.style.display = 'none'; } break; case 'orientationOnByDefault': if (config[key]) startOrientation(); break; } } } if (isPreview) { // Restore original values if changed for preview if (title) config.title = title; else delete config.title; if (author) config.author = author; else delete config.author; } } /** * Toggles fullscreen mode. * @private */ function toggleFullscreen() { if (loaded && !error) { if (!fullscreenActive) { try { if (container.requestFullscreen) { container.requestFullscreen(); } else if (container.mozRequestFullScreen) { container.mozRequestFullScreen(); } else if (container.msRequestFullscreen) { container.msRequestFullscreen(); } else { container.webkitRequestFullScreen(); } } catch(event) { // Fullscreen doesn't work } } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitCancelFullScreen) { document.webkitCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } } } /** * Event handler for fullscreen changes. * @private */ function onFullScreenChange(resize) { if (document.fullscreenElement || document.fullscreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement) { controls.fullscreen.classList.add('pnlm-fullscreen-toggle-button-active'); fullscreenActive = true; } else { controls.fullscreen.classList.remove('pnlm-fullscreen-toggle-button-active'); fullscreenActive = false; } if (resize !== 'resize') fireEvent('fullscreenchange', fullscreenActive); // Resize renderer (deal with browser quirks and fixes #155) renderer.resize(); setHfov(config.hfov); animateInit(); } /** * Increases panorama zoom. For use with zoom button. * @private */ function zoomIn() { if (loaded) { setHfov(config.hfov - 5); animateInit(); } } /** * Decreases panorama zoom. For use with zoom button. * @private */ function zoomOut() { if (loaded) { setHfov(config.hfov + 5); animateInit(); } } /** * Clamps horzontal field of view to viewer's limits. * @private * @param {number} hfov - Input horizontal field of view (in degrees) * @return {number} - Clamped horizontal field of view (in degrees) */ function constrainHfov(hfov) { // Keep field of view within bounds var minHfov = config.minHfov; if (config.type == 'multires' && renderer && !config.multiResMinHfov) { minHfov = Math.min(minHfov, renderer.getCanvas().width / (config.multiRes.cubeResolution / 90 * 0.9)); } if (minHfov > config.maxHfov) { // Don't change view if bounds don't make sense console.log('HFOV bounds do not make sense (minHfov > maxHfov).'); return config.hfov; } var newHfov = config.hfov; if (hfov < minHfov) { newHfov = minHfov; } else if (hfov > config.maxHfov) { newHfov = config.maxHfov; } else { newHfov = hfov; } // Optionally avoid showing background (empty space) on top or bottom by adapting newHfov if (config.avoidShowingBackground && renderer) { var canvas = renderer.getCanvas(); newHfov = Math.min(newHfov, Math.atan(Math.tan((config.maxPitch - config.minPitch) / 360 * Math.PI) / canvas.height * canvas.width) * 360 / Math.PI); } return newHfov; } /** * Sets viewer's horizontal field of view. * @private * @param {number} hfov - Desired horizontal field of view in degrees. */ function setHfov(hfov) { config.hfov = constrainHfov(hfov); fireEvent('zoomchange', config.hfov); } /** * Stops auto rotation and animated moves. * @private */ function stopAnimation() { animatedMove = {}; autoRotateSpeed = config.autoRotate ? config.autoRotate : autoRotateSpeed; config.autoRotate = false; } /** * Loads panorama. * @private */ function load() { // Since WebGL error handling is very general, first we clear any error box // since it is a new scene and the error from previous maybe because of lacking // memory etc and not because of a lack of WebGL support etc clearError(); loaded = false; controls.load.style.display = 'none'; infoDisplay.load.box.style.display = 'inline'; init(); } /** * Loads scene. * @private * @param {string} sceneId - Identifier of scene configuration to merge in. * @param {number} targetPitch - Pitch viewer should be centered on once scene loads. * @param {number} targetYaw - Yaw viewer should be centered on once scene loads. * @param {number} targetHfov - HFOV viewer should use once scene loads. * @param {boolean} [fadeDone] - If `true`, fade setup is skipped. */ function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) { if (!loaded) fadeDone = true; // Don't try to fade when there isn't a scene loaded loaded = false; animatedMove = {}; // Set up fade if specified var fadeImg, workingPitch, workingYaw, workingHfov; if (config.sceneFadeDuration && !fadeDone) { var data = renderer.render(config.pitch * Math.PI / 180, config.yaw * Math.PI / 180, config.hfov * Math.PI / 180, {returnImage: true}); if (data !== undefined) { fadeImg = new Image(); fadeImg.className = 'pnlm-fade-img'; fadeImg.style.transition = 'opacity ' + (config.sceneFadeDuration / 1000) + 's'; fadeImg.style.width = '100%'; fadeImg.style.height = '100%'; fadeImg.onload = function() { loadScene(sceneId, targetPitch, targetYaw, targetHfov, true); }; fadeImg.src = data; renderContainer.appendChild(fadeImg); renderer.fadeImg = fadeImg; return; } } // Set new pointing if (targetPitch === 'same') { workingPitch = config.pitch; } else { workingPitch = targetPitch; } if (targetYaw === 'same') { workingYaw = config.yaw; } else if (targetYaw === 'sameAzimuth') { workingYaw = config.yaw + (config.northOffset || 0) - (initialConfig.scenes[sceneId].northOffset || 0); } else { workingYaw = targetYaw; } if (targetHfov === 'same') { workingHfov = config.hfov; } else { workingHfov = targetHfov; } // Destroy hot spots from previous scene destroyHotSpots(); // Create the new config for the scene mergeConfig(sceneId); // Stop motion speed.yaw = speed.pitch = speed.hfov = 0; // Reload scene processOptions(); if (workingPitch !== undefined) { config.pitch = workingPitch; } if (workingYaw !== undefined) { config.yaw = workingYaw; } if (workingHfov !== undefined) { config.hfov = workingHfov; } fireEvent('scenechange', sceneId); load(); } /** * Stop using device orientation. * @private */ function stopOrientation() { window.removeEventListener('deviceorientation', orientationListener); controls.orientation.classList.remove('pnlm-orientation-button-active'); orientation = false; } /** * Start using device orientation. * @private */ function startOrientation() { if (!orientationSupport) return; if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') { DeviceOrientationEvent.requestPermission().then(function(response) { if (response == 'granted') { orientation = 1; window.addEventListener('deviceorientation', orientationListener); controls.orientation.classList.add('pnlm-orientation-button-active'); } }); } else { orientation = 1; window.addEventListener('deviceorientation', orientationListener); controls.orientation.classList.add('pnlm-orientation-button-active'); } } /** * Escapes HTML string (to mitigate possible DOM XSS attacks). * @private * @param {string} s - String to escape * @returns {string} Escaped string */ function escapeHTML(s) { if (!initialConfig.escapeHTML) return String(s).split('\n').join('
'); return String(s).split(/&/g).join('&') .split('"').join('"') .split("'").join(''') .split('<').join('<') .split('>').join('>') .split('/').join('/') .split('\n').join('
'); // Allow line breaks } /** * Removes possibility of XSS attacks with URLs. * The URL cannot be of protocol 'javascript'. * @private * @param {string} url - URL to sanitize * @param {boolean} href - True if URL is for link (blocks data URIs) * @returns {string} Sanitized URL */ function sanitizeURL(url, href) { try { var decoded_url = decodeURIComponent(unescape(url)).replace(/[^\w:]/g, '').toLowerCase(); } catch (e) { return 'about:blank'; } if (decoded_url.indexOf('javascript:') === 0 || decoded_url.indexOf('vbscript:') === 0) { console.log('Script URL removed.'); return 'about:blank'; } if (href && decoded_url.indexOf('data:') === 0) { console.log('Data URI removed from link.'); return 'about:blank'; } return url; } /** * Unescapes HTML entities. * Copied from Marked.js 0.7.0. * @private * @param {string} url - URL to sanitize * @param {boolean} href - True if URL is for link (blocks data URIs) * @returns {string} Sanitized URL */ function unescape(html) { // Explicitly match decimal, hex, and named HTML entities return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) { n = n.toLowerCase(); if (n === 'colon') return ':'; if (n.charAt(0) === '#') { return n.charAt(1) === 'x' ? String.fromCharCode(parseInt(n.substring(2), 16)) : String.fromCharCode(+n.substring(1)); } return ''; }); } /** * Removes possibility of XSS atacks with URLs for CSS. * The URL will be sanitized with `sanitizeURL()` and single quotes * and double quotes escaped. * @private * @param {string} url - URL to sanitize * @returns {string} Sanitized URL */ function sanitizeURLForCss(url) { return sanitizeURL(url) .replace(/"/g, '%22') .replace(/'/g, '%27'); } /** * Checks whether or not a panorama is loaded. * @memberof Viewer * @instance * @returns {boolean} `true` if a panorama is loaded, else `false` */ this.isLoaded = function() { return Boolean(loaded); }; /** * Returns the pitch of the center of the view. * @memberof Viewer * @instance * @returns {number} Pitch in degrees */ this.getPitch = function() { return config.pitch; }; /** * Sets the pitch of the center of the view. * @memberof Viewer * @instance * @param {number} pitch - Pitch in degrees * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation * @param {function} [callback] - Function to call when animation finishes * @param {object} [callbackArgs] - Arguments to pass to callback function * @returns {Viewer} `this` */ this.setPitch = function(pitch, animated, callback, callbackArgs) { latestInteraction = Date.now(); if (Math.abs(pitch - config.pitch) <= eps) { if (typeof callback == 'function') callback(callbackArgs); return this; } animated = animated == undefined ? 1000: Number(animated); if (animated) { animatedMove.pitch = { 'startTime': Date.now(), 'startPosition': config.pitch, 'endPosition': pitch, 'duration': animated }; if (typeof callback == 'function') setTimeout(function(){callback(callbackArgs);}, animated); } else { config.pitch = pitch; } animateInit(); return this; }; /** * Returns the minimum and maximum allowed pitches (in degrees). * @memberof Viewer * @instance * @returns {number[]} [minimum pitch, maximum pitch] */ this.getPitchBounds = function() { return [config.minPitch, config.maxPitch]; }; /** * Set the minimum and maximum allowed pitches (in degrees). * @memberof Viewer * @instance * @param {number[]} bounds - [minimum pitch, maximum pitch] * @returns {Viewer} `this` */ this.setPitchBounds = function(bounds) { config.minPitch = Math.max(-90, Math.min(bounds[0], 90)); config.maxPitch = Math.max(-90, Math.min(bounds[1], 90)); return this; }; /** * Returns the yaw of the center of the view. * @memberof Viewer * @instance * @returns {number} Yaw in degrees */ this.getYaw = function() { return (config.yaw + 540) % 360 - 180; }; /** * Sets the yaw of the center of the view. * @memberof Viewer * @instance * @param {number} yaw - Yaw in degrees [-180, 180] * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation * @param {function} [callback] - Function to call when animation finishes * @param {object} [callbackArgs] - Arguments to pass to callback function * @returns {Viewer} `this` */ this.setYaw = function(yaw, animated, callback, callbackArgs) { latestInteraction = Date.now(); if (Math.abs(yaw - config.yaw) <= eps) { if (typeof callback == 'function') callback(callbackArgs); return this; } animated = animated == undefined ? 1000: Number(animated); yaw = ((yaw + 180) % 360) - 180; // Keep in bounds if (animated) { // Animate in shortest direction if (config.yaw - yaw > 180) yaw += 360; else if (yaw - config.yaw > 180) yaw -= 360; animatedMove.yaw = { 'startTime': Date.now(), 'startPosition': config.yaw, 'endPosition': yaw, 'duration': animated }; if (typeof callback == 'function') setTimeout(function(){callback(callbackArgs);}, animated); } else { config.yaw = yaw; } animateInit(); return this; }; /** * Returns the minimum and maximum allowed pitches (in degrees). * @memberof Viewer * @instance * @returns {number[]} [yaw pitch, maximum yaw] */ this.getYawBounds = function() { return [config.minYaw, config.maxYaw]; }; /** * Set the minimum and maximum allowed yaws (in degrees [-360, 360]). * @memberof Viewer * @instance * @param {number[]} bounds - [minimum yaw, maximum yaw] * @returns {Viewer} `this` */ this.setYawBounds = function(bounds) { config.minYaw = Math.max(-360, Math.min(bounds[0], 360)); config.maxYaw = Math.max(-360, Math.min(bounds[1], 360)); return this; }; /** * Returns the horizontal field of view. * @memberof Viewer * @instance * @returns {number} Horizontal field of view in degrees */ this.getHfov = function() { return config.hfov; }; /** * Sets the horizontal field of view. * @memberof Viewer * @instance * @param {number} hfov - Horizontal field of view in degrees * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation * @param {function} [callback] - Function to call when animation finishes * @param {object} [callbackArgs] - Arguments to pass to callback function * @returns {Viewer} `this` */ this.setHfov = function(hfov, animated, callback, callbackArgs) { latestInteraction = Date.now(); if (Math.abs(hfov - config.hfov) <= eps) { if (typeof callback == 'function') callback(callbackArgs); return this; } animated = animated == undefined ? 1000: Number(animated); if (animated) { animatedMove.hfov = { 'startTime': Date.now(), 'startPosition': config.hfov, 'endPosition': constrainHfov(hfov), 'duration': animated }; if (typeof callback == 'function') setTimeout(function(){callback(callbackArgs);}, animated); } else { setHfov(hfov); } animateInit(); return this; }; /** * Returns the minimum and maximum allowed horizontal fields of view * (in degrees). * @memberof Viewer * @instance * @returns {number[]} [minimum hfov, maximum hfov] */ this.getHfovBounds = function() { return [config.minHfov, config.maxHfov]; }; /** * Set the minimum and maximum allowed horizontal fields of view (in degrees). * @memberof Viewer * @instance * @param {number[]} bounds - [minimum hfov, maximum hfov] * @returns {Viewer} `this` */ this.setHfovBounds = function(bounds) { config.minHfov = Math.max(0, bounds[0]); config.maxHfov = Math.max(0, bounds[1]); return this; }; /** * Set a new view. Any parameters not specified remain the same. * @memberof Viewer * @instance * @param {number} [pitch] - Target pitch * @param {number} [yaw] - Target yaw * @param {number} [hfov] - Target hfov * @param {boolean|number} [animated=1000] - Animation duration in milliseconds or false for no animation * @param {function} [callback] - Function to call when animation finishes * @param {object} [callbackArgs] - Arguments to pass to callback function * @returns {Viewer} `this` */ this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) { animated = animated == undefined ? 1000: Number(animated); if (pitch !== undefined && Math.abs(pitch - config.pitch) > eps) { this.setPitch(pitch, animated, callback, callbackArgs); callback = undefined; } if (yaw !== undefined && Math.abs(yaw - config.yaw) > eps) { this.setYaw(yaw, animated, callback, callbackArgs); callback = undefined; } if (hfov !== undefined && Math.abs(hfov - config.hfov) > eps) { this.setHfov(hfov, animated, callback, callbackArgs); callback = undefined; } if (typeof callback == 'function') callback(callbackArgs); return this; }; /** * Returns the panorama's north offset. * @memberof Viewer * @instance * @returns {number} North offset in degrees */ this.getNorthOffset = function() { return config.northOffset; }; /** * Sets the panorama's north offset. * @memberof Viewer * @instance * @param {number} heading - North offset in degrees * @returns {Viewer} `this` */ this.setNorthOffset = function(heading) { config.northOffset = Math.min(360, Math.max(0, heading)); animateInit(); return this; }; /** * Returns the panorama's horizon roll. * @memberof Viewer * @instance * @returns {number} Horizon roll in degrees */ this.getHorizonRoll = function() { return config.horizonRoll; }; /** * Sets the panorama's horizon roll. * @memberof Viewer * @instance * @param {number} roll - Horizon roll in degrees [-90, 90] * @returns {Viewer} `this` */ this.setHorizonRoll = function(roll) { config.horizonRoll = Math.min(90, Math.max(-90, roll)); renderer.setPose(config.horizonPitch * Math.PI / 180, config.horizonRoll * Math.PI / 180); animateInit(); return this; }; /** * Returns the panorama's horizon pitch. * @memberof Viewer * @instance * @returns {number} Horizon pitch in degrees */ this.getHorizonPitch = function() { return config.horizonPitch; }; /** * Sets the panorama's horizon pitch. * @memberof Viewer * @instance * @param {number} pitch - Horizon pitch in degrees [-90, 90] * @returns {Viewer} `this` */ this.setHorizonPitch = function(pitch) { config.horizonPitch = Math.min(90, Math.max(-90, pitch)); renderer.setPose(config.horizonPitch * Math.PI / 180, config.horizonRoll * Math.PI / 180); animateInit(); return this; }; /** * Start auto rotation. * * Before starting rotation, the viewer is panned to `pitch`. * @memberof Viewer * @instance * @param {number} [speed] - Auto rotation speed / direction. If not specified, previous value is used. * @param {number} [pitch] - The pitch to rotate at. If not specified, inital pitch is used. * @returns {Viewer} `this` */ this.startAutoRotate = function(speed, pitch) { speed = speed || autoRotateSpeed || 1; pitch = pitch === undefined ? origPitch : pitch; config.autoRotate = speed; _this.lookAt(pitch, undefined, origHfov, 3000); animateInit(); return this; }; /** * Stop auto rotation. * @memberof Viewer * @instance * @returns {Viewer} `this` */ this.stopAutoRotate = function() { autoRotateSpeed = config.autoRotate ? config.autoRotate : autoRotateSpeed; config.autoRotate = false; config.autoRotateInactivityDelay = -1; return this; }; /** * Stops all movement. * @memberof Viewer * @instance */ this.stopMovement = function() { stopAnimation(); speed = {'yaw': 0, 'pitch': 0, 'hfov': 0}; }; /** * Returns the panorama renderer. * @memberof Viewer * @instance * @returns {Renderer} */ this.getRenderer = function() { return renderer; }; /** * Sets update flag for dynamic content. * @memberof Viewer * @instance * @param {boolean} bool - Whether or not viewer should update even when still * @returns {Viewer} `this` */ this.setUpdate = function(bool) { update = bool === true; if (renderer === undefined) onImageLoad(); else animateInit(); return this; }; /** * Calculate panorama pitch and yaw from location of mouse event. * @memberof Viewer * @instance * @param {MouseEvent} event - Document mouse down event. * @returns {number[]} [pitch, yaw] */ this.mouseEventToCoords = function(event) { return mouseEventToCoords(event); }; /** * Change scene being viewed. * @memberof Viewer * @instance * @param {string} sceneId - Identifier of scene to switch to. * @param {number} [pitch] - Pitch to use with new scene * @param {number} [yaw] - Yaw to use with new scene * @param {number} [hfov] - HFOV to use with new scene * @returns {Viewer} `this` */ this.loadScene = function(sceneId, pitch, yaw, hfov) { if (loaded !== false) loadScene(sceneId, pitch, yaw, hfov); return this; }; /** * Get ID of current scene. * @memberof Viewer * @instance * @returns {string} ID of current scene */ this.getScene = function() { return config.scene; }; /** * Add a new scene. * @memberof Viewer * @instance * @param {string} sceneId - The ID of the new scene * @param {string} config - The configuration of the new scene * @returns {Viewer} `this` */ this.addScene = function(sceneId, config) { initialConfig.scenes[sceneId] = config; return this; }; /** * Remove a scene. * @memberof Viewer * @instance * @param {string} sceneId - The ID of the scene * @returns {boolean} False if the scene is the current scene or if the scene doesn't exists, else true */ this.removeScene = function(sceneId) { if (config.scene === sceneId || !initialConfig.scenes.hasOwnProperty(sceneId)) return false; delete initialConfig.scenes[sceneId]; return true; }; /** * Toggle fullscreen. * @memberof Viewer * @instance * @returns {Viewer} `this` */ this.toggleFullscreen = function() { toggleFullscreen(); return this; }; /** * Get configuration of current scene. * @memberof Viewer * @instance * @returns {Object} Configuration of current scene */ this.getConfig = function() { return config; }; /** * Get viewer's container element. * @memberof Viewer * @instance * @returns {HTMLElement} Container `div` element */ this.getContainer = function() { return container; }; /** * Add a new hot spot. * @memberof Viewer * @instance * @param {Object} hs - The configuration for the hot spot * @param {string} [sceneId] - Adds hot spot to specified scene if provided, else to current scene * @returns {Viewer} `this` * @throws Throws an error if the scene ID is provided but invalid */ this.addHotSpot = function(hs, sceneId) { if (sceneId === undefined && config.scene === undefined) { // Not a tour config.hotSpots.push(hs); } else { // Tour var id = sceneId !== undefined ? sceneId : config.scene; if (initialConfig.scenes.hasOwnProperty(id)) { if (!initialConfig.scenes[id].hasOwnProperty('hotSpots')) { initialConfig.scenes[id].hotSpots = []; // Create hot spots array if needed if (id == config.scene) config.hotSpots = initialConfig.scenes[id].hotSpots; // Link to current config } initialConfig.scenes[id].hotSpots.push(hs); // Add hot spot to config } else { throw 'Invalid scene ID!'; } } if (sceneId === undefined || config.scene == sceneId) { // Add to current scene createHotSpot(hs); if (loaded) renderHotSpot(hs); } return this; }; /** * Remove a hot spot. * @memberof Viewer * @instance * @param {string} hotSpotId - The ID of the hot spot * @param {string} [sceneId] - Removes hot spot from specified scene if provided, else from current scene * @returns {boolean} True if deletion is successful, else false */ this.removeHotSpot = function(hotSpotId, sceneId) { if (sceneId === undefined || config.scene == sceneId) { if (!config.hotSpots) return false; for (var i = 0; i < config.hotSpots.length; i++) { if (config.hotSpots[i].hasOwnProperty('id') && config.hotSpots[i].id === hotSpotId) { // Delete hot spot DOM elements var current = config.hotSpots[i].div; while (current.parentNode != renderContainer) current = current.parentNode; renderContainer.removeChild(current); delete config.hotSpots[i].div; // Remove hot spot from configuration config.hotSpots.splice(i, 1); return true; } } } else { if (initialConfig.scenes.hasOwnProperty(sceneId)) { if (!initialConfig.scenes[sceneId].hasOwnProperty('hotSpots')) return false; for (var j = 0; j < initialConfig.scenes[sceneId].hotSpots.length; j++) { if (initialConfig.scenes[sceneId].hotSpots[j].hasOwnProperty('id') && initialConfig.scenes[sceneId].hotSpots[j].id === hotSpotId) { // Remove hot spot from configuration initialConfig.scenes[sceneId].hotSpots.splice(j, 1); return true; } } } else { return false; } } }; /** * This method should be called if the viewer's container is resized. * @memberof Viewer * @instance */ this.resize = function() { if (renderer) onDocumentResize(); }; /** * Check if a panorama is loaded. * @memberof Viewer * @instance * @returns {boolean} True if a panorama is loaded, else false */ this.isLoaded = function() { return loaded; }; /** * Check if device orientation control is supported. * @memberof Viewer * @instance * @returns {boolean} True if supported, else false */ this.isOrientationSupported = function() { return orientationSupport || false; }; /** * Stop using device orientation. * @memberof Viewer * @instance */ this.stopOrientation = function() { stopOrientation(); }; /** * Start using device orientation (does nothing if not supported). * @memberof Viewer * @instance */ this.startOrientation = function() { if (orientationSupport) startOrientation(); }; /** * Check if device orientation control is currently activated. * @memberof Viewer * @instance * @returns {boolean} True if active, else false */ this.isOrientationActive = function() { return Boolean(orientation); }; /** * Subscribe listener to specified event. * @memberof Viewer * @instance * @param {string} type - Type of event to subscribe to. * @param {Function} listener - Listener function to subscribe to event. * @returns {Viewer} `this` */ this.on = function(type, listener) { externalEventListeners[type] = externalEventListeners[type] || []; externalEventListeners[type].push(listener); return this; }; /** * Remove an event listener (or listeners). * @memberof Viewer * @param {string} [type] - Type of event to remove listeners from. If not specified, all listeners are removed. * @param {Function} [listener] - Listener function to remove. If not specified, all listeners of specified type are removed. * @returns {Viewer} `this` */ this.off = function(type, listener) { if (!type) { // Remove all listeners if type isn't specified externalEventListeners = {}; return this; } if (listener) { var i = externalEventListeners[type].indexOf(listener); if (i >= 0) { // Remove listener if found externalEventListeners[type].splice(i, 1); } if (externalEventListeners[type].length == 0) { // Remove category if empty delete externalEventListeners[type]; } } else { // Remove category of listeners if listener isn't specified delete externalEventListeners[type]; } return this; }; /** * Fire listeners attached to specified event. * @private * @param {string} [type] - Type of event to fire listeners for. */ function fireEvent(type) { if (type in externalEventListeners) { // Reverse iteration is useful, if event listener is removed inside its definition for (var i = externalEventListeners[type].length; i > 0; i--) { externalEventListeners[type][externalEventListeners[type].length - i].apply(null, [].slice.call(arguments, 1)); } } } /** * Destructor. * @instance * @memberof Viewer */ this.destroy = function() { destroyed = true; clearTimeout(autoRotateStart); if (renderer) renderer.destroy(); if (listenersAdded) { document.removeEventListener('mousemove', onDocumentMouseMove, false); document.removeEventListener('mouseup', onDocumentMouseUp, false); container.removeEventListener('mozfullscreenchange', onFullScreenChange, false); container.removeEventListener('webkitfullscreenchange', onFullScreenChange, false); container.removeEventListener('msfullscreenchange', onFullScreenChange, false); container.removeEventListener('fullscreenchange', onFullScreenChange, false); window.removeEventListener('resize', onDocumentResize, false); window.removeEventListener('orientationchange', onDocumentResize, false); container.removeEventListener('keydown', onDocumentKeyPress, false); container.removeEventListener('keyup', onDocumentKeyUp, false); container.removeEventListener('blur', clearKeys, false); document.removeEventListener('mouseleave', onDocumentMouseUp, false); } container.innerHTML = ''; container.classList.remove('pnlm-container'); }; } return { viewer: function(container, config) { return new Viewer(container, config); } }; })(window, document); Mantle Financial Solutions | We take the headache out of Bookkeeping

Web Design

Your content goes here. Edit or remove this text inline.

Logo Design

Your content goes here. Edit or remove this text inline.

Web Development

Your content goes here. Edit or remove this text inline.

White Labeling

Your content goes here. Edit or remove this text inline.

VIEW ALL SERVICES 

You work hard for your money.
You shouldn’t have to work hard
to
organize it. 

Business Owners deserve the money they make but managing it can be a headache. We get it.
We classify your money and make it simple for you to understand your finances so you can keep more, make the best decisions, and focus on doing what you love.

LEARN MORE

“We are very pleased with all aspects of doing business with Mantle Financial Solutions! We needed timely and professional assistance for our business. . . We greatly appreciate the service provided, as well as the affordable cost to achieve the necessary task. We look forward to working with Mantle Financial Solutions again in the future!” -Deleware, Ohio

Hava and her team are amazing. Very prompt answering questions to maintain quality bookkeeping. Strategic thinking skills to meet Buisness needs. Highly recommend. -Stephanie, Grove City, OH

OUR SERVICES

Tax Advisory

Monthly Bookkeeping

QBO Catch-Up

Personalized QBO Training

Consulting/Outsource CFO

Advanced QBO Set-Up

ARTICLES

Mid-Year Financial Reset Checklist for Business Owners

We are halfway through the year, making this the perfect time for a financial reset. We help...

Is your Bookkeeping Software Set Up Correctly?

As your business grows, your bookkeeping software should grow with it. We help clients who have...

How to Review your Business Expenses Each Month

Running a business comes with a steady stream of expenses, and it is easy to pay them without...

Cash Flow is Becoming a Bigger Focus Than Ever in 2026

With rising costs, economic uncertainty, and changing customer spending habits, cash flow is...

Preparing Your Business Finances Before Vacation

Taking a vacation as a business owner can be difficult when you're worried about what might happen...

What Business Owners Often Get Wrong About Accounting Basics

One of the most common things we see as bookkeepers is business owners making financial decisions...

Costly Payroll Mistakes Small Business Owners Often Make

Payroll mistakes can become expensive quickly for small businesses. Between payroll taxes,...

The Risks of Automated Bookkeeping Business Owners Should Know

AI and automated bookkeeping tools are becoming more common in business finance, and there is no...

The Pricing Mistakes That Keep Small Businesses Stuck

One of the biggest things we see behind the scenes is businesses making sales but still struggling...

Common Budgeting Mistakes Business Owners Make

Business budgeting is just as important as personal budgeting because your business is what funds...

Hava’s team brings unparalleled expertise to the world of QuickBooks navigation. They can take your receipts out of the box you keep them in and get them into a system that is easy for you to manage, or even easier to let them manage! If you need a bookkeeper, this is the team you need. If you need a CFO but don’t have the business bandwidth to outright hire one – Mantle Financial Solutions can step into that role as well. -David, Deleware, OH

Hava does a great job working through exactly what her clients need to be successful!” -My Own Bath 

Very prompt and accurate service! Use them every month for bookkeeping and highly recommend. Hava Laudon is thorough and always available to explain and answer questions!. -Ryan, Polaris