text: (function (global, factory) {
  	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) :
  	typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) :
  	(global = global || self, factory(global.PANOLENS = {}, global.THREE));
  }(this, function (exports, THREE) { 'use strict';
  
  	const version="0.11.0";const dependencies={three:"^0.105.2"};
  
  	/**
  	 * REVISION
  	 * @module REVISION
  	 * @example PANOLENS.REVISION
  	 * @type {string} revision
  	 */
  	const REVISION = version.split( '.' )[ 1 ];
  
  	/**
  	 * VERSION
  	 * @module VERSION
  	 * @example PANOLENS.VERSION
  	 * @type {string} version
  	 */
  	const VERSION = version;
  
  	/**
  	 * THREEJS REVISION
  	 * @module THREE_REVISION
  	 * @example PANOLENS.THREE_REVISION
  	 * @type {string} threejs revision
  	 */
  	const THREE_REVISION = dependencies.three.split( '.' )[ 1 ];
  
  	/**
  	 * THREEJS VERSION
  	 * @module THREE_VERSION
  	 * @example PANOLENS.THREE_VERSION
  	 * @type {string} threejs version
  	 */
  	const THREE_VERSION = dependencies.three.replace( /[^0-9.]/g, '' );
  
  	/**
  	 * CONTROLS
  	 * @module CONTROLS
  	 * @example PANOLENS.CONTROLS.ORBIT
  	 * @property {number} ORBIT 0
  	 * @property {number} DEVICEORIENTATION 1
  	 */
  	const CONTROLS = { ORBIT: 0, DEVICEORIENTATION: 1 };
  
  	/**
  	 * MODES
  	 * @module MODES
  	 * @example PANOLENS.MODES.UNKNOWN
  	 * @property {number} UNKNOWN 0
  	 * @property {number} NORMAL 1
  	 * @property {number} CARDBOARD 2
  	 * @property {number} STEREO 3
  	 */
  	const MODES = { UNKNOWN: 0, NORMAL: 1, CARDBOARD: 2, STEREO: 3 };
  
  	/**
  	 * Data URI Images
  	 * @module DataImage
  	 * @example PANOLENS.DataImage.Info
  	 * @property {string} Info Information Icon
  	 * @property {string} Arrow Arrow Icon
  	 * @property {string} FullscreenEnter Fullscreen Enter Icon
  	 * @property {string} FullscreenLeave Fullscreen Leave Icon
  	 * @property {string} VideoPlay Video Play Icon
  	 * @property {string} VideoPause Video Pause Icon
  	 * @property {string} WhiteTile White Tile Icon
  	 * @property {string} Setting Settings Icon
  	 * @property {string} ChevronRight Chevron Right Icon
  	 * @property {string} Check Check Icon
  	 * @property {string} ViewIndicator View Indicator Icon
  	 */
  	const DataImage = {
  	    Info: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAABAAAAAQADq8/hgAAADBklEQVR42u2bP08UQRiHnzFaSYCI/xoksdBIqGwIiYWRUBISExpCQ0ej38FWOmlIKKhoMPEbaCxsrrHiYrQgOSlQEaICrT+LHSPZzNzt3s3c3Hn7lHvLzvv82L2dm30XKioqKgYY062BJF0HpoA7wARwBbhsPz4DjoEG8AnYNcZ8Sx1Op8IXJM1KWpdUV3nq9m9nJV1I7VNGfEzSM0mNNqR9NOwxx1L7NRMflbQm6SSgeJ4TO8Zoat+8/LKkg4jieQ4kLaf2RtKwpJ0uiufZkTScSn5S0l5C+b/sSZrstvyMpKPU5uc4kjTTjkvpeYCkaeA1/+7hvcIZMGuMqUULQNIU8Aa4ltrWwyHwyBizGzwASSPAe+B2assW7AH3jTE/i+xcZoa12Qfy2Bo3i+5cKABl99zF1GYlWFTBeULLS0DZrOsDcDNggTXgc27bLWA64BhfgHvGmB8dHUXZ1DM0S45xliKMs9bKr+klIOkqsBrwv9JtVq1DewEAT4Ch1BYdMGQdygeg7Df4SmqDAKyoyXpCszPgITCeuvoAjFuX0gE8jljUdv7bCtiOOJ7XpdUZ8L/gdXHOA5QtYH5NXXVgbrgWWn1nwFTqaiPgdPIFcDd1tRFwOl307DwRuZgXwLvctgfA04hjOp18AcReZ6sZY16e3yDpUuQxnU6+S2AkcjEpcDr1zxOXSPgCKLSa0mc4nXwB/EpdbQScTr4AGqmrjYDTyRfAx9TVRsDp5Aug8LJyH+F0cgZg58z11BUHpO5ruGh2G3ybuuqAeF2aBfAqddUB8bq0OgP2U1cegH3aOQOMMb+BrdTVB2DLupQLwLIOnKY26IBT6+ClaQDGmO/ARmqLDtiwDn7HVkcY+EdjNoTlCI+tYhO2iUppm6HKslPUq2qQKHpUe8AFsjaUXuUQWCgqXyoAG8IuME/WkNRrnAHzZfqDSgdgQ6gBc2Td3b3CMTBXtkOsIzTIjZLnQhjcVtlcEIPZLJ0LoVvt8s/Va+3yuSAG84UJRxB98cpM9dJURUVFxSDzBxKde4Lk3/h2AAAAAElFTkSuQmCC', 
  	    Arrow: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAABAAAAAQADq8/hgAAADPklEQVR42u2bMUscQRiG30/SRaJEI1ZKUiRErNIELRUbQYSAnX8hpVUgkDYp0wgWVjYW+QcJaQzYpLojJIXhtDDEKBpj65ti58ixmdmb2ZvZ7+T2AUHudmfmeXf2bnb3O6CmpqZmgJGqOiI5AWAWwEMA0wDuArht3r4CcAagBeAbgIaI/NQOp1fhIZKLJN+SbDKcptl3keSQtk+I+BjJVyRbJaRdtEybY9p+ReKjJN+QvIwonufS9DGq7ZuXXyd5nFA8zzHJdW1vkLxDcrdC8Ty7JO9oyc+QPFCUb3NAcqZq+TmSp9rmHZySnCvjErwOIPkUwHv8+w7vF64ALIrIfrIASM4C+ADgnratgxMACyLSiB4AyREAnwE80LbswgGAJyJy4bNxyApr6wbIw4xxy3djrwCYfeeuaZsFsEbPdULXU4DZqusLgMkEA21P05EEbf8A8FhEzos28pkBLxLKL5s/r/M1kEkz9vKQHGeatf05yfmOfubNa7G5JDle5NhtBjwHMBz5yFwAWBaRT+0XzP8pZsKwcQiH2fX8Ycojb+kzxUw4ZJn7CSQXqpRPHMKCq7+iZJ71Mvdy/DftXSQ6HcJdSDaqPPKW/mPOBO+lcbvzCU35RCFM2PpwnQKzZQfdgfe0dxH5dLA6uQJ4pC2fIASrkyuA6X6QjxyC1ckVQNn7bNHlI4ZgdXIFUObiJJl8pBCsTjGfuIwA2Cv4FN7xbYjkjqsRAHuIePXoCiDF1Zk2VidXAL+1R5sAq5MrgJb2aBNgdXIF8FV7tAmwOrkCCFs73wysTtYATHFCU3vEEWm6Ci6KvgY/ao86Ik6XogDeaY86Ik6XbjPgSHvkEThCwQy45XpDRK5JbgN4GWkgUyR9H65MRQxgW0SunZ5FezK7pfwd8e8MV8UfAPdF5Jdrg8JrAbPjprZFD2wWyQP6j8ZSEufRmGlgQ9umBBvd5IOgbjFUKLu+XnWBhG+rpsFVZGUo/coJgFVf+aAATAgNACvICpL6jSsAKyH1QcEBmBD2ASwhq+7uF84ALIVWiPUEB7lQsiOEwS2VzQUxmMXSuRCqKpd/zX4rl88FMZg/mLAEcSN+MlP/aKqmpqZmkPkL0hSjwOpNKxwAAAAASUVORK5CYII=',
  	    FullscreenEnter: 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4KICAgIDxwYXRoIGQ9Ik03IDE0SDV2NWg1di0ySDd2LTN6bS0yLTRoMlY3aDNWNUg1djV6bTEyIDdoLTN2Mmg1di01aC0ydjN6TTE0IDV2MmgzdjNoMlY1aC01eiIvPgo8L3N2Zz4=',
  	    FullscreenLeave: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggc3R5bGU9ImZpbGw6I2ZmZiIgZD0iTTE0LDE0SDE5VjE2SDE2VjE5SDE0VjE0TTUsMTRIMTBWMTlIOFYxNkg1VjE0TTgsNUgxMFYxMEg1VjhIOFY1TTE5LDhWMTBIMTRWNUgxNlY4SDE5WiIgLz48L3N2Zz4=',
  	    VideoPlay: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggc3R5bGU9ImZpbGw6I2ZmZiIgZD0iTTgsNS4xNFYxOS4xNEwxOSwxMi4xNEw4LDUuMTRaIiAvPjwvc3ZnPg==',
  	    VideoPause: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggc3R5bGU9ImZpbGw6I2ZmZiIgZD0iTTE0LDE5LjE0SDE4VjUuMTRIMTRNNiwxOS4xNEgxMFY1LjE0SDZWMTkuMTRaIiAvPjwvc3ZnPg==',
  	    WhiteTile: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIABAMAAAAGVsnJAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB1WlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjE8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24+MjwvdGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KAtiABQAAACRQTFRFAAAAAAAABgYGBwcHHh4eKysrx8fHy8vLzMzM7OzsAAAABgYG+q7SZgAAAAp0Uk5TAP7+/v7+/v7+/iJx/a8AAAOwSURBVHja7d0hbsNAEAVQo6SFI6XEcALDcgNLvUBvEBQVhpkWVYWlhSsVFS7t5QIshRt695lEASZP+8c7a1kzDL1fz+/zyuvzp6FbvoddrL6uDd1yGZ5eXldeb18N3fIx7A+58prmhm65DfvDcd0952lu6JabFbD/zVprZj1lzcys+fj9z8xTZtbT8rv8yWlu6BYAIgAAAAAAAAAAAABAM6QXEAEAAAAAAAAAgJ2gnaAIiIA3Q2qAGgAAAAAAAAAAAAAAAAAAAAAAAAAAQJsADkVFAAAAAAA8Bj0GRUAEREAEREAEREAEREAEAAAAAAAAAAB2gnaCIiACPplRA9QANUAERAAAAEVQERQBERCBVlfAcZ3aeZobusUKMGBhV6KUElHGKBERJR6/fxExRkQZl9/lT8S1oVsuhqyYMmPKjCkzvfcCpsxohrwY0Q06EAEAAAAAAAAAAACgGdILiAAAAAAAAAAAwE7QTlAERMCbITVADQAAAAAAAAAAAAAAAAAAAAAAAAAAwKmwQ1ERAAAAAACPQY9BERABERABERABERABERABAAAAAAAAAICdoJ2gCIiAT2bUADVADRABEQAAQBFUBEVABERgEyvAlJm+V4ApM6bMmDJjyowpM6bMdN0LmDKjGfJiRDfoQAQAAAAAAAAAAACAZkgvIAIAAAAAAAAAADtBO0EREAFvhtQANQAAAAAAAAAAAAAAAAAAAAAAAAAAAKfCDkVFAAAAAAA8Bj0GRUAEREAEREAEREAEREAEAAAAAAAAAAB2gnaCIiACPplRA9QANUAERAAAAEVQERQBERCBTawAU2b6XgGmzJgyY8qMKTOmzJgy03UvYMqMZsiLEd2gAxEAAAAAAAAAAAAAmiG9gAgAAAAAAAAAAOwE7QRFQAS8GVID1AAAAAAAAAAAAAAAAAAAAAAAAAAAAJwKOxQVAQAAAADwGPQYFAEREAEREAEREAEREAERAAAAAAAAAADYCdoJioAI+GRGDVAD1AAREAEAABRBRVAEREAENrECTJnpewWYMmPKjCkzpsyYMmPKTNe9gCkzmiEvRnSDDkQAAAAAAAAAAAAAaIb0AiIAAAAAAAAAALATtBMUARHwZkgNUAMAAAAAAAAAAAAAAAAAAAAAAAAAAHAq7FBUBAAAAADAY9BjUAREQAREQAREQAREQAREAAAAAAAAAABgJ2gnKAIi4JMZNUANUANEQAQAAFAEFUEREAER2MQKMGWm7xVgyowpM50PWen9ugNGXz1XaocAFgAAAABJRU5ErkJggg==',
  	    Setting: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAABAAAAAQADq8/hgAAADn0lEQVR42u2bzUsVURjGnyO6CPzAMnTjppAo3LTwH1CqTfaxbeOiRS37A0wXtROFVi1aRBs3LWohSIGbQAQXViBGRhG0UIRKUCpK7q/FnOB2uc6cOXNmRnGe3eW+H8/7zLln3vNxpQoVKlQ4wjBFJAFOSRqX1O7osivpvjHmU1nChBZglvSYLYJbS0EanCvIJzWK+gnsyH34/8OuMaYjb265jwCgz6N4SWq3vodbAEmnS/KtBDgoAgyU5BteAOAkMAPcBroc7PskDWfgN+wyDwBdltMMcDI3tYBnde/pHeARMNTErgd4APzweP834oeN1dMkz5DlsFNn/yyv4kdiSK4At4AO4CqwGaDwRmza2B0210qM7YhrXU59ANAq6bWkwQTTn5KO5fIE0uVYlXTeGLOXFMx1DrjlULwKKN41x6DlnIjEEQCckPRe0okCiguJr5LOGGO+xhm5jICJQ1i8LOeJJKPYEQAMKvrtt5ZdjSf2FM0Fq/sZJI2A6UNcvCz36TiDfUcAcE1SPu/U6Mm8k/TFfu6XdFb5iX3dGPM8lQfwNod3+TowBnQ3yddtv1vPIe+b1JIBiwEJ1IAJ208k5W21trWA+V/5CHAcmAtU/A2P/DcCiTAHHE8tgCVhgLvAXgYCk17Jo/yTGfLuWe7Zd72AC8CWB4n3OAz7mLytNkZabAEXMhfeQKYfWEpJZCxA3rGUOZeA/qDF15FpAz47EvlNk9neI2e3jeWCz0BbmvipNkSMMX8kuSZYM8Z8zyqAjbHmaN5mOeYjgIXrU93MWrxHrNQjrqiDkQMLHwG+OdqF3NN3jeXKzU8AoF1SzdH8XKhJUO7HZDXLMbwAwICkJUULFxe0SbqSVQAbw3Xi7Ze0ZLmGAzAKbHs0JGU1QtvAaIjCW4B7ZOvJy2qFa5a730RPtBiaz0CgnkiZi6F5fBZDVMvho7EhcuS3xJJ2hV9IupgTqaLw0hhzab8vq23xOG/r+LDsKjLgYVzxUnU0ltwK2wDezUyJmEwqXgp/PL4rvxthaeCSI+zxuA10J8ZkWdJNSb2SLkvayKHwDRu71+ZajrG941J8agALDQ3GU/a/IvMkYCPzmCbtLNEVmacNtgs5iP9fYVNEV1Q6Hez7yNZSL+J2SarTcpqiyV2iUkG0IvPFvbz5FbEn+KEk3wMjwMeSfCsBXFBdly9CAPk9ydyffpECuB5tZfVJjaKWueOSfinln6YK4lahQoUKRxd/AcRPGTcQCAUQAAAAAElFTkSuQmCC',
  	    ChevronRight: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTguNTksMTYuNThMMTMuMTcsMTJMOC41OSw3LjQxTDEwLDZMMTYsMTJMMTAsMThMOC41OSwxNi41OFoiIC8+PC9zdmc+',
  	    Check: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTIxLDdMOSwxOUwzLjUsMTMuNUw0LjkxLDEyLjA5TDksMTYuMTdMMTkuNTksNS41OUwyMSw3WiIgLz48L3N2Zz4=',
  	    ViewIndicator: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBpZD0idmlldy1pbmRpY2F0b3IiIGhlaWdodD0iMzAiIHdpZHRoPSIzMCIgdmlld0JveD0iLTIuNSAtMSAzMCAzMCI+Cgk8c3R5bGUgdHlwZT0idGV4dC9jc3MiPi5zdDB7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLW1pdGVybGltaXQ6MTA7ZmlsbDpub25lO30uc3Qxe3N0cm9rZS13aWR0aDo2O3N0cm9rZS1taXRlcmxpbWl0OjEwO30KCTwvc3R5bGU+Cgk8Zz4KCQk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNIDEyLjUgMCBBIDEyLjUgMTIuNSAwIDAgMCAtMTIuNSAwIEEgMTIuNSAxMi41IDAgMCAwIDEyLjUgMCIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwxMywxNS41KSI+PC9wYXRoPgoJCTxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0gMTMgMCBMIDEwIDIgTCAxNiAyIFoiPjwvcGF0aD4KCQk8cGF0aCBjbGFzcz0ic3QyIiBkPSJNIDIgMCBBIDIgMiAwIDAgMCAtMiAwIEEgMiAyIDAgMCAwIDIgMCIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwxMywxNS41KSI+PC9wYXRoPgoJCTxwYXRoIGNsYXNzPSJzdDEiIGlkPSJpbmRpY2F0b3IiIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMTMsMTUuNSkiPjwvcGF0aD4KCTwvZz4KPC9zdmc+'
  	};
  
  	/**
  	 * @module ImageLoader
  	 * @description Image loader with progress based on {@link https://github.com/mrdoob/three.js/blob/master/src/loaders/ImageLoader.js}
  	 */
  	const ImageLoader = {
  
  	    /**
  	     * Load image
  	     * @example PANOLENS.ImageLoader.load( IMAGE_URL )
  	     * @method load
  	     * @param  {string}   url        - An image url
  	     * @param  {function} onLoad     - On load callback
  	     * @param  {function} onProgress - In progress callback
  	     * @param  {function} onError    - On error callback
  	     */
  	    load: function ( url, onLoad = () => {}, onProgress = () => {}, onError = () => {} ) {
  
  	        // Enable cache
  	        THREE.Cache.enabled = true;
  
  	        let cached, request, arrayBufferView, blob, urlCreator, image, reference;
  		
  	        // Reference key
  	        for ( let iconName in DataImage ) {
  		
  	            if ( DataImage.hasOwnProperty( iconName ) && url === DataImage[ iconName ] ) {
  		
  	                reference = iconName;
  		
  	            }
  		
  	        }
  		
  	        // Cached
  	        cached = THREE.Cache.get( reference ? reference : url );
  		
  	        if ( cached !== undefined ) {
  		
  	            if ( onLoad ) {
  		
  	                setTimeout( function () {
  		
  	                    onProgress( { loaded: 1, total: 1 } );
  	                    onLoad( cached );
  		
  	                }, 0 );
  		
  	            }
  		
  	            return cached;
  		
  	        }
  			
  	        // Construct a new XMLHttpRequest
  	        urlCreator = window.URL || window.webkitURL;
  	        image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );
  		
  	        // Add to cache
  	        THREE.Cache.add( reference ? reference : url, image );
  		
  	        const onImageLoaded = () => {
  		
  	            urlCreator.revokeObjectURL( image.src );
  	            onLoad( image );
  		
  	        };
  
  	        if ( url.indexOf( 'data:' ) === 0 ) {
  
  	            image.addEventListener( 'load', onImageLoaded, false );
  	            image.src = url;
  	            return image;
  	        }
  		
  	        image.crossOrigin = this.crossOrigin !== undefined ? this.crossOrigin : '';
  		
  	        request = new window.XMLHttpRequest();
  	        request.open( 'GET', url, true );
  	        request.responseType = 'arraybuffer';
  	        request.addEventListener( 'error', onError );
  	        request.addEventListener( 'progress', event => {
  
  	            if  ( !event ) return;
  
  	            const { loaded, total, lengthComputable } = event;
  	            
  	            if ( lengthComputable ) {
  		
  	                onProgress( { loaded, total } );
  		
  	            }
  		
  	        } );
  	        
  	        request.addEventListener( 'loadend', event => {
  
  	            if  ( !event ) return;
  	            const { currentTarget: { response } } = event;
  
  	            arrayBufferView = new Uint8Array( response );
  	            blob = new window.Blob( [ arrayBufferView ] );
  					
  	            image.addEventListener( 'load', onImageLoaded, false );
  	            image.src = urlCreator.createObjectURL( blob );
  		
  	        } );
  		
  	        request.send(null);
  		
  	    }
  
  	};
  
  	/**
  	 * @module TextureLoader
  	 * @description Texture loader based on {@link https://github.com/mrdoob/three.js/blob/master/src/loaders/TextureLoader.js}
  	 */
  	const TextureLoader = {
  
  	    /**
  	     * Load image texture
  	     * @example PANOLENS.TextureLoader.load( IMAGE_URL )
  	     * @method load
  	     * @param  {string}   url        - An image url
  	     * @param  {function} onLoad     - On load callback
  	     * @param  {function} onProgress - In progress callback
  	     * @param  {function} onError    - On error callback
  	     * @return {THREE.Texture}   	 - Image texture
  	     */
  	    load: function ( url, onLoad = () => {}, onProgress, onError ) {
  
  	        var texture = new THREE.Texture(); 
  
  	        ImageLoader.load( url, function ( image ) {
  
  	            texture.image = image;
  
  	            // JPEGs can't have an alpha channel, so memory can be saved by storing them as RGB.
  	            const isJPEG = url.search( /\.(jpg|jpeg)$/ ) > 0 || url.search( /^data\:image\/jpeg/ ) === 0;
  
  	            texture.format = isJPEG ? THREE.RGBFormat : THREE.RGBAFormat;
  	            texture.needsUpdate = true;
  
  	            onLoad( texture );
  
  	        }, onProgress, onError );
  
  	        return texture;
  
  	    }
  
  	};
  
  	/**
  	 * @module CubeTextureLoader
  	 * @description Cube Texture Loader based on {@link https://github.com/mrdoob/three.js/blob/master/src/loaders/CubeTextureLoader.js}
  	 */
  	const CubeTextureLoader = {
  
  	    /**
  	     * Load 6 images as a cube texture
  	     * @example PANOLENS.CubeTextureLoader.load( [ 'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png' ] )
  	     * @method load
  	     * @param  {array}   urls        - array of 6 urls to images, one for each side of the CubeTexture. The urls should be specified in the following order: pos-x, neg-x, pos-y, neg-y, pos-z, neg-z
  	     * @param  {function} onLoad     - On load callback
  	     * @param  {function} onProgress - In progress callback
  	     * @param  {function} onError    - On error callback
  	     * @return {THREE.CubeTexture}   - Cube texture
  	     */
  	    load: function ( urls, onLoad = () => {}, onProgress = () => {}, onError ) {
  
  		   var texture, loaded, progress, all, loadings;
  
  		   texture = new THREE.CubeTexture( [] );
  
  		   loaded = 0;
  		   progress = {};
  		   all = {};
  
  		   urls.map( function ( url, index ) {
  
  			   ImageLoader.load( url, function ( image ) {
  
  				   texture.images[ index ] = image;
  
  				   loaded++;
  
  				   if ( loaded === 6 ) {
  
  					   texture.needsUpdate = true;
  
  					   onLoad( texture );
  
  				   }
  
  			   }, function ( event ) {
  
  				   progress[ index ] = { loaded: event.loaded, total: event.total };
  
  				   all.loaded = 0;
  				   all.total = 0;
  				   loadings = 0;
  
  				   for ( var i in progress ) {
  
  					   loadings++;
  					   all.loaded += progress[ i ].loaded;
  					   all.total += progress[ i ].total;
  
  				   }
  
  				   if ( loadings < 6 ) {
  
  					   all.total = all.total / loadings * 6;
  
  				   }
  
  				   onProgress( all );
  
  			   }, onError );
  
  		   } );
  
  		   return texture;
  
  	    }
  
  	};
  
  	/**
  	 * @classdesc User Media
  	 * @constructor
  	 * @param {object} [constraints={ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, facingMode: { exact: 'environment' } }, audio: false }]
  	 */
  	function Media ( constraints ) {
  
  	    const defaultConstraints = { video: { width: { ideal: 1920 }, height: { ideal: 1080 }, facingMode: { exact: 'environment' } }, audio: false };
  
  	    this.constraints = Object.assign( defaultConstraints, constraints );
  
  	    this.container = null;
  	    this.scene = null;
  	    this.element = null;
  	    this.devices = [];
  	    this.stream = null;
  	    this.ratioScalar = 1;
  	    this.videoDeviceIndex = 0;
  
  	}
  	Media.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype ), {
  
  	    setContainer: function ( container ) {
  
  	        this.container = container;
  
  	    },
  
  	    setScene: function ( scene ) {
  
  	        this.scene = scene;
  
  	    },
  
  	    /**
  	     * Enumerate devices
  	     * @memberOf Media
  	     * @instance
  	     * @returns {Promise}
  	     */
  	    enumerateDevices: function () {
  
  	        const devices = this.devices;
  	        const resolvedPromise = new Promise( resolve => { resolve( devices ); } );
  
  	        return devices.length > 0 ? resolvedPromise : window.navigator.mediaDevices.enumerateDevices();
  
  	    },
  
  	    /**
  	     * Switch to next available video device
  	     * @memberOf Media
  	     * @instance
  	     */
  	    switchNextVideoDevice: function () {
  
  	        const stop = this.stop.bind( this );
  	        const start = this.start.bind( this );
  	        const setVideDeviceIndex = this.setVideDeviceIndex.bind( this );
  
  	        let index = this.videoDeviceIndex;
  
  	        this.getDevices( 'video' )
  	            .then( devices => {
  	                stop();
  	                index++;
  	                if ( index >= devices.length ) {
  	                    setVideDeviceIndex( 0 );
  	                    index--;
  	                } else {
  	                    setVideDeviceIndex( index );
  	                }
  
  	                start( devices[ index ] );
  	            
  
  	            } );
  
  	    },
  
  	    /**
  	     * Get devices
  	     * @param {string} type - type keyword to match device.kind
  	     * @memberOf Media
  	     * @instance
  	     */
  	    getDevices: function ( type = 'video' ) {
  
  	        const devices = this.devices;
  	        const validate = _devices => {
  
  	            return _devices.map( device => { 
  	                
  	                if ( !devices.includes( device ) ) { devices.push( device ); }
  	                return device; 
  	            
  	            } );
  	            
  	        };
  	        const filter = _devices => {
  
  	            const reg = new RegExp( type, 'i' );
  	            return _devices.filter( device => reg.test( device.kind ) );
  
  	        };
  
  	        return this.enumerateDevices()
  	            .then( validate )
  	            .then( filter );
  
  	    },
  
  	    /**
  	     * Get user media
  	     * @param {MediaStreamConstraints} constraints
  	     * @memberOf Media
  	     * @instance
  	     */
  	    getUserMedia: function ( constraints ) {
  
  	        const setMediaStream = this.setMediaStream.bind( this );
  	        const playVideo = this.playVideo.bind( this );
  	        const onCatchError = error => { console.warn( `PANOLENS.Media: ${error}` ); };
  
  	        return window.navigator.mediaDevices.getUserMedia( constraints )
  	            .then( setMediaStream )
  	            .then( playVideo )
  	            .catch( onCatchError );
  
  	    },
  
  	    /**
  	     * Set video device index
  	     * @param {number} index 
  	     * @memberOf Media
  	     * @instance
  	     */
  	    setVideDeviceIndex: function ( index ) {
  
  	        this.videoDeviceIndex = index;
  
  	    },
  
  	    /**
  	     * Start streaming
  	     * @param {MediaDeviceInfo} [targetDevice]
  	     * @memberOf Media
  	     * @instance
  	     */
  	    start: function( targetDevice ) {
  
  	        const constraints = this.constraints;
  	        const getUserMedia = this.getUserMedia.bind( this );
  	        const onVideoDevices = devices => {
  
  	            if ( !devices || devices.length === 0 ) {
  
  	                throw Error( 'no video device found' );
  
  	            }
  
  	            const device = targetDevice || devices[ 0 ];
  	            constraints.video.deviceId = device.deviceId;
  
  	            return getUserMedia( constraints );
  
  	        };
  
  	        this.element = this.createVideoElement();
  
  	        return this.getDevices().then( onVideoDevices );
  
  	    },
  
  	    /**
  	     * Stop streaming
  	     * @memberOf Media
  	     * @instance
  	     */
  	    stop: function () {
  
  	        const stream = this.stream;
  
  	        if ( stream && stream.active ) {
  
  	            const track = stream.getTracks()[ 0 ];
  
  	            track.stop();
  
  	            window.removeEventListener( 'resize', this.onWindowResize.bind( this ) );
  
  	            this.element = null;
  	            this.stream = null;
  
  	        }
  
  	    },
  
  	    /**
  	     * Set media stream
  	     * @param {MediaStream} stream 
  	     * @memberOf Media
  	     * @instance
  	     */
  	    setMediaStream: function ( stream ) {
  
  	        this.stream = stream;
  	        this.element.srcObject = stream;
  
  	        if ( this.scene ) {
  
  	            this.scene.background = this.createVideoTexture();
  
  	        }
  	        
  	        window.addEventListener( 'resize', this.onWindowResize.bind( this ) );
  
  	    },
  
  	    /**
  	     * Play video element
  	     * @memberOf Media
  	     * @instance
  	     */
  	    playVideo: function () {
  
  	        const { element } = this;
  
  	        if ( element ) {
  
  	            element.play();
  	            this.dispatchEvent( { type: 'play' } );
  
  	        }
  
  	    },
  
  	    /**
  	     * Pause video element
  	     * @memberOf Media
  	     * @instance
  	     */
  	    pauseVideo: function () {
  
  	        const { element } = this;
  
  	        if ( element ) {
  
  	            element.pause();
  	            this.dispatchEvent( { type: 'pause' } );
  
  	        }
  
  	    },
  
  	    /**
  	     * Create video texture
  	     * @memberOf Media
  	     * @instance
  	     * @returns {THREE.VideoTexture}
  	     */
  	    createVideoTexture: function () {
  
  	        const video = this.element;
  	        const texture = new THREE.VideoTexture( video );
  
  	        texture.generateMipmaps = false;
  	        texture.minFilter = THREE.LinearFilter;
  	        texture.magFilter = THREE.LinearFilter;
  	        texture.format = THREE.RGBFormat;
  	        texture.center.set( 0.5, 0.5 );
  
  	        video.addEventListener( 'canplay', this.onWindowResize.bind( this ) );
  
  	        return texture;
  
  	    },
  
  	    /**
  	     * Create video element
  	     * @memberOf Media
  	     * @instance
  	     * @returns {HTMLVideoElement}
  	     * @fires Media#canplay
  	     */
  	    createVideoElement: function() {
  
  	        const dispatchEvent = this.dispatchEvent.bind( this );
  	        const video = document.createElement( 'video' );
  
  	        /**
  	         * Video can play event
  	         * @type {object}
  	         * @event Media#canplay
  	         */
  	        const canPlay = () => dispatchEvent( { type: 'canplay' } );
  	        
  	        video.setAttribute( 'autoplay', '' );
  	        video.setAttribute( 'muted', '' );
  	        video.setAttribute( 'playsinline', '' );
  
  	        video.style.position = 'absolute';
  	        video.style.top = '0';
  	        video.style.left = '0';
  	        video.style.width = '100%';
  	        video.style.height = '100%';
  	        video.style.objectPosition = 'center';
  	        video.style.objectFit = 'cover';
  	        video.style.display = this.scene ? 'none' : '';
  
  	        video.addEventListener( 'canplay', canPlay );
  
  	        return video;
  
  	    },
  
  	    /**
  	     * On window resize event
  	     * @param {Event} event 
  	     * @memberOf Media
  	     * @instance
  	     */
  	    onWindowResize: function () {
  
  	        if ( this.element && this.element.videoWidth && this.element.videoHeight && this.scene ) {
  
  	            const { clientWidth: width, clientHeight: height } = this.container;
  	            const texture = this.scene.background;
  	            const { videoWidth, videoHeight } = this.element;
  	            const cameraRatio = videoHeight / videoWidth;
  	            const viewportRatio = this.container ? width / height : 1.0;
  	            const ratio = cameraRatio * viewportRatio * this.ratioScalar;
  
  	            if ( width > height ) {
  	                texture.repeat.set( ratio, 1 );
  	            } else {
  	                texture.repeat.set( 1, 1 / ratio );
  	            }
  
  	        }
  
  	    }
  
  	} );
  
  	/**
  	 * @classdesc Reticle 3D Sprite
  	 * @constructor
  	 * @param {THREE.Color} [color=0xffffff] - Color of the reticle sprite
  	 * @param {boolean} [autoSelect=true] - Auto selection
  	 * @param {number} [dwellTime=1500] - Duration for dwelling sequence to complete
  	 */
  
  	function Reticle ( color = 0xffffff, autoSelect = true, dwellTime = 1500 ) {
  
  	    this.dpr = window.devicePixelRatio;
  
  	    const { canvas, context } = this.createCanvas();
  	    const material = new THREE.SpriteMaterial( { color, map: this.createCanvasTexture( canvas ) } );
  
  	    THREE.Sprite.call( this, material );
  
  	    this.canvasWidth = canvas.width;
  	    this.canvasHeight = canvas.height;
  	    this.context = context;
  	    this.color = color instanceof THREE.Color ? color : new THREE.Color( color );    
  
  	    this.autoSelect = autoSelect;
  	    this.dwellTime = dwellTime;
  	    this.rippleDuration = 500;
  	    this.position.z = -10;
  	    this.center.set( 0.5, 0.5 );
  	    this.scale.set( 0.5, 0.5, 1 );
  
  	    this.startTimestamp = null;
  	    this.timerId = null;
  	    this.callback = null;
  
  	    this.frustumCulled = false;
  
  	    this.updateCanvasArcByProgress( 0 );
  
  	}
  	Reticle.prototype = Object.assign( Object.create( THREE.Sprite.prototype ), {
  
  	    constructor: Reticle,
  
  	    /**
  	     * Set material color
  	     * @param {THREE.Color} color 
  	     * @memberOf Reticle
  	     * @instance
  	     */
  	    setColor: function ( color ) {
  
  	        this.material.color.copy( color instanceof THREE.Color ? color : new THREE.Color( color ) );
  
  	    },
  
  	    /**
  	     * Create canvas texture
  	     * @param {HTMLCanvasElement} canvas 
  	     * @memberOf Reticle
  	     * @instance
  	     * @returns {THREE.CanvasTexture}
  	     */
  	    createCanvasTexture: function ( canvas ) {
  
  	        const texture = new THREE.CanvasTexture( canvas );
  	        texture.minFilter = THREE.LinearFilter;
  	        texture.magFilter = THREE.LinearFilter;
  	        texture.generateMipmaps = false;
  
  	        return texture;
  
  	    },
  
  	    /**
  	     * Create canvas element
  	     * @memberOf Reticle
  	     * @instance
  	     * @returns {object} object
  	     * @returns {HTMLCanvasElement} object.canvas
  	     * @returns {CanvasRenderingContext2D} object.context
  	     */
  	    createCanvas: function () {
  
  	        const width = 32;
  	        const height = 32;
  	        const canvas = document.createElement( 'canvas' );
  	        const context = canvas.getContext( '2d' );
  	        const dpr = this.dpr;
  
  	        canvas.width = width * dpr;
  	        canvas.height = height * dpr;
  	        context.scale( dpr, dpr );
  
  	        context.shadowBlur = 5;
  	        context.shadowColor = 'rgba(200,200,200,0.9)';
  
  	        return { canvas, context };
  
  	    },
  
  	    /**
  	     * Update canvas arc by progress
  	     * @param {number} progress 
  	     * @memberOf Reticle
  	     * @instance
  	     */
  	    updateCanvasArcByProgress: function ( progress ) {
  
  	        const context = this.context;
  	        const { canvasWidth, canvasHeight, material } = this;
  	        const dpr = this.dpr;
  	        const degree = progress * Math.PI * 2;
  	        const color = this.color.getStyle();
  	        const x = canvasWidth * 0.5 / dpr;
  	        const y = canvasHeight * 0.5 / dpr;
  	        const lineWidth = 3;
  	        
  	        context.clearRect( 0, 0, canvasWidth, canvasHeight );
  	        context.beginPath();
  
  	        if ( progress === 0 ) {
  	            context.arc( x, y, canvasWidth / 16, 0, 2 * Math.PI );
  	            context.fillStyle = color;
  	            context.fill();
  	        } else {
  	            context.arc( x, y, canvasWidth / 4 - lineWidth, -Math.PI / 2, -Math.PI / 2 + degree );
  	            context.strokeStyle = color;
  	            context.lineWidth = lineWidth;
  	            context.stroke();
  	        }
  
  	        context.closePath();
  
  	        material.map.needsUpdate = true;
  
  	    },
  
  	    /**
  	     * Ripple effect
  	     * @memberOf Reticle
  	     * @instance
  	     * @fires Reticle#reticle-ripple-start
  	     * @fires Reticle#reticle-ripple-end
  	     */
  	    ripple: function () {
  
  	        const context = this.context;
  	        const { canvasWidth, canvasHeight, material } = this;
  	        const duration = this.rippleDuration;
  	        const timestamp = performance.now();
  	        const color = this.color;
  	        const dpr = this.dpr;
  	        const x = canvasWidth * 0.5 / dpr;
  	        const y = canvasHeight * 0.5 / dpr;
  
  	        const update = () => {
  
  	            const timerId = window.requestAnimationFrame( update );
  	            const elapsed = performance.now() - timestamp;
  	            const progress = elapsed / duration;
  	            const opacity = 1.0 - progress > 0 ? 1.0 - progress : 0;
  	            const radius = progress * canvasWidth * 0.5 / dpr;
  
  	            context.clearRect( 0, 0, canvasWidth, canvasHeight );
  	            context.beginPath();
  	            context.arc( x, y, radius, 0, Math.PI * 2 );
  	            context.fillStyle = `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${opacity})`;
  	            context.fill();
  	            context.closePath();
  
  	            if ( progress >= 1.0 ) {
  
  	                window.cancelAnimationFrame( timerId );
  	                this.updateCanvasArcByProgress( 0 );
  
  	                /**
  	                 * Reticle ripple end event
  	                 * @type {object}
  	                 * @event Reticle#reticle-ripple-end
  	                 */
  	                this.dispatchEvent( { type: 'reticle-ripple-end' } );
  
  	            }
  
  	            material.map.needsUpdate = true;
  
  	        };
  
  	        /**
  	         * Reticle ripple start event
  	         * @type {object}
  	         * @event Reticle#reticle-ripple-start
  	         */
  	        this.dispatchEvent( { type: 'reticle-ripple-start' } );
  
  	        update();
  
  	    },
  
  	    /**
  	     * Make reticle visible
  	     * @memberOf Reticle
  	     * @instance
  	     */
  	    show: function () {
  
  	        this.visible = true;
  
  	    },
  
  	    /**
  	     * Make reticle invisible
  	     * @memberOf Reticle
  	     * @instance
  	     */
  	    hide: function () {
  
  	        this.visible = false;
  
  	    },
  
  	    /**
  	     * Start dwelling
  	     * @param {function} callback 
  	     * @memberOf Reticle
  	     * @instance
  	     * @fires Reticle#reticle-start
  	     */
  	    start: function ( callback ) {
  
  	        if ( !this.autoSelect ) {
  
  	            return;
  
  	        }
  
  	        /**
  	         * Reticle start event
  	         * @type {object}
  	         * @event Reticle#reticle-start
  	         */
  	        this.dispatchEvent( { type: 'reticle-start' } );
  
  	        this.startTimestamp = performance.now();
  	        this.callback = callback;
  	        this.update();
  
  	    },
  
  	    /**
  	     * End dwelling
  	     * @memberOf Reticle
  	     * @instance
  	     * @fires Reticle#reticle-end
  	     */
  	    end: function(){
  
  	        if ( !this.startTimestamp ) { return; }
  
  	        window.cancelAnimationFrame( this.timerId );
  
  	        this.updateCanvasArcByProgress( 0 );
  	        this.callback = null;
  	        this.timerId = null;
  	        this.startTimestamp = null;
  
  	        /**
  	         * Reticle end event
  	         * @type {object}
  	         * @event Reticle#reticle-end
  	         */
  	        this.dispatchEvent( { type: 'reticle-end' } );
  
  	    },
  
  	    /**
  	     * Update dwelling
  	     * @memberOf Reticle
  	     * @instance
  	     * @fires Reticle#reticle-update
  	     */
  	    update: function () {
  
  	        this.timerId = window.requestAnimationFrame( this.update.bind( this ) );
  
  	        const elapsed = performance.now() - this.startTimestamp;
  	        const progress = elapsed / this.dwellTime;
  
  	        this.updateCanvasArcByProgress( progress );
  
  	        /**
  	         * Reticle update event
  	         * @type {object}
  	         * @event Reticle#reticle-update
  	         */
  	        this.dispatchEvent( { type: 'reticle-update', progress } );
  
  	        if ( progress >= 1.0 ) {
  
  	            window.cancelAnimationFrame( this.timerId );
  	            if ( this.callback ) { this.callback(); }
  	            this.end();
  	            this.ripple();
  
  	        }
  
  	    }
  
  	} );
  
  	function createCommonjsModule(fn, module) {
  		return module = { exports: {} }, fn(module, module.exports), module.exports;
  	}
  
  	var Tween = createCommonjsModule(function (module, exports) {
  	/**
  	 * Tween.js - Licensed under the MIT license
  	 * https://github.com/tweenjs/tween.js
  	 * ----------------------------------------------
  	 *
  	 * See https://github.com/tweenjs/tween.js/graphs/contributors for the full list of contributors.
  	 * Thank you all, you're awesome!
  	 */
  
  
  	var _Group = function () {
  		this._tweens = {};
  		this._tweensAddedDuringUpdate = {};
  	};
  
  	_Group.prototype = {
  		getAll: function () {
  
  			return Object.keys(this._tweens).map(function (tweenId) {
  				return this._tweens[tweenId];
  			}.bind(this));
  
  		},
  
  		removeAll: function () {
  
  			this._tweens = {};
  
  		},
  
  		add: function (tween) {
  
  			this._tweens[tween.getId()] = tween;
  			this._tweensAddedDuringUpdate[tween.getId()] = tween;
  
  		},
  
  		remove: function (tween) {
  
  			delete this._tweens[tween.getId()];
  			delete this._tweensAddedDuringUpdate[tween.getId()];
  
  		},
  
  		update: function (time, preserve) {
  
  			var tweenIds = Object.keys(this._tweens);
  
  			if (tweenIds.length === 0) {
  				return false;
  			}
  
  			time = time !== undefined ? time : TWEEN.now();
  
  			// Tweens are updated in "batches". If you add a new tween during an update, then the
  			// new tween will be updated in the next batch.
  			// If you remove a tween during an update, it may or may not be updated. However,
  			// if the removed tween was added during the current batch, then it will not be updated.
  			while (tweenIds.length > 0) {
  				this._tweensAddedDuringUpdate = {};
  
  				for (var i = 0; i < tweenIds.length; i++) {
  
  					var tween = this._tweens[tweenIds[i]];
  
  					if (tween && tween.update(time) === false) {
  						tween._isPlaying = false;
  
  						if (!preserve) {
  							delete this._tweens[tweenIds[i]];
  						}
  					}
  				}
  
  				tweenIds = Object.keys(this._tweensAddedDuringUpdate);
  			}
  
  			return true;
  
  		}
  	};
  
  	var TWEEN = new _Group();
  
  	TWEEN.Group = _Group;
  	TWEEN._nextId = 0;
  	TWEEN.nextId = function () {
  		return TWEEN._nextId++;
  	};
  
  
  	// Include a performance.now polyfill.
  	// In node.js, use process.hrtime.
  	if (typeof (self) === 'undefined' && typeof (process) !== 'undefined' && process.hrtime) {
  		TWEEN.now = function () {
  			var time = process.hrtime();
  
  			// Convert [seconds, nanoseconds] to milliseconds.
  			return time[0] * 1000 + time[1] / 1000000;
  		};
  	}
  	// In a browser, use self.performance.now if it is available.
  	else if (typeof (self) !== 'undefined' &&
  	         self.performance !== undefined &&
  			 self.performance.now !== undefined) {
  		// This must be bound, because directly assigning this function
  		// leads to an invocation exception in Chrome.
  		TWEEN.now = self.performance.now.bind(self.performance);
  	}
  	// Use Date.now if it is available.
  	else if (Date.now !== undefined) {
  		TWEEN.now = Date.now;
  	}
  	// Otherwise, use 'new Date().getTime()'.
  	else {
  		TWEEN.now = function () {
  			return new Date().getTime();
  		};
  	}
  
  
  	TWEEN.Tween = function (object, group) {
  		this._object = object;
  		this._valuesStart = {};
  		this._valuesEnd = {};
  		this._valuesStartRepeat = {};
  		this._duration = 1000;
  		this._repeat = 0;
  		this._repeatDelayTime = undefined;
  		this._yoyo = false;
  		this._isPlaying = false;
  		this._reversed = false;
  		this._delayTime = 0;
  		this._startTime = null;
  		this._easingFunction = TWEEN.Easing.Linear.None;
  		this._interpolationFunction = TWEEN.Interpolation.Linear;
  		this._chainedTweens = [];
  		this._onStartCallback = null;
  		this._onStartCallbackFired = false;
  		this._onUpdateCallback = null;
  		this._onRepeatCallback = null;
  		this._onCompleteCallback = null;
  		this._onStopCallback = null;
  		this._group = group || TWEEN;
  		this._id = TWEEN.nextId();
  
  	};
  
  	TWEEN.Tween.prototype = {
  		getId: function () {
  			return this._id;
  		},
  
  		isPlaying: function () {
  			return this._isPlaying;
  		},
  
  		to: function (properties, duration) {
  
  			this._valuesEnd = Object.create(properties);
  
  			if (duration !== undefined) {
  				this._duration = duration;
  			}
  
  			return this;
  
  		},
  
  		duration: function duration(d) {
  			this._duration = d;
  			return this;
  		},
  
  		start: function (time) {
  
  			this._group.add(this);
  
  			this._isPlaying = true;
  
  			this._onStartCallbackFired = false;
  
  			this._startTime = time !== undefined ? typeof time === 'string' ? TWEEN.now() + parseFloat(time) : time : TWEEN.now();
  			this._startTime += this._delayTime;
  
  			for (var property in this._valuesEnd) {
  
  				// Check if an Array was provided as property value
  				if (this._valuesEnd[property] instanceof Array) {
  
  					if (this._valuesEnd[property].length === 0) {
  						continue;
  					}
  
  					// Create a local copy of the Array with the start value at the front
  					this._valuesEnd[property] = [this._object[property]].concat(this._valuesEnd[property]);
  
  				}
  
  				// If `to()` specifies a property that doesn't exist in the source object,
  				// we should not set that property in the object
  				if (this._object[property] === undefined) {
  					continue;
  				}
  
  				// Save the starting value.
  				this._valuesStart[property] = this._object[property];
  
  				if ((this._valuesStart[property] instanceof Array) === false) {
  					this._valuesStart[property] *= 1.0; // Ensures we're using numbers, not strings
  				}
  
  				this._valuesStartRepeat[property] = this._valuesStart[property] || 0;
  
  			}
  
  			return this;
  
  		},
  
  		stop: function () {
  
  			if (!this._isPlaying) {
  				return this;
  			}
  
  			this._group.remove(this);
  			this._isPlaying = false;
  
  			if (this._onStopCallback !== null) {
  				this._onStopCallback(this._object);
  			}
  
  			this.stopChainedTweens();
  			return this;
  
  		},
  
  		end: function () {
  
  			this.update(Infinity);
  			return this;
  
  		},
  
  		stopChainedTweens: function () {
  
  			for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
  				this._chainedTweens[i].stop();
  			}
  
  		},
  
  		group: function (group) {
  			this._group = group;
  			return this;
  		},
  
  		delay: function (amount) {
  
  			this._delayTime = amount;
  			return this;
  
  		},
  
  		repeat: function (times) {
  
  			this._repeat = times;
  			return this;
  
  		},
  
  		repeatDelay: function (amount) {
  
  			this._repeatDelayTime = amount;
  			return this;
  
  		},
  
  		yoyo: function (yoyo) {
  
  			this._yoyo = yoyo;
  			return this;
  
  		},
  
  		easing: function (easingFunction) {
  
  			this._easingFunction = easingFunction;
  			return this;
  
  		},
  
  		interpolation: function (interpolationFunction) {
  
  			this._interpolationFunction = interpolationFunction;
  			return this;
  
  		},
  
  		chain: function () {
  
  			this._chainedTweens = arguments;
  			return this;
  
  		},
  
  		onStart: function (callback) {
  
  			this._onStartCallback = callback;
  			return this;
  
  		},
  
  		onUpdate: function (callback) {
  
  			this._onUpdateCallback = callback;
  			return this;
  
  		},
  
  		onRepeat: function onRepeat(callback) {
  
  			this._onRepeatCallback = callback;
  			return this;
  
  		},
  
  		onComplete: function (callback) {
  
  			this._onCompleteCallback = callback;
  			return this;
  
  		},
  
  		onStop: function (callback) {
  
  			this._onStopCallback = callback;
  			return this;
  
  		},
  
  		update: function (time) {
  
  			var property;
  			var elapsed;
  			var value;
  
  			if (time < this._startTime) {
  				return true;
  			}
  
  			if (this._onStartCallbackFired === false) {
  
  				if (this._onStartCallback !== null) {
  					this._onStartCallback(this._object);
  				}
  
  				this._onStartCallbackFired = true;
  			}
  
  			elapsed = (time - this._startTime) / this._duration;
  			elapsed = (this._duration === 0 || elapsed > 1) ? 1 : elapsed;
  
  			value = this._easingFunction(elapsed);
  
  			for (property in this._valuesEnd) {
  
  				// Don't update properties that do not exist in the source object
  				if (this._valuesStart[property] === undefined) {
  					continue;
  				}
  
  				var start = this._valuesStart[property] || 0;
  				var end = this._valuesEnd[property];
  
  				if (end instanceof Array) {
  
  					this._object[property] = this._interpolationFunction(end, value);
  
  				} else {
  
  					// Parses relative end values with start as base (e.g.: +10, -3)
  					if (typeof (end) === 'string') {
  
  						if (end.charAt(0) === '+' || end.charAt(0) === '-') {
  							end = start + parseFloat(end);
  						} else {
  							end = parseFloat(end);
  						}
  					}
  
  					// Protect against non numeric properties.
  					if (typeof (end) === 'number') {
  						this._object[property] = start + (end - start) * value;
  					}
  
  				}
  
  			}
  
  			if (this._onUpdateCallback !== null) {
  				this._onUpdateCallback(this._object, elapsed);
  			}
  
  			if (elapsed === 1) {
  
  				if (this._repeat > 0) {
  
  					if (isFinite(this._repeat)) {
  						this._repeat--;
  					}
  
  					// Reassign starting values, restart by making startTime = now
  					for (property in this._valuesStartRepeat) {
  
  						if (typeof (this._valuesEnd[property]) === 'string') {
  							this._valuesStartRepeat[property] = this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property]);
  						}
  
  						if (this._yoyo) {
  							var tmp = this._valuesStartRepeat[property];
  
  							this._valuesStartRepeat[property] = this._valuesEnd[property];
  							this._valuesEnd[property] = tmp;
  						}
  
  						this._valuesStart[property] = this._valuesStartRepeat[property];
  
  					}
  
  					if (this._yoyo) {
  						this._reversed = !this._reversed;
  					}
  
  					if (this._repeatDelayTime !== undefined) {
  						this._startTime = time + this._repeatDelayTime;
  					} else {
  						this._startTime = time + this._delayTime;
  					}
  
  					if (this._onRepeatCallback !== null) {
  						this._onRepeatCallback(this._object);
  					}
  
  					return true;
  
  				} else {
  
  					if (this._onCompleteCallback !== null) {
  
  						this._onCompleteCallback(this._object);
  					}
  
  					for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
  						// Make the chained tweens start exactly at the time they should,
  						// even if the `update()` method was called way past the duration of the tween
  						this._chainedTweens[i].start(this._startTime + this._duration);
  					}
  
  					return false;
  
  				}
  
  			}
  
  			return true;
  
  		}
  	};
  
  
  	TWEEN.Easing = {
  
  		Linear: {
  
  			None: function (k) {
  
  				return k;
  
  			}
  
  		},
  
  		Quadratic: {
  
  			In: function (k) {
  
  				return k * k;
  
  			},
  
  			Out: function (k) {
  
  				return k * (2 - k);
  
  			},
  
  			InOut: function (k) {
  
  				if ((k *= 2) < 1) {
  					return 0.5 * k * k;
  				}
  
  				return - 0.5 * (--k * (k - 2) - 1);
  
  			}
  
  		},
  
  		Cubic: {
  
  			In: function (k) {
  
  				return k * k * k;
  
  			},
  
  			Out: function (k) {
  
  				return --k * k * k + 1;
  
  			},
  
  			InOut: function (k) {
  
  				if ((k *= 2) < 1) {
  					return 0.5 * k * k * k;
  				}
  
  				return 0.5 * ((k -= 2) * k * k + 2);
  
  			}
  
  		},
  
  		Quartic: {
  
  			In: function (k) {
  
  				return k * k * k * k;
  
  			},
  
  			Out: function (k) {
  
  				return 1 - (--k * k * k * k);
  
  			},
  
  			InOut: function (k) {
  
  				if ((k *= 2) < 1) {
  					return 0.5 * k * k * k * k;
  				}
  
  				return - 0.5 * ((k -= 2) * k * k * k - 2);
  
  			}
  
  		},
  
  		Quintic: {
  
  			In: function (k) {
  
  				return k * k * k * k * k;
  
  			},
  
  			Out: function (k) {
  
  				return --k * k * k * k * k + 1;
  
  			},
  
  			InOut: function (k) {
  
  				if ((k *= 2) < 1) {
  					return 0.5 * k * k * k * k * k;
  				}
  
  				return 0.5 * ((k -= 2) * k * k * k * k + 2);
  
  			}
  
  		},
  
  		Sinusoidal: {
  
  			In: function (k) {
  
  				return 1 - Math.cos(k * Math.PI / 2);
  
  			},
  
  			Out: function (k) {
  
  				return Math.sin(k * Math.PI / 2);
  
  			},
  
  			InOut: function (k) {
  
  				return 0.5 * (1 - Math.cos(Math.PI * k));
  
  			}
  
  		},
  
  		Exponential: {
  
  			In: function (k) {
  
  				return k === 0 ? 0 : Math.pow(1024, k - 1);
  
  			},
  
  			Out: function (k) {
  
  				return k === 1 ? 1 : 1 - Math.pow(2, - 10 * k);
  
  			},
  
  			InOut: function (k) {
  
  				if (k === 0) {
  					return 0;
  				}
  
  				if (k === 1) {
  					return 1;
  				}
  
  				if ((k *= 2) < 1) {
  					return 0.5 * Math.pow(1024, k - 1);
  				}
  
  				return 0.5 * (- Math.pow(2, - 10 * (k - 1)) + 2);
  
  			}
  
  		},
  
  		Circular: {
  
  			In: function (k) {
  
  				return 1 - Math.sqrt(1 - k * k);
  
  			},
  
  			Out: function (k) {
  
  				return Math.sqrt(1 - (--k * k));
  
  			},
  
  			InOut: function (k) {
  
  				if ((k *= 2) < 1) {
  					return - 0.5 * (Math.sqrt(1 - k * k) - 1);
  				}
  
  				return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1);
  
  			}
  
  		},
  
  		Elastic: {
  
  			In: function (k) {
  
  				if (k === 0) {
  					return 0;
  				}
  
  				if (k === 1) {
  					return 1;
  				}
  
  				return -Math.pow(2, 10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI);
  
  			},
  
  			Out: function (k) {
  
  				if (k === 0) {
  					return 0;
  				}
  
  				if (k === 1) {
  					return 1;
  				}
  
  				return Math.pow(2, -10 * k) * Math.sin((k - 0.1) * 5 * Math.PI) + 1;
  
  			},
  
  			InOut: function (k) {
  
  				if (k === 0) {
  					return 0;
  				}
  
  				if (k === 1) {
  					return 1;
  				}
  
  				k *= 2;
  
  				if (k < 1) {
  					return -0.5 * Math.pow(2, 10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI);
  				}
  
  				return 0.5 * Math.pow(2, -10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI) + 1;
  
  			}
  
  		},
  
  		Back: {
  
  			In: function (k) {
  
  				var s = 1.70158;
  
  				return k * k * ((s + 1) * k - s);
  
  			},
  
  			Out: function (k) {
  
  				var s = 1.70158;
  
  				return --k * k * ((s + 1) * k + s) + 1;
  
  			},
  
  			InOut: function (k) {
  
  				var s = 1.70158 * 1.525;
  
  				if ((k *= 2) < 1) {
  					return 0.5 * (k * k * ((s + 1) * k - s));
  				}
  
  				return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2);
  
  			}
  
  		},
  
  		Bounce: {
  
  			In: function (k) {
  
  				return 1 - TWEEN.Easing.Bounce.Out(1 - k);
  
  			},
  
  			Out: function (k) {
  
  				if (k < (1 / 2.75)) {
  					return 7.5625 * k * k;
  				} else if (k < (2 / 2.75)) {
  					return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
  				} else if (k < (2.5 / 2.75)) {
  					return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
  				} else {
  					return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
  				}
  
  			},
  
  			InOut: function (k) {
  
  				if (k < 0.5) {
  					return TWEEN.Easing.Bounce.In(k * 2) * 0.5;
  				}
  
  				return TWEEN.Easing.Bounce.Out(k * 2 - 1) * 0.5 + 0.5;
  
  			}
  
  		}
  
  	};
  
  	TWEEN.Interpolation = {
  
  		Linear: function (v, k) {
  
  			var m = v.length - 1;
  			var f = m * k;
  			var i = Math.floor(f);
  			var fn = TWEEN.Interpolation.Utils.Linear;
  
  			if (k < 0) {
  				return fn(v[0], v[1], f);
  			}
  
  			if (k > 1) {
  				return fn(v[m], v[m - 1], m - f);
  			}
  
  			return fn(v[i], v[i + 1 > m ? m : i + 1], f - i);
  
  		},
  
  		Bezier: function (v, k) {
  
  			var b = 0;
  			var n = v.length - 1;
  			var pw = Math.pow;
  			var bn = TWEEN.Interpolation.Utils.Bernstein;
  
  			for (var i = 0; i <= n; i++) {
  				b += pw(1 - k, n - i) * pw(k, i) * v[i] * bn(n, i);
  			}
  
  			return b;
  
  		},
  
  		CatmullRom: function (v, k) {
  
  			var m = v.length - 1;
  			var f = m * k;
  			var i = Math.floor(f);
  			var fn = TWEEN.Interpolation.Utils.CatmullRom;
  
  			if (v[0] === v[m]) {
  
  				if (k < 0) {
  					i = Math.floor(f = m * (1 + k));
  				}
  
  				return fn(v[(i - 1 + m) % m], v[i], v[(i + 1) % m], v[(i + 2) % m], f - i);
  
  			} else {
  
  				if (k < 0) {
  					return v[0] - (fn(v[0], v[0], v[1], v[1], -f) - v[0]);
  				}
  
  				if (k > 1) {
  					return v[m] - (fn(v[m], v[m], v[m - 1], v[m - 1], f - m) - v[m]);
  				}
  
  				return fn(v[i ? i - 1 : 0], v[i], v[m < i + 1 ? m : i + 1], v[m < i + 2 ? m : i + 2], f - i);
  
  			}
  
  		},
  
  		Utils: {
  
  			Linear: function (p0, p1, t) {
  
  				return (p1 - p0) * t + p0;
  
  			},
  
  			Bernstein: function (n, i) {
  
  				var fc = TWEEN.Interpolation.Utils.Factorial;
  
  				return fc(n) / fc(i) / fc(n - i);
  
  			},
  
  			Factorial: (function () {
  
  				var a = [1];
  
  				return function (n) {
  
  					var s = 1;
  
  					if (a[n]) {
  						return a[n];
  					}
  
  					for (var i = n; i > 1; i--) {
  						s *= i;
  					}
  
  					a[n] = s;
  					return s;
  
  				};
  
  			})(),
  
  			CatmullRom: function (p0, p1, p2, p3, t) {
  
  				var v0 = (p2 - p0) * 0.5;
  				var v1 = (p3 - p1) * 0.5;
  				var t2 = t * t;
  				var t3 = t * t2;
  
  				return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (- 3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
  
  			}
  
  		}
  
  	};
  
  	// UMD (Universal Module Definition)
  	(function (root) {
  
  		{
  
  			// Node.js
  			module.exports = TWEEN;
  
  		}
  
  	})();
  	});
  
  	/**
  	 * @classdesc Information spot attached to panorama
  	 * @constructor
  	 * @param {number} [scale=300] - Default scale
  	 * @param {string} [imageSrc=PANOLENS.DataImage.Info] - Image overlay info
  	 * @param {boolean} [animated=true] - Enable default hover animation
  	 */
  	function Infospot ( scale = 300, imageSrc, animated ) {
  		
  	    const duration = 500, scaleFactor = 1.3;
  
  	    imageSrc = imageSrc || DataImage.Info;
  
  	    THREE.Sprite.call( this );
  
  	    this.type = 'infospot';
  
  	    this.animated = animated !== undefined ? animated : true;
  	    this.isHovering = false;
  
  	    /*
  	     * TODO: Three.js bug hotfix for sprite raycasting r104
  	     * https://github.com/mrdoob/three.js/issues/14624
  	     */
  	    this.frustumCulled = false;
  
  	    this.element = null;
  	    this.toPanorama = null;
  	    this.cursorStyle = null;
  
  	    this.mode = MODES.NORMAL;
  
  	    this.scale.set( scale, scale, 1 );
  	    this.rotation.y = Math.PI;
  
  	    this.container = null;
  
  	    this.originalRaycast = this.raycast;
  
  	    // Event Handler
  	    this.HANDLER_FOCUS = null;	
  
  	    this.material.side = THREE.DoubleSide;
  	    this.material.depthTest = false;
  	    this.material.transparent = true;
  	    this.material.opacity = 0;
  
  	    this.scaleUpAnimation = new Tween.Tween();
  	    this.scaleDownAnimation = new Tween.Tween();
  
  
  	    const postLoad = function ( texture ) {
  
  	        if ( !this.material ) { return; }
  
  	        const ratio = texture.image.width / texture.image.height;
  	        const textureScale = new THREE.Vector3();
  
  	        texture.image.width = texture.image.naturalWidth || 64;
  	        texture.image.height = texture.image.naturalHeight || 64;
  
  	        this.scale.set( ratio * scale, scale, 1 );
  
  	        textureScale.copy( this.scale );
  
  	        this.scaleUpAnimation = new Tween.Tween( this.scale )
  	            .to( { x: textureScale.x * scaleFactor, y: textureScale.y * scaleFactor }, duration )
  	            .easing( Tween.Easing.Elastic.Out );
  
  	        this.scaleDownAnimation = new Tween.Tween( this.scale )
  	            .to( { x: textureScale.x, y: textureScale.y }, duration )
  	            .easing( Tween.Easing.Elastic.Out );
  
  	        this.material.map = texture;
  	        this.material.needsUpdate = true;
  
  	    }.bind( this );
  
  	    // Add show and hide animations
  	    this.showAnimation = new Tween.Tween( this.material )
  	        .to( { opacity: 1 }, duration )
  	        .onStart( this.enableRaycast.bind( this, true ) )
  	        .easing( Tween.Easing.Quartic.Out );
  
  	    this.hideAnimation = new Tween.Tween( this.material )
  	        .to( { opacity: 0 }, duration )
  	        .onStart( this.enableRaycast.bind( this, false ) )
  	        .easing( Tween.Easing.Quartic.Out );
  
  	    // Attach event listeners
  	    this.addEventListener( 'click', this.onClick );
  	    this.addEventListener( 'hover', this.onHover );
  	    this.addEventListener( 'hoverenter', this.onHoverStart );
  	    this.addEventListener( 'hoverleave', this.onHoverEnd );
  	    this.addEventListener( 'panolens-dual-eye-effect', this.onDualEyeEffect );
  	    this.addEventListener( 'panolens-container', this.setContainer.bind( this ) );
  	    this.addEventListener( 'dismiss', this.onDismiss );
  	    this.addEventListener( 'panolens-infospot-focus', this.setFocusMethod );
  
  	    TextureLoader.load( imageSrc, postLoad );	
  
  	}
  	Infospot.prototype = Object.assign( Object.create( THREE.Sprite.prototype ), {
  
  	    constructor: Infospot,
  
  	    /**
  	     * Set infospot container
  	     * @param {HTMLElement|object} data - Data with container information
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    setContainer: function ( data ) {
  
  	        let container;
  		
  	        if ( data instanceof HTMLElement ) {
  		
  	            container = data;
  		
  	        } else if ( data && data.container ) {
  		
  	            container = data.container;
  		
  	        }
  		
  	        // Append element if exists
  	        if ( container && this.element ) {
  		
  	            container.appendChild( this.element );
  		
  	        }
  		
  	        this.container = container;
  		
  	    },
  
  	    /**
  	     * Get container
  	     * @memberOf Infospot
  	     * @instance
  	     * @return {HTMLElement} - The container of this infospot
  	     */
  	    getContainer: function () {
  
  	        return this.container;
  
  	    },
  
  	    /**
  	     * This will be called by a click event
  	     * Translate and lock the hovering element if any
  	     * @param  {object} event - Event containing mouseEvent with clientX and clientY
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    onClick: function ( event ) {
  
  	        if ( this.element && this.getContainer() ) {
  
  	            this.onHoverStart( event );
  
  	            // Lock element
  	            this.lockHoverElement();
  
  	        }
  
  	    },
  
  	    /**
  	     * Dismiss current element if any
  	     * @param  {object} event - Dismiss event
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    onDismiss: function () {
  
  	        if ( this.element ) {
  
  	            this.unlockHoverElement();
  	            this.onHoverEnd();
  
  	        }
  
  	    },
  
  	    /**
  	     * This will be called by a mouse hover event
  	     * Translate the hovering element if any
  	     * @param  {object} event - Event containing mouseEvent with clientX and clientY
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    onHover: function () {},
  
  	    /**
  	     * This will be called on a mouse hover start
  	     * Sets cursor style to 'pointer', display the element and scale up the infospot
  	     * @param {object} event
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    onHoverStart: function ( event ) {
  
  	        if ( !this.getContainer() ) { return; }
  
  	        const cursorStyle = this.cursorStyle || ( this.mode === MODES.NORMAL ? 'pointer' : 'default' );
  	        const { scaleDownAnimation, scaleUpAnimation, element } = this;
  
  	        this.isHovering = true;
  	        this.container.style.cursor = cursorStyle;
  			
  	        if ( this.animated ) {
  
  	            scaleDownAnimation.stop();
  	            scaleUpAnimation.start();
  
  	        }
  			
  	        if ( element && event.mouseEvent.clientX >= 0 && event.mouseEvent.clientY >= 0 ) {
  
  	            const { left, right, style } = element;
  
  	            if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) {
  
  	                style.display = 'none';
  	                left.style.display = 'block';
  	                right.style.display = 'block';
  
  	                // Store element width for reference
  	                element._width = left.clientWidth;
  	                element._height = left.clientHeight;
  
  	            } else {
  
  	                style.display = 'block';
  	                if ( left ) { left.style.display = 'none'; }
  	                if ( right ) { right.style.display = 'none'; }
  
  	                // Store element width for reference
  	                element._width = element.clientWidth;
  	                element._height = element.clientHeight;
  
  	            }
  				
  	        }
  
  	    },
  
  	    /**
  	     * This will be called on a mouse hover end
  	     * Sets cursor style to 'default', hide the element and scale down the infospot
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    onHoverEnd: function () {
  
  	        if ( !this.getContainer() ) { return; }
  
  	        const { scaleDownAnimation, scaleUpAnimation, element } = this;
  
  	        this.isHovering = false;
  	        this.container.style.cursor = 'default';
  
  	        if ( this.animated ) {
  
  	            scaleUpAnimation.stop();
  	            scaleDownAnimation.start();
  
  	        }
  
  	        if ( element && !this.element.locked ) {
  
  	            const { left, right, style } = element;
  
  	            style.display = 'none';
  	            if ( left ) { left.style.display = 'none'; }
  	            if ( right ) { right.style.display = 'none'; }
  
  	            this.unlockHoverElement();
  
  	        }
  
  	    },
  
  	    /**
  	     * On dual eye effect handler
  	     * Creates duplicate left and right element
  	     * @param  {object} event - panolens-dual-eye-effect event
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    onDualEyeEffect: function ( event ) {
  			
  	        if ( !this.getContainer() ) { return; }
  
  	        let element, halfWidth, halfHeight;
  
  	        this.mode = event.mode;
  
  	        element = this.element;
  
  	        halfWidth = this.container.clientWidth / 2;
  	        halfHeight = this.container.clientHeight / 2;
  
  	        if ( !element ) {
  
  	            return;
  
  	        }
  
  	        if ( !element.left && !element.right ) {
  
  	            element.left = element.cloneNode( true );
  	            element.right = element.cloneNode( true );
  
  	        }
  
  	        if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) {
  
  	            element.left.style.display = element.style.display;
  	            element.right.style.display = element.style.display;
  	            element.style.display = 'none';
  
  	        } else {
  
  	            element.style.display = element.left.style.display;
  	            element.left.style.display = 'none';
  	            element.right.style.display = 'none';
  
  	        }
  
  	        // Update elements translation
  	        this.translateElement( halfWidth, halfHeight );
  
  	        this.container.appendChild( element.left );
  	        this.container.appendChild( element.right );
  
  	    },
  
  	    /**
  	     * Translate the hovering element by css transform
  	     * @param  {number} x - X position on the window screen
  	     * @param  {number} y - Y position on the window screen
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    translateElement: function ( x, y ) {
  
  	        if ( !this.element._width || !this.element._height || !this.getContainer() ) {
  
  	            return;
  
  	        }
  
  	        let left, top, element, width, height, delta, container;
  
  	        container = this.container;
  	        element = this.element;
  	        width = element._width / 2;
  	        height = element._height / 2;
  	        delta = element.verticalDelta !== undefined ? element.verticalDelta : 40;
  
  	        left = x - width;
  	        top = y - height - delta;
  
  	        if ( ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) 
  					&& element.left && element.right
  					&& !( x === container.clientWidth / 2 && y === container.clientHeight / 2 ) ) {
  
  	            left = container.clientWidth / 4 - width + ( x - container.clientWidth / 2 );
  	            top = container.clientHeight / 2 - height - delta + ( y - container.clientHeight / 2 );
  
  	            this.setElementStyle( 'transform', element.left, 'translate(' + left + 'px, ' + top + 'px)' );
  
  	            left += container.clientWidth / 2;
  
  	            this.setElementStyle( 'transform', element.right, 'translate(' + left + 'px, ' + top + 'px)' );
  
  	        } else {
  
  	            this.setElementStyle( 'transform', element, 'translate(' + left + 'px, ' + top + 'px)' );
  
  	        }
  
  	    },
  
  	    /**
  	     * Set vendor specific css
  	     * @param {string} type - CSS style name
  	     * @param {HTMLElement} element - The element to be modified
  	     * @param {string} value - Style value
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    setElementStyle: function ( type, element, value ) {
  
  	        const style = element.style;
  
  	        if ( type === 'transform' ) {
  
  	            style.webkitTransform = style.msTransform = style.transform = value;
  
  	        }
  
  	    },
  
  	    /**
  	     * Set hovering text content
  	     * @param {string} text - Text to be displayed
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    setText: function ( text ) {
  
  	        if ( this.element ) {
  
  	            this.element.textContent = text;
  
  	        }
  
  	    },
  
  	    /**
  	     * Set cursor css style on hover
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    setCursorHoverStyle: function ( style ) {
  
  	        this.cursorStyle = style;
  
  	    },
  
  	    /**
  	     * Add hovering text element
  	     * @param {string} text - Text to be displayed
  	     * @param {number} [delta=40] - Vertical delta to the infospot
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    addHoverText: function ( text, delta = 40 ) {
  
  	        if ( !this.element ) {
  
  	            this.element = document.createElement( 'div' );
  	            this.element.style.display = 'none';
  	            this.element.style.color = '#fff';
  	            this.element.style.top = 0;
  	            this.element.style.maxWidth = '50%';
  	            this.element.style.maxHeight = '50%';
  	            this.element.style.textShadow = '0 0 3px #000000';
  	            this.element.style.fontFamily = '"Trebuchet MS", Helvetica, sans-serif';
  	            this.element.style.position = 'absolute';
  	            this.element.classList.add( 'panolens-infospot' );
  	            this.element.verticalDelta = delta;
  
  	        }
  
  	        this.setText( text );
  
  	    },
  
  	    /**
  	     * Add hovering element by cloning an element
  	     * @param {HTMLDOMElement} el - Element to be cloned and displayed
  	     * @param {number} [delta=40] - Vertical delta to the infospot
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    addHoverElement: function ( el, delta = 40 ) {
  
  	        if ( !this.element ) { 
  
  	            this.element = el.cloneNode( true );
  	            this.element.style.display = 'none';
  	            this.element.style.top = 0;
  	            this.element.style.position = 'absolute';
  	            this.element.classList.add( 'panolens-infospot' );
  	            this.element.verticalDelta = delta;
  
  	        }
  
  	    },
  
  	    /**
  	     * Remove hovering element
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    removeHoverElement: function () {
  
  	        if ( this.element ) { 
  
  	            if ( this.element.left ) {
  
  	                this.container.removeChild( this.element.left );
  	                this.element.left = null;
  
  	            }
  
  	            if ( this.element.right ) {
  
  	                this.container.removeChild( this.element.right );
  	                this.element.right = null;
  
  	            }
  
  	            this.container.removeChild( this.element );
  	            this.element = null;
  
  	        }
  
  	    },
  
  	    /**
  	     * Lock hovering element
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    lockHoverElement: function () {
  
  	        if ( this.element ) { 
  
  	            this.element.locked = true;
  
  	        }
  
  	    },
  
  	    /**
  	     * Unlock hovering element
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    unlockHoverElement: function () {
  
  	        if ( this.element ) { 
  
  	            this.element.locked = false;
  
  	        }
  
  	    },
  
  	    /**
  	     * Enable raycasting
  	     * @param {boolean} [enabled=true]
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    enableRaycast: function ( enabled = true ) {
  
  	        if ( enabled ) {
  
  	            this.raycast = this.originalRaycast;
  
  	        } else {
  
  	            this.raycast = () => {};
  
  	        }
  
  	    },
  
  	    /**
  	     * Show infospot
  	     * @param  {number} [delay=0] - Delay time to show
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    show: function ( delay = 0 ) {
  
  	        const { animated, hideAnimation, showAnimation, material } = this;
  
  	        if ( animated ) {
  
  	            hideAnimation.stop();
  	            showAnimation.delay( delay ).start();
  
  	        } else {
  
  	            this.enableRaycast( true );
  	            material.opacity = 1;
  
  	        }
  
  	    },
  
  	    /**
  	     * Hide infospot
  	     * @param  {number} [delay=0] - Delay time to hide
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    hide: function ( delay = 0 ) {
  
  	        const { animated, hideAnimation, showAnimation, material } = this;
  
  	        if ( animated ) {
  
  	            showAnimation.stop();
  	            hideAnimation.delay( delay ).start();
  
  	        } else {
  
  	            this.enableRaycast( false );
  	            material.opacity = 0;
  
  	        }
  			
  	    },
  
  	    /**
  	     * Set focus event handler
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    setFocusMethod: function ( event ) {
  
  	        if ( event ) {
  
  	            this.HANDLER_FOCUS = event.method;
  
  	        }
  
  	    },
  
  	    /**
  	     * Focus camera center to this infospot
  	     * @param {number} [duration=1000] - Duration to tween
  	     * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    focus: function ( duration, easing ) {
  
  	        if ( this.HANDLER_FOCUS ) {
  
  	            this.HANDLER_FOCUS( this.position, duration, easing );
  	            this.onDismiss();
  
  	        }
  
  	    },
  
  	    /**
  	     * Dispose
  	     * @memberOf Infospot
  	     * @instance
  	     */
  	    dispose: function () {
  
  	        const { geometry, material } = this;
  	        const { map } = material;
  
  	        this.removeHoverElement();
  
  	        if ( this.parent ) {
  
  	            this.parent.remove( this );
  
  	        }
  
  	        if ( map ) { map.dispose(); material.map = null; }
  	        if ( geometry ) { geometry.dispose(); this.geometry = null; }
  	        if ( material ) { material.dispose(); this.material = null; }
  
  	    }
  
  	} );
  
  	/**
  	 * @classdesc Widget for controls
  	 * @constructor
  	 * @param {HTMLElement} container - A domElement where default control widget will be attached to
  	 */
  	function Widget ( container ) {
  
  	    if ( !container ) {
  
  	        console.warn( 'PANOLENS.Widget: No container specified' );
  
  	    }
  
  	    THREE.EventDispatcher.call( this );
  
  	    this.DEFAULT_TRANSITION  = 'all 0.27s ease';
  	    this.TOUCH_ENABLED = !!(( 'ontouchstart' in window ) || window.DocumentTouch && document instanceof DocumentTouch);
  	    this.PREVENT_EVENT_HANDLER = function ( event ) {
  	        event.preventDefault();
  	        event.stopPropagation();
  	    };
  
  	    this.container = container;
  
  	    this.barElement = null;
  	    this.fullscreenElement = null;
  	    this.videoElement = null;
  	    this.settingElement = null;
  
  	    this.mainMenu = null;
  
  	    this.activeMainItem = null;
  	    this.activeSubMenu = null;
  	    this.mask = null;
  
  	}
  
  	Widget.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype ), {
  
  	    constructor: Widget,
  
  	    /**
  	     * Add control bar
  	     * @memberOf Widget
  	     * @instance
  	     */
  	    addControlBar: function () {
  
  	        if ( !this.container ) {
  
  	            console.warn( 'Widget container not set' ); 
  	            return; 
  	        }
  
  	        var scope = this, bar, styleTranslate, styleOpacity, gradientStyle;
  
  	        gradientStyle = 'linear-gradient(bottom, rgba(0,0,0,0.2), rgba(0,0,0,0))';
  
  	        bar = document.createElement( 'div' );
  	        bar.style.width = '100%';
  	        bar.style.height = '44px';
  	        bar.style.float = 'left';
  	        bar.style.transform = bar.style.webkitTransform = bar.style.msTransform = 'translateY(-100%)';
  	        bar.style.background = '-webkit-' + gradientStyle;
  	        bar.style.background = '-moz-' + gradientStyle;
  	        bar.style.background = '-o-' + gradientStyle;
  	        bar.style.background = '-ms-' + gradientStyle;
  	        bar.style.background = gradientStyle;
  	        bar.style.transition = this.DEFAULT_TRANSITION;
  	        bar.style.pointerEvents = 'none';
  	        bar.isHidden = false;
  	        bar.toggle = function () {
  	            bar.isHidden = !bar.isHidden;
  	            styleTranslate = bar.isHidden ? 'translateY(0)' : 'translateY(-100%)';
  	            styleOpacity = bar.isHidden ? 0 : 1;
  	            bar.style.transform = bar.style.webkitTransform = bar.style.msTransform = styleTranslate;
  	            bar.style.opacity = styleOpacity;
  	        };
  
  	        // Menu
  	        var menu = this.createDefaultMenu();
  	        this.mainMenu = this.createMainMenu( menu );
  	        bar.appendChild( this.mainMenu );
  
  	        // Mask
  	        var mask = this.createMask();
  	        this.mask = mask;
  	        this.container.appendChild( mask );
  
  	        // Dispose
  	        bar.dispose = function () {
  
  	            if ( scope.fullscreenElement ) {
  
  	                bar.removeChild( scope.fullscreenElement );
  	                scope.fullscreenElement.dispose();
  	                scope.fullscreenElement = null;
  
  	            }
  
  	            if ( scope.settingElement ) {
  
  	                bar.removeChild( scope.settingElement );
  	                scope.settingElement.dispose();
  	                scope.settingElement = null;
  
  	            }
  
  	            if ( scope.videoElement ) {
  
  	                bar.removeChild( scope.videoElement );
  	                scope.videoElement.dispose();
  	                scope.videoElement = null;
  
  	            }
  
  	        };
  
  	        this.container.appendChild( bar );
  
  	        // Mask events
  	        this.mask.addEventListener( 'mousemove', this.PREVENT_EVENT_HANDLER, true );
  	        this.mask.addEventListener( 'mouseup', this.PREVENT_EVENT_HANDLER, true );
  	        this.mask.addEventListener( 'mousedown', this.PREVENT_EVENT_HANDLER, true );
  	        this.mask.addEventListener( scope.TOUCH_ENABLED ? 'touchend' : 'click', function ( event ) {
  
  	            event.preventDefault();
  	            event.stopPropagation();
  
  	            scope.mask.hide();
  	            scope.settingElement.deactivate();
  
  	        }, false );
  
  	        // Event listener
  	        this.addEventListener( 'control-bar-toggle', bar.toggle );
  
  	        this.barElement = bar;
  
  	    },
  
  	    /**
  	     * Create default menu
  	     * @memberOf Widget
  	     * @instance
  	     */
  	    createDefaultMenu: function () {
  
  	        var scope = this, handler;
  
  	        handler = function ( method, data ) {
  
  	            return function () {
  
  	                scope.dispatchEvent( { 
  
  	                    type: 'panolens-viewer-handler', 
  	                    method: method, 
  	                    data: data 
  
  	                } ); 
  
  	            };
  
  	        };
  
  	        return [
  
  	            { 
  	                title: 'Control', 
  	                subMenu: [ 
  	                    { 
  	                        title: this.TOUCH_ENABLED ? 'Touch' : 'Mouse', 
  	                        handler: handler( 'enableControl', CONTROLS.ORBIT )
  	                    },
  	                    { 
  	                        title: 'Sensor', 
  	                        handler: handler( 'enableControl', CONTROLS.DEVICEORIENTATION ) 
  	                    } 
  	                ]
  	            },
  
  	            { 
  	                title: 'Mode', 
  	                subMenu: [ 
  	                    { 
  	                        title: 'Normal',
  	                        handler: handler( 'disableEffect' )
  	                    }, 
  	                    { 
  	                        title: 'Cardboard',
  	                        handler: handler( 'enableEffect', MODES.CARDBOARD )
  	                    },
  	                    { 
  	                        title: 'Stereoscopic',
  	                        handler: handler( 'enableEffect', MODES.STEREO )
  	                    }
  	                ]
  	            }
  
  	        ];
  
  	    },
  
  	    /**
  	     * Add buttons on top of control bar
  	     * @param {string} name - The control button name to be created
  	     * @memberOf Widget
  	     * @instance
  	     */
  	    addControlButton: function ( name ) {
  
  	        let element;
  
  	        switch( name ) {
  
  	        case 'fullscreen':
  
  	            element = this.createFullscreenButton();
  	            this.fullscreenElement = element; 
  
  	            break;
  
  	        case 'setting':
  
  	            element = this.createSettingButton();
  	            this.settingElement = element;
  
  	            break;
  
  	        case 'video':
  
  	            element = this.createVideoControl();
  	            this.videoElement = element;
  
  	            break;
  
  	        default:
  
  	            return;
  
  	        }
  
  	        if ( !element ) {
  
  	            return;
  
  	        }
  
  	        this.barElement.appendChild( element );
  
  	    },
  
  	    /**
  	     * Create modal mask
  	     * @memberOf Widget
  	     * @instance
  	     */
  	    createMask: function () {
  
  	        const element = document.createElement( 'div' );
  	        element.style.position = 'absolute';
  	        element.style.top = 0;
  	        element.style.left = 0;
  	        element.style.width = '100%';
  	        element.style.height = '100%';
  	        element.style.background = 'transparent';
  	        element.style.display = 'none';
  
  	        element.show = function () {
  
  	            this.style.display = 'block';
  
  	        };
  
  	        element.hide = function () {
  
  	            this.style.display = 'none';
  
  	        };
  
  	        return element;
  
  	    },
  
  	    /**
  	     * Create Setting button to toggle menu
  	     * @memberOf Widget
  	     * @instance
  	     */
  	    createSettingButton: function () {
  
  	        let scope = this, item;
  
  	        function onTap ( event ) {
  
  	            event.preventDefault();
  	            event.stopPropagation();
  
  	            scope.mainMenu.toggle();
  
  	            if ( this.activated ) {
  		
  	                this.deactivate();
  
  	            } else {
  
  	                this.activate();
  
  	            }
  
  	        }
  
  	        item = this.createCustomItem( { 
  
  	            style: { 
  
  	                backgroundImage: 'url("' + DataImage.Setting + '")',
  	                webkitTransition: this.DEFAULT_TRANSITION,
  	                transition: this.DEFAULT_TRANSITION
  
  	            },
  
  	            onTap: onTap
  
  	        } );
  
  	        item.activate = function () {
  
  	            this.style.transform = 'rotate3d(0,0,1,90deg)';
  	            this.activated = true;
  	            scope.mask.show();
  
  	        };
  
  	        item.deactivate = function () {
  
  	            this.style.transform = 'rotate3d(0,0,0,0)';
  	            this.activated = false;
  	            scope.mask.hide();
  
  	            if ( scope.mainMenu && scope.mainMenu.visible ) {
  
  	                scope.mainMenu.hide();
  					
  	            }
  
  	            if ( scope.activeSubMenu && scope.activeSubMenu.visible ) {
  
  	                scope.activeSubMenu.hide();
  
  	            }
  
  	            if ( scope.mainMenu && scope.mainMenu._width ) {
  
  	                scope.mainMenu.changeSize( scope.mainMenu._width );
  	                scope.mainMenu.unslideAll();
  
  	            }
  				
  	        };
  
  	        item.activated = false;
  
  	        return item;
  
  	    },
  
  	    /**
  	     * Create Fullscreen button
  	     * @return {HTMLSpanElement} - The dom element icon for fullscreen
  	     * @memberOf Widget
  	     * @instance
  	     * @fires Widget#panolens-viewer-handler
  	     */
  	    createFullscreenButton: function () {
  
  	        let scope = this, item, isFullscreen = false, tapSkipped = true, stylesheetId;
  
  	        const { container } = this;
  
  	        stylesheetId = 'panolens-style-addon';
  
  	        // Don't create button if no support
  	        if ( !document.fullscreenEnabled       && 
  				!document.webkitFullscreenEnabled &&
  				!document.mozFullScreenEnabled    &&
  				!document.msFullscreenEnabled ) {
  	            return;
  	        }
  
  	        function onTap ( event ) {
  
  	            event.preventDefault();
  	            event.stopPropagation();
  
  	            tapSkipped = false;
  
  	            if ( !isFullscreen ) {
  
  	                if ( container.requestFullscreen ) { container.requestFullscreen(); }
  	                if ( container.msRequestFullscreen ) { container.msRequestFullscreen(); }
  	                if ( container.mozRequestFullScreen ) { container.mozRequestFullScreen(); }
  	                if ( container.webkitRequestFullscreen ) { container.webkitRequestFullscreen( Element.ALLOW_KEYBOARD_INPUT ); }
  	              
  	                isFullscreen = true;
  
  	            } else {
  
  	                if ( document.exitFullscreen ) { document.exitFullscreen(); }
  	                if ( document.msExitFullscreen ) { document.msExitFullscreen(); }
  	                if ( document.mozCancelFullScreen ) { document.mozCancelFullScreen(); }
  	                if ( document.webkitExitFullscreen ) { document.webkitExitFullscreen( ); }
  
  	                isFullscreen = false;
  
  	            }
  
  	            this.style.backgroundImage = ( isFullscreen ) 
  	                ? 'url("' + DataImage.FullscreenLeave + '")' 
  	                : 'url("' + DataImage.FullscreenEnter + '")';
  
  	        }
  
  	        function onFullScreenChange () {
  
  	            if ( tapSkipped ) {
  
  	                isFullscreen = !isFullscreen; 
  
  	                item.style.backgroundImage = ( isFullscreen ) 
  	                    ? 'url("' + DataImage.FullscreenLeave + '")' 
  	                    : 'url("' + DataImage.FullscreenEnter + '")';
  
  	            }
  
  	            /**
  	             * Viewer handler event
  	             * @type {object}
  	             * @event Widget#panolens-viewer-handler
  	             * @property {string} method - 'onWindowResize' function call on Viewer
  	             */
  	            scope.dispatchEvent( { type: 'panolens-viewer-handler', method: 'onWindowResize' } );
  
  	            tapSkipped = true;
  
  	        }
  
  	        document.addEventListener( 'fullscreenchange', onFullScreenChange, false );
  	        document.addEventListener( 'webkitfullscreenchange', onFullScreenChange, false );
  	        document.addEventListener( 'mozfullscreenchange', onFullScreenChange, false );
  	        document.addEventListener( 'MSFullscreenChange', onFullScreenChange, false );
  
  	        item = this.createCustomItem( { 
  
  	            style: { 
  
  	                backgroundImage: 'url("' + DataImage.FullscreenEnter + '")' 
  
  	            },
  
  	            onTap: onTap
  
  	        } );
  
  	        // Add fullscreen stlye if not exists
  	        if ( !document.querySelector( stylesheetId ) ) {
  	            const sheet = document.createElement( 'style' );
  	            sheet.id = stylesheetId;
  	            sheet.innerHTML = ':-webkit-full-screen { width: 100% !important; height: 100% !important }';
  	            document.body.appendChild( sheet );
  	        }
  			
  	        return item;
  
  	    },
  
  	    /**
  	     * Create video control container
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLSpanElement} - The dom element icon for video control
  	     */
  	    createVideoControl: function () {
  
  	        const item = document.createElement( 'span' );
  	        item.style.display = 'none';
  	        item.show = function () { 
  
  	            item.style.display = '';
  
  	        };
  
  	        item.hide = function () { 
  
  	            item.style.display = 'none';
  	            item.controlButton.paused = true;
  	            item.controlButton.update();
  
  	        };
  
  	        item.controlButton = this.createVideoControlButton();
  	        item.seekBar = this.createVideoControlSeekbar();
  			
  	        item.appendChild( item.controlButton );
  	        item.appendChild( item.seekBar );
  
  	        item.dispose = function () {
  
  	            item.removeChild( item.controlButton );
  	            item.removeChild( item.seekBar );
  
  	            item.controlButton.dispose();
  	            item.controlButton = null;
  
  	            item.seekBar.dispose();
  	            item.seekBar = null;
  
  	        };
  
  	        this.addEventListener( 'video-control-show', item.show );
  	        this.addEventListener( 'video-control-hide', item.hide );
  
  	        return item;
  
  	    },
  
  	    /**
  	     * Create video control button
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLSpanElement} - The dom element icon for video control
  	     * @fires Widget#panolens-viewer-handler
  	     */
  	    createVideoControlButton: function () {
  
  	        const scope = this;
  
  	        function onTap ( event ) {
  
  	            event.preventDefault();
  	            event.stopPropagation();
  
  	            /**
  	             * Viewer handler event
  	             * @type {object}
  	             * @event Widget#panolens-viewer-handler
  	             * @property {string} method - 'toggleVideoPlay' function call on Viewer
  	             */
  	            scope.dispatchEvent( { type: 'panolens-viewer-handler', method: 'toggleVideoPlay', data: !this.paused } );
  
  	            this.paused = !this.paused;
  
  	            item.update();
  
  	        }
  	        const item = this.createCustomItem( { 
  
  	            style: { 
  
  	                float: 'left',
  	                backgroundImage: 'url("' + DataImage.VideoPlay + '")'
  
  	            },
  
  	            onTap: onTap
  
  	        } );
  
  	        item.paused = true;
  
  	        item.update = function ( paused ) {
  
  	            this.paused = paused !== undefined ? paused : this.paused;
  
  	            this.style.backgroundImage = 'url("' + ( this.paused 
  	                ? DataImage.VideoPlay 
  	                : DataImage.VideoPause ) + '")';
  
  	        };
  
  	        return item;
  
  	    },
  
  	    /**
  	     * Create video seekbar
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLSpanElement} - The dom element icon for video seekbar
  	     * @fires Widget#panolens-viewer-handler
  	     */
  	    createVideoControlSeekbar: function () {
  
  	        let scope = this, item, progressElement, progressElementControl,
  	            isDragging = false, mouseX, percentageNow, percentageNext;
  
  	        progressElement = document.createElement( 'div' );
  	        progressElement.style.width = '0%';
  	        progressElement.style.height = '100%';
  	        progressElement.style.backgroundColor = '#fff';
  
  	        progressElementControl = document.createElement( 'div' );
  	        progressElementControl.style.float = 'right';
  	        progressElementControl.style.width = '14px';
  	        progressElementControl.style.height = '14px';
  	        progressElementControl.style.transform = 'translate(7px, -5px)';
  	        progressElementControl.style.borderRadius = '50%';
  	        progressElementControl.style.backgroundColor = '#ddd';
  
  	        progressElementControl.addEventListener( 'mousedown', onMouseDown, { passive: true } );
  	        progressElementControl.addEventListener( 'touchstart', onMouseDown,  { passive: true } );
  
  	        function onMouseDown ( event ) {
  
  	            event.stopPropagation();
  				
  	            isDragging = true;
  				
  	            mouseX = event.clientX || ( event.changedTouches && event.changedTouches[0].clientX );
  
  	            percentageNow = parseInt( progressElement.style.width ) / 100;
  
  	            addControlListeners();
  	        }
  
  	        function onVideoControlDrag ( event ) {
  
  	            if( isDragging ){
  
  	                const clientX = event.clientX || ( event.changedTouches && event.changedTouches[0].clientX );
  					
  	                percentageNext = ( clientX - mouseX ) / item.clientWidth;
  
  	                percentageNext = percentageNow + percentageNext;
  
  	                percentageNext = percentageNext > 1 ? 1 : ( ( percentageNext < 0 ) ? 0 : percentageNext );
  
  	                item.setProgress ( percentageNext );
  
  	                /**
  	                 * Viewer handler event
  	                 * @type {object}
  	                 * @event Widget#panolens-viewer-handler
  	                 * @property {string} method - 'setVideoCurrentTime' function call on Viewer
  	                 * @property {number} data - Percentage of current video. Range from 0.0 to 1.0
  	                 */
  	                scope.dispatchEvent( { type: 'panolens-viewer-handler', method: 'setVideoCurrentTime', data: percentageNext } );
  
  	            }
  
  	        }
  
  	        function onVideoControlStop ( event ) {
  
  	            event.stopPropagation();
  
  	            isDragging = false;
  
  	            removeControlListeners();
  
  	        }
  
  	        function addControlListeners () {
  
  	            scope.container.addEventListener( 'mousemove', onVideoControlDrag, { passive: true } );
  	            scope.container.addEventListener( 'mouseup', onVideoControlStop, { passive: true } );
  	            scope.container.addEventListener( 'touchmove', onVideoControlDrag, { passive: true } );
  	            scope.container.addEventListener( 'touchend', onVideoControlStop, { passive: true } );
  
  
  	        }
  
  	        function removeControlListeners () {
  
  	            scope.container.removeEventListener( 'mousemove', onVideoControlDrag, false );
  	            scope.container.removeEventListener( 'mouseup', onVideoControlStop, false );
  	            scope.container.removeEventListener( 'touchmove', onVideoControlDrag, false );
  	            scope.container.removeEventListener( 'touchend', onVideoControlStop, false );
  
  	        }
  
  	        function onTap ( event ) {
  
  	            event.preventDefault();
  	            event.stopPropagation();
  
  	            if ( event.target === progressElementControl ) { return; }
  
  	            const percentage = ( event.changedTouches && event.changedTouches.length > 0 )
  	                ? ( event.changedTouches[0].pageX - event.target.getBoundingClientRect().left ) / this.clientWidth
  	                : event.offsetX / this.clientWidth;
  
  	            /**
  	             * Viewer handler event
  	             * @type {object}
  	             * @property {string} method - 'setVideoCurrentTime' function call on Viewer
  	             * @property {number} data - Percentage of current video. Range from 0.0 to 1.0
  	             */
  	            scope.dispatchEvent( { type: 'panolens-viewer-handler', method: 'setVideoCurrentTime', data: percentage } );
  
  	            item.setProgress( event.offsetX / this.clientWidth );
  
  	        }
  	        function onDispose () {
  
  	            removeControlListeners();
  	            progressElement = null;
  	            progressElementControl = null;
  
  	        }
  
  	        progressElement.appendChild( progressElementControl );
  
  	        item = this.createCustomItem( {
  
  	            style: { 
  
  	                float: 'left',
  	                width: '30%',
  	                height: '4px',
  	                marginTop: '20px',
  	                backgroundColor: 'rgba(188,188,188,0.8)'
  
  	            },
  
  	            onTap: onTap,
  	            onDispose: onDispose
  
  	        } );
  
  	        item.appendChild( progressElement );
  
  	        item.setProgress = function( percentage ) {
  
  	            progressElement.style.width = percentage * 100 + '%';
  
  	        };		
  
  	        this.addEventListener( 'video-update', function ( event ) { 
  
  	            item.setProgress( event.percentage ); 
  
  	        } );
  
  	        item.progressElement = progressElement;
  	        item.progressElementControl = progressElementControl;
  
  	        return item;
  
  	    },
  
  	    /**
  	     * Create menu item
  	     * @param  {string} title - Title to display
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLElement} - An anchor tag element
  	     */
  	    createMenuItem: function ( title ) {
  
  	        const scope = this; 
  	        const item = document.createElement( 'a' );
  	        item.textContent = title;
  	        item.style.display = 'block';
  	        item.style.padding = '10px';
  	        item.style.textDecoration = 'none';
  	        item.style.cursor = 'pointer';
  	        item.style.pointerEvents = 'auto';
  	        item.style.transition = this.DEFAULT_TRANSITION;
  
  	        item.slide = function ( right ) {
  
  	            this.style.transform = 'translateX(' + ( right ? '' : '-' ) + '100%)';
  
  	        };
  
  	        item.unslide = function () {
  
  	            this.style.transform = 'translateX(0)';
  
  	        };
  
  	        item.setIcon = function ( url ) {
  
  	            if ( this.icon ) {
  
  	                this.icon.style.backgroundImage = 'url(' + url + ')';
  
  	            }
  
  	        };
  
  	        item.setSelectionTitle = function ( title ) {
  
  	            if ( this.selection ) {
  
  	                this.selection.textContent = title;
  
  	            }
  
  	        };
  
  	        item.addSelection = function ( name ) {
  				
  	            const selection = document.createElement( 'span' );
  	            selection.style.fontSize = '13px';
  	            selection.style.fontWeight = '300';
  	            selection.style.float = 'right';
  
  	            this.selection = selection;
  	            this.setSelectionTitle( name );
  	            this.appendChild( selection );
  				
  	            return this;
  
  	        };
  
  	        item.addIcon = function ( url = DataImage.ChevronRight, left = false, flip = false ) {
  				
  	            const element = document.createElement( 'span' );
  	            element.style.float = left ? 'left' : 'right';
  	            element.style.width = '17px';
  	            element.style.height = '17px';
  	            element.style[ 'margin' + ( left ? 'Right' : 'Left' ) ] = '12px';
  	            element.style.backgroundSize = 'cover';
  
  	            if ( flip ) {
  
  	                element.style.transform = 'rotateZ(180deg)';
  
  	            }
  
  	            this.icon = element;
  	            this.setIcon( url );
  	            this.appendChild( element );
  
  	            return this;
  
  	        };
  
  	        item.addSubMenu = function ( title, items ) {
  
  	            this.subMenu = scope.createSubMenu( title, items );
  
  	            return this;
  
  	        };
  
  	        item.addEventListener( 'mouseenter', function () {
  				
  	            this.style.backgroundColor = '#e0e0e0';
  
  	        }, false );
  
  	        item.addEventListener( 'mouseleave', function () {
  				
  	            this.style.backgroundColor = '#fafafa';
  
  	        }, false );
  
  	        return item;
  
  	    },
  
  	    /**
  	     * Create menu item header
  	     * @param  {string} title - Title to display
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLElement} - An anchor tag element
  	     */
  	    createMenuItemHeader: function ( title ) {
  
  	        const header = this.createMenuItem( title );
  
  	        header.style.borderBottom = '1px solid #333';
  	        header.style.paddingBottom = '15px';
  
  	        return header;
  
  	    },
  
  	    /**
  	     * Create main menu
  	     * @param  {array} menus - Menu array list
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLElement} - A span element
  	     */
  	    createMainMenu: function ( menus ) {
  			
  	        let scope = this, menu = this.createMenu();
  
  	        menu._width = 200;
  	        menu.changeSize( menu._width );
  
  	        function onTap ( event ) {
  
  	            event.preventDefault();
  	            event.stopPropagation();
  
  	            let mainMenu = scope.mainMenu, subMenu = this.subMenu;
  
  	            function onNextTick () {
  
  	                mainMenu.changeSize( subMenu.clientWidth );
  	                subMenu.show();
  	                subMenu.unslideAll();
  
  	            }
  
  	            mainMenu.hide();
  	            mainMenu.slideAll();
  	            mainMenu.parentElement.appendChild( subMenu );
  
  	            scope.activeMainItem = this;
  	            scope.activeSubMenu = subMenu;
  
  	            window.requestAnimationFrame( onNextTick );
  
  	        }
  	        for ( var i = 0; i < menus.length; i++ ) {
  
  	            var item = menu.addItem( menus[ i ].title );
  
  	            item.style.paddingLeft = '20px';
  
  	            item.addIcon()
  	                .addEventListener( scope.TOUCH_ENABLED ? 'touchend' : 'click', onTap, false );
  
  	            if ( menus[ i ].subMenu && menus[ i ].subMenu.length > 0 ) {
  
  	                var title = menus[ i ].subMenu[ 0 ].title;
  
  	                item.addSelection( title )
  	                    .addSubMenu( menus[ i ].title, menus[ i ].subMenu );
  
  	            }
  
  	        }
  
  	        return menu;
  
  	    },
  
  	    /**
  	     * Create sub menu
  	     * @param {string} title - Sub menu title
  	     * @param {array} items - Item array list
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLElement} - A span element
  	     */
  	    createSubMenu: function ( title, items ) {
  
  	        let scope = this, menu, subMenu = this.createMenu();
  
  	        subMenu.items = items;
  	        subMenu.activeItem = null;
  
  	        function onTap ( event ) {
  
  	            event.preventDefault();
  	            event.stopPropagation();
  
  	            menu = scope.mainMenu;
  	            menu.changeSize( menu._width );
  	            menu.unslideAll();
  	            menu.show();
  	            subMenu.slideAll( true );
  	            subMenu.hide();
  
  	            if ( this.type !== 'header' ) {
  
  	                subMenu.setActiveItem( this );
  	                scope.activeMainItem.setSelectionTitle( this.textContent );
  
  	                if ( this.handler ) { this.handler(); }
  
  	            }
  
  	        }
  
  	        subMenu.addHeader( title ).addIcon( undefined, true, true ).addEventListener( scope.TOUCH_ENABLED ? 'touchend' : 'click', onTap, false );
  
  	        for ( let i = 0; i < items.length; i++ ) {
  
  	            const item = subMenu.addItem( items[ i ].title );
  
  	            item.style.fontWeight = 300;
  	            item.handler = items[ i ].handler;
  	            item.addIcon( ' ', true );
  	            item.addEventListener( scope.TOUCH_ENABLED ? 'touchend' : 'click', onTap, false );
  
  	            if ( !subMenu.activeItem ) {
  
  	                subMenu.setActiveItem( item );
  
  	            }
  
  	        }
  
  	        subMenu.slideAll( true );
  
  	        return subMenu;
  			
  	    },
  
  	    /**
  	     * Create general menu
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLElement} - A span element
  	     */
  	    createMenu: function () {
  
  	        const scope = this;
  	        const menu = document.createElement( 'span' );
  	        const style = menu.style;
  
  	        style.padding = '5px 0';
  	        style.position = 'fixed';
  	        style.bottom = '100%';
  	        style.right = '14px';
  	        style.backgroundColor = '#fafafa';
  	        style.fontFamily = 'Helvetica Neue';
  	        style.fontSize = '14px';
  	        style.visibility = 'hidden';
  	        style.opacity = 0;
  	        style.boxShadow = '0 0 12pt rgba(0,0,0,0.25)';
  	        style.borderRadius = '2px';
  	        style.overflow = 'hidden';
  	        style.willChange = 'width, height, opacity';
  	        style.pointerEvents = 'auto';
  	        style.transition = this.DEFAULT_TRANSITION;
  
  	        menu.visible = false;
  
  	        menu.changeSize = function ( width, height ) {
  
  	            if ( width ) {
  
  	                this.style.width = width + 'px';
  
  	            }
  
  	            if ( height ) {
  
  	                this.style.height = height + 'px';
  
  	            }
  
  	        };
  
  	        menu.show = function () {
  
  	            this.style.opacity = 1;
  	            this.style.visibility = 'visible';
  	            this.visible = true;
  
  	        };
  
  	        menu.hide = function () {
  
  	            this.style.opacity = 0;
  	            this.style.visibility = 'hidden';
  	            this.visible = false;
  
  	        };
  
  	        menu.toggle = function () {
  
  	            if ( this.visible ) {
  
  	                this.hide();
  
  	            } else {
  
  	                this.show();
  
  	            }
  
  	        };
  
  	        menu.slideAll = function ( right ) {
  
  	            for ( let i = 0; i < menu.children.length; i++ ){
  
  	                if ( menu.children[ i ].slide ) {
  
  	                    menu.children[ i ].slide( right );
  
  	                }
  
  	            }
  
  	        };
  
  	        menu.unslideAll = function () {
  
  	            for ( let i = 0; i < menu.children.length; i++ ){
  
  	                if ( menu.children[ i ].unslide ) {
  
  	                    menu.children[ i ].unslide();
  
  	                }
  
  	            }
  
  	        };
  
  	        menu.addHeader = function ( title ) {
  
  	            const header = scope.createMenuItemHeader( title );
  	            header.type = 'header';
  
  	            this.appendChild( header );
  
  	            return header;
  
  	        };
  
  	        menu.addItem = function ( title ) {
  
  	            const item = scope.createMenuItem( title );
  	            item.type = 'item';
  
  	            this.appendChild( item );
  
  	            return item;
  
  	        };
  
  	        menu.setActiveItem = function ( item ) {
  
  	            if ( this.activeItem ) {
  
  	                this.activeItem.setIcon( ' ' );
  
  	            }
  
  	            item.setIcon( DataImage.Check );
  
  	            this.activeItem = item;
  
  	        };
  
  	        menu.addEventListener( 'mousemove', this.PREVENT_EVENT_HANDLER, true );
  	        menu.addEventListener( 'mouseup', this.PREVENT_EVENT_HANDLER, true );
  	        menu.addEventListener( 'mousedown', this.PREVENT_EVENT_HANDLER, true );
  
  	        return menu;
  
  	    },
  
  	    /**
  	     * Create custom item element
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLSpanElement} - The dom element icon
  	     */
  	    createCustomItem: function ( options = {} ) {
  
  	        const scope = this;
  	        const item = options.element || document.createElement( 'span' );
  	        const { onDispose } = options;
  
  	        item.style.cursor = 'pointer';
  	        item.style.float = 'right';
  	        item.style.width = '44px';
  	        item.style.height = '100%';
  	        item.style.backgroundSize = '60%';
  	        item.style.backgroundRepeat = 'no-repeat';
  	        item.style.backgroundPosition = 'center';
  	        item.style.webkitUserSelect = 
  			item.style.MozUserSelect = 
  			item.style.userSelect = 'none';
  	        item.style.position = 'relative';
  	        item.style.pointerEvents = 'auto';
  
  	        // White glow on icon
  	        item.addEventListener( scope.TOUCH_ENABLED ? 'touchstart' : 'mouseenter', function() {
  	            item.style.filter = 
  				item.style.webkitFilter = 'drop-shadow(0 0 5px rgba(255,255,255,1))';
  	        }, { passive: true });
  	        item.addEventListener( scope.TOUCH_ENABLED ? 'touchend' : 'mouseleave', function() {
  	            item.style.filter = 
  				item.style.webkitFilter = '';
  	        }, { passive: true });
  
  	        this.mergeStyleOptions( item, options.style );
  
  	        if ( options.onTap ) {
  
  	            item.addEventListener( scope.TOUCH_ENABLED ? 'touchend' : 'click', options.onTap, false );
  
  	        }
  
  	        item.dispose = function () {
  
  	            item.removeEventListener( scope.TOUCH_ENABLED ? 'touchend' : 'click', options.onTap, false );
  
  	            if ( onDispose ) { options.onDispose(); }
  
  	        };
  			
  	        return item;
  
  	    },
  
  	    /**
  	     * Merge item css style
  	     * @param  {HTMLElement} element - The element to be merged with style
  	     * @param  {object} options - The style options
  	     * @memberOf Widget
  	     * @instance
  	     * @return {HTMLElement} - The same element with merged styles
  	     */
  	    mergeStyleOptions: function ( element, options = {} ) {
  
  	        for ( let property in options ){
  
  	            if ( options.hasOwnProperty( property ) ) {
  
  	                element.style[ property ] = options[ property ];
  
  	            }
  
  	        }
  
  	        return element;
  
  	    },
  
  	    /**
  	     * Dispose widgets by detaching dom elements from container
  	     * @memberOf Widget
  	     * @instance
  	     */
  	    dispose: function () {
  
  	        if ( this.barElement ) {
  	            this.container.removeChild( this.barElement );
  	            this.barElement.dispose();
  	            this.barElement = null;
  
  	        }
  
  	    }
  		
  	} );
  
  	/**
  	 * @classdesc Base Panorama
  	 * @constructor
  	 * @param {THREE.Geometry} geometry - The geometry for this panorama
  	 * @param {THREE.Material} material - The material for this panorama
  	 */
  	function Panorama ( geometry, material ) {
  
  	    THREE.Mesh.call( this, geometry, material );
  
  	    this.type = 'panorama';
  
  	    this.ImageQualityLow = 1;
  	    this.ImageQualityFair = 2;
  	    this.ImageQualityMedium = 3;
  	    this.ImageQualityHigh = 4;
  	    this.ImageQualitySuperHigh = 5;
  
  	    this.animationDuration = 1000;
  
  	    this.defaultInfospotSize = 350;
  
  	    this.container = undefined;
  
  	    this.loaded = false;
  
  	    this.linkedSpots = [];
  
  	    this.isInfospotVisible = false;
  		
  	    this.linkingImageURL = undefined;
  	    this.linkingImageScale = undefined;
  
  	    this.material.side = THREE.BackSide;
  	    this.material.opacity = 0;
  
  	    this.scale.x *= -1;
  	    this.renderOrder = -1;
  
  	    this.active = false;
  
  	    this.infospotAnimation = new Tween.Tween( this ).to( {}, this.animationDuration / 2 );
  
  	    this.addEventListener( 'load', this.fadeIn.bind( this ) );
  	    this.addEventListener( 'panolens-container', this.setContainer.bind( this ) );
  	    this.addEventListener( 'click', this.onClick.bind( this ) );
  
  	    this.setupTransitions();
  
  	}
  
  	Panorama.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), {
  
  	    constructor: Panorama,
  
  	    /**
  	     * Adding an object
  	     * To counter the scale.x = -1, it will automatically add an 
  	     * empty object with inverted scale on x
  	     * @memberOf Panorama
  	     * @instance
  	     * @param {THREE.Object3D} object - The object to be added
  	     */
  	    add: function ( object ) {
  
  	        let invertedObject;
  
  	        if ( arguments.length > 1 ) {
  
  	            for ( var i = 0; i < arguments.length; i ++ ) {
  
  	                this.add( arguments[ i ] );
  
  	            }
  
  	            return this;
  
  	        }
  
  	        // In case of infospots
  	        if ( object instanceof Infospot ) {
  
  	            invertedObject = object;
  
  	            if ( object.dispatchEvent ) {
  
  	                const { container } = this;
  
  	                if ( container ) { object.dispatchEvent( { type: 'panolens-container', container } ); }
  					
  	                object.dispatchEvent( { type: 'panolens-infospot-focus', method: function ( vector, duration, easing ) {
  
  	                    /**
  	                     * Infospot focus handler event
  	                     * @type {object}
  	                     * @event Panorama#panolens-viewer-handler
  	                     * @property {string} method - Viewer function name
  	                     * @property {*} data - The argument to be passed into the method
  	                     */
  	                    this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'tweenControlCenter', data: [ vector, duration, easing ] } );
  
  
  	                }.bind( this ) } );
  	            }
  
  	        } else {
  
  	            // Counter scale.x = -1 effect
  	            invertedObject = new THREE.Object3D();
  	            invertedObject.scale.x = -1;
  	            invertedObject.scalePlaceHolder = true;
  	            invertedObject.add( object );
  
  	        }
  
  	        THREE.Object3D.prototype.add.call( this, invertedObject );
  
  	    },
  
  	    load: function () {
  
  	        this.onLoad();
  			
  	    },
  
  	    /**
  	     * Click event handler
  	     * @param  {object} event - Click event
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Infospot#dismiss
  	     */
  	    onClick: function ( event ) {
  
  	        if ( event.intersects && event.intersects.length === 0 ) {
  
  	            this.traverse( function ( object ) {
  
  	                /**
  	                 * Dimiss event
  	                 * @type {object}
  	                 * @event Infospot#dismiss
  	                 */
  	                object.dispatchEvent( { type: 'dismiss' } );
  
  	            } );
  
  	        }
  
  	    },
  
  	    /**
  	     * Set container of this panorama 
  	     * @param {HTMLElement|object} data - Data with container information
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Infospot#panolens-container
  	     */
  	    setContainer: function ( data ) {
  
  	        let container;
  
  	        if ( data instanceof HTMLElement ) {
  
  	            container = data;
  
  	        } else if ( data && data.container ) {
  
  	            container = data.container;
  
  	        }
  
  	        if ( container ) {
  
  	            this.children.forEach( function ( child ) {
  
  	                if ( child instanceof Infospot && child.dispatchEvent ) {
  
  	                    /**
  	                     * Set container event
  	                     * @type {object}
  	                     * @event Infospot#panolens-container
  	                     * @property {HTMLElement} container - The container of this panorama
  	                     */
  	                    child.dispatchEvent( { type: 'panolens-container', container: container } );
  
  	                }
  
  	            } );
  
  	            this.container = container;
  
  	        }
  
  	    },
  
  	    /**
  	     * This will be called when panorama is loaded
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Panorama#load
  	     */
  	    onLoad: function () {
  
  	        this.loaded = true;
  
  	        /**
  	         * Load panorama event
  	         * @type {object}
  	         * @event Panorama#load
  	         */
  	        this.dispatchEvent( { type: 'load' } );
  
  	    },
  
  	    /**
  	     * This will be called when panorama is in progress
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Panorama#progress
  	     */
  	    onProgress: function ( progress ) {
  
  	        /**
  	         * Loading panorama progress event
  	         * @type {object}
  	         * @event Panorama#progress
  	         * @property {object} progress - The progress object containing loaded and total amount
  	         */
  	        this.dispatchEvent( { type: 'progress', progress: progress } );
  
  	    },
  
  	    /**
  	     * This will be called when panorama loading has error
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Panorama#error
  	     */
  	    onError: function () {
  
  	        /**
  	         * Loading panorama error event
  	         * @type {object}
  	         * @event Panorama#error
  	         */
  	        this.dispatchEvent( { type: 'error' } );
  
  	    },
  
  	    /**
  	     * Get zoom level based on window width
  	     * @memberOf Panorama
  	     * @instance
  	     * @return {number} zoom level indicating image quality
  	     */
  	    getZoomLevel: function () {
  
  	        let zoomLevel;
  
  	        if ( window.innerWidth <= 800 ) {
  
  	            zoomLevel = this.ImageQualityFair;
  
  	        } else if ( window.innerWidth > 800 &&  window.innerWidth <= 1280 ) {
  
  	            zoomLevel = this.ImageQualityMedium;
  
  	        } else if ( window.innerWidth > 1280 && window.innerWidth <= 1920 ) {
  
  	            zoomLevel = this.ImageQualityHigh;
  
  	        } else if ( window.innerWidth > 1920 ) {
  
  	            zoomLevel = this.ImageQualitySuperHigh;
  
  	        } else {
  
  	            zoomLevel = this.ImageQualityLow;
  
  	        }
  
  	        return zoomLevel;
  
  	    },
  
  	    /**
  	     * Update texture of a panorama
  	     * @memberOf Panorama
  	     * @instance
  	     * @param {THREE.Texture} texture - Texture to be updated
  	     */
  	    updateTexture: function ( texture ) {
  
  	        this.material.map = texture;
  	        this.material.needsUpdate = true;
  
  	    },
  
  	    /**
  	     * Toggle visibility of infospots in this panorama
  	     * @param  {boolean} isVisible - Visibility of infospots
  	     * @param  {number} delay - Delay in milliseconds to change visibility
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Panorama#infospot-animation-complete
  	     */
  	    toggleInfospotVisibility: function ( isVisible, delay ) {
  
  	        delay = ( delay !== undefined ) ? delay : 0;
  
  	        const visible = ( isVisible !== undefined ) ? isVisible : ( this.isInfospotVisible ? false : true );
  
  	        this.traverse( function ( object ) {
  
  	            if ( object instanceof Infospot ) {
  
  	                if ( visible ) {
  
  	                    object.show( delay );
  
  	                } else {
  
  	                    object.hide( delay );
  
  	                }
  
  	            }
  
  	        } );
  
  	        this.isInfospotVisible = visible;
  
  	        // Animation complete event
  	        this.infospotAnimation.onComplete( function () {
  
  	            /**
  	             * Complete toggling infospot visibility
  	             * @event Panorama#infospot-animation-complete
  	             * @type {object} 
  	             */
  	            this.dispatchEvent( { type: 'infospot-animation-complete', visible: visible } );
  
  	        }.bind( this ) ).delay( delay ).start();
  
  	    },
  
  	    /**
  	     * Set image of this panorama's linking infospot
  	     * @memberOf Panorama
  	     * @instance
  	     * @param {string} url   - Url to the image asset
  	     * @param {number} scale - Scale factor of the infospot
  	     */
  	    setLinkingImage: function ( url, scale ) {
  
  	        this.linkingImageURL = url;
  	        this.linkingImageScale = scale;
  
  	    },
  
  	    /**
  	     * Link one-way panorama
  	     * @param  {Panorama} pano  - The panorama to be linked to
  	     * @param  {THREE.Vector3} position - The position of infospot which navigates to the pano
  	     * @param  {number} [imageScale=300] - Image scale of linked infospot
  	     * @param  {string} [imageSrc=DataImage.Arrow] - The image source of linked infospot
  	     * @memberOf Panorama
  	     * @instance
  	     */
  	    link: function ( pano, position, imageScale, imageSrc ) {
  
  	        let scale, img;
  
  	        this.visible = true;
  
  	        if ( !position ) {
  
  	            console.warn( 'Please specify infospot position for linking' );
  
  	            return;
  
  	        }
  
  	        // Infospot scale
  	        if ( imageScale !== undefined ) {
  
  	            scale = imageScale;
  
  	        } else if ( pano.linkingImageScale !== undefined ) {
  
  	            scale = pano.linkingImageScale;
  
  	        } else {
  
  	            scale = 300;
  
  	        }
  
  
  	        // Infospot image
  	        if ( imageSrc ) {
  
  	            img = imageSrc;
  
  	        } else if ( pano.linkingImageURL ) {
  
  	            img = pano.linkingImageURL;
  
  	        } else {
  
  	            img = DataImage.Arrow;
  
  	        }
  
  	        // Creates a new infospot
  	        const spot = new Infospot( scale, img );
  	        spot.position.copy( position );
  	        spot.toPanorama = pano;
  	        spot.addEventListener( 'click', function () {
  
  	            /**
  	             * Viewer handler event
  	             * @type {object}
  	             * @event Panorama#panolens-viewer-handler
  	             * @property {string} method - Viewer function name
  	             * @property {*} data - The argument to be passed into the method
  	             */
  	            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'setPanorama', data: pano } );
  
  	        }.bind( this ) );
  
  	        this.linkedSpots.push( spot );
  
  	        this.add( spot );
  
  	        this.visible = false;
  
  	    },
  
  	    reset: function () {
  
  	        this.children.length = 0;	
  
  	    },
  
  	    setupTransitions: function () {
  
  	        this.fadeInAnimation = new Tween.Tween( this.material )
  	            .easing( Tween.Easing.Quartic.Out )
  	            .onStart( function () {
  
  	                this.visible = true;
  	                // this.material.visible = true;
  
  	                /**
  	                 * Enter panorama fade in start event
  	                 * @event Panorama#enter-fade-start
  	                 * @type {object} 
  	                 */
  	                this.dispatchEvent( { type: 'enter-fade-start' } );
  
  	            }.bind( this ) );
  
  	        this.fadeOutAnimation = new Tween.Tween( this.material )
  	            .easing( Tween.Easing.Quartic.Out )
  	            .onComplete( function () {
  
  	                this.visible = false;
  	                // this.material.visible = true;
  
  	                /**
  	                 * Leave panorama complete event
  	                 * @event Panorama#leave-complete
  	                 * @type {object} 
  	                 */
  	                this.dispatchEvent( { type: 'leave-complete' } );
  
  	            }.bind( this ) );
  
  	        this.enterTransition = new Tween.Tween( this )
  	            .easing( Tween.Easing.Quartic.Out )
  	            .onComplete( function () {
  
  	                /**
  	                 * Enter panorama and animation complete event
  	                 * @event Panorama#enter-complete
  	                 * @type {object} 
  	                 */
  	                this.dispatchEvent( { type: 'enter-complete' } );
  
  	            }.bind ( this ) )
  	            .start();
  
  	        this.leaveTransition = new Tween.Tween( this )
  	            .easing( Tween.Easing.Quartic.Out );
  
  	    },
  
  	    onFadeAnimationUpdate: function () {
  
  	        const alpha = this.material.opacity;
  	        const { uniforms } = this.material;
  
  	        if ( uniforms && uniforms.opacity ) {
  	            uniforms.opacity.value = alpha;
  	        }
  
  	    },
  
  	    /**
  	     * Start fading in animation
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Panorama#enter-fade-complete
  	     */
  	    fadeIn: function ( duration ) {
  
  	        duration = duration >= 0 ? duration : this.animationDuration;
  
  	        this.fadeOutAnimation.stop();
  	        this.fadeInAnimation
  	            .to( { opacity: 1 }, duration )
  	            .onUpdate( this.onFadeAnimationUpdate.bind( this ) )
  	            .onComplete( function () {
  
  	                this.toggleInfospotVisibility( true, duration / 2 );
  
  	                /**
  	                 * Enter panorama fade complete event
  	                 * @event Panorama#enter-fade-complete
  	                 * @type {object} 
  	                 */
  	                this.dispatchEvent( { type: 'enter-fade-complete' } );			
  
  	            }.bind( this ) )
  	            .start();
  
  	    },
  
  	    /**
  	     * Start fading out animation
  	     * @memberOf Panorama
  	     * @instance
  	     */
  	    fadeOut: function ( duration ) {
  
  	        duration = duration >= 0 ? duration : this.animationDuration;
  
  	        this.fadeInAnimation.stop();
  	        this.fadeOutAnimation
  	            .to( { opacity: 0 }, duration )
  	            .onUpdate( this.onFadeAnimationUpdate.bind( this ) )
  	            .start();
  
  	    },
  
  	    /**
  	     * This will be called when entering a panorama 
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Panorama#enter
  	     * @fires Panorama#enter-start
  	     */
  	    onEnter: function () {
  
  	        const duration = this.animationDuration;
  
  	        this.leaveTransition.stop();
  	        this.enterTransition
  	            .to( {}, duration )
  	            .onStart( function () {
  
  	                /**
  	                 * Enter panorama and animation starting event
  	                 * @event Panorama#enter-start
  	                 * @type {object} 
  	                 */
  	                this.dispatchEvent( { type: 'enter-start' } );
  					
  	                if ( this.loaded ) {
  
  	                    this.fadeIn( duration );
  
  	                } else {
  
  	                    this.load();
  
  	                }
  					
  	            }.bind( this ) )
  	            .start();
  
  	        /**
  	         * Enter panorama event
  	         * @event Panorama#enter
  	         * @type {object} 
  	         */
  	        this.dispatchEvent( { type: 'enter' } );
  
  	        this.children.forEach( child => {
  
  	            child.dispatchEvent( { type: 'panorama-enter' } );
  
  	        } );
  
  	        this.active = true;
  
  	    },
  
  	    /**
  	     * This will be called when leaving a panorama
  	     * @memberOf Panorama
  	     * @instance
  	     * @fires Panorama#leave
  	     */
  	    onLeave: function () {
  
  	        const duration = this.animationDuration;
  
  	        this.enterTransition.stop();
  	        this.leaveTransition
  	            .to( {}, duration )
  	            .onStart( function () {
  
  	                /**
  	                 * Leave panorama and animation starting event
  	                 * @event Panorama#leave-start
  	                 * @type {object} 
  	                 */
  	                this.dispatchEvent( { type: 'leave-start' } );
  
  	                this.fadeOut( duration );
  	                this.toggleInfospotVisibility( false );
  
  	            }.bind( this ) )
  	            .start();
  
  	        /**
  	         * Leave panorama event
  	         * @event Panorama#leave
  	         * @type {object} 
  	         */
  	        this.dispatchEvent( { type: 'leave' } );
  
  	        this.children.forEach( child => {
  
  	            child.dispatchEvent( { type: 'panorama-leave' } );
  
  	        } );
  
  	        this.active = false;
  
  	    },
  
  	    /**
  	     * Dispose panorama
  	     * @memberOf Panorama
  	     * @instance
  	     */
  	    dispose: function () {
  
  	        this.infospotAnimation.stop();
  	        this.fadeInAnimation.stop();
  	        this.fadeOutAnimation.stop();
  	        this.enterTransition.stop();
  	        this.leaveTransition.stop();
  
  	        /**
  	         * On panorama dispose handler
  	         * @type {object}
  	         * @event Panorama#panolens-viewer-handler
  	         * @property {string} method - Viewer function name
  	         * @property {*} data - The argument to be passed into the method
  	         */
  	        this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'onPanoramaDispose', data: this } );
  
  	        // recursive disposal on 3d objects
  	        function recursiveDispose ( object ) {
  
  	            const { geometry, material } = object;
  
  	            for ( var i = object.children.length - 1; i >= 0; i-- ) {
  
  	                recursiveDispose( object.children[i] );
  	                object.remove( object.children[i] );
  
  	            }
  
  	            if ( object instanceof Infospot ) {
  
  	                object.dispose();
  
  	            }
  				
  	            if ( geometry ) { geometry.dispose(); object.geometry = null; }
  	            if ( material ) { material.dispose(); object.material = null; }
  
  	        }
  
  	        recursiveDispose( this );
  
  	        if ( this.parent ) {
  
  	            this.parent.remove( this );
  
  	        }
  
  	    }
  
  	} );
  
  	/**
  	 * @classdesc Equirectangular based image panorama
  	 * @constructor
  	 * @param {string} image - Image url or HTMLImageElement
  	 */
  	function ImagePanorama ( image, user_radius = 9999, user_height = 8000, _geometry, _material ) {
  
  	    const radius = user_radius;
  	    const geometry = _geometry || new THREE.CylinderBufferGeometry( radius, radius, user_height, 64, 1, true);
  	    const material = _material || new THREE.MeshBasicMaterial( { opacity: 0, transparent: true } );
  
  	    Panorama.call( this, geometry, material );
  
  	    this.src = image;
  	    this.radius = radius;
  
  	}
  
  	ImagePanorama.prototype = Object.assign( Object.create( Panorama.prototype ), {
  
  	    constructor: ImagePanorama,
  
  	    /**
  	     * Load image asset
  	     * @param  {*} src - Url or image element
  	     * @memberOf ImagePanorama
  	     * @instance
  	     */
  	    load: function ( src ) {
  
  	        src = src || this.src;
  
  	        if ( !src ) { 
  
  	            console.warn( 'Image source undefined' );
  
  	            return; 
  
  	        } else if ( typeof src === 'string' ) {
  
  	            TextureLoader.load( src, this.onLoad.bind( this ), this.onProgress.bind( this ), this.onError.bind( this ) );
  
  	        } else if ( src instanceof HTMLImageElement ) {
  
  	            this.onLoad( new THREE.Texture( src ) );
  
  	        }
  
  	    },
  
  	    /**
  	     * This will be called when image is loaded
  	     * @param  {THREE.Texture} texture - Texture to be updated
  	     * @memberOf ImagePanorama
  	     * @instance
  	     */
  	    onLoad: function ( texture ) {
  
  	        texture.minFilter = texture.magFilter = THREE.LinearFilter;
  	        texture.needsUpdate = true;
  			
  	        this.updateTexture( texture );
  
  	        window.requestAnimationFrame( Panorama.prototype.onLoad.bind( this ) );
  
  	    },
  
  	    /**
  	     * Reset
  	     * @memberOf ImagePanorama
  	     * @instance
  	     */
  	    reset: function () {
  
  	        Panorama.prototype.reset.call( this );
  
  	    },
  
  	    /**
  	     * Dispose
  	     * @memberOf ImagePanorama
  	     * @instance
  	     */
  	    dispose: function () {
  
  	        const { material: { map } } = this;
  
  	        // Release cached image
  	        THREE.Cache.remove( this.src );
  
  	        if ( map ) { map.dispose(); }
  
  	        Panorama.prototype.dispose.call( this );
  
  	    }
  
  	} );
  
  	/**
  	 * @classdesc Empty panorama
  	 * @constructor
  	 */
  	function EmptyPanorama () {
  
  	    const geometry = new THREE.BufferGeometry();
  	    const material = new THREE.MeshBasicMaterial( { color: 0x000000, opacity: 0, transparent: true } );
  
  	    geometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array(), 1 ) );
  
  	    Panorama.call( this, geometry, material );
  
  	}
  
  	EmptyPanorama.prototype = Object.assign( Object.create( Panorama.prototype ), {
  
  	    constructor: EmptyPanorama
  
  	} );
  
  	/**
  	 * @classdesc Cubemap-based panorama
  	 * @constructor
  	 * @param {array} images - Array of 6 urls to images, one for each side of the CubeTexture. The urls should be specified in the following order: pos-x, neg-x, pos-y, neg-y, pos-z, neg-z
  	 */
  	function CubePanorama ( images = [] ){
  
  	    const edgeLength = 10000;
  	    const shader = Object.assign( {}, THREE.ShaderLib[ 'cube' ] );
  	    const geometry = new THREE.BoxBufferGeometry( edgeLength, edgeLength, edgeLength );
  	    const material = new THREE.ShaderMaterial( {
  
  	        fragmentShader: shader.fragmentShader,
  	        vertexShader: shader.vertexShader,
  	        uniforms: shader.uniforms,
  	        side: THREE.BackSide,
  	        transparent: true
  
  	    } );
  
  	    Panorama.call( this, geometry, material );
  
  	    this.images = images;
  	    this.edgeLength = edgeLength;
  	    this.material.uniforms.opacity.value = 0;
  
  	}
  
  	CubePanorama.prototype = Object.assign( Object.create( Panorama.prototype ), {
  
  	    constructor: CubePanorama,
  
  	    /**
  	     * Load 6 images and bind listeners
  	     * @memberOf CubePanorama
  	     * @instance
  	     */
  	    load: function () {
  
  	        CubeTextureLoader.load( 	
  
  	            this.images, 
  
  	            this.onLoad.bind( this ), 
  	            this.onProgress.bind( this ), 
  	            this.onError.bind( this ) 
  
  	        );
  
  	    },
  
  	    /**
  	     * This will be called when 6 textures are ready
  	     * @param  {THREE.CubeTexture} texture - Cube texture
  	     * @memberOf CubePanorama
  	     * @instance
  	     */
  	    onLoad: function ( texture ) {
  			
  	        this.material.uniforms[ 'tCube' ].value = texture;
  
  	        Panorama.prototype.onLoad.call( this );
  
  	    },
  
  	    /**
  	     * Dispose
  	     * @memberOf CubePanorama
  	     * @instance
  	     */
  	    dispose: function () {	
  
  	        const { value } = this.material.uniforms.tCube;
  
  	        this.images.forEach( ( image ) => { THREE.Cache.remove( image ); } );
  
  	        if ( value instanceof THREE.CubeTexture ) {
  
  	            value.dispose();
  
  	        }
  
  	        Panorama.prototype.dispose.call( this );
  
  	    }
  
  	} );
  
  	/**
  	 * @classdesc Basic panorama with 6 pre-defined grid images
  	 * @constructor
  	 */
  	function BasicPanorama () {
  
  	    const images = [];
  
  	    for ( let i = 0; i < 6; i++ ) {
  
  	        images.push( DataImage.WhiteTile );
  
  	    }
  
  	    CubePanorama.call( this, images );
  
  	}
  
  	BasicPanorama.prototype = Object.assign( Object.create( CubePanorama.prototype ), {
  
  	    constructor: BasicPanorama
  
  	} );
  
  	/**
  	 * @classdesc Video Panorama
  	 * @constructor
  	 * @param {string} src - Equirectangular video url
  	 * @param {object} [options] - Option for video settings
  	 * @param {HTMLElement} [options.videoElement] - HTML5 video element contains the video
  	 * @param {boolean} [options.loop=true] - Specify if the video should loop in the end
  	 * @param {boolean} [options.muted=true] - Mute the video or not. Need to be true in order to autoplay on some browsers
  	 * @param {boolean} [options.autoplay=false] - Specify if the video should auto play
  	 * @param {boolean} [options.playsinline=true] - Specify if video should play inline for iOS. If you want it to auto play inline, set both autoplay and muted options to true
  	 * @param {string} [options.crossOrigin="anonymous"] - Sets the cross-origin attribute for the video, which allows for cross-origin videos in some browsers (Firefox, Chrome). Set to either "anonymous" or "use-credentials".
  	 * @param {number} [radius=5000] - The minimum radius for this panoram
  	 */
  	function VideoPanorama ( src, options = {} ) {
  
  	    const radius = 5000;
  	    const geometry = new THREE.SphereBufferGeometry( radius, 60, 40 );
  	    const material = new THREE.MeshBasicMaterial( { opacity: 0, transparent: true } );
  
  	    Panorama.call( this, geometry, material );
  
  	    this.src = src;
  
  	    this.options = {
  
  	        videoElement: document.createElement( 'video' ),
  	        loop: true,
  	        muted: true,
  	        autoplay: false,
  	        playsinline: true,
  	        crossOrigin: 'anonymous'
  
  	    };
  
  	    Object.assign( this.options, options );
  
  	    this.videoElement = this.options.videoElement;
  	    this.videoProgress = 0;
  	    this.radius = radius;
  
  	    this.addEventListener( 'leave', this.pauseVideo.bind( this ) );
  	    this.addEventListener( 'enter-fade-start', this.resumeVideoProgress.bind( this ) );
  	    this.addEventListener( 'video-toggle', this.toggleVideo.bind( this ) );
  	    this.addEventListener( 'video-time', this.setVideoCurrentTime.bind( this ) );
  
  	}
  	VideoPanorama.prototype = Object.assign( Object.create( Panorama.prototype ), {
  
  	    constructor: VideoPanorama,
  
  	    isMobile: function () {
  
  	        let check = false;
  	        (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})( window.navigator.userAgent || window.navigator.vendor || window.opera );
  	        return check;
  
  	    },
  
  	    /**
  	     * Load video panorama
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @fires  Panorama#panolens-viewer-handler
  	     */
  	    load: function () {
  
  	        const { muted, loop, autoplay, playsinline, crossOrigin } = this.options;
  	        const video = this.videoElement;
  	        const material = this.material;
  	        const onProgress = this.onProgress.bind( this );
  	        const onLoad = this.onLoad.bind( this );
  
  	        video.loop = loop;
  	        video.autoplay = autoplay;
  	        video.playsinline = playsinline;
  	        video.crossOrigin = crossOrigin;
  	        video.muted = muted;
  			
  	        if ( playsinline ) {
  
  	            video.setAttribute( 'playsinline', '' );
  	            video.setAttribute( 'webkit-playsinline', '' );
  
  	        } 
  
  	        const onloadeddata = function() {
  
  	            this.setVideoTexture( video );
  
  	            if ( autoplay ) {
  
  	                /**
  	                 * Viewer handler event
  	                 * @type {object}
  	                 * @property {string} method - 'updateVideoPlayButton'
  	                 * @property {boolean} data - Pause video or not
  	                 */
  	                this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: false } );
  
  	            }
  
  	            // For mobile silent autoplay
  	            if ( this.isMobile() ) {
  
  	                video.pause();
  
  	                if ( autoplay && muted ) {
  
  	                    /**
  	                     * Viewer handler event
  	                     * @type {object}
  	                     * @property {string} method - 'updateVideoPlayButton'
  	                     * @property {boolean} data - Pause video or not
  	                     */
  	                    this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: false } );
  
  	                } else {
  
  	                    /**
  	                     * Viewer handler event
  	                     * @type {object}
  	                     * @property {string} method - 'updateVideoPlayButton'
  	                     * @property {boolean} data - Pause video or not
  	                     */
  	                    this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: true } );
  
  	                }
  					
  	            }
  
  	            const loaded = () => {
  
  	                // Fix for threejs r89 delayed update
  	                material.map.needsUpdate = true;
  
  	                onProgress( { loaded: 1, total: 1 } );
  	                onLoad();
  
  	            };
  
  	            window.requestAnimationFrame( loaded );
  				
  	        };
  
  	        /**
  	         * Ready state of the audio/video element
  	         * 0 = HAVE_NOTHING - no information whether or not the audio/video is ready
  	         * 1 = HAVE_METADATA - metadata for the audio/video is ready
  	         * 2 = HAVE_CURRENT_DATA - data for the current playback position is available, but not enough data to play next frame/millisecond
  	         * 3 = HAVE_FUTURE_DATA - data for the current and at least the next frame is available
  	         * 4 = HAVE_ENOUGH_DATA - enough data available to start playing
  	         */
  	        if ( video.readyState > 2 ) {
  
  	            onloadeddata.call( this );
  
  	        } else {
  
  	            if ( video.querySelectorAll( 'source' ).length === 0 ) {
  
  	                const source = document.createElement( 'source' );
  	                source.src = this.src;
  	                video.appendChild( source );
  
  	            }
  
  	            video.load();
  	        }
  
  	        video.addEventListener( 'loadeddata', onloadeddata.bind( this ) );
  			
  	        video.addEventListener( 'timeupdate', function () {
  
  	            this.videoProgress = video.duration >= 0 ? video.currentTime / video.duration : 0;
  
  	            /**
  	             * Viewer handler event
  	             * @type {object}
  	             * @property {string} method - 'onVideoUpdate'
  	             * @property {number} data - The percentage of video progress. Range from 0.0 to 1.0
  	             */
  	            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'onVideoUpdate', data: this.videoProgress } );
  
  	        }.bind( this ) );
  
  	        video.addEventListener( 'ended', function () {
  				
  	            if ( !loop ) {
  
  	                this.resetVideo();
  	                this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: true } );
  
  	            }
  
  	        }.bind( this ), false ); 
  
  	    },
  
  	    /**
  	     * Set video texture
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @param {HTMLVideoElement} video  - The html5 video element
  	     * @fires Panorama#panolens-viewer-handler
  	     */
  	    setVideoTexture: function ( video ) {
  
  	        if ( !video ) return;
  
  	        const videoTexture = new THREE.VideoTexture( video );
  	        videoTexture.minFilter = THREE.LinearFilter;
  	        videoTexture.magFilter = THREE.LinearFilter;
  	        videoTexture.format = THREE.RGBFormat;
  
  	        this.updateTexture( videoTexture );
  		
  	    },
  
  	    /**
  	     * Reset
  	     * @memberOf VideoPanorama
  	     * @instance
  	     */
  	    reset: function () {
  
  	        this.videoElement = undefined;	
  
  	        Panorama.prototype.reset.call( this );
  
  	    },
  
  	    /**
  	     * Check if video is paused
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @return {boolean} - is video paused or not
  	     */
  	    isVideoPaused: function () {
  
  	        return this.videoElement.paused;
  
  	    },
  
  	    /**
  	     * Toggle video to play or pause
  	     * @memberOf VideoPanorama
  	     * @instance
  	     */
  	    toggleVideo: function () {
  
  	        const video = this.videoElement;
  
  	        if ( !video ) { return; }
  
  	        video[ video.paused ? 'play' : 'pause' ]();
  
  	    },
  
  	    /**
  	     * Set video currentTime
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @param {object} event - Event contains percentage. Range from 0.0 to 1.0
  	     */
  	    setVideoCurrentTime: function ( { percentage } ) {
  
  	        const video = this.videoElement;
  
  	        if ( video && !Number.isNaN( percentage ) && percentage !== 1 ) {
  
  	            video.currentTime = video.duration * percentage;
  
  	            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'onVideoUpdate', data: percentage } );
  
  	        }
  
  	    },
  
  	    /**
  	     * Play video
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @fires VideoPanorama#play
  	     * @fires VideoPanorama#play-error
  	     */
  	    playVideo: function () {
  
  	        const video = this.videoElement;
  	        const playVideo = this.playVideo.bind( this );
  	        const dispatchEvent = this.dispatchEvent.bind( this );
  	        const onSuccess = () => {
  
  	            /**
  	             * Play event
  	             * @type {object}
  	             * @event VideoPanorama#play
  	             *
  	             */
  	            dispatchEvent( { type: 'play' } );
  
  	        };
  	        const onError = ( error ) => {
  
  	            // Error playing video. Retry next frame. Possibly Waiting for user interaction
  	            window.requestAnimationFrame( playVideo );
  
  	            /**
  	             * Play event
  	             * @type {object}
  	             * @event VideoPanorama#play-error
  	             *
  	             */
  	            dispatchEvent( { type: 'play-error', error } );
  
  	        };
  
  	        if ( video && video.paused ) {
  
  	            video.play().then( onSuccess ).catch( onError );
  
  	        }
  
  	    },
  
  	    /**
  	     * Pause video
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @fires VideoPanorama#pause
  	     */
  	    pauseVideo: function () {
  
  	        const video = this.videoElement;
  
  	        if ( video && !video.paused ) {
  
  	            video.pause();
  
  	        }
  
  	        /**
  	         * Pause event
  	         * @type {object}
  	         * @event VideoPanorama#pause
  	         *
  	         */
  	        this.dispatchEvent( { type: 'pause' } );
  
  	    },
  
  	    /**
  	     * Resume video
  	     * @memberOf VideoPanorama
  	     * @instance
  	     */
  	    resumeVideoProgress: function () {
  
  	        const video = this.videoElement;
  
  	        if ( video.readyState >= 4 && video.autoplay && !this.isMobile() ) {
  
  	            this.playVideo();
  
  	            /**
  	             * Viewer handler event
  	             * @type {object}
  	             * @property {string} method - 'updateVideoPlayButton'
  	             * @property {boolean} data - Pause video or not
  	             */
  	            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: false } );
  
  	        } else {
  
  	            this.pauseVideo();
  
  	            /**
  	             * Viewer handler event
  	             * @type {object}
  	             * @property {string} method - 'updateVideoPlayButton'
  	             * @property {boolean} data - Pause video or not
  	             */
  	            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: true } );
  
  	        }
  
  	        this.setVideoCurrentTime( { percentage: this.videoProgress } );
  
  	    },
  
  	    /**
  	     * Reset video at stating point
  	     * @memberOf VideoPanorama
  	     * @instance
  	     */
  	    resetVideo: function () {
  
  	        const video = this.videoElement;
  
  	        if ( video ) {
  
  	            this.setVideoCurrentTime( { percentage: 0 } );
  
  	        }
  
  	    },
  
  	    /**
  	     * Check if video is muted
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @return {boolean} - is video muted or not
  	     */
  	    isVideoMuted: function () {
  
  	        return this.videoElement.muted;
  
  	    },
  
  	    /**
  	     * Mute video
  	     * @memberOf VideoPanorama
  	     * @instance
  	     */
  	    muteVideo: function () {
  
  	        const video = this.videoElement;
  
  	        if ( video && !video.muted ) {
  
  	            video.muted = true;
  
  	        }
  
  	        this.dispatchEvent( { type: 'volumechange' } );
  
  	    },
  
  	    /**
  	     * Unmute video
  	     * @memberOf VideoPanorama
  	     * @instance
  	     */
  	    unmuteVideo: function () {
  
  	        const video = this.videoElement;
  
  	        if ( video && this.isVideoMuted() ) {
  
  	            video.muted = false;
  
  	        }
  
  	        this.dispatchEvent( { type: 'volumechange' } );
  
  	    },
  
  	    /**
  	     * Returns the video element
  	     * @memberOf VideoPanorama
  	     * @instance
  	     * @returns {HTMLElement}
  	     */
  	    getVideoElement: function () {
  
  	        return this.videoElement;
  
  	    },
  
  	    /**
  	     * Dispose video panorama
  	     * @memberOf VideoPanorama
  	     * @instance
  	     */
  	    dispose: function () {
  
  	        const { material: { map } } = this;
  
  	        this.pauseVideo();
  			
  	        this.removeEventListener( 'leave', this.pauseVideo.bind( this ) );
  	        this.removeEventListener( 'enter-fade-start', this.resumeVideoProgress.bind( this ) );
  	        this.removeEventListener( 'video-toggle', this.toggleVideo.bind( this ) );
  	        this.removeEventListener( 'video-time', this.setVideoCurrentTime.bind( this ) );
  
  	        if ( map ) { map.dispose(); }
  
  	        Panorama.prototype.dispose.call( this );
  
  	    }
  
  	} );
  
  	/**
  	 * @classdesc Google Street View Loader
  	 * @constructor
  	 * @param {object} parameters 
  	 */
  	function GoogleStreetviewLoader ( parameters = {} ) {
  
  	    this._parameters = parameters;
  	    this._zoom = null;
  	    this._panoId = null;
  	    this._panoClient = new google.maps.StreetViewService();
  	    this._count = 0;
  	    this._total = 0;
  	    this._canvas = [];
  	    this._ctx = [];
  	    this._wc = 0;
  	    this._hc = 0;
  	    this.result = null;
  	    this.rotation = 0;
  	    this.copyright = '';
  	    this.onSizeChange = null;
  	    this.onPanoramaLoad = null;
  
  	    this.levelsW = [ 1, 2, 4, 7, 13, 26 ];
  	    this.levelsH = [ 1, 1, 2, 4, 7, 13 ];
  
  	    this.widths = [ 416, 832, 1664, 3328, 6656, 13312 ];
  	    this.heights = [ 416, 416, 832, 1664, 3328, 6656 ];
  
  	    this.maxW = 6656;
  	    this.maxH = 6656;
  
  	    let gl;
  
  	    try {
  
  	        const canvas = document.createElement( 'canvas' );
  
  	        gl = canvas.getContext( 'experimental-webgl' );
  
  	        if( !gl ) {
  
  	            gl = canvas.getContext( 'webgl' );
  
  	        }
  
  	    }
  	    catch ( error ) {
  
  	    }
  
  	    this.maxW = Math.max( gl.getParameter( gl.MAX_TEXTURE_SIZE ), this.maxW );
  	    this.maxH = Math.max( gl.getParameter( gl.MAX_TEXTURE_SIZE ), this.maxH );
  
  	}
  
  	Object.assign( GoogleStreetviewLoader.prototype, {
  
  	    constructor: GoogleStreetviewLoader,
  
  	    /**
  	     * Set progress
  	     * @param {number} loaded 
  	     * @param {number} total 
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    setProgress: function ( loaded, total ) {
  
  	        if ( this.onProgress ) {
  
  	            this.onProgress( { loaded: loaded, total: total } );
  
  	        }
  			
  	    },
  
  	    /**
  	     * Adapt texture to zoom
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    adaptTextureToZoom: function () {
  
  	        const w = this.widths [ this._zoom ];
  	        const h = this.heights[ this._zoom ];
  
  	        const maxW = this.maxW;
  	        const maxH = this.maxH;
  
  	        this._wc = Math.ceil( w / maxW );
  	        this._hc = Math.ceil( h / maxH );
  
  	        for( let y = 0; y < this._hc; y++ ) {
  	            for( let x = 0; x < this._wc; x++ ) {
  	                const c = document.createElement( 'canvas' );
  	                if( x < ( this._wc - 1 ) ) c.width = maxW; else c.width = w - ( maxW * x );
  	                if( y < ( this._hc - 1 ) ) c.height = maxH; else c.height = h - ( maxH * y );
  	                this._canvas.push( c );
  	                this._ctx.push( c.getContext( '2d' ) );
  	            }
  	        }
  
  	    },
  
  	    /**
  	     * Compose from tile
  	     * @param {number} x 
  	     * @param {number} y 
  	     * @param {*} texture 
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    composeFromTile: function ( x, y, texture ) {
  
  	        const maxW = this.maxW;
  	        const maxH = this.maxH;
  
  	        x *= 512;
  	        y *= 512;
  
  	        const px = Math.floor( x / maxW );
  	        const py = Math.floor( y / maxH );
  
  	        x -= px * maxW;
  	        y -= py * maxH;
  
  	        this._ctx[ py * this._wc + px ].drawImage( texture, 0, 0, texture.width, texture.height, x, y, 512, 512 );
  
  	        this.progress();
  			
  	    },
  
  	    /**
  	     * Progress
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    progress: function() {
  
  	        this._count++;
  			
  	        this.setProgress( this._count, this._total );
  			
  	        if ( this._count === this._total) {
  
  	            this.canvas = this._canvas;
  	            this.panoId = this._panoId;
  	            this.zoom = this._zoom;
  
  	            if ( this.onPanoramaLoad ) {
  
  	                this.onPanoramaLoad( this._canvas[ 0 ] );
  
  	            }
  
  	        }
  	    },
  
  	    /**
  	     * Compose panorama
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    composePanorama: function () {
  
  	        this.setProgress( 0, 1 );
  			
  	        const w = this.levelsW[ this._zoom ];
  	        const h = this.levelsH[ this._zoom ];
  	        const self = this;
  				
  	        this._count = 0;
  	        this._total = w * h;
  
  	        const { useWebGL } = this._parameters;
  
  	        for( let y = 0; y < h; y++ ) {
  	            for( let x = 0; x < w; x++ ) {
  	                const url = 'https://geo0.ggpht.com/cbk?cb_client=maps_sv.tactile&authuser=0&hl=en&output=tile&zoom=' + this._zoom + '&x=' + x + '&y=' + y + '&panoid=' + this._panoId + '&nbt&fover=2';
  	                ( function( x, y ) { 
  	                    if( useWebGL ) {
  	                        const texture = TextureLoader.load( url, null, function() {
  	                            self.composeFromTile( x, y, texture );
  	                        } );
  	                    } else {
  	                        const img = new Image();
  	                        img.addEventListener( 'load', function() {
  	                            self.composeFromTile( x, y, this );			
  	                        } );
  	                        img.crossOrigin = '';
  	                        img.src = url;
  	                    }
  	                } )( x, y );
  	            }
  	        }
  			
  	    },
  
  	    /**
  	     * Load
  	     * @param {string} panoid 
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    load: function ( panoid ) {
  
  	        this.loadPano( panoid );
  
  	    },
  
  	    /**
  	     * Load panorama
  	     * @param {string} id
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    loadPano: function( id ) {
  
  	        const self = this;
  	        this._panoClient.getPanoramaById( id, function (result, status) {
  	            if (status === google.maps.StreetViewStatus.OK) {
  	                self.result = result;
  	                self.copyright = result.copyright;
  	                self._panoId = result.location.pano;
  	                self.composePanorama();
  	            }
  	        });
  			
  	    },
  
  	    /**
  	     * Set zoom level
  	     * @param {number} z 
  	     * @memberOf GoogleStreetviewLoader
  	     * @instance
  	     */
  	    setZoom: function( z ) {
  
  	        this._zoom = z;
  	        this.adaptTextureToZoom();
  	    }
  		
  	} );
  
  	/**
  	 * @classdesc Google streetview panorama
  	 * @description [How to get Panorama ID]{@link http://stackoverflow.com/questions/29916149/google-maps-streetview-how-to-get-panorama-id}
  	 * @constructor
  	 * @param {string} panoId - Panorama id from Google Streetview 
  	 * @param {string} [apiKey] - Google Street View API Key
  	 */
  	function GoogleStreetviewPanorama ( panoId, apiKey ) {
  
  	    ImagePanorama.call( this );
  
  	    this.panoId = panoId;
  
  	    this.gsvLoader = null;
  
  	    this.loadRequested = false;
  
  	    this.setupGoogleMapAPI( apiKey );
  
  	}
  
  	GoogleStreetviewPanorama.prototype = Object.assign( Object.create( ImagePanorama.prototype ), {
  
  	    constructor: GoogleStreetviewPanorama,
  
  	    /**
  	     * Load Google Street View by panorama id
  	     * @param {string} panoId - Gogogle Street View panorama id
  	     * @memberOf GoogleStreetviewPanorama
  	     * @instance
  	     */
  	    load: function ( panoId ) {
  
  	        this.loadRequested = true;
  
  	        panoId = ( panoId || this.panoId ) || {};
  
  	        if ( panoId && this.gsvLoader ) {
  
  	            this.loadGSVLoader( panoId );
  
  	        }
  
  	    },
  
  	    /**
  	     * Setup Google Map API
  	     * @param {string}  apiKey
  	     * @memberOf GoogleStreetviewPanorama
  	     * @instance
  	     */
  	    setupGoogleMapAPI: function ( apiKey ) {
  
  	        const script = document.createElement( 'script' );
  	        script.src = 'https://maps.googleapis.com/maps/api/js?';
  	        script.src += apiKey ? 'key=' + apiKey : '';
  	        script.onreadystatechange = this.setGSVLoader.bind( this );
  	        script.onload = this.setGSVLoader.bind( this );
  
  	        document.querySelector( 'head' ).appendChild( script );
  
  	    },
  
  	    /**
  	     * Set GSV Loader
  	     * @memberOf GoogleStreetviewPanorama
  	     * @instance
  	     */
  	    setGSVLoader: function () {
  
  	        this.gsvLoader = new GoogleStreetviewLoader();
  
  	        if ( this.loadRequested ) {
  
  	            this.load();
  
  	        }
  
  	    },
  
  	    /**
  	     * Get GSV Loader
  	     * @memberOf GoogleStreetviewPanorama
  	     * @instance
  	     * @return {GoogleStreetviewLoader} GSV Loader instance
  	     */
  	    getGSVLoader: function () {
  
  	        return this.gsvLoader;
  
  	    },
  
  	    /**
  	     * Load GSV Loader
  	     * @param  {string} panoId - Gogogle Street View panorama id
  	     * @memberOf GoogleStreetviewPanorama
  	     * @instance
  	     */
  	    loadGSVLoader: function ( panoId ) {
  
  	        this.loadRequested = false;
  
  	        this.gsvLoader.onProgress = this.onProgress.bind( this );
  
  	        this.gsvLoader.onPanoramaLoad = this.onLoad.bind( this );
  
  	        this.gsvLoader.setZoom( this.getZoomLevel() );
  
  	        this.gsvLoader.load( panoId );
  
  	        this.gsvLoader.loaded = true;
  	    },
  
  	    /**
  	     * This will be called when panorama is loaded
  	     * @param  {HTMLCanvasElement} canvas - Canvas where the tiles have been drawn
  	     * @memberOf GoogleStreetviewPanorama
  	     * @instance
  	     */
  	    onLoad: function ( canvas ) {
  
  	        ImagePanorama.prototype.onLoad.call( this, new THREE.Texture( canvas ) );
  
  	    },
  
  	    /**
  	     * Reset
  	     * @memberOf GoogleStreetviewPanorama
  	     * @instance
  	     */
  	    reset: function () {
  
  	        this.gsvLoader = undefined;
  
  	        ImagePanorama.prototype.reset.call( this );
  
  	    }
  
  	} );
  
  	/**
  	 * Stereographic projection shader
  	 * based on http://notlion.github.io/streetview-stereographic
  	 * @author pchen66
  	 */
  
  	/**
  	 * @description Stereograhpic Shader
  	 * @module StereographicShader
  	 * @property {object} uniforms
  	 * @property {THREE.Texture} uniforms.tDiffuse diffuse map
  	 * @property {number} uniforms.resolution image resolution
  	 * @property {THREE.Matrix4} uniforms.transform transformation matrix
  	 * @property {number} uniforms.zoom image zoom factor
  	 * @property {number} uniforms.opacity image opacity
  	 * @property {string} vertexShader vertex shader
  	 * @property {string} fragmentShader fragment shader
  	 */
  	const StereographicShader = {
  
  	    uniforms: {
  
  	        'tDiffuse': { value: new THREE.Texture() },
  	        'resolution': { value: 1.0 },
  	        'transform': { value: new THREE.Matrix4() },
  	        'zoom': { value: 1.0 },
  	        'opacity': { value: 1.0 }
  
  	    },
  
  	    vertexShader: [
  
  	        'varying vec2 vUv;',
  
  	        'void main() {',
  
  	        'vUv = uv;',
  	        'gl_Position = vec4( position, 1.0 );',
  
  	        '}' 
  
  	    ].join( '\n' ),
  
  	    fragmentShader: [
  
  	        'uniform sampler2D tDiffuse;',
  	        'uniform float resolution;',
  	        'uniform mat4 transform;',
  	        'uniform float zoom;',
  	        'uniform float opacity;',
  
  	        'varying vec2 vUv;',
  
  	        'const float PI = 3.141592653589793;',
  
  	        'void main(){',
  
  	        'vec2 position = -1.0 +  2.0 * vUv;',
  
  	        'position *= vec2( zoom * resolution, zoom * 0.5 );',
  
  	        'float x2y2 = position.x * position.x + position.y * position.y;',
  	        'vec3 sphere_pnt = vec3( 2. * position, x2y2 - 1. ) / ( x2y2 + 1. );',
  
  	        'sphere_pnt = vec3( transform * vec4( sphere_pnt, 1.0 ) );',
  
  	        'vec2 sampleUV = vec2(',
  	        '(atan(sphere_pnt.y, sphere_pnt.x) / PI + 1.0) * 0.5,',
  	        '(asin(sphere_pnt.z) / PI + 0.5)',
  	        ');',
  
  	        'gl_FragColor = texture2D( tDiffuse, sampleUV );',
  
  	        'gl_FragColor.a *= opacity;',
  
  	        '}'
  
  	    ].join( '\n' )
  
  	};
  
  	/**
  	 * @classdesc Little Planet
  	 * @constructor
  	 * @param {string} type 		- Type of little planet basic class
  	 * @param {string} source 		- URL for the image source
  	 * @param {number} [size=10000] - Size of plane geometry
  	 * @param {number} [ratio=0.5]  - Ratio of plane geometry's height against width
  	 */
  	function LittlePlanet ( type = 'image', source, size = 10000, ratio = 0.5 ) {
  
  	    if ( type === 'image' ) {
  
  	        ImagePanorama.call( this, source, this.createGeometry( size, ratio ), this.createMaterial( size ) );
  
  	    }
  
  	    this.size = size;
  	    this.ratio = ratio;
  	    this.EPS = 0.000001;
  	    this.frameId = null;
  
  	    this.dragging = false;
  	    this.userMouse = new THREE.Vector2();
  
  	    this.quatA = new THREE.Quaternion();
  	    this.quatB = new THREE.Quaternion();
  	    this.quatCur = new THREE.Quaternion();
  	    this.quatSlerp = new THREE.Quaternion();
  
  	    this.vectorX = new THREE.Vector3( 1, 0, 0 );
  	    this.vectorY = new THREE.Vector3( 0, 1, 0 );
  
  	    this.addEventListener( 'window-resize', this.onWindowResize );
  
  	}
  
  	LittlePlanet.prototype = Object.assign( Object.create( ImagePanorama.prototype ), {
  
  	    constructor: LittlePlanet,
  
  	    add: function ( object ) {
  
  	        if ( arguments.length > 1 ) {
  				
  	            for ( let i = 0; i < arguments.length; i ++ ) {
  
  	                this.add( arguments[ i ] );
  
  	            }
  
  	            return this;
  
  	        }
  
  	        if ( object instanceof Infospot ) {
  
  	            object.material.depthTest = false;
  				
  	        }
  
  	        ImagePanorama.prototype.add.call( this, object );
  
  	    },
  
  	    createGeometry: function ( size, ratio ) {
  
  	        return new THREE.PlaneBufferGeometry( size, size * ratio );
  
  	    },
  
  	    createMaterial: function ( size ) {
  
  	        const shader = Object.assign( {}, StereographicShader ), uniforms = shader.uniforms;
  
  	        uniforms.zoom.value = size;
  	        uniforms.opacity.value = 0.0;
  
  	        return new THREE.ShaderMaterial( {
  
  	            uniforms: uniforms,
  	            vertexShader: shader.vertexShader,
  	            fragmentShader: shader.fragmentShader,
  	            side: THREE.BackSide,
  	            transparent: true
  
  	        } );
  			
  	    },
  
  	    registerMouseEvents: function () {
  
  	        this.container.addEventListener( 'mousedown', this.onMouseDown.bind( this ), { passive: true } );
  	        this.container.addEventListener( 'mousemove', this.onMouseMove.bind( this ), { passive: true } );
  	        this.container.addEventListener( 'mouseup', this.onMouseUp.bind( this ), { passive: true } );
  	        this.container.addEventListener( 'touchstart', this.onMouseDown.bind( this ), { passive: true } );
  	        this.container.addEventListener( 'touchmove', this.onMouseMove.bind( this ), { passive: true } );
  	        this.container.addEventListener( 'touchend', this.onMouseUp.bind( this ), { passive: true } );
  	        this.container.addEventListener( 'mousewheel', this.onMouseWheel.bind( this ), { passive: false } );
  	        this.container.addEventListener( 'DOMMouseScroll', this.onMouseWheel.bind( this ), { passive: false } );
  	        this.container.addEventListener( 'contextmenu', this.onContextMenu.bind( this ), { passive: true } );
  			
  	    },
  
  	    unregisterMouseEvents: function () {
  
  	        this.container.removeEventListener( 'mousedown', this.onMouseDown.bind( this ), false );
  	        this.container.removeEventListener( 'mousemove', this.onMouseMove.bind( this ), false );
  	        this.container.removeEventListener( 'mouseup', this.onMouseUp.bind( this ), false );
  	        this.container.removeEventListener( 'touchstart', this.onMouseDown.bind( this ), false );
  	        this.container.removeEventListener( 'touchmove', this.onMouseMove.bind( this ), false );
  	        this.container.removeEventListener( 'touchend', this.onMouseUp.bind( this ), false );
  	        this.container.removeEventListener( 'mousewheel', this.onMouseWheel.bind( this ), false );
  	        this.container.removeEventListener( 'DOMMouseScroll', this.onMouseWheel.bind( this ), false );
  	        this.container.removeEventListener( 'contextmenu', this.onContextMenu.bind( this ), false );
  			
  	    },
  
  	    onMouseDown: function ( event ) {
  
  	        const inputCount = ( event.touches && event.touches.length ) || 1 ;
  
  	        switch ( inputCount ) {
  
  	        case 1:
  
  	            const x = ( event.clientX >= 0 ) ? event.clientX : event.touches[ 0 ].clientX;
  	            const y = ( event.clientY >= 0 ) ? event.clientY : event.touches[ 0 ].clientY;
  
  	            this.dragging = true;
  	            this.userMouse.set( x, y );
  
  	            break;
  
  	        case 2:
  
  	            const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
  	            const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
  	            const distance = Math.sqrt( dx * dx + dy * dy );
  	            this.userMouse.pinchDistance = distance;
  
  	            break;
  
  	        default:
  
  	            break;
  
  	        }
  
  	        this.onUpdateCallback();
  
  	    },
  
  	    onMouseMove: function ( event ) {
  
  	        const inputCount = ( event.touches && event.touches.length ) || 1 ;
  
  	        switch ( inputCount ) {
  
  	        case 1:
  
  	            const x = ( event.clientX >= 0 ) ? event.clientX : event.touches[ 0 ].clientX;
  	            const y = ( event.clientY >= 0 ) ? event.clientY : event.touches[ 0 ].clientY;
  
  	            const angleX = THREE.Math.degToRad( x - this.userMouse.x ) * 0.4;
  	            const angleY = THREE.Math.degToRad( y - this.userMouse.y ) * 0.4;
  
  	            if ( this.dragging ) {
  	                this.quatA.setFromAxisAngle( this.vectorY, angleX );
  	                this.quatB.setFromAxisAngle( this.vectorX, angleY );
  	                this.quatCur.multiply( this.quatA ).multiply( this.quatB );
  	                this.userMouse.set( x, y );
  	            }
  
  	            break;
  
  	        case 2:
  
  	            const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
  	            const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
  	            const distance = Math.sqrt( dx * dx + dy * dy );
  
  	            this.addZoomDelta( this.userMouse.pinchDistance - distance );
  
  	            break;
  
  	        default:
  
  	            break;
  
  	        }
  
  	    },
  
  	    onMouseUp: function () {
  
  	        this.dragging = false;
  
  	    },
  
  	    onMouseWheel: function ( event ) {
  
  	        event.preventDefault();
  	        event.stopPropagation();
  
  	        let delta = 0;
  
  	        if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9
  
  	            delta = event.wheelDelta;
  
  	        } else if ( event.detail !== undefined ) { // Firefox
  
  	            delta = - event.detail;
  
  	        }
  
  	        this.addZoomDelta( delta );
  	        this.onUpdateCallback();
  
  	    },
  
  	    addZoomDelta: function ( delta ) {
  
  	        const uniforms = this.material.uniforms;
  	        const lowerBound = this.size * 0.1;
  	        const upperBound = this.size * 10;
  
  	        uniforms.zoom.value += delta;
  
  	        if ( uniforms.zoom.value <= lowerBound ) {
  
  	            uniforms.zoom.value = lowerBound;
  
  	        } else if ( uniforms.zoom.value >= upperBound ) {
  
  	            uniforms.zoom.value = upperBound;
  
  	        }
  
  	    },
  
  	    onUpdateCallback: function () {
  
  	        this.frameId = window.requestAnimationFrame( this.onUpdateCallback.bind( this ) );
  
  	        this.quatSlerp.slerp( this.quatCur, 0.1 );
  
  	        if ( this.material ) {
  
  	            this.material.uniforms.transform.value.makeRotationFromQuaternion( this.quatSlerp );
  
  	        }
  	        
  	        if ( !this.dragging && 1.0 - this.quatSlerp.clone().dot( this.quatCur ) < this.EPS ) {
  				
  	            window.cancelAnimationFrame( this.frameId );
  
  	        }
  
  	    },
  
  	    reset: function () {
  
  	        this.quatCur.set( 0, 0, 0, 1 );
  	        this.quatSlerp.set( 0, 0, 0, 1 );
  	        this.onUpdateCallback();
  
  	    },
  
  	    onLoad: function ( texture ) {
  
  	        this.material.uniforms.resolution.value = this.container.clientWidth / this.container.clientHeight;
  
  	        this.registerMouseEvents();
  	        this.onUpdateCallback();
  			
  	        this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'disableControl' } );
  
  	        ImagePanorama.prototype.onLoad.call( this, texture );
  			
  	    },
  
  	    onLeave: function () {
  
  	        this.unregisterMouseEvents();
  
  	        this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'enableControl', data: CONTROLS.ORBIT } );
  
  	        window.cancelAnimationFrame( this.frameId );
  
  	        ImagePanorama.prototype.onLeave.call( this );
  			
  	    },
  
  	    onWindowResize: function () {
  
  	        this.material.uniforms.resolution.value = this.container.clientWidth / this.container.clientHeight;
  
  	    },
  
  	    onContextMenu: function () {
  
  	        this.dragging = false;
  
  	    },
  
  	    dispose: function () {	
  
  	        this.unregisterMouseEvents();
  
  	        ImagePanorama.prototype.dispose.call( this );
  
  	    }
  
  	});
  
  	/**
  	 * @classdesc Image Little Planet
  	 * @constructor
  	 * @param {string} source 		- URL for the image source
  	 * @param {number} [size=10000] - Size of plane geometry
  	 * @param {number} [ratio=0.5]  - Ratio of plane geometry's height against width
  	 */
  	function ImageLittlePlanet ( source, size, ratio ) {
  
  	    LittlePlanet.call( this, 'image', source, size, ratio );
  
  	}
  
  	ImageLittlePlanet.prototype = Object.assign( Object.create( LittlePlanet.prototype ), {
  
  	    constructor: ImageLittlePlanet,
  
  	    /**
  	     * On loaded with texture
  	     * @param {THREE.Texture} texture
  	     * @memberOf ImageLittlePlanet
  	     * @instance
  	     */
  	    onLoad: function ( texture ) {
  
  	        this.updateTexture( texture );
  
  	        LittlePlanet.prototype.onLoad.call( this, texture );
  
  	    },
  	    
  	    /**
  	     * Update texture
  	     * @param {THREE.Texture} texture 
  	     * @memberOf ImageLittlePlanet
  	     * @instance
  	     */
  	    updateTexture: function ( texture ) {
  
  	        texture.minFilter = texture.magFilter = THREE.LinearFilter;
  			
  	        this.material.uniforms[ 'tDiffuse' ].value = texture;
  
  	    },
  
  	    /**
  	     * Dispose
  	     * @memberOf ImageLittlePlanet
  	     * @instance
  	     */
  	    dispose: function () {
  
  	        const tDiffuse = this.material.uniforms[ 'tDiffuse' ];
  
  	        if ( tDiffuse && tDiffuse.value ) {
  
  	            tDiffuse.value.dispose();
  
  	        }
  
  	        LittlePlanet.prototype.dispose.call( this );
  
  	    }
  
  	} );
  
  	/**
  	 * @classdesc Camera panorama
  	 * @description See {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints|MediaStreamConstraints} for constraints
  	 * @param {object} - camera constraints
  	 * @constructor
  	 */
  	function CameraPanorama ( constraints ) {
  
  	    const radius = 5000;
  	    const geometry = new THREE.SphereBufferGeometry( radius, 60, 40 );
  	    const material = new THREE.MeshBasicMaterial( { visible: false });
  
  	    Panorama.call( this, geometry, material );
  
  	    this.media = new Media( constraints );
  	    this.radius = radius;
  
  	    this.addEventListener( 'enter', this.start.bind( this ) );
  	    this.addEventListener( 'leave', this.stop.bind( this ) );
  	    this.addEventListener( 'panolens-container', this.onPanolensContainer.bind( this ) );
  	    this.addEventListener( 'panolens-scene', this.onPanolensScene.bind( this ) );
  
  	}
  
  	CameraPanorama.prototype = Object.assign( Object.create( Panorama.prototype ), {
  
  	    constructor: CameraPanorama,
  
  	    /**
  	     * On container event
  	     * @param {object} event
  	     * @memberOf CameraPanorama
  	     * @instance
  	     */
  	    onPanolensContainer: function ( { container } ) {
  
  	        this.media.setContainer( container );
  
  	    },
  
  	    /**
  	     * On scene event
  	     * @param {object} event 
  	     * @memberOf CameraPanorama
  	     * @instance
  	     */
  	    onPanolensScene: function ( { scene } ) {
  
  	        this.media.setScene( scene );
  
  	    },
  
  	    /**
  	     * Start camera streaming
  	     * @memberOf CameraPanorama
  	     * @instance
  	     * @returns {Promise}
  	     */
  	    start: function () {
  
  	        return this.media.start();
  
  	    },
  
  	    /**
  	     * Stop camera streaming
  	     * @memberOf CameraPanorama
  	     * @instance
  	     */
  	    stop: function () {
  
  	        this.media.stop();
  
  	    },
  
  	} );
  
  	/**
  	 * @classdesc Orbit Controls
  	 * @constructor
  	 * @external OrbitControls
  	 * @param {THREE.Object} object 
  	 * @param {HTMLElement} domElement 
  	 */
  	function OrbitControls ( object, domElement ) {
  
  	    this.object = object;
  	    this.domElement = ( domElement !== undefined ) ? domElement : document;
  	    this.frameId = null;
  
  	    // API
  
  	    // Set to false to disable this control
  	    this.enabled = true;
  
  	    /*
  	     * "target" sets the location of focus, where the control orbits around
  	     * and where it pans with respect to.
  	     */
  	    this.target = new THREE.Vector3();
  
  	    // center is old, deprecated; use "target" instead
  	    this.center = this.target;
  
  	    /*
  	     * This option actually enables dollying in and out; left as "zoom" for
  	     * backwards compatibility
  	     */
  	    this.noZoom = false;
  	    this.zoomSpeed = 1.0;
  
  	    // Limits to how far you can dolly in and out ( PerspectiveCamera only )
  	    this.minDistance = 0;
  	    this.maxDistance = Infinity;
  
  	    // Limits to how far you can zoom in and out ( OrthographicCamera only )
  	    this.minZoom = 0;
  	    this.maxZoom = Infinity;
  
  	    // Set to true to disable this control
  	    this.noRotate = false;
  	    this.rotateSpeed = -0.15;
  
  	    // Set to true to disable this control
  	    this.noPan = true;
  	    this.keyPanSpeed = 7.0;	// pixels moved per arrow key push
  
  	    // Set to true to automatically rotate around the target
  	    this.autoRotate = false;
  	    this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
  
  	    /*
  	     * How far you can orbit vertically, upper and lower limits.
  	     * Range is 0 to Math.PI radians.
  	     */
  	    this.minPolarAngle = 0; // radians
  	    this.maxPolarAngle = Math.PI; // radians
  
  	    // Momentum
  	  	this.momentumDampingFactor = 0.90;
  	  	this.momentumScalingFactor = -0.005;
  	  	this.momentumKeydownFactor = 20;
  
  	  	// Fov
  	  	this.minFov = 30;
  	  	this.maxFov = 120;
  
  	    /*
  	     * How far you can orbit horizontally, upper and lower limits.
  	     * If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
  	     */
  	    this.minAzimuthAngle = - Infinity; // radians
  	    this.maxAzimuthAngle = Infinity; // radians
  
  	    // Set to true to disable use of the keys
  	    this.noKeys = false;
  
  	    // The four arrow keys
  	    this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
  
  	    // Mouse buttons
  	    this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
  
  	    /*
  	     * //////////
  	     * internals
  	     */
  
  	    var scope = this;
  
  	    var EPS = 10e-8;
  	    var MEPS = 10e-5;
  
  	    var rotateStart = new THREE.Vector2();
  	    var rotateEnd = new THREE.Vector2();
  	    var rotateDelta = new THREE.Vector2();
  
  	    var panStart = new THREE.Vector2();
  	    var panEnd = new THREE.Vector2();
  	    var panDelta = new THREE.Vector2();
  	    var panOffset = new THREE.Vector3();
  
  	    var offset = new THREE.Vector3();
  
  	    var dollyStart = new THREE.Vector2();
  	    var dollyEnd = new THREE.Vector2();
  	    var dollyDelta = new THREE.Vector2();
  
  	    var theta = 0;
  	    var phi = 0;
  	    var phiDelta = 0;
  	    var thetaDelta = 0;
  	    var scale = 1;
  	    var pan = new THREE.Vector3();
  
  	    var lastPosition = new THREE.Vector3();
  	    var lastQuaternion = new THREE.Quaternion();
  
  	    var momentumLeft = 0, momentumUp = 0;
  	    var eventPrevious;
  	    var momentumOn = false;
  
  	    var keyUp, keyBottom, keyLeft, keyRight;
  
  	    var STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 };
  
  	    var state = STATE.NONE;
  
  	    // for reset
  
  	    this.target0 = this.target.clone();
  	    this.position0 = this.object.position.clone();
  	    this.zoom0 = this.object.zoom;
  
  	    // so camera.up is the orbit axis
  
  	    var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
  	    var quatInverse = quat.clone().inverse();
  
  	    // events
  
  	    var changeEvent = { type: 'change' };
  	    var startEvent = { type: 'start' };
  	    var endEvent = { type: 'end' };
  
  	    this.setLastQuaternion = function ( quaternion ) {
  	        lastQuaternion.copy( quaternion );
  	        scope.object.quaternion.copy( quaternion );
  	    };
  
  	    this.getLastPosition = function () {
  	        return lastPosition;
  	    };
  
  	    this.rotateLeft = function ( angle ) {
  
  	        if ( angle === undefined ) {
  
  	            angle = getAutoRotationAngle();
  
  	        }
  
  	        thetaDelta -= angle;
  
  
  	    };
  
  	    this.rotateUp = function ( angle ) {
  
  	        if ( angle === undefined ) {
  
  	            angle = getAutoRotationAngle();
  
  	        }
  
  	        phiDelta -= angle;
  
  	    };
  
  	    // pass in distance in world space to move left
  	    this.panLeft = function ( distance ) {
  
  	        var te = this.object.matrix.elements;
  
  	        // get X column of matrix
  	        panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] );
  	        panOffset.multiplyScalar( - distance );
  
  	        pan.add( panOffset );
  
  	    };
  
  	    // pass in distance in world space to move up
  	    this.panUp = function ( distance ) {
  
  	        var te = this.object.matrix.elements;
  
  	        // get Y column of matrix
  	        panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] );
  	        panOffset.multiplyScalar( distance );
  
  	        pan.add( panOffset );
  
  	    };
  
  	    /*
  	     * pass in x,y of change desired in pixel space,
  	     * right and down are positive
  	     */
  	    this.pan = function ( deltaX, deltaY ) {
  
  	        var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
  
  	        if ( scope.object instanceof THREE.PerspectiveCamera ) {
  
  	            // perspective
  	            var position = scope.object.position;
  	            var offset = position.clone().sub( scope.target );
  	            var targetDistance = offset.length();
  
  	            // half of the fov is center to top of screen
  	            targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
  
  	            // we actually don't use screenWidth, since perspective camera is fixed to screen height
  	            scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight );
  	            scope.panUp( 2 * deltaY * targetDistance / element.clientHeight );
  
  	        } else if ( scope.object instanceof THREE.OrthographicCamera ) {
  
  	            // orthographic
  	            scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth );
  	            scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight );
  
  	        } else {
  
  	            // camera neither orthographic or perspective
  	            console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
  
  	        }
  
  	    };
  
  	    this.momentum = function(){
  			
  	        if ( !momentumOn ) return;
  
  	        if ( Math.abs( momentumLeft ) < MEPS && Math.abs( momentumUp ) < MEPS ) { 
  
  	            momentumOn = false; 
  	            return;
  	        }
  
  	        momentumUp   *= this.momentumDampingFactor;
  	        momentumLeft *= this.momentumDampingFactor;
  
  	        thetaDelta -= this.momentumScalingFactor * momentumLeft;
  	        phiDelta   -= this.momentumScalingFactor * momentumUp;
  
  	    };
  
  	    this.dollyIn = function ( dollyScale ) {
  
  	        if ( dollyScale === undefined ) {
  
  	            dollyScale = getZoomScale();
  
  	        }
  
  	        if ( scope.object instanceof THREE.PerspectiveCamera ) {
  
  	            scale /= dollyScale;
  
  	        } else if ( scope.object instanceof THREE.OrthographicCamera ) {
  
  	            scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) );
  	            scope.object.updateProjectionMatrix();
  	            scope.dispatchEvent( changeEvent );
  
  	        } else {
  
  	            console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
  
  	        }
  
  	    };
  
  	    this.dollyOut = function ( dollyScale ) {
  
  	        if ( dollyScale === undefined ) {
  
  	            dollyScale = getZoomScale();
  
  	        }
  
  	        if ( scope.object instanceof THREE.PerspectiveCamera ) {
  
  	            scale *= dollyScale;
  
  	        } else if ( scope.object instanceof THREE.OrthographicCamera ) {
  
  	            scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) );
  	            scope.object.updateProjectionMatrix();
  	            scope.dispatchEvent( changeEvent );
  
  	        } else {
  
  	            console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
  
  	        }
  
  	    };
  
  	    this.update = function ( ignoreUpdate ) {
  
  	        var position = this.object.position;
  
  	        offset.copy( position ).sub( this.target );
  
  	        // rotate offset to "y-axis-is-up" space
  	        offset.applyQuaternion( quat );
  
  	        // angle from z-axis around y-axis
  
  	        theta = Math.atan2( offset.x, offset.z );
  
  	        // angle from y-axis
  
  	        phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
  
  	        if ( this.autoRotate && state === STATE.NONE ) {
  
  	            this.rotateLeft( getAutoRotationAngle() );
  
  	        }
  
  	        this.momentum();
  
  	        theta += thetaDelta;
  	        phi += phiDelta;
  
  	        // restrict theta to be between desired limits
  	        theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) );
  
  	        // restrict phi to be between desired limits
  	        phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );
  
  	        // restrict phi to be betwee EPS and PI-EPS
  	        phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
  
  	        var radius = offset.length() * scale;
  
  	        // restrict radius to be between desired limits
  	        radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
  
  	        // move target to panned location
  	        this.target.add( pan );
  
  	        offset.x = radius * Math.sin( phi ) * Math.sin( theta );
  	        offset.y = radius * Math.cos( phi );
  	        offset.z = radius * Math.sin( phi ) * Math.cos( theta );
  
  	        // rotate offset back to "camera-up-vector-is-up" space
  	        offset.applyQuaternion( quatInverse );
  
  	        position.copy( this.target ).add( offset );
  
  	        this.object.lookAt( this.target );
  
  	        thetaDelta = 0;
  	        phiDelta = 0;
  	        scale = 1;
  	        pan.set( 0, 0, 0 );
  
  	        /*
  	         * update condition is:
  	         * min(camera displacement, camera rotation in radians)^2 > EPS
  	         * using small-angle approximation cos(x/2) = 1 - x^2 / 8
  	         */
  	        if ( lastPosition.distanceToSquared( this.object.position ) > EPS
  			    || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) {
  
  	            if ( ignoreUpdate !== true ) { this.dispatchEvent( changeEvent ); }
  
  	            lastPosition.copy( this.object.position );
  	            lastQuaternion.copy (this.object.quaternion );
  
  	        }
  
  	    };
  
  
  	    this.reset = function () {
  
  	        state = STATE.NONE;
  
  	        this.target.copy( this.target0 );
  	        this.object.position.copy( this.position0 );
  	        this.object.zoom = this.zoom0;
  
  	        this.object.updateProjectionMatrix();
  	        this.dispatchEvent( changeEvent );
  
  	        this.update();
  
  	    };
  
  	    this.getPolarAngle = function () {
  
  	        return phi;
  
  	    };
  
  	    this.getAzimuthalAngle = function () {
  
  	        return theta;
  
  	    };
  
  	    function getAutoRotationAngle() {
  
  	        return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
  
  	    }
  
  	    function getZoomScale() {
  
  	        return Math.pow( 0.95, scope.zoomSpeed );
  
  	    }
  
  	    function onMouseDown( event ) {
  
  	        momentumOn = false;
  
  	   		momentumLeft = momentumUp = 0;
  
  	        if ( scope.enabled === false ) return;
  	        event.preventDefault();
  
  	        if ( event.button === scope.mouseButtons.ORBIT ) {
  	            if ( scope.noRotate === true ) return;
  
  	            state = STATE.ROTATE;
  
  	            rotateStart.set( event.clientX, event.clientY );
  
  	        } else if ( event.button === scope.mouseButtons.ZOOM ) {
  	            if ( scope.noZoom === true ) return;
  
  	            state = STATE.DOLLY;
  
  	            dollyStart.set( event.clientX, event.clientY );
  
  	        } else if ( event.button === scope.mouseButtons.PAN ) {
  	            if ( scope.noPan === true ) return;
  
  	            state = STATE.PAN;
  
  	            panStart.set( event.clientX, event.clientY );
  
  	        }
  
  	        if ( state !== STATE.NONE ) {
  	            document.addEventListener( 'mousemove', onMouseMove, false );
  	            document.addEventListener( 'mouseup', onMouseUp, false );
  	            scope.dispatchEvent( startEvent );
  	        }
  
  	        scope.update();
  
  	    }
  
  	    function onMouseMove( event ) {
  
  	        if ( scope.enabled === false ) return;
  
  	        event.preventDefault();
  
  	        var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
  
  	        if ( state === STATE.ROTATE ) {
  
  	            if ( scope.noRotate === true ) return;
  
  	            rotateEnd.set( event.clientX, event.clientY );
  	            rotateDelta.subVectors( rotateEnd, rotateStart );
  
  	            // rotating across whole screen goes 360 degrees around
  	            scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
  
  	            // rotating up and down along whole screen attempts to go 360, but limited to 180
  	            scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
  
  	            rotateStart.copy( rotateEnd );
  
  	            if( eventPrevious ){
  	                momentumLeft = event.clientX - eventPrevious.clientX;
  	                momentumUp = event.clientY - eventPrevious.clientY;
  	            }
  
  	            eventPrevious = event;
  
  	        } else if ( state === STATE.DOLLY ) {
  
  	            if ( scope.noZoom === true ) return;
  
  	            dollyEnd.set( event.clientX, event.clientY );
  	            dollyDelta.subVectors( dollyEnd, dollyStart );
  
  	            if ( dollyDelta.y > 0 ) {
  
  	                scope.dollyIn();
  
  	            } else if ( dollyDelta.y < 0 ) {
  
  	                scope.dollyOut();
  
  	            }
  
  	            dollyStart.copy( dollyEnd );
  
  	        } else if ( state === STATE.PAN ) {
  
  	            if ( scope.noPan === true ) return;
  
  	            panEnd.set( event.clientX, event.clientY );
  	            panDelta.subVectors( panEnd, panStart );
  
  	            scope.pan( panDelta.x, panDelta.y );
  
  	            panStart.copy( panEnd );
  
  	        }
  
  	        if ( state !== STATE.NONE ) scope.update();
  
  	    }
  
  	    function onMouseUp( /* event */ ) {
  
  	        momentumOn = true;
  
  	        eventPrevious = undefined;
  
  	        if ( scope.enabled === false ) return;
  
  	        document.removeEventListener( 'mousemove', onMouseMove, false );
  	        document.removeEventListener( 'mouseup', onMouseUp, false );
  	        scope.dispatchEvent( endEvent );
  	        state = STATE.NONE;
  
  	    }
  
  	    function onMouseWheel( event ) {
  
  	        if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return;
  
  	        event.preventDefault();
  	        event.stopPropagation();
  
  	        var delta = 0;
  
  	        if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9
  
  	            delta = event.wheelDelta;
  
  	        } else if ( event.detail !== undefined ) { // Firefox
  
  	            delta = - event.detail;
  
  	        }
  
  	        if ( delta > 0 ) {
  
  	            // scope.dollyOut();
  	            scope.object.fov = ( scope.object.fov < scope.maxFov ) 
  	                ? scope.object.fov + 1
  	                : scope.maxFov;
  	            scope.object.updateProjectionMatrix();
  
  	        } else if ( delta < 0 ) {
  
  	            // scope.dollyIn();
  	            scope.object.fov = ( scope.object.fov > scope.minFov ) 
  	                ? scope.object.fov - 1
  	                : scope.minFov;
  	            scope.object.updateProjectionMatrix();
  
  	        }
  
  	        scope.update();
  	        scope.dispatchEvent( changeEvent );
  	        scope.dispatchEvent( startEvent );
  	        scope.dispatchEvent( endEvent );
  
  	    }
  
  	    function onKeyUp ( event ) {
  
  	        switch ( event.keyCode ) {
  
  	        case scope.keys.UP:
  	            keyUp = false;
  	            break;
  
  	        case scope.keys.BOTTOM:
  	            keyBottom = false;
  	            break;
  
  	        case scope.keys.LEFT:
  	            keyLeft = false;
  	            break;
  
  	        case scope.keys.RIGHT:
  	            keyRight = false;
  	            break;
  
  	        }
  
  	    }
  
  	    function onKeyDown( event ) {
  
  	        if ( scope.enabled === false || scope.noKeys === true || scope.noRotate === true ) return;
  
  	        switch ( event.keyCode ) {
  
  	        case scope.keys.UP:
  	            keyUp = true;
  	            break;
  
  	        case scope.keys.BOTTOM:
  	            keyBottom = true;
  	            break;
  
  	        case scope.keys.LEFT:
  	            keyLeft = true;
  	            break;
  
  	        case scope.keys.RIGHT:
  	            keyRight = true;
  	            break;
  
  	        }
  
  	        if (keyUp || keyBottom || keyLeft || keyRight) {
  
  	            momentumOn = true;
  
  	            if (keyUp) momentumUp = - scope.rotateSpeed * scope.momentumKeydownFactor;
  	            if (keyBottom) momentumUp = scope.rotateSpeed * scope.momentumKeydownFactor;
  	            if (keyLeft) momentumLeft = - scope.rotateSpeed * scope.momentumKeydownFactor;
  	            if (keyRight) momentumLeft = scope.rotateSpeed * scope.momentumKeydownFactor;
  
  	        }
  
  	    }
  
  	    function touchstart( event ) {
  
  	        momentumOn = false;
  
  	        momentumLeft = momentumUp = 0;
  
  	        if ( scope.enabled === false ) return;
  
  	        switch ( event.touches.length ) {
  
  	        case 1:	// one-fingered touch: rotate
  
  	            if ( scope.noRotate === true ) return;
  
  	            state = STATE.TOUCH_ROTATE;
  
  	            rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
  	            break;
  
  	        case 2:	// two-fingered touch: dolly
  
  	            if ( scope.noZoom === true ) return;
  
  	            state = STATE.TOUCH_DOLLY;
  
  	            var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
  	            var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
  	            var distance = Math.sqrt( dx * dx + dy * dy );
  
  	            dollyStart.set( 0, distance );
  
  	            break;
  
  	        case 3: // three-fingered touch: pan
  
  	            if ( scope.noPan === true ) return;
  
  	            state = STATE.TOUCH_PAN;
  
  	            panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
  	            break;
  
  	        default:
  
  	            state = STATE.NONE;
  
  	        }
  
  	        if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent );
  
  	    }
  
  	    function touchmove( event ) {
  
  	        if ( scope.enabled === false ) return;
  
  	        event.preventDefault();
  	        event.stopPropagation();
  
  	        var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
  
  	        switch ( event.touches.length ) {
  
  	        case 1: // one-fingered touch: rotate
  
  	            if ( scope.noRotate === true ) return;
  	            if ( state !== STATE.TOUCH_ROTATE ) return;
  
  	            rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
  	            rotateDelta.subVectors( rotateEnd, rotateStart );
  
  	            // rotating across whole screen goes 360 degrees around
  	            scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
  	            // rotating up and down along whole screen attempts to go 360, but limited to 180
  	            scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
  
  	            rotateStart.copy( rotateEnd );
  
  	            if( eventPrevious ){
  	                momentumLeft = event.touches[ 0 ].pageX - eventPrevious.pageX;
  	                momentumUp = event.touches[ 0 ].pageY - eventPrevious.pageY;
  	            }
  
  	            eventPrevious = {
  	                pageX: event.touches[ 0 ].pageX,
  	                pageY: event.touches[ 0 ].pageY,
  	            };
  
  	            scope.update();
  	            break;
  
  	        case 2: // two-fingered touch: dolly
  
  	            if ( scope.noZoom === true ) return;
  	            if ( state !== STATE.TOUCH_DOLLY ) return;
  
  	            var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
  	            var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
  	            var distance = Math.sqrt( dx * dx + dy * dy );
  
  	            dollyEnd.set( 0, distance );
  	            dollyDelta.subVectors( dollyEnd, dollyStart );
  
  	            if ( dollyDelta.y < 0 ) {
  
  	                scope.object.fov = ( scope.object.fov < scope.maxFov ) 
  	                    ? scope.object.fov + 1
  	                    : scope.maxFov;
  	                scope.object.updateProjectionMatrix();
  
  	            } else if ( dollyDelta.y > 0 ) {
  
  	                scope.object.fov = ( scope.object.fov > scope.minFov ) 
  	                    ? scope.object.fov - 1
  	                    : scope.minFov;
  	                scope.object.updateProjectionMatrix();
  
  	            }
  
  	            dollyStart.copy( dollyEnd );
  
  	            scope.update();
  	            scope.dispatchEvent( changeEvent );
  	            break;
  
  	        case 3: // three-fingered touch: pan
  
  	            if ( scope.noPan === true ) return;
  	            if ( state !== STATE.TOUCH_PAN ) return;
  
  	            panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
  	            panDelta.subVectors( panEnd, panStart );
  
  	            scope.pan( panDelta.x, panDelta.y );
  
  	            panStart.copy( panEnd );
  
  	            scope.update();
  	            break;
  
  	        default:
  
  	            state = STATE.NONE;
  
  	        }
  
  	    }
  
  	    function touchend( /* event */ ) {
  
  	        momentumOn = true;
  
  	        eventPrevious = undefined;
  
  	        if ( scope.enabled === false ) return;
  
  	        scope.dispatchEvent( endEvent );
  	        state = STATE.NONE;
  
  	    }
  
  	    this.dispose = function() {
  
  	        this.domElement.removeEventListener( 'mousedown', onMouseDown );
  	        this.domElement.removeEventListener( 'mousewheel', onMouseWheel );
  	        this.domElement.removeEventListener( 'DOMMouseScroll', onMouseWheel );
  
  	        this.domElement.removeEventListener( 'touchstart', touchstart );
  	        this.domElement.removeEventListener( 'touchend', touchend );
  	        this.domElement.removeEventListener( 'touchmove', touchmove );
  
  	        window.removeEventListener( 'keyup', onKeyUp );
  	        window.removeEventListener( 'keydown', onKeyDown );
  
  	    };
  
  	    // this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
  	    this.domElement.addEventListener( 'mousedown', onMouseDown, { passive: false } );
  	    this.domElement.addEventListener( 'mousewheel', onMouseWheel, { passive: false } );
  	    this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, { passive: false } ); // firefox
  
  	    this.domElement.addEventListener( 'touchstart', touchstart, { passive: false } );
  	    this.domElement.addEventListener( 'touchend', touchend, { passive: false } );
  	    this.domElement.addEventListener( 'touchmove', touchmove, { passive: false } );
  
  	    window.addEventListener( 'keyup', onKeyUp, { passive: false } );
  	    window.addEventListener( 'keydown', onKeyDown, { passive: false } );
  
  	    // force an update at start
  	    this.update();
  
  	}
  	OrbitControls.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype ), {
  
  	    constructor: OrbitControls
  
  	} );
  
  	/**
  	 * @classdesc Device Orientation Control
  	 * @constructor
  	 * @external DeviceOrientationControls
  	 * @param {THREE.Camera} camera 
  	 * @param {HTMLElement} domElement 
  	 */
  	function DeviceOrientationControls ( camera, domElement ) {
  
  	    var scope = this;
  	    var changeEvent = { type: 'change' };
  
  	    var rotY = 0;
  	    var rotX = 0;
  	    var tempX = 0;
  	    var tempY = 0;
  
  	    this.camera = camera;
  	    this.camera.rotation.reorder( 'YXZ' );
  	    this.domElement = ( domElement !== undefined ) ? domElement : document;
  
  	    this.enabled = true;
  
  	    this.deviceOrientation = {};
  	    this.screenOrientation = 0;
  
  	    this.alpha = 0;
  	    this.alphaOffsetAngle = 0;
  
  
  	    var onDeviceOrientationChangeEvent = function( event ) {
  
  	        scope.deviceOrientation = event;
  
  	    };
  
  	    var onScreenOrientationChangeEvent = function() {
  
  	        scope.screenOrientation = window.orientation || 0;
  
  	    };
  
  	    var onTouchStartEvent = function (event) {
  
  	        event.preventDefault();
  	        event.stopPropagation();
  
  	        tempX = event.touches[ 0 ].pageX;
  	        tempY = event.touches[ 0 ].pageY;
  
  	    };
  
  	    var onTouchMoveEvent = function (event) {
  
  	        event.preventDefault();
  	        event.stopPropagation();
  
  	        rotY += THREE.Math.degToRad( ( event.touches[ 0 ].pageX - tempX ) / 4 );
  	        rotX += THREE.Math.degToRad( ( tempY - event.touches[ 0 ].pageY ) / 4 );
  
  	        scope.updateAlphaOffsetAngle( rotY );
  
  	        tempX = event.touches[ 0 ].pageX;
  	        tempY = event.touches[ 0 ].pageY;
  
  	    };
  
  	    // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y''
  
  	    var setCameraQuaternion = function( quaternion, alpha, beta, gamma, orient ) {
  
  	        var zee = new THREE.Vector3( 0, 0, 1 );
  
  	        var euler = new THREE.Euler();
  
  	        var q0 = new THREE.Quaternion();
  
  	        var q1 = new THREE.Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis
  
  	        var vectorFingerY;
  	        var fingerQY = new THREE.Quaternion();
  	        var fingerQX = new THREE.Quaternion();
  
  	        if ( scope.screenOrientation == 0 ) {
  
  	            vectorFingerY = new THREE.Vector3( 1, 0, 0 );
  	            fingerQY.setFromAxisAngle( vectorFingerY, -rotX );
  
  	        } else if ( scope.screenOrientation == 180 ) {
  
  	            vectorFingerY = new THREE.Vector3( 1, 0, 0 );
  	            fingerQY.setFromAxisAngle( vectorFingerY, rotX );
  
  	        } else if ( scope.screenOrientation == 90 ) {
  
  	            vectorFingerY = new THREE.Vector3( 0, 1, 0 );
  	            fingerQY.setFromAxisAngle( vectorFingerY, rotX );
  
  	        } else if ( scope.screenOrientation == - 90) {
  
  	            vectorFingerY = new THREE.Vector3( 0, 1, 0 );
  	            fingerQY.setFromAxisAngle( vectorFingerY, -rotX );
  
  	        }
  
  	        q1.multiply( fingerQY );
  	        q1.multiply( fingerQX );
  
  	        euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us
  
  	        quaternion.setFromEuler( euler ); // orient the device
  
  	        quaternion.multiply( q1 ); // camera looks out the back of the device, not the top
  
  	        quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); // adjust for screen orientation
  
  	    };
  
  	    this.connect = function() {
  
  	        onScreenOrientationChangeEvent(); // run once on load
  
  	        window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, { passive: true } );
  	        window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, { passive: true } );
  	        window.addEventListener( 'deviceorientation', this.update.bind( this ), { passive: true } );
  
  	        scope.domElement.addEventListener( 'touchstart', onTouchStartEvent, { passive: false } );
  	        scope.domElement.addEventListener( 'touchmove', onTouchMoveEvent, { passive: false } );
  
  	        scope.enabled = true;
  
  	    };
  
  	    this.disconnect = function() {
  
  	        window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
  	        window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
  	        window.removeEventListener( 'deviceorientation', this.update.bind( this ), false );
  
  	        scope.domElement.removeEventListener( 'touchstart', onTouchStartEvent, false );
  	        scope.domElement.removeEventListener( 'touchmove', onTouchMoveEvent, false );
  
  	        scope.enabled = false;
  
  	    };
  
  	    this.update = function( ignoreUpdate ) {
  
  	        if ( scope.enabled === false ) return;
  
  	        var alpha = scope.deviceOrientation.alpha ? THREE.Math.degToRad( scope.deviceOrientation.alpha ) + scope.alphaOffsetAngle : 0; // Z
  	        var beta = scope.deviceOrientation.beta ? THREE.Math.degToRad( scope.deviceOrientation.beta ) : 0; // X'
  	        var gamma = scope.deviceOrientation.gamma ? THREE.Math.degToRad( scope.deviceOrientation.gamma ) : 0; // Y''
  	        var orient = scope.screenOrientation ? THREE.Math.degToRad( scope.screenOrientation ) : 0; // O
  
  	        setCameraQuaternion( scope.camera.quaternion, alpha, beta, gamma, orient );
  	        scope.alpha = alpha;
  
  	        if ( ignoreUpdate !== true ) { scope.dispatchEvent( changeEvent ); }
  
  	    };
  
  	    this.updateAlphaOffsetAngle = function( angle ) {
  
  	        this.alphaOffsetAngle = angle;
  	        this.update();
  
  	    };
  
  	    this.dispose = function() {
  
  	        this.disconnect();
  
  	    };
  
  	    this.connect();
  
  	}
  	DeviceOrientationControls.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype), {
  
  	    constructor: DeviceOrientationControls
  
  	} );
  
  	/**
  	 * @classdesc Google Cardboard Effect Composer
  	 * @constructor
  	 * @external CardboardEffect
  	 * @param {THREE.WebGLRenderer} renderer 
  	 */
  	function CardboardEffect ( renderer ) {
  
  	    var _camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
  
  	    var _scene = new THREE.Scene();
  
  	    var _stereo = new THREE.StereoCamera();
  	    _stereo.aspect = 0.5;
  
  	    var _params = { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBAFormat };
  
  	    var _renderTarget = new THREE.WebGLRenderTarget( 512, 512, _params );
  	    _renderTarget.scissorTest = true;
  	    _renderTarget.texture.generateMipmaps = false;
  
  	    /*
  	     * Distortion Mesh ported from:
  	     * https://github.com/borismus/webvr-boilerplate/blob/master/src/distortion/barrel-distortion-fragment.js
  	     */
  
  	    var distortion = new THREE.Vector2( 0.441, 0.156 );
  
  	    var geometry = new THREE.PlaneBufferGeometry( 1, 1, 10, 20 ).removeAttribute( 'normal' ).toNonIndexed();
  
  	    var positions = geometry.attributes.position.array;
  	    var uvs = geometry.attributes.uv.array;
  
  	    // duplicate
  	    geometry.attributes.position.count *= 2;
  	    geometry.attributes.uv.count *= 2;
  
  	    var positions2 = new Float32Array( positions.length * 2 );
  	    positions2.set( positions );
  	    positions2.set( positions, positions.length );
  
  	    var uvs2 = new Float32Array( uvs.length * 2 );
  	    uvs2.set( uvs );
  	    uvs2.set( uvs, uvs.length );
  
  	    var vector = new THREE.Vector2();
  	    var length = positions.length / 3;
  
  	    for ( var i = 0, l = positions2.length / 3; i < l; i ++ ) {
  
  	        vector.x = positions2[ i * 3 + 0 ];
  	        vector.y = positions2[ i * 3 + 1 ];
  
  	        var dot = vector.dot( vector );
  	        var scalar = 1.5 + ( distortion.x + distortion.y * dot ) * dot;
  
  	        var offset = i < length ? 0 : 1;
  
  	        positions2[ i * 3 + 0 ] = ( vector.x / scalar ) * 1.5 - 0.5 + offset;
  	        positions2[ i * 3 + 1 ] = ( vector.y / scalar ) * 3.0;
  
  	        uvs2[ i * 2 ] = ( uvs2[ i * 2 ] + offset ) * 0.5;
  
  	    }
  
  	    geometry.attributes.position.array = positions2;
  	    geometry.attributes.uv.array = uvs2;
  
  	    //
  
  	    var material = new THREE.MeshBasicMaterial( { map: _renderTarget.texture } );
  	    var mesh = new THREE.Mesh( geometry, material );
  	    _scene.add( mesh );
  
  	    //
  
  	    this.setSize = function ( width, height ) {
  
  	        renderer.setSize( width, height );
  
  	        var pixelRatio = renderer.getPixelRatio();
  
  	        _renderTarget.setSize( width * pixelRatio, height * pixelRatio );
  
  	    };
  
  	    this.render = function ( scene, camera ) {
  
  	        scene.updateMatrixWorld();
  
  	        if ( camera.parent === null ) camera.updateMatrixWorld();
  
  	        _stereo.update( camera );
  
  	        var width = _renderTarget.width / 2;
  	        var height = _renderTarget.height;
  
  	        if ( renderer.autoClear ) renderer.clear();
  
  	        _renderTarget.scissor.set( 0, 0, width, height );
  	        _renderTarget.viewport.set( 0, 0, width, height );
  	        renderer.setRenderTarget( _renderTarget );
  	        renderer.render( scene, _stereo.cameraL );
  
  	        renderer.clearDepth();
  
  	        _renderTarget.scissor.set( width, 0, width, height );
  	        _renderTarget.viewport.set( width, 0, width, height );
  	        renderer.setRenderTarget( _renderTarget );
  	        renderer.render( scene, _stereo.cameraR );
  
  	        renderer.clearDepth();
  
  	        renderer.setRenderTarget( null );
  	        renderer.render( _scene, _camera );
  	    };
  
  	}
  
  	/**
  	 * @classdesc Stereo Effect Composer
  	 * @constructor
  	 * @external StereoEffect
  	 * @param {THREE.WebGLRenderer} renderer 
  	 */
  	const StereoEffect = function ( renderer ) {
  
  	    var _stereo = new THREE.StereoCamera();
  	    _stereo.aspect = 0.5;
  	    var size = new THREE.Vector2();
  
  	    this.setEyeSeparation = function ( eyeSep ) {
  
  	        _stereo.eyeSep = eyeSep;
  
  	    };
  
  	    this.setSize = function ( width, height ) {
  
  	        renderer.setSize( width, height );
  
  	    };
  
  	    this.render = function ( scene, camera ) {
  
  	        scene.updateMatrixWorld();
  
  	        if ( camera.parent === null ) camera.updateMatrixWorld();
  
  	        _stereo.update( camera );
  
  	        renderer.getSize( size );
  
  	        if ( renderer.autoClear ) renderer.clear();
  	        renderer.setScissorTest( true );
  
  	        renderer.setScissor( 0, 0, size.width / 2, size.height );
  	        renderer.setViewport( 0, 0, size.width / 2, size.height );
  	        renderer.render( scene, _stereo.cameraL );
  
  	        renderer.setScissor( size.width / 2, 0, size.width / 2, size.height );
  	        renderer.setViewport( size.width / 2, 0, size.width / 2, size.height );
  	        renderer.render( scene, _stereo.cameraR );
  
  	        renderer.setScissorTest( false );
  
  	    };
  
  	};
  
  	/**
  	 * @classdesc Viewer contains pre-defined scene, camera and renderer
  	 * @constructor
  	 * @param {object} [options] - Use custom or default config options
  	 * @param {HTMLElement} [options.container] - A HTMLElement to host the canvas
  	 * @param {THREE.Scene} [options.scene=THREE.Scene] - A THREE.Scene which contains panorama and 3D objects
  	 * @param {THREE.Camera} [options.camera=THREE.PerspectiveCamera] - A THREE.Camera to view the scene
  	 * @param {THREE.WebGLRenderer} [options.renderer=THREE.WebGLRenderer] - A THREE.WebGLRenderer to render canvas
  	 * @param {boolean} [options.controlBar=true] - Show/hide control bar on the bottom of the container
  	 * @param {array}   [options.controlButtons=[]] - Button names to mount on controlBar if controlBar exists, Defaults to ['fullscreen', 'setting', 'video']
  	 * @param {boolean} [options.autoHideControlBar=false] - Auto hide control bar when click on non-active area
  	 * @param {boolean} [options.autoHideInfospot=true] - Auto hide infospots when click on non-active area
  	 * @param {boolean} [options.horizontalView=false] - Allow only horizontal camera control
  	 * @param {number}  [options.clickTolerance=10] - Distance tolerance to tigger click / tap event
  	 * @param {number}  [options.cameraFov=60] - Camera field of view value
  	 * @param {boolean} [options.reverseDragging=false] - Reverse dragging direction
  	 * @param {boolean} [options.enableReticle=false] - Enable reticle for mouseless interaction other than VR mode
  	 * @param {number}  [options.dwellTime=1500] - Dwell time for reticle selection in ms
  	 * @param {boolean} [options.autoReticleSelect=true] - Auto select a clickable target after dwellTime
  	 * @param {boolean} [options.viewIndicator=false] - Adds an angle view indicator in upper left corner
  	 * @param {number}  [options.indicatorSize=30] - Size of View Indicator
  	 * @param {string}  [options.output='none'] - Whether and where to output raycast position. Could be 'console' or 'overlay'
  	 * @param {boolean} [options.autoRotate=false] - Auto rotate
  	 * @param {number}  [options.autoRotateSpeed=2.0] - Auto rotate speed as in degree per second. Positive is counter-clockwise and negative is clockwise.
  	 * @param {number}  [options.autoRotateActivationDuration=5000] - Duration before auto rotatation when no user interactivity in ms
  	 */
  	function Viewer ( options ) {
  
  	    let container;
  
  	    options = options || {};
  	    options.controlBar = options.controlBar !== undefined ? options.controlBar : true;
  	    options.controlButtons = options.controlButtons || [ 'fullscreen', 'setting', 'video' ];
  	    options.autoHideControlBar = options.autoHideControlBar !== undefined ? options.autoHideControlBar : false;
  	    options.autoHideInfospot = options.autoHideInfospot !== undefined ? options.autoHideInfospot : true;
  	    options.horizontalView = options.horizontalView !== undefined ? options.horizontalView : false;
  	    options.clickTolerance = options.clickTolerance || 10;
  	    options.cameraFov = options.cameraFov || 60;
  	    options.reverseDragging = options.reverseDragging || false;
  	    options.enableReticle = options.enableReticle || false;
  	    options.dwellTime = options.dwellTime || 1500;
  	    options.autoReticleSelect = options.autoReticleSelect !== undefined ? options.autoReticleSelect : true;
  	    options.viewIndicator = options.viewIndicator !== undefined ? options.viewIndicator : false;
  	    options.indicatorSize = options.indicatorSize || 30;
  	    options.output = options.output ? options.output : 'none';
  	    options.autoRotate = options.autoRotate || false;
  	    options.autoRotateSpeed = options.autoRotateSpeed || 2.0;
  	    options.autoRotateActivationDuration = options.autoRotateActivationDuration || 5000;
  
  	    this.options = options;
  
  	    /*
  	     * CSS Icon
  	     * const styleLoader = new StyleLoader();
  	     * styleLoader.inject( 'icono' );
  	     */
  
  	    // Container
  	    if ( options.container ) {
  
  	        container = options.container;
  	        container._width = container.clientWidth;
  	        container._height = container.clientHeight;
  
  	    } else {
  
  	        container = document.createElement( 'div' );
  	        container.classList.add( 'panolens-container' );
  	        container.style.width = '100%';
  	        container.style.height = '100%';
  	        container._width = window.innerWidth;
  	        container._height = window.innerHeight;
  	        document.body.appendChild( container );
  
  	    }
  
  	    this.container = container;
  
  	    this.camera = options.camera || new THREE.PerspectiveCamera( this.options.cameraFov, this.container.clientWidth / this.container.clientHeight, 1, 10000 );
  	    this.scene = options.scene || new THREE.Scene();
  	    this.renderer = options.renderer || new THREE.WebGLRenderer( { alpha: true, antialias: false } );
  	    this.sceneReticle = new THREE.Scene();
  
  	    this.viewIndicatorSize = this.options.indicatorSize;
  
  	    this.reticle = {};
  	    this.tempEnableReticle = this.options.enableReticle;
  
  	    this.mode = MODES.NORMAL;
  
  	    this.panorama = null;
  	    this.widget = null;
  
  	    this.hoverObject = null;
  	    this.infospot = null;
  	    this.pressEntityObject = null;
  	    this.pressObject = null;
  
  	    this.raycaster = new THREE.Raycaster();
  	    this.raycasterPoint = new THREE.Vector2();
  	    this.userMouse = new THREE.Vector2();
  	    this.updateCallbacks = [];
  	    this.requestAnimationId = null;
  
  	    this.cameraFrustum = new THREE.Frustum();
  	    this.cameraViewProjectionMatrix = new THREE.Matrix4();
  
  	    this.autoRotateRequestId = null;
  
  	    this.outputDivElement = null;
  
  	    this.touchSupported = 'ontouchstart' in window || window.DocumentTouch && document instanceof DocumentTouch;
  
  	    // Handler references
  	    this.HANDLER_MOUSE_DOWN = this.onMouseDown.bind( this );
  	    this.HANDLER_MOUSE_UP = this.onMouseUp.bind( this );
  	    this.HANDLER_MOUSE_MOVE = this.onMouseMove.bind( this );
  	    this.HANDLER_WINDOW_RESIZE = this.onWindowResize.bind( this );
  	    this.HANDLER_KEY_DOWN = this.onKeyDown.bind( this );
  	    this.HANDLER_KEY_UP = this.onKeyUp.bind( this );
  	    this.HANDLER_TAP = this.onTap.bind( this, {
  	        clientX: this.container.clientWidth / 2,
  	        clientY: this.container.clientHeight / 2
  	    } );
  
  	    // Flag for infospot output
  	    this.OUTPUT_INFOSPOT = false;
  
  	    // Animations
  	    this.tweenLeftAnimation = new Tween.Tween();
  	    this.tweenUpAnimation = new Tween.Tween();
  
  	    // Renderer
  	    this.renderer.setPixelRatio( window.devicePixelRatio );
  	    this.renderer.setSize( this.container.clientWidth, this.container.clientHeight );
  	    this.renderer.setClearColor( 0x000000, 0 );
  	    this.renderer.autoClear = false;
  
  	    // Append Renderer Element to container
  	    this.renderer.domElement.classList.add( 'panolens-canvas' );
  	    this.renderer.domElement.style.display = 'block';
  	    this.container.style.backgroundColor = '#000';
  	    this.container.appendChild( this.renderer.domElement );
  
  	    // Camera Controls
  	    this.OrbitControls = new OrbitControls( this.camera, this.container );
  	    this.OrbitControls.id = 'orbit';
  	    this.OrbitControls.minDistance = 1;
  	    this.OrbitControls.noPan = true;
  	    this.OrbitControls.autoRotate = this.options.autoRotate;
  	    this.OrbitControls.autoRotateSpeed = this.options.autoRotateSpeed;
  
  	    this.DeviceOrientationControls = new DeviceOrientationControls( this.camera, this.container );
  	    this.DeviceOrientationControls.id = 'device-orientation';
  	    this.DeviceOrientationControls.enabled = false;
  	    this.camera.position.z = 1;
  
  	    // Register change event if passiveRenering
  	    if ( this.options.passiveRendering ) {
  
  	        console.warn( 'passiveRendering is now deprecated' );
  
  	    }
  
  	    // Controls
  	    this.controls = [ this.OrbitControls, this.DeviceOrientationControls ];
  	    this.control = this.OrbitControls;
  
  	    // Cardboard effect
  	    this.CardboardEffect = new CardboardEffect( this.renderer );
  	    this.CardboardEffect.setSize( this.container.clientWidth, this.container.clientHeight );
  
  	    // Stereo effect
  	    this.StereoEffect = new StereoEffect( this.renderer );
  	    this.StereoEffect.setSize( this.container.clientWidth, this.container.clientHeight );
  
  	    this.effect = this.CardboardEffect;
  
  	    // Add default hidden reticle
  	    this.addReticle();
  
  	    // Lock horizontal view
  	    if ( this.options.horizontalView ) {
  	        this.OrbitControls.minPolarAngle = Math.PI / 2;
  	        this.OrbitControls.maxPolarAngle = Math.PI / 2;
  	    }
  
  	    // Add Control UI
  	    if ( this.options.controlBar !== false ) {
  	        this.addDefaultControlBar( this.options.controlButtons );
  	    }
  
  	    // Add View Indicator
  	    if ( this.options.viewIndicator ) {
  	        this.addViewIndicator();
  	    }
  
  	    // Reverse dragging direction
  	    if ( this.options.reverseDragging ) {
  	        this.reverseDraggingDirection();
  	    }
  
  	    // Register event if reticle is enabled, otherwise defaults to mouse
  	    if ( this.options.enableReticle ) {
  	        this.enableReticleControl();
  	    } else {
  	        this.registerMouseAndTouchEvents();
  	    }
  
  	    // Output infospot position to an overlay container if specified
  	    if ( this.options.output === 'overlay' ) {
  	        this.addOutputElement();
  	    }
  
  	    // Register dom event listeners
  	    this.registerEventListeners();
  
  	    // Animate
  	    this.animate.call( this );
  
  	}
  	Viewer.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype ), {
  
  	    constructor: Viewer,
  
  	    /**
  	     * Add an object to the scene
  	     * Automatically hookup with panolens-viewer-handler listener
  	     * to communicate with viewer method
  	     * @param {THREE.Object3D} object - The object to be added
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    add: function ( object ) {
  
  	        if ( arguments.length > 1 ) {
  
  	            for ( let i = 0; i < arguments.length; i ++ ) {
  
  	                this.add( arguments[ i ] );
  
  	            }
  
  	            return this;
  
  	        }
  
  	        this.scene.add( object );
  
  	        // All object added to scene has 'panolens-viewer-handler' event to handle viewer communication
  	        if ( object.addEventListener ) {
  
  	            object.addEventListener( 'panolens-viewer-handler', this.eventHandler.bind( this ) );
  
  	        }
  
  	        // All object added to scene being passed with container
  	        if ( object instanceof Panorama && object.dispatchEvent ) {
  
  	            object.dispatchEvent( { type: 'panolens-container', container: this.container } );
  
  	        }
  
  	        if ( object instanceof CameraPanorama ) {
  
  	            object.dispatchEvent( { type: 'panolens-scene', scene: this.scene } );
  
  	        }
  
  	        // Hookup default panorama event listeners
  	        if ( object.type === 'panorama' ) {
  
  	            this.addPanoramaEventListener( object );
  
  	            if ( !this.panorama ) {
  
  	                this.setPanorama( object );
  
  	            }
  
  	        }
  
  	    },
  
  	    /**
  	     * Remove an object from the scene
  	     * @param  {THREE.Object3D} object - Object to be removed
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    remove: function ( object ) {
  
  	        if ( object.removeEventListener ) {
  
  	            object.removeEventListener( 'panolens-viewer-handler', this.eventHandler.bind( this ) );
  
  	        }
  
  	        this.scene.remove( object );
  
  	    },
  
  	    /**
  	     * Add default control bar
  	     * @param {array} array - The control buttons array
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    addDefaultControlBar: function ( array ) {
  
  	        if ( this.widget ) {
  
  	            console.warn( 'Default control bar exists' );
  	            return;
  
  	        }
  
  	        const widget = new Widget( this.container );
  	        widget.addEventListener( 'panolens-viewer-handler', this.eventHandler.bind( this ) );
  	        widget.addControlBar();
  	        array.forEach( buttonName => {
  
  	            widget.addControlButton( buttonName );
  
  	        } );
  
  	        this.widget = widget;
  
  	    },
  
  	    /**
  	     * Set a panorama to be the current one
  	     * @param {Panorama} pano - Panorama to be set
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    setPanorama: function ( pano ) {
  
  	        const leavingPanorama = this.panorama;
  
  	        if ( pano.type === 'panorama' && leavingPanorama !== pano ) {
  
  	            // Clear exisiting infospot
  	            this.hideInfospot();
  
  	            const afterEnterComplete = function () {
  
  	                if ( leavingPanorama ) { leavingPanorama.onLeave(); }
  	                pano.removeEventListener( 'enter-fade-start', afterEnterComplete );
  
  	            };
  
  	            pano.addEventListener( 'enter-fade-start', afterEnterComplete );
  
  	            // Assign and enter panorama
  	            (this.panorama = pano).onEnter();
  				
  	        }
  
  	    },
  
  	    /**
  	     * Event handler to execute commands from child objects
  	     * @param {object} event - The dispatched event with method as function name and data as an argument
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    eventHandler: function ( event ) {
  
  	        if ( event.method && this[ event.method ] ) {
  
  	            this[ event.method ]( event.data );
  
  	        }
  
  	    },
  
  	    /**
  	     * Dispatch event to all descendants
  	     * @param  {object} event - Event to be passed along
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    dispatchEventToChildren: function ( event ) {
  
  	        this.scene.traverse( function ( object ) {
  
  	            if ( object.dispatchEvent ) {
  
  	                object.dispatchEvent( event );
  
  	            }
  
  	        });
  
  	    },
  
  	    /**
  	     * Set widget content
  	     * @method activateWidgetItem
  	     * @param  {integer} controlIndex - Control index
  	     * @param  {integer} mode - Modes for effects
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    activateWidgetItem: function ( controlIndex, mode ) {
  
  	        const mainMenu = this.widget.mainMenu;
  	        const ControlMenuItem = mainMenu.children[ 0 ];
  	        const ModeMenuItem = mainMenu.children[ 1 ];
  
  	        let item;
  
  	        if ( controlIndex !== undefined ) {
  
  	            switch ( controlIndex ) {
  
  	            case 0:
  
  	                item = ControlMenuItem.subMenu.children[ 1 ];
  
  	                break;
  
  	            case 1:
  
  	                item = ControlMenuItem.subMenu.children[ 2 ];
  
  	                break;
  						
  	            default:
  
  	                item = ControlMenuItem.subMenu.children[ 1 ];
  
  	                break;	
  
  	            }
  
  	            ControlMenuItem.subMenu.setActiveItem( item );
  	            ControlMenuItem.setSelectionTitle( item.textContent );
  
  	        }
  
  	        if ( mode !== undefined ) {
  
  	            switch( mode ) {
  
  	            case MODES.CARDBOARD:
  
  	                item = ModeMenuItem.subMenu.children[ 2 ];
  
  	                break;
  
  	            case MODES.STEREO:
  
  	                item = ModeMenuItem.subMenu.children[ 3 ];
  						
  	                break;
  
  	            default:
  
  	                item = ModeMenuItem.subMenu.children[ 1 ];
  
  	                break;
  	            }
  
  	            ModeMenuItem.subMenu.setActiveItem( item );
  	            ModeMenuItem.setSelectionTitle( item.textContent );
  
  	        }
  
  	    },
  
  	    /**
  	     * Enable rendering effect
  	     * @param  {MODES} mode - Modes for effects
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    enableEffect: function ( mode ) {
  
  	        if ( this.mode === mode ) { return; }
  	        if ( mode === MODES.NORMAL ) { this.disableEffect(); return; }
  	        else { this.mode = mode; }
  
  	        const fov = this.camera.fov;
  
  	        switch( mode ) {
  
  	        case MODES.CARDBOARD:
  
  	            this.effect = this.CardboardEffect;
  	            this.enableReticleControl();
  
  	            break;
  
  	        case MODES.STEREO:
  
  	            this.effect = this.StereoEffect;
  	            this.enableReticleControl();
  					
  	            break;
  
  	        default:
  
  	            this.effect = null;
  	            this.disableReticleControl();
  
  	            break;
  
  	        }
  
  	        this.activateWidgetItem( undefined, this.mode );
  
  	        /**
  	         * Dual eye effect event
  	         * @type {object}
  	         * @event Infospot#panolens-dual-eye-effect
  	         * @property {MODES} mode - Current display mode
  	         */
  	        this.dispatchEventToChildren( { type: 'panolens-dual-eye-effect', mode: this.mode } );
  
  	        // Force effect stereo camera to update by refreshing fov
  	        this.camera.fov = fov + 10e-3;
  	        this.effect.setSize( this.container.clientWidth, this.container.clientHeight );
  	        this.render();
  	        this.camera.fov = fov;
  
  	        /**
  	         * Dispatch mode change event
  	         * @type {object}
  	         * @event Viewer#mode-change
  	         * @property {MODES} mode - Current display mode
  	         */
  	        this.dispatchEvent( { type: 'mode-change', mode: this.mode } );
  
  	    },
  
  	    /**
  	     * Disable additional rendering effect
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    disableEffect: function () {
  
  	        if ( this.mode === MODES.NORMAL ) { return; }
  
  	        this.mode = MODES.NORMAL;
  	        this.disableReticleControl();
  
  	        this.activateWidgetItem( undefined, this.mode );
  
  	        /**
  	         * Dual eye effect event
  	         * @type {object}
  	         * @event Infospot#panolens-dual-eye-effect
  	         * @property {MODES} mode - Current display mode
  	         */
  	        this.dispatchEventToChildren( { type: 'panolens-dual-eye-effect', mode: this.mode } );
  
  	        this.renderer.setSize( this.container.clientWidth, this.container.clientHeight );
  	        this.render();
  
  	        /**
  	         * Dispatch mode change event
  	         * @type {object}
  	         * @event Viewer#mode-change
  	         * @property {MODES} mode - Current display mode
  	         */
  	        this.dispatchEvent( { type: 'mode-change', mode: this.mode } );
  	    },
  
  	    /**
  	     * Enable reticle control
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    enableReticleControl: function () {
  
  	        if ( this.reticle.visible ) { return; }
  
  	        this.tempEnableReticle = true;
  
  	        // Register reticle event and unregister mouse event
  	        this.unregisterMouseAndTouchEvents();
  	        this.reticle.show();
  	        this.registerReticleEvent();
  	        this.updateReticleEvent();
  
  	    },
  
  	    /**
  	     * Disable reticle control
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    disableReticleControl: function () {
  
  	        this.tempEnableReticle = false;
  
  	        // Register mouse event and unregister reticle event
  	        if ( !this.options.enableReticle ) {
  
  	            this.reticle.hide();
  	            this.unregisterReticleEvent();
  	            this.registerMouseAndTouchEvents();
  
  	        } else {
  
  	            this.updateReticleEvent();
  
  	        }
  
  	    },
  
  	    /**
  	     * Enable auto rotation
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    enableAutoRate: function () {
  
  	        this.options.autoRotate = true;
  	        this.OrbitControls.autoRotate = true;
  
  	    },
  
  	    /**
  	     * Disable auto rotation
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    disableAutoRate: function () {
  
  	        clearTimeout( this.autoRotateRequestId );
  	        this.options.autoRotate = false;
  	        this.OrbitControls.autoRotate = false;
  
  	    },
  
  	    /**
  	     * Toggle video play or stop
  	     * @param {boolean} pause
  	     * @memberOf Viewer
  	     * @instance
  	     * @fires Viewer#video-toggle
  	     */
  	    toggleVideoPlay: function ( pause ) {
  
  	        if ( this.panorama instanceof VideoPanorama ) {
  
  	            /**
  	             * Toggle video event
  	             * @type {object}
  	             * @event Viewer#video-toggle
  	             */
  	            this.panorama.dispatchEvent( { type: 'video-toggle', pause: pause } );
  
  	        }
  
  	    },
  
  	    /**
  	     * Set currentTime in a video
  	     * @param {number} percentage - Percentage of a video. Range from 0.0 to 1.0
  	     * @memberOf Viewer
  	     * @instance
  	     * @fires Viewer#video-time
  	     */
  	    setVideoCurrentTime: function ( percentage ) {
  
  	        if ( this.panorama instanceof VideoPanorama ) {
  
  	            /**
  	             * Setting video time event
  	             * @type {object}
  	             * @event Viewer#video-time
  	             * @property {number} percentage - Percentage of a video. Range from 0.0 to 1.0
  	             */
  	            this.panorama.dispatchEvent( { type: 'video-time', percentage: percentage } );
  
  	        }
  
  	    },
  
  	    /**
  	     * This will be called when video updates if an widget is present
  	     * @param {number} percentage - Percentage of a video. Range from 0.0 to 1.0
  	     * @memberOf Viewer
  	     * @instance
  	     * @fires Viewer#video-update
  	     */
  	    onVideoUpdate: function ( percentage ) {
  
  	        const { widget } = this;
  
  	        /**
  	         * Video update event
  	         * @type {object}
  	         * @event Viewer#video-update
  	         * @property {number} percentage - Percentage of a video. Range from 0.0 to 1.0
  	         */
  	        if( widget ) { widget.dispatchEvent( { type: 'video-update', percentage: percentage } ); }
  
  	    },
  
  	    /**
  	     * Add update callback to be called every animation frame
  	     * @param {function} callback
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    addUpdateCallback: function ( fn ) {
  
  	        if ( fn ) {
  
  	            this.updateCallbacks.push( fn );
  
  	        }
  
  	    },
  
  	    /**
  	     * Remove update callback
  	     * @param  {function} fn - The function to be removed
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    removeUpdateCallback: function ( fn ) {
  
  	        const index = this.updateCallbacks.indexOf( fn );
  
  	        if ( fn && index >= 0 ) {
  
  	            this.updateCallbacks.splice( index, 1 );
  
  	        }
  
  	    },
  
  	    /**
  	     * Show video widget
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    showVideoWidget: function () {
  
  	        const { widget } = this;
  
  	        /**
  	         * Show video widget event
  	         * @type {object}
  	         * @event Viewer#video-control-show
  	         */
  	        if( widget ) { widget.dispatchEvent( { type: 'video-control-show' } ); }
  
  	    },
  
  	    /**
  	     * Hide video widget
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    hideVideoWidget: function () {
  
  	        const { widget } = this;
  
  	        /**
  	         * Hide video widget
  	         * @type {object}
  	         * @event Viewer#video-control-hide
  	         */
  	        if( widget ) { widget.dispatchEvent( { type: 'video-control-hide' } ); }
  
  	    },
  
  	    /**
  	     * Update video play button
  	     * @param {boolean} paused 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    updateVideoPlayButton: function ( paused ) {
  
  	        const { widget } = this;
  
  	        if ( widget && widget.videoElement && widget.videoElement.controlButton ) {
  
  	            widget.videoElement.controlButton.update( paused );
  
  	        }
  
  	    },
  
  	    /**
  	     * Add default panorama event listeners
  	     * @param {Panorama} pano - The panorama to be added with event listener
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    addPanoramaEventListener: function ( pano ) {
  
  	        // Set camera control on every panorama
  	        pano.addEventListener( 'enter-fade-start', this.setCameraControl.bind( this ) );
  
  	        // Show and hide widget event only when it's VideoPanorama
  	        if ( pano instanceof VideoPanorama ) {
  
  	            pano.addEventListener( 'enter-fade-start', this.showVideoWidget.bind( this ) );
  	            pano.addEventListener( 'leave', function () {
  
  	                if ( !(this.panorama instanceof VideoPanorama) ) {
  
  	                    this.hideVideoWidget.call( this );
  
  	                }
  					
  	            }.bind( this ) );
  
  	        }
  
  	    },
  
  	    /**
  	     * Set camera control
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    setCameraControl: function () {
  
  	        this.OrbitControls.target.copy( this.panorama.position );
  
  	    },
  
  	    /**
  	     * Get current camera control
  	     * @return {object} - Current navigation control
  	     * @memberOf Viewer
  	     * @instance
  	     * @returns {THREE.OrbitControls|THREE.DeviceOrientationControls}
  	     */
  	    getControl: function () {
  
  	        return this.control;
  
  	    },
  
  	    /**
  	     * Get scene
  	     * @memberOf Viewer
  	     * @instance
  	     * @return {THREE.Scene} - Current scene which the viewer is built on
  	     */
  	    getScene: function () {
  
  	        return this.scene;
  
  	    },
  
  	    /**
  	     * Get camera
  	     * @memberOf Viewer
  	     * @instance
  	     * @return {THREE.Camera} - The scene camera
  	     */
  	    getCamera: function () {
  
  	        return this.camera;
  
  	    },
  
  	    /**
  	     * Get renderer
  	     * @memberOf Viewer
  	     * @instance
  	     * @return {THREE.WebGLRenderer} - The renderer using webgl
  	     */
  	    getRenderer: function () {
  
  	        return this.renderer;
  
  	    },
  
  	    /**
  	     * Get container
  	     * @memberOf Viewer
  	     * @instance
  	     * @return {HTMLElement} - The container holds rendererd canvas
  	     */
  	    getContainer: function () {
  
  	        return this.container;
  
  	    },
  
  	    /**
  	     * Get control id
  	     * @memberOf Viewer
  	     * @instance
  	     * @return {string} - Control id. 'orbit' or 'device-orientation'
  	     */
  	    getControlId: function () {
  
  	        return this.control.id;
  
  	    },
  
  	    /**
  	     * Get next navigation control id
  	     * @memberOf Viewer
  	     * @instance
  	     * @return {string} - Next control id
  	     */
  	    getNextControlId: function () {
  
  	        return this.controls[ this.getNextControlIndex() ].id;
  
  	    },
  
  	    /**
  	     * Get next navigation control index
  	     * @memberOf Viewer
  	     * @instance
  	     * @return {number} - Next control index
  	     */
  	    getNextControlIndex: function () {
  
  	        const controls = this.controls;
  	        const control = this.control;
  	        const nextIndex = controls.indexOf( control ) + 1;
  
  	        return ( nextIndex >= controls.length ) ? 0 : nextIndex;
  
  	    },
  
  	    /**
  	     * Set field of view of camera
  	     * @param {number} fov
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    setCameraFov: function ( fov ) {
  
  	        this.camera.fov = fov;
  	        this.camera.updateProjectionMatrix();
  
  	    },
  
  	    /**
  	     * Enable control by index
  	     * @param  {CONTROLS} index - Index of camera control
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    enableControl: function ( index ) {
  
  	        index = ( index >= 0 && index < this.controls.length ) ? index : 0;
  
  	        this.control.enabled = false;
  
  	        this.control = this.controls[ index ];
  
  	        this.control.enabled = true;
  
  	        switch ( index ) {
  
  	        case CONTROLS.ORBIT:
  
  	            this.camera.position.copy( this.panorama.position );
  	            this.camera.position.z += 1;
  
  	            break;
  
  	        case CONTROLS.DEVICEORIENTATION:
  
  	            this.camera.position.copy( this.panorama.position );
  
  	            break;
  
  	        default:
  
  	            break;
  	        }
  
  	        this.control.update();
  
  	        this.activateWidgetItem( index, undefined );
  
  	    },
  
  	    /**
  	     * Disable current control
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    disableControl: function () {
  
  	        this.control.enabled = false;
  
  	    },
  
  	    /**
  	     * Toggle next control
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    toggleNextControl: function () {
  
  	        this.enableControl( this.getNextControlIndex() );
  
  	    },
  
  	    /**
  	     * Screen Space Projection
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    getScreenVector: function ( worldVector ) {
  
  	        const vector = worldVector.clone();
  	        const widthHalf = ( this.container.clientWidth ) / 2;
  	        const heightHalf = this.container.clientHeight / 2;
  
  	        vector.project( this.camera );
  
  	        vector.x = ( vector.x * widthHalf ) + widthHalf;
  	        vector.y = - ( vector.y * heightHalf ) + heightHalf;
  	        vector.z = 0;
  
  	        return vector;
  
  	    },
  
  	    /**
  	     * Check Sprite in Viewport
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    checkSpriteInViewport: function ( sprite ) {
  
  	        this.camera.matrixWorldInverse.getInverse( this.camera.matrixWorld );
  	        this.cameraViewProjectionMatrix.multiplyMatrices( this.camera.projectionMatrix, this.camera.matrixWorldInverse );
  	        this.cameraFrustum.setFromMatrix( this.cameraViewProjectionMatrix );
  
  	        return sprite.visible && this.cameraFrustum.intersectsSprite( sprite );
  
  	    },
  
  	    /**
  	     * Reverse dragging direction
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    reverseDraggingDirection: function () {
  
  	        this.OrbitControls.rotateSpeed *= -1;
  	        this.OrbitControls.momentumScalingFactor *= -1;
  
  	    },
  
  	    /**
  	     * Add reticle 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    addReticle: function () {
  
  	        this.reticle = new Reticle( 0xffffff, true, this.options.dwellTime );
  	        this.reticle.hide();
  	        this.camera.add( this.reticle );
  	        this.sceneReticle.add( this.camera );
  
  	    },
  
  	    /**
  	     * Tween control looking center
  	     * @param {THREE.Vector3} vector - Vector to be looked at the center
  	     * @param {number} [duration=1000] - Duration to tween
  	     * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    tweenControlCenter: function ( vector, duration, easing ) {
  
  	        if ( this.control !== this.OrbitControls ) {
  
  	            return;
  
  	        }
  
  	        // Pass in arguments as array
  	        if ( vector instanceof Array ) {
  
  	            duration = vector[ 1 ];
  	            easing = vector[ 2 ];
  	            vector = vector[ 0 ];
  
  	        }
  
  	        duration = duration !== undefined ? duration : 1000;
  	        easing = easing || Tween.Easing.Exponential.Out;
  
  	        let scope, ha, va, chv, cvv, hv, vv, vptc, ov, nv;
  
  	        scope = this;
  
  	        chv = this.camera.getWorldDirection( new THREE.Vector3() );
  	        cvv = chv.clone();
  
  	        vptc = this.panorama.getWorldPosition( new THREE.Vector3() ).sub( this.camera.getWorldPosition( new THREE.Vector3() ) );
  
  	        hv = vector.clone();
  	        // Scale effect
  	        hv.x *= -1;
  	        hv.add( vptc ).normalize();
  	        vv = hv.clone();
  
  	        chv.y = 0;
  	        hv.y = 0;
  
  	        ha = Math.atan2( hv.z, hv.x ) - Math.atan2( chv.z, chv.x );
  	        ha = ha > Math.PI ? ha - 2 * Math.PI : ha;
  	        ha = ha < -Math.PI ? ha + 2 * Math.PI : ha;
  	        va = Math.abs( cvv.angleTo( chv ) + ( cvv.y * vv.y <= 0 ? vv.angleTo( hv ) : -vv.angleTo( hv ) ) );
  	        va *= vv.y < cvv.y ? 1 : -1;
  
  	        ov = { left: 0, up: 0 };
  	        nv = { left: 0, up: 0 };
  
  	        this.tweenLeftAnimation.stop();
  	        this.tweenUpAnimation.stop();
  
  	        this.tweenLeftAnimation = new Tween.Tween( ov )
  	            .to( { left: ha }, duration )
  	            .easing( easing )
  	            .onUpdate(function(ov){
  	                scope.control.rotateLeft( ov.left - nv.left );
  	                nv.left = ov.left;
  	            })
  	            .start();
  
  	        this.tweenUpAnimation = new Tween.Tween( ov )
  	            .to( { up: va }, duration )
  	            .easing( easing )
  	            .onUpdate(function(ov){
  	                scope.control.rotateUp( ov.up - nv.up );
  	                nv.up = ov.up;
  	            })
  	            .start();
  
  	    },
  
  	    /**
  	     * Tween control looking center by object
  	     * @param {THREE.Object3D} object - Object to be looked at the center
  	     * @param {number} [duration=1000] - Duration to tween
  	     * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    tweenControlCenterByObject: function ( object, duration, easing ) {
  
  	        let isUnderScalePlaceHolder = false;
  
  	        object.traverseAncestors( function ( ancestor ) {
  
  	            if ( ancestor.scalePlaceHolder ) {
  
  	                isUnderScalePlaceHolder = true;
  
  	            }
  	        } );
  
  	        if ( isUnderScalePlaceHolder ) {
  
  	            const invertXVector = new THREE.Vector3( -1, 1, 1 );
  
  	            this.tweenControlCenter( object.getWorldPosition( new THREE.Vector3() ).multiply( invertXVector ), duration, easing );
  
  	        } else {
  
  	            this.tweenControlCenter( object.getWorldPosition( new THREE.Vector3() ), duration, easing );
  
  	        }
  
  	    },
  
  	    /**
  	     * This is called when window size is changed
  	     * @fires Viewer#window-resize
  	     * @param {number} [windowWidth] - Specify if custom element has changed width
  	     * @param {number} [windowHeight] - Specify if custom element has changed height
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onWindowResize: function ( windowWidth, windowHeight ) {
  
  	        let width, height;
  
  	        const expand = this.container.classList.contains( 'panolens-container' ) || this.container.isFullscreen;
  
  	        if ( windowWidth !== undefined && windowHeight !== undefined ) {
  
  	            width = windowWidth;
  	            height = windowHeight;
  	            this.container._width = windowWidth;
  	            this.container._height = windowHeight;
  
  	        } else {
  
  	            const isAndroid = /(android)/i.test(window.navigator.userAgent);
  
  	            const adjustWidth = isAndroid 
  	                ? Math.min(document.documentElement.clientWidth, window.innerWidth || 0) 
  	                : Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
  
  	            const adjustHeight = isAndroid 
  	                ? Math.min(document.documentElement.clientHeight, window.innerHeight || 0) 
  	                : Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
  
  	            width = expand ? adjustWidth : this.container.clientWidth;
  	            height = expand ? adjustHeight : this.container.clientHeight;
  
  	            this.container._width = width;
  	            this.container._height = height;
  
  	        }
  
  	        this.camera.aspect = width / height;
  	        this.camera.updateProjectionMatrix();
  
  	        this.renderer.setSize( width, height );
  
  	        // Update reticle
  	        if ( this.options.enableReticle || this.tempEnableReticle ) {
  
  	            this.updateReticleEvent();
  
  	        }
  
  	        /**
  	         * Window resizing event
  	         * @type {object}
  	         * @event Viewer#window-resize
  	         * @property {number} width  - Width of the window
  	         * @property {number} height - Height of the window
  	         */
  	        this.dispatchEvent( { type: 'window-resize', width: width, height: height });
  	        this.scene.traverse( function ( object ) {
  
  	            if ( object.dispatchEvent ) {
  
  	                object.dispatchEvent( { type: 'window-resize', width: width, height: height });
  
  	            }
  
  	        } );
  
  	    },
  
  	    /**
  	     * Add output element
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    addOutputElement: function () {
  
  	        const element = document.createElement( 'div' );
  	        element.style.position = 'absolute';
  	        element.style.right = '10px';
  	        element.style.top = '10px';
  	        element.style.color = '#fff';
  	        this.container.appendChild( element );
  	        this.outputDivElement = element;
  
  	    },
  
  	    /**
  	     * Output position in developer console by holding down Ctrl button
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    outputPosition: function () {
  
  	        const intersects = this.raycaster.intersectObject( this.panorama, true );
  
  	        if ( intersects.length > 0 ) {
  
  	            const point = intersects[ 0 ].point.clone();
  	            const converter = new THREE.Vector3( -1, 1, 1 );
  	            const world = this.panorama.getWorldPosition( new THREE.Vector3() );
  	            point.sub( world ).multiply( converter );
  
  	            const message = `${point.x.toFixed(2)}, ${point.y.toFixed(2)}, ${point.z.toFixed(2)}`;
  
  	            if ( point.length() === 0 ) { return; }
  
  	            switch ( this.options.output ) {
  
  	            case 'console':
  	                console.info( message );
  	                break;
  
  	            case 'overlay':
  	                this.outputDivElement.textContent = message;
  	                break;
  
  	            default:
  	                break;
  
  	            }
  
  	        }
  
  	    },
  
  	    /**
  	     * On mouse down
  	     * @param {MouseEvent} event 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onMouseDown: function ( event ) {
  
  	        event.preventDefault();
  
  	        this.userMouse.x = ( event.clientX >= 0 ) ? event.clientX : event.touches[0].clientX;
  	        this.userMouse.y = ( event.clientY >= 0 ) ? event.clientY : event.touches[0].clientY;
  	        this.userMouse.type = 'mousedown';
  	        this.onTap( event );
  
  	    },
  
  	    /**
  	     * On mouse move
  	     * @param {MouseEvent} event 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onMouseMove: function ( event ) {
  
  	        event.preventDefault();
  	        this.userMouse.type = 'mousemove';
  	        this.onTap( event );
  
  	    },
  
  	    /**
  	     * On mouse up
  	     * @param {MouseEvent} event 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onMouseUp: function ( event ) {
  
  	        let onTarget = false;
  
  	        this.userMouse.type = 'mouseup';
  
  	        const type = ( this.userMouse.x >= event.clientX - this.options.clickTolerance 
  					&& this.userMouse.x <= event.clientX + this.options.clickTolerance
  					&& this.userMouse.y >= event.clientY - this.options.clickTolerance
  					&& this.userMouse.y <= event.clientY + this.options.clickTolerance ) 
  					||  ( event.changedTouches 
  					&& this.userMouse.x >= event.changedTouches[0].clientX - this.options.clickTolerance
  					&& this.userMouse.x <= event.changedTouches[0].clientX + this.options.clickTolerance 
  					&& this.userMouse.y >= event.changedTouches[0].clientY - this.options.clickTolerance
  					&& this.userMouse.y <= event.changedTouches[0].clientY + this.options.clickTolerance ) 
  	            ? 'click' : undefined;
  
  	        // Event should happen on canvas
  	        if ( event && event.target && !event.target.classList.contains( 'panolens-canvas' ) ) { return; }
  
  	        event.preventDefault();
  
  	        if ( event.changedTouches && event.changedTouches.length === 1 ) {
  
  	            onTarget = this.onTap( { clientX: event.changedTouches[0].clientX, clientY: event.changedTouches[0].clientY }, type );
  			
  	        } else {
  
  	            onTarget = this.onTap( event, type );
  
  	        }
  
  	        this.userMouse.type = 'none';
  
  	        if ( onTarget ) { 
  
  	            return; 
  
  	        }
  
  	        if ( type === 'click' ) {
  
  	            const { options: { autoHideInfospot, autoHideControlBar }, panorama, toggleControlBar } = this;
  
  	            if ( autoHideInfospot && panorama ) {
  
  	                panorama.toggleInfospotVisibility();
  
  	            }
  
  	            if ( autoHideControlBar ) {
  
  	                toggleControlBar();
  
  	            }
  
  	        }
  
  	    },
  
  	    /**
  	     * On tap eveny frame
  	     * @param {MouseEvent} event 
  	     * @param {string} type 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onTap: function ( event, type ) {
  
  	        const { left, top } = this.container.getBoundingClientRect();
  	        const { clientWidth, clientHeight } = this.container;
  
  	        this.raycasterPoint.x = ( ( event.clientX - left ) / clientWidth ) * 2 - 1;
  	        this.raycasterPoint.y = - ( ( event.clientY - top ) / clientHeight ) * 2 + 1;
  
  	        this.raycaster.setFromCamera( this.raycasterPoint, this.camera );
  
  	        // Return if no panorama 
  	        if ( !this.panorama ) { 
  
  	            return; 
  
  	        }
  
  	        // output infospot information
  	        if ( event.type !== 'mousedown' && this.touchSupported || this.OUTPUT_INFOSPOT ) { 
  
  	            this.outputPosition(); 
  
  	        }
  
  	        const intersects = this.raycaster.intersectObjects( this.panorama.children, true );
  	        const intersect_entity = this.getConvertedIntersect( intersects );
  	        const intersect = ( intersects.length > 0 ) ? intersects[0].object : undefined;
  
  	        if ( this.userMouse.type === 'mouseup'  ) {
  
  	            if ( intersect_entity && this.pressEntityObject === intersect_entity && this.pressEntityObject.dispatchEvent ) {
  
  	                this.pressEntityObject.dispatchEvent( { type: 'pressstop-entity', mouseEvent: event } );
  
  	            }
  
  	            this.pressEntityObject = undefined;
  
  	        }
  
  	        if ( this.userMouse.type === 'mouseup'  ) {
  
  	            if ( intersect && this.pressObject === intersect && this.pressObject.dispatchEvent ) {
  
  	                this.pressObject.dispatchEvent( { type: 'pressstop', mouseEvent: event } );
  
  	            }
  
  	            this.pressObject = undefined;
  
  	        }
  
  	        if ( type === 'click' ) {
  
  	            this.panorama.dispatchEvent( { type: 'click', intersects: intersects, mouseEvent: event } );
  
  	            if ( intersect_entity && intersect_entity.dispatchEvent ) {
  
  	                intersect_entity.dispatchEvent( { type: 'click-entity', mouseEvent: event } );
  
  	            }
  
  	            if ( intersect && intersect.dispatchEvent ) {
  
  	                intersect.dispatchEvent( { type: 'click', mouseEvent: event } );
  
  	            }
  
  	        } else {
  
  	            this.panorama.dispatchEvent( { type: 'hover', intersects: intersects, mouseEvent: event } );
  
  	            if ( ( this.hoverObject && intersects.length > 0 && this.hoverObject !== intersect_entity )
  					|| ( this.hoverObject && intersects.length === 0 ) ){
  
  	                if ( this.hoverObject.dispatchEvent ) {
  
  	                    this.hoverObject.dispatchEvent( { type: 'hoverleave', mouseEvent: event } );
  
  	                    this.reticle.end();
  
  	                }
  
  	                this.hoverObject = undefined;
  
  	            }
  
  	            if ( intersect_entity && intersects.length > 0 ) {
  
  	                if ( this.hoverObject !== intersect_entity ) {
  
  	                    this.hoverObject = intersect_entity;
  
  	                    if ( this.hoverObject.dispatchEvent ) {
  
  	                        this.hoverObject.dispatchEvent( { type: 'hoverenter', mouseEvent: event } );
  
  	                        // Start reticle timer
  	                        if ( this.options.autoReticleSelect && this.options.enableReticle || this.tempEnableReticle ) {
  	                            this.reticle.start( this.onTap.bind( this, event, 'click' ) );
  	                        }
  
  	                    }
  
  	                }
  
  	                if ( this.userMouse.type === 'mousedown' && this.pressEntityObject != intersect_entity ) {
  
  	                    this.pressEntityObject = intersect_entity;
  
  	                    if ( this.pressEntityObject.dispatchEvent ) {
  
  	                        this.pressEntityObject.dispatchEvent( { type: 'pressstart-entity', mouseEvent: event } );
  
  	                    }
  
  	                }
  
  	                if ( this.userMouse.type === 'mousedown' && this.pressObject != intersect ) {
  
  	                    this.pressObject = intersect;
  
  	                    if ( this.pressObject.dispatchEvent ) {
  
  	                        this.pressObject.dispatchEvent( { type: 'pressstart', mouseEvent: event } );
  
  	                    }
  
  	                }
  
  	                if ( this.userMouse.type === 'mousemove' || this.options.enableReticle ) {
  
  	                    if ( intersect && intersect.dispatchEvent ) {
  
  	                        intersect.dispatchEvent( { type: 'hover', mouseEvent: event } );
  
  	                    }
  
  	                    if ( this.pressEntityObject && this.pressEntityObject.dispatchEvent ) {
  
  	                        this.pressEntityObject.dispatchEvent( { type: 'pressmove-entity', mouseEvent: event } );
  
  	                    }
  
  	                    if ( this.pressObject && this.pressObject.dispatchEvent ) {
  
  	                        this.pressObject.dispatchEvent( { type: 'pressmove', mouseEvent: event } );
  
  	                    }
  
  	                }
  
  	            }
  
  	            if ( !intersect_entity && this.pressEntityObject && this.pressEntityObject.dispatchEvent ) {
  
  	                this.pressEntityObject.dispatchEvent( { type: 'pressstop-entity', mouseEvent: event } );
  
  	                this.pressEntityObject = undefined;
  
  	            }
  
  	            if ( !intersect && this.pressObject && this.pressObject.dispatchEvent ) {
  
  	                this.pressObject.dispatchEvent( { type: 'pressstop', mouseEvent: event } );
  
  	                this.pressObject = undefined;
  
  	            }
  
  	        }
  
  	        // Infospot handler
  	        if ( intersect && intersect instanceof Infospot ) {
  
  	            this.infospot = intersect;
  				
  	            if ( type === 'click' ) {
  
  	                return true;
  
  	            }
  				
  
  	        } else if ( this.infospot ) {
  
  	            this.hideInfospot();
  
  	        }
  
  	        // Auto rotate
  	        if ( this.options.autoRotate && this.userMouse.type !== 'mousemove' ) {
  
  	            // Auto-rotate idle timer
  	            clearTimeout( this.autoRotateRequestId );
  
  	            if ( this.control === this.OrbitControls ) {
  
  	                this.OrbitControls.autoRotate = false;
  	                this.autoRotateRequestId = window.setTimeout( this.enableAutoRate.bind( this ), this.options.autoRotateActivationDuration );
  
  	            }
  
  	        }		
  
  	    },
  
  	    /**
  	     * Get converted intersect
  	     * @param {array} intersects 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    getConvertedIntersect: function ( intersects ) {
  
  	        let intersect;
  
  	        for ( let i = 0; i < intersects.length; i++ ) {
  
  	            if ( intersects[i].distance >= 0 && intersects[i].object && !intersects[i].object.passThrough ) {
  
  	                if ( intersects[i].object.entity && intersects[i].object.entity.passThrough ) {
  	                    continue;
  	                } else if ( intersects[i].object.entity && !intersects[i].object.entity.passThrough ) {
  	                    intersect = intersects[i].object.entity;
  	                    break;
  	                } else {
  	                    intersect = intersects[i].object;
  	                    break;
  	                }
  
  	            }
  
  	        }
  
  	        return intersect;
  
  	    },
  
  	    /**
  	     * Hide infospot
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    hideInfospot: function () {
  
  	        if ( this.infospot ) {
  
  	            this.infospot.onHoverEnd();
  
  	            this.infospot = undefined;
  
  	        }
  
  	    },
  
  	    /**
  	     * Toggle control bar
  	     * @memberOf Viewer
  	     * @instance
  	     * @fires Viewer#control-bar-toggle
  	     */
  	    toggleControlBar: function () {
  
  	        const { widget } = this;
  
  	        /**
  	         * Toggle control bar event
  	         * @type {object}
  	         * @event Viewer#control-bar-toggle
  	         */
  	        if ( widget ) {
  
  	            widget.dispatchEvent( { type: 'control-bar-toggle' } );
  
  	        }
  
  	    },
  
  	    /**
  	     * On key down
  	     * @param {KeyboardEvent} event 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onKeyDown: function ( event ) {
  
  	        if ( this.options.output && this.options.output !== 'none' && event.key === 'Control' ) {
  
  	            this.OUTPUT_INFOSPOT = true;
  
  	        }
  
  	    },
  
  	    /**
  	     * On key up
  	     * @param {KeyboardEvent} event 
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onKeyUp: function () {
  
  	        this.OUTPUT_INFOSPOT = false;
  
  	    },
  
  	    /**
  	     * Update control and callbacks
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    update: function () {
  
  	        Tween.update();
  
  	        this.updateCallbacks.forEach( function( callback ){ callback(); } );
  
  	        this.control.update();
  
  	        this.scene.traverse( function( child ){
  	            if ( child instanceof Infospot 
  					&& child.element 
  					&& ( this.hoverObject === child 
  						|| child.element.style.display !== 'none' 
  						|| (child.element.left && child.element.left.style.display !== 'none')
  						|| (child.element.right && child.element.right.style.display !== 'none') ) ) {
  	                if ( this.checkSpriteInViewport( child ) ) {
  	                    const { x, y } = this.getScreenVector( child.getWorldPosition( new THREE.Vector3() ) );
  	                    child.translateElement( x, y );
  	                } else {
  	                    child.onDismiss();
  	                }
  					
  	            }
  	        }.bind( this ) );
  
  	    },
  
  	    /**
  	     * Rendering function to be called on every animation frame
  	     * Render reticle last
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    render: function () {
  
  	        if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) {
  
  	            this.renderer.clear();
  	            this.effect.render( this.scene, this.camera );
  	            this.effect.render( this.sceneReticle, this.camera );
  				
  
  	        } else {
  
  	            this.renderer.clear();
  	            this.renderer.render( this.scene, this.camera );
  	            this.renderer.clearDepth();
  	            this.renderer.render( this.sceneReticle, this.camera );
  
  	        }
  
  	    },
  
  	    /**
  	     * Animate
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    animate: function () {
  
  	        this.requestAnimationId = window.requestAnimationFrame( this.animate.bind( this ) );
  
  	        this.onChange();
  
  	    },
  
  	    /**
  	     * On change
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onChange: function () {
  
  	        this.update();
  	        this.render();
  
  	    },
  
  	    /**
  	     * Register mouse and touch event on container
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    registerMouseAndTouchEvents: function () {
  
  	        const options = { passive: false };
  
  	        this.container.addEventListener( 'mousedown' , 	this.HANDLER_MOUSE_DOWN, options );
  	        this.container.addEventListener( 'mousemove' , 	this.HANDLER_MOUSE_MOVE, options );
  	        this.container.addEventListener( 'mouseup'	 , 	this.HANDLER_MOUSE_UP  , options );
  	        this.container.addEventListener( 'touchstart', 	this.HANDLER_MOUSE_DOWN, options );
  	        this.container.addEventListener( 'touchend'  , 	this.HANDLER_MOUSE_UP  , options );
  
  	    },
  
  	    /**
  	     * Unregister mouse and touch event on container
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    unregisterMouseAndTouchEvents: function () {
  
  	        this.container.removeEventListener( 'mousedown' ,  this.HANDLER_MOUSE_DOWN, false );
  	        this.container.removeEventListener( 'mousemove' ,  this.HANDLER_MOUSE_MOVE, false );
  	        this.container.removeEventListener( 'mouseup'	,  this.HANDLER_MOUSE_UP  , false );
  	        this.container.removeEventListener( 'touchstart',  this.HANDLER_MOUSE_DOWN, false );
  	        this.container.removeEventListener( 'touchend'  ,  this.HANDLER_MOUSE_UP  , false );
  
  	    },
  
  	    /**
  	     * Register reticle event
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    registerReticleEvent: function () {
  
  	        this.addUpdateCallback( this.HANDLER_TAP );
  
  	    },
  
  	    /**
  	     * Unregister reticle event
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    unregisterReticleEvent: function () {
  
  	        this.removeUpdateCallback( this.HANDLER_TAP );
  
  	    },
  
  	    /**
  	     * Update reticle event
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    updateReticleEvent: function () {
  
  	        const clientX = this.container.clientWidth / 2 + this.container.offsetLeft;
  	        const clientY = this.container.clientHeight / 2;
  
  	        this.removeUpdateCallback( this.HANDLER_TAP );
  	        this.HANDLER_TAP = this.onTap.bind( this, { clientX, clientY } );
  	        this.addUpdateCallback( this.HANDLER_TAP );
  
  	    },
  
  	    /**
  	     * Register container and window listeners
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    registerEventListeners: function () {
  
  	        // Resize Event
  	        window.addEventListener( 'resize' , this.HANDLER_WINDOW_RESIZE, true );
  
  	        // Keyboard Event
  	        window.addEventListener( 'keydown', this.HANDLER_KEY_DOWN, true );
  	        window.addEventListener( 'keyup'  , this.HANDLER_KEY_UP	 , true );
  
  	    },
  
  	    /**
  	     * Unregister container and window listeners
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    unregisterEventListeners: function () {
  
  	        // Resize Event
  	        window.removeEventListener( 'resize' , this.HANDLER_WINDOW_RESIZE, true );
  
  	        // Keyboard Event
  	        window.removeEventListener( 'keydown', this.HANDLER_KEY_DOWN, true );
  	        window.removeEventListener( 'keyup'  , this.HANDLER_KEY_UP  , true );
  
  	    },
  
  	    /**
  	     * Dispose all scene objects and clear cache
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    dispose: function () {
  
  	        this.tweenLeftAnimation.stop();
  	        this.tweenUpAnimation.stop();
  
  	        // Unregister dom event listeners
  	        this.unregisterEventListeners();
  
  	        // recursive disposal on 3d objects
  	        function recursiveDispose ( object ) {
  
  	            for ( let i = object.children.length - 1; i >= 0; i-- ) {
  
  	                recursiveDispose( object.children[i] );
  	                object.remove( object.children[i] );
  
  	            }
  
  	            if ( object instanceof Panorama || object instanceof Infospot ) {
  
  	                object.dispose();
  	                object = null;
  
  	            } else if ( object.dispatchEvent ){
  
  	                object.dispatchEvent( 'dispose' );
  
  	            }
  
  	        }
  
  	        recursiveDispose( this.scene );
  
  	        // dispose widget
  	        if ( this.widget ) {
  
  	            this.widget.dispose();
  	            this.widget = null;
  
  	        }
  
  	        // clear cache
  	        if ( THREE.Cache && THREE.Cache.enabled ) {
  
  	            THREE.Cache.clear();
  
  	        }
  
  	    },
  
  	    /**
  	     * Destroy viewer by disposing and stopping requestAnimationFrame
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    destroy: function () {
  
  	        this.dispose();
  	        this.render();
  	        window.cancelAnimationFrame( this.requestAnimationId );		
  
  	    },
  
  	    /**
  	     * On panorama dispose
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    onPanoramaDispose: function ( panorama ) {
  
  	        if ( panorama instanceof VideoPanorama ) {
  
  	            this.hideVideoWidget();
  
  	        }
  
  	        if ( panorama === this.panorama ) {
  
  	            this.panorama = null;
  
  	        }
  
  	    },
  
  	    /**
  	     * Load ajax call
  	     * @param {string} url - URL to be requested
  	     * @param {function} [callback] - Callback after request completes
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    loadAsyncRequest: function ( url, callback = () => {} ) {
  
  	        const request = new window.XMLHttpRequest();
  	        request.onloadend = function ( event ) {
  	            callback( event );
  	        };
  	        request.open( 'GET', url, true );
  	        request.send( null );
  
  	    },
  
  	    /**
  	     * View indicator in upper left
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    addViewIndicator: function () {
  
  	        const scope = this;
  
  	        function loadViewIndicator ( asyncEvent ) {
  
  	            if ( asyncEvent.loaded === 0 ) return;
  
  	            const viewIndicatorDiv = asyncEvent.target.responseXML.documentElement;
  	            viewIndicatorDiv.style.width = scope.viewIndicatorSize + 'px';
  	            viewIndicatorDiv.style.height = scope.viewIndicatorSize + 'px';
  	            viewIndicatorDiv.style.position = 'absolute';
  	            viewIndicatorDiv.style.top = '10px';
  	            viewIndicatorDiv.style.left = '10px';
  	            viewIndicatorDiv.style.opacity = '0.5';
  	            viewIndicatorDiv.style.cursor = 'pointer';
  	            viewIndicatorDiv.id = 'panolens-view-indicator-container';
  
  	            scope.container.appendChild( viewIndicatorDiv );
  
  	            const indicator = viewIndicatorDiv.querySelector( '#indicator' );
  	            const setIndicatorD = function () {
  
  	                scope.radius = scope.viewIndicatorSize * 0.225;
  	                scope.currentPanoAngle = scope.camera.rotation.y - THREE.Math.degToRad( 90 );
  	                scope.fovAngle = THREE.Math.degToRad( scope.camera.fov ) ;
  	                scope.leftAngle = -scope.currentPanoAngle - scope.fovAngle / 2;
  	                scope.rightAngle = -scope.currentPanoAngle + scope.fovAngle / 2;
  	                scope.leftX = scope.radius * Math.cos( scope.leftAngle );
  	                scope.leftY = scope.radius * Math.sin( scope.leftAngle );
  	                scope.rightX = scope.radius * Math.cos( scope.rightAngle );
  	                scope.rightY = scope.radius * Math.sin( scope.rightAngle );
  	                scope.indicatorD = 'M ' + scope.leftX + ' ' + scope.leftY + ' A ' + scope.radius + ' ' + scope.radius + ' 0 0 1 ' + scope.rightX + ' ' + scope.rightY;
  
  	                if ( scope.leftX && scope.leftY && scope.rightX && scope.rightY && scope.radius ) {
  
  	                    indicator.setAttribute( 'd', scope.indicatorD );
  
  	                }
  
  	            };
  
  	            scope.addUpdateCallback( setIndicatorD );
  
  	            const indicatorOnMouseEnter = function () {
  
  	                this.style.opacity = '1';
  
  	            };
  
  	            const indicatorOnMouseLeave = function () {
  
  	                this.style.opacity = '0.5';
  
  	            };
  
  	            viewIndicatorDiv.addEventListener( 'mouseenter', indicatorOnMouseEnter );
  	            viewIndicatorDiv.addEventListener( 'mouseleave', indicatorOnMouseLeave );
  	        }
  
  	        this.loadAsyncRequest( DataImage.ViewIndicator, loadViewIndicator );
  
  	    },
  
  	    /**
  	     * Append custom control item to existing control bar
  	     * @param {object} [option={}] - Style object to overwirte default element style. It takes 'style', 'onTap' and 'group' properties.
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    appendControlItem: function ( option ) {
  
  	        const item = this.widget.createCustomItem( option );		
  
  	        if ( option.group === 'video' ) {
  
  	            this.widget.videoElement.appendChild( item );
  
  	        } else {
  
  	            this.widget.barElement.appendChild( item );
  
  	        }
  
  	        return item;
  
  	    },
  
  	    /**
  	     * Clear all cached files
  	     * @memberOf Viewer
  	     * @instance
  	     */
  	    clearAllCache: function () {
  
  	        THREE.Cache.clear();
  
  	    }
  
  	} );
  
  	if ( THREE.REVISION != THREE_REVISION ) {
  
  	    console.warn( `three.js version is not matched. Please consider use the target revision ${THREE_REVISION}` );
  
  	}
  
  	/**
  	 * Panolens.js
  	 * @author pchen66
  	 * @namespace PANOLENS
  	 */
  	window.TWEEN = Tween;
  
  	exports.BasicPanorama = BasicPanorama;
  	exports.CONTROLS = CONTROLS;
  	exports.CameraPanorama = CameraPanorama;
  	exports.CubePanorama = CubePanorama;
  	exports.CubeTextureLoader = CubeTextureLoader;
  	exports.DataImage = DataImage;
  	exports.EmptyPanorama = EmptyPanorama;
  	exports.GoogleStreetviewPanorama = GoogleStreetviewPanorama;
  	exports.ImageLittlePlanet = ImageLittlePlanet;
  	exports.ImageLoader = ImageLoader;
  	exports.ImagePanorama = ImagePanorama;
  	exports.Infospot = Infospot;
  	exports.LittlePlanet = LittlePlanet;
  	exports.MODES = MODES;
  	exports.Media = Media;
  	exports.Panorama = Panorama;
  	exports.REVISION = REVISION;
  	exports.Reticle = Reticle;
  	exports.THREE_REVISION = THREE_REVISION;
  	exports.THREE_VERSION = THREE_VERSION;
  	exports.TextureLoader = TextureLoader;
  	exports.VERSION = VERSION;
  	exports.VideoPanorama = VideoPanorama;
  	exports.Viewer = Viewer;
  	exports.Widget = Widget;
  
  	Object.defineProperty(exports, '__esModule', { value: true });
  
  }));
  