/* * Pannellum - An HTML5 based Panorama Viewer * Copyright (c) 2011-2016 Matthew Petroff * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ window.pannellum = (function(window, document, undefined) { 'use strict'; /** * Creates a new panorama viewer. * @constructor * @param {HTMLElement|string} container - The container (div) element for the * viewer, or its ID. * @param {Object} initialConfig - Inital configuration for viewer. */ function Viewer(container, initialConfig) { // Declare variables var config, renderer, oldRenderer, preview, isUserInteracting = false, latestInteraction = Date.now(), onPointerDownPointerX = 0, onPointerDownPointerY = 0, onPointerDownPointerDist = -1, onPointerDownYaw = 0, onPointerDownPitch = 0, keysDown = new Array(10), fullscreenActive = false, loaded = false, error = false, isTimedOut = false, listenersAdded = false, panoImage, prevTime, yawSpeed = 0, pitchSpeed = 0, zoomSpeed = 0, animating = false, update = false, // Should we update when still to render dynamic content hotspotsCreated = false; var defaultConfig = { hfov: 100, minHfov: 50, maxHfov: 120, pitch: 0, minPitch: -85, maxPitch: 85, yaw: 0, minYaw: -180, maxYaw: 180, haov: 360, vaov: 180, vOffset: 0, autoRotate: false, autoRotateInactivityDelay: -1, type: 'equirectangular', northOffset: 0, showFullscreenCtrl: true, dynamic: false, keyboardZoom: true }; // Initialize container container = typeof container === 'string' ? document.getElementById(container) : container; container.className += ' pnlm-container'; container.tabIndex = 0; // Create container for renderer var renderContainer = document.createElement('div'); renderContainer.className = 'pnlm-render-container'; container.appendChild(renderContainer); var dragFix = document.createElement('div'); dragFix.className = 'pnlm-dragfix'; container.appendChild(dragFix); // Display about information on right click var aboutMsg = document.createElement('span'); aboutMsg.className = 'pnlm-about-msg'; aboutMsg.innerHTML = 'Pannellum'; container.appendChild(aboutMsg); dragFix.addEventListener('contextmenu', aboutMessage); // Create info display var infoDisplay = {}; // Panorama info infoDisplay.container = document.createElement('div'); infoDisplay.container.className = 'pnlm-panorama-info'; infoDisplay.title = document.createElement('div'); infoDisplay.title.className = 'pnlm-title-box'; infoDisplay.container.appendChild(infoDisplay.title); infoDisplay.author = document.createElement('div'); infoDisplay.author.className = 'pnlm-author-box'; infoDisplay.container.appendChild(infoDisplay.author); container.appendChild(infoDisplay.container); // Load box infoDisplay.load = {}; infoDisplay.load.box = document.createElement('div'); infoDisplay.load.box.className = 'pnlm-load-box'; infoDisplay.load.box.innerHTML = '
Loading...
'; infoDisplay.load.lbox = document.createElement('div'); infoDisplay.load.lbox.className = 'pnlm-lbox'; infoDisplay.load.lbox.innerHTML = ''; infoDisplay.load.box.appendChild(infoDisplay.load.lbox); infoDisplay.load.lbar = document.createElement('div'); infoDisplay.load.lbar.className = 'pnlm-lbar'; infoDisplay.load.lbarFill = document.createElement('div'); infoDisplay.load.lbarFill.className = 'pnlm-lbar-fill'; infoDisplay.load.lbar.appendChild(infoDisplay.load.lbarFill); infoDisplay.load.box.appendChild(infoDisplay.load.lbar); infoDisplay.load.msg = document.createElement('p'); infoDisplay.load.msg.className = 'pnlm-lmsg'; infoDisplay.load.box.appendChild(infoDisplay.load.msg); container.appendChild(infoDisplay.load.box); // Error message infoDisplay.errorMsg = document.createElement('div'); infoDisplay.errorMsg.className = 'pnlm-error-msg pnlm-info-box'; container.appendChild(infoDisplay.errorMsg); // Create controls var controls = {}; // Load button controls.load = document.createElement('div'); controls.load.className = 'pnlm-load-button'; controls.load.innerHTML = 'Click to
Load
Panorama
';
controls.load.addEventListener('click', load);
container.appendChild(controls.load);
// Zoom controls
controls.zoom = document.createElement('div');
controls.zoom.className = 'pnlm-zoom-controls pnlm-controls';
controls.zoomIn = document.createElement('div');
controls.zoomIn.className = 'pnlm-zoom-in pnlm-sprite pnlm-control';
controls.zoomIn.addEventListener('click', zoomIn);
controls.zoom.appendChild(controls.zoomIn);
controls.zoomOut = document.createElement('div');
controls.zoomOut.className = 'pnlm-zoom-out pnlm-sprite pnlm-control';
controls.zoomOut.addEventListener('click', zoomOut);
controls.zoom.appendChild(controls.zoomOut);
container.appendChild(controls.zoom);
// Fullscreen toggle
controls.fullscreen = document.createElement('div');
controls.fullscreen.addEventListener('click', toggleFullscreen);
controls.fullscreen.className = 'pnlm-fullscreen-toggle-button pnlm-sprite pnlm-fullscreen-toggle-button-inactive pnlm-controls pnlm-control';
if (document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled)
container.appendChild(controls.fullscreen);
// Compass
var compass = document.createElement('div');
compass.className = 'pnlm-compass pnlm-controls pnlm-control';
container.appendChild(compass);
// Load and process configuration
if (initialConfig.firstScene) {
// Activate first scene if specified in URL
mergeConfig(initialConfig.firstScene);
} else if (initialConfig.default && initialConfig.default.firstScene) {
// Activate first scene if specified in file
mergeConfig(initialConfig.default.firstScene);
} else {
mergeConfig(null);
}
processOptions();
/**
* Initializes viewer.
* @private
*/
function init() {
// Display an error for IE 9 as it doesn't work but also doesn't otherwise
// show an error (older versions don't work at all)
// Based on: http://stackoverflow.com/a/10965203
var div = document.createElement("div");
div.innerHTML = "";
if (div.getElementsByTagName("i").length == 1) {
anError();
return;
}
var i, p;
if (config.type == 'cubemap') {
panoImage = [];
for (i = 0; i < 6; i++) {
panoImage.push(new Image());
panoImage[i].crossOrigin = 'anonymous';
}
infoDisplay.load.lbox.style.display = 'block';
infoDisplay.load.lbar.style.display = 'none';
} else if (config.type == 'multires') {
var c = JSON.parse(JSON.stringify(config.multiRes)); // Deep copy
if (config.basePath && config.multiRes.basePath) { // avoid 'undefined' in path, check (optional) multiRes.basePath, too
c.basePath = config.basePath + config.multiRes.basePath;
} else if (config.basePath) {
c.basePath = config.basePath;
}
panoImage = c;
} else {
if (config.dynamic === true) {
panoImage = config.panorama;
} else {
if (config.panorama === undefined) {
anError('No panorama image was specified.');
return;
}
panoImage = new Image();
}
}
// From http://stackoverflow.com/a/19709846
var absoluteURL = function(url) {
return new RegExp('^(?:[a-z]+:)?//', 'i').test(url) | url[0] == '/';
};
// Configure image loading
if (config.type == 'cubemap') {
// Quick loading counter for synchronous loading
var itemsToLoad = 6;
var onLoad = function() {
itemsToLoad--;
if (itemsToLoad === 0) {
onImageLoad();
}
};
var onError = function(e) {
var a = document.createElement('a');
a.href = e.target.src;
a.innerHTML = a.href;
anError('The file ' + a.outerHTML + ' could not be accessed.');
};
for (i = 0; i < panoImage.length; i++) {
panoImage[i].onload = onLoad;
panoImage[i].onerror = onError;
p = config.cubeMap[i];
if (config.basePath && !absoluteURL(p)) {
p = config.basePath + p;
}
panoImage[i].src = encodeURI(p);
}
} else if (config.type == 'multires') {
onImageLoad();
} else {
p = '';
if (config.basePath) {
p = config.basePath;
}
if (config.dynamic !== true) {
// Still image
p = absoluteURL(config.panorama) ? config.panorama : p + config.panorama;
panoImage.onload = function() {
window.URL.revokeObjectURL(this.src); // Clean up
onImageLoad();
};
var xhr = new XMLHttpRequest();
xhr.onloadend = function() {
if (xhr.status != 200) {
// Display error if image can't be loaded
var a = document.createElement('a');
a.href = encodeURI(p);
a.innerHTML = a.href;
anError('The file ' + a.outerHTML + ' could not be accessed.');
}
var img = this.response;
parseGPanoXMP(img);
infoDisplay.load.msg.innerHTML = '';
};
xhr.onprogress = function(e) {
if (e.lengthComputable) {
// Display progress
var percent = e.loaded / e.total * 100;
infoDisplay.load.lbarFill.style.width = percent + '%';
var unit, numerator, denominator;
if (e.total > 1e6) {
unit = 'MB';
numerator = (e.loaded / 1e6).toFixed(2);
denominator = (e.total / 1e6).toFixed(2);
} else if (e.total > 1e3) {
unit = 'kB';
numerator = (e.loaded / 1e3).toFixed(1);
denominator = (e.total / 1e3).toFixed(1);
} else {
unit = 'B';
numerator = e.loaded;
denominator = e.total;
}
infoDisplay.load.msg.innerHTML = numerator + ' / ' + denominator + ' ' + unit;
} else {
// Display loading spinner
infoDisplay.load.lbox.style.display = 'block';
infoDisplay.load.lbar.style.display = 'none';
}
};
try {
xhr.open('GET', p, true);
} catch (e) {
// Malformed URL
anError('There is something wrong with the panorama URL.');
}
xhr.responseType = 'blob';
xhr.setRequestHeader('Accept', 'image/*,*/*;q=0.9');
xhr.send();
}
}
container.classList.add('pnlm-grab');
container.classList.remove('pnlm-grabbing');
}
/**
* Create renderer and initialize event listeners once image is loaded.
* @private
*/
function onImageLoad() {
renderer = new libpannellum.renderer(renderContainer, panoImage, config.type, config.dynamic);
if (config.dynamic !== true) {
// Allow image to be garbage collected
panoImage = undefined;
}
// Only add event listeners once
if (!listenersAdded) {
listenersAdded = true;
container.addEventListener('mousedown', onDocumentMouseDown, false);
document.addEventListener('mousemove', onDocumentMouseMove, false);
document.addEventListener('mouseup', onDocumentMouseUp, false);
container.addEventListener('mousewheel', onDocumentMouseWheel, false);
container.addEventListener('DOMMouseScroll', onDocumentMouseWheel, false);
container.addEventListener('mozfullscreenchange', onFullScreenChange, false);
container.addEventListener('webkitfullscreenchange', onFullScreenChange, false);
container.addEventListener('msfullscreenchange', onFullScreenChange, false);
container.addEventListener('fullscreenchange', onFullScreenChange, false);
window.addEventListener('resize', onDocumentResize, false);
container.addEventListener('keydown', onDocumentKeyPress, false);
container.addEventListener('keyup', onDocumentKeyUp, false);
container.addEventListener('blur', clearKeys, false);
document.addEventListener('mouseleave', onDocumentMouseUp, false);
container.addEventListener('touchstart', onDocumentTouchStart, false);
container.addEventListener('touchmove', onDocumentTouchMove, false);
container.addEventListener('touchend', onDocumentTouchEnd, false);
container.addEventListener('pointerdown', onDocumentPointerDown, false);
container.addEventListener('pointermove', onDocumentPointerMove, false);
container.addEventListener('pointerup', onDocumentPointerUp, false);
container.addEventListener('pointerleave', onDocumentPointerUp, false);
// Deal with MS pointer events
if (window.navigator.pointerEnabled)
container.style.touchAction = 'none';
}
renderInit();
setTimeout(function(){isTimedOut = true;}, 500);
}
/**
* Parses Google Photo Sphere XMP Metadata.
* https://developers.google.com/photo-sphere/metadata/
* @private
* @param {Image} image - Image to read XMP metadata from.
*/
function parseGPanoXMP(image) {
var reader = new FileReader();
reader.addEventListener('loadend', function() {
var img = reader.result;
// This awful browser specific test exists because iOS 8 does not work
// with non-progressive encoded JPEGs.
if (navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/)) {
var flagIndex = img.indexOf('\xff\xc2');
if (flagIndex < 0 || flagIndex > 65536) {
anError("Due to iOS 8's broken WebGL implementation, only " +
"progressive encoded JPEGs work for your device (this " +
"panorama uses standard encoding).");
}
}
var start = img.indexOf('
' + error + '
'; } else { infoDisplay.errorMsg.innerHTML = 'Your browser does not have the necessary WebGL support to display this panorama.
'; } controls.load.style.display = 'none'; infoDisplay.load.box.style.display = 'none'; infoDisplay.errorMsg.style.display = 'table'; error = true; renderContainer.style.display = 'none'; } /** * Hides error message display. * @private */ function clearError() { infoDisplay.load.box.style.display = 'none'; infoDisplay.errorMsg.style.display = 'none'; error = false; } /** * Displays about message. * @private * @param {MouseEvent} event - Right click location */ function aboutMessage(event) { var pos = mousePosition(event); aboutMsg.style.left = pos.x + 'px'; aboutMsg.style.top = pos.y + 'px'; clearTimeout(aboutMessage.t1); clearTimeout(aboutMessage.t2); aboutMsg.style.display = 'block'; aboutMsg.style.opacity = 1; aboutMessage.t1 = setTimeout(function() {aboutMsg.style.opacity = 0;}, 2000); aboutMessage.t2 = setTimeout(function() {aboutMsg.style.display = 'none';}, 2500); event.preventDefault(); } /** * Calculate mouse position relative to top left of viewer container. * @private * @param {MouseEvent} event - Mouse event to use in calculation * @returns {Object} Calculated X and Y coordinates */ function mousePosition(event) { var bounds = container.getBoundingClientRect(); var pos = {}; pos.x = event.clientX - bounds.left; pos.y = event.clientY - bounds.top; return pos; } /** * Event handler for mouse clicks. Initializes panning. Prints center and click * location coordinates when hot spot debugging is enabled. * @private * @param {MouseEvent} event - Document mouse down event. */ function onDocumentMouseDown(event) { // Override default action event.preventDefault(); // But not all of it container.focus(); // Only do something if the panorama is loaded if (!loaded) { return; } // Calculate mouse position relative to top left of viewer container var pos = mousePosition(event); // Log pitch / yaw of mouse click when debugging / placing hot spots if (config.hotSpotDebug) { var coords = mouseEventToCoords(event); console.log('Pitch: ' + coords[0] + ', Yaw: ' + coords[1] + ', Center Pitch: ' + config.pitch + ', Center Yaw: ' + config.yaw + ', HFOV: ' + config.hfov); } // Turn off auto-rotation if enabled config.autoRotate = false; isUserInteracting = true; latestInteraction = Date.now(); onPointerDownPointerX = pos.x; onPointerDownPointerY = pos.y; onPointerDownYaw = config.yaw; onPointerDownPitch = config.pitch; container.classList.add('pnlm-grabbing'); container.classList.remove('pnlm-grab'); animateInit(); } /** * Calculate panorama pitch and yaw from location of mouse event. * @private * @param {MouseEvent} event - Document mouse down event. * @returns {number[]} [pitch, yaw] */ function mouseEventToCoords(event) { var pos = mousePosition(event); var canvas = renderer.getCanvas(); var x = pos.x / canvas.width * 2 - 1; var y = (1 - pos.y / canvas.height * 2) * canvas.height / canvas.width; var focal = 1 / Math.tan(config.hfov * Math.PI / 360); var s = Math.sin(config.pitch * Math.PI / 180); var c = Math.cos(config.pitch * Math.PI / 180); var a = focal * c - y * s; var root = Math.sqrt(x*x + a*a); var pitch = Math.atan((y * c + focal * s) / root) * 180 / Math.PI; var yaw = Math.atan2(x / root, a / root) * 180 / Math.PI + config.yaw; return [pitch, yaw]; } /** * Event handler for mouse moves. Pans center of view. * @private * @param {MouseEvent} event - Document mouse move event. */ function onDocumentMouseMove(event) { if (isUserInteracting && loaded) { latestInteraction = Date.now(); var canvas = renderer.getCanvas(); var pos = mousePosition(event); //TODO: This still isn't quite right var yaw = ((Math.atan(onPointerDownPointerX / canvas.width * 2 - 1) - Math.atan(pos.x / canvas.width * 2 - 1)) * 180 / Math.PI * config.hfov / 90) + onPointerDownYaw; yawSpeed = (yaw - config.yaw) % 360 * 0.2; config.yaw = yaw; var vfov = 2 * Math.atan(Math.tan(config.hfov/360*Math.PI) * canvas.height / canvas.width) * 180 / Math.PI; var pitch = ((Math.atan(pos.y / canvas.height * 2 - 1) - Math.atan(onPointerDownPointerY / canvas.height * 2 - 1)) * 180 / Math.PI * vfov / 90) + onPointerDownPitch; pitchSpeed = (pitch - config.pitch) * 0.2; config.pitch = pitch; } } /** * Event handler for mouse up events. Stops panning. * @private */ function onDocumentMouseUp() { if (!isUserInteracting) { return; } isUserInteracting = false; if (Date.now() - latestInteraction > 15) { // Prevents jump when user rapidly moves mouse, stops, and then // releases the mouse button pitchSpeed = yawSpeed = 0; } container.classList.add('pnlm-grab'); container.classList.remove('pnlm-grabbing'); } /** * Event handler for touches. Initializes panning if one touch or zooming if * two touches. * @private * @param {TouchEvent} event - Document touch start event. */ function onDocumentTouchStart(event) { // Only do something if the panorama is loaded if (!loaded) { return; } // Calculate touch position relative to top left of viewer container var pos0 = mousePosition(event.targetTouches[0]); onPointerDownPointerX = pos0.x; onPointerDownPointerY = pos0.y; if (event.targetTouches.length == 2) { // Down pointer is the center of the two fingers var pos1 = mousePosition(event.targetTouches[1]); onPointerDownPointerX += (pos1.x - pos0.x) * 0.5; onPointerDownPointerY += (pos1.y - pos0.y) * 0.5; onPointerDownPointerDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + (pos0.y - pos1.y) * (pos0.y - pos1.y)); } isUserInteracting = true; latestInteraction = Date.now(); onPointerDownYaw = config.yaw; onPointerDownPitch = config.pitch; animateInit(); } /** * Event handler for touch movements. Pans center of view if one touch or * adjusts zoom if two touches. * @private * @param {TouchEvent} event - Document touch move event. */ function onDocumentTouchMove(event) { // Override default action event.preventDefault(); if (loaded) { latestInteraction = Date.now(); } if (isUserInteracting && loaded) { var pos0 = mousePosition(event.targetTouches[0]); var clientX = pos0.x; var clientY = pos0.y; if (event.targetTouches.length == 2 && onPointerDownPointerDist != -1) { var pos1 = mousePosition(event.targetTouches[1]); clientX += (pos1.x - pos0.x) * 0.5; clientY += (pos1.y - pos0.y) * 0.5; var clientDist = Math.sqrt((pos0.x - pos1.x) * (pos0.x - pos1.x) + (pos0.y - pos1.y) * (pos0.y - pos1.y)); setHfov(config.hfov + (onPointerDownPointerDist - clientDist) * 0.1); onPointerDownPointerDist = clientDist; } var yaw = (onPointerDownPointerX - clientX) * 0.1 + onPointerDownYaw; yawSpeed = (yaw - config.yaw) % 360 * 0.2; config.yaw = yaw; var pitch = (clientY - onPointerDownPointerY) * 0.1 + onPointerDownPitch; pitchSpeed = (pitch - config.pitch) * 0.2; config.pitch = pitch; } } /** * Event handler for end of touches. Stops panning and/or zooming. * @private */ function onDocumentTouchEnd() { isUserInteracting = false; if (Date.now() - latestInteraction > 150) { pitchSpeed = yawSpeed = 0; } onPointerDownPointerDist = -1; } var pointerIDs = [], pointerCoordinates = []; /** * Event handler for touch starts in IE / Edge. * @private * @param {PointerEvent} event - Document pointer down event. */ function onDocumentPointerDown(event) { if (event.pointerType == 'touch') { pointerIDs.push(event.pointerId); pointerCoordinates.push({clientX: event.clientX, clientY: event.clientY}); event.targetTouches = pointerCoordinates; onDocumentTouchStart(event); event.preventDefault(); } } /** * Event handler for touch moves in IE / Edge. * @private * @param {PointerEvent} event - Document pointer move event. */ function onDocumentPointerMove(event) { if (event.pointerType == 'touch') { for (var i = 0; i < pointerIDs.length; i++) { if (event.pointerId == pointerIDs[i]) { pointerCoordinates[i] = {clientX: event.clientX, clientY: event.clientY}; event.targetTouches = pointerCoordinates; onDocumentTouchMove(event); //event.preventDefault(); return; } } } } /** * Event handler for touch ends in IE / Edge. * @private * @param {PointerEvent} event - Document pointer up event. */ function onDocumentPointerUp(event) { if (event.pointerType == 'touch') { var defined = false; for (var i = 0; i < pointerIDs.length; i++) { if (event.pointerId == pointerIDs[i]) pointerIDs[i] = undefined; if (pointerIDs[i]) defined = true; } if (!defined) { pointerIDs = []; pointerCoordinates = []; onDocumentTouchEnd(); } event.preventDefault(); } } /** * Event handler for mouse wheel. Changes zoom. * @private * @param {WheelEvent} event - Document mouse wheel event. */ function onDocumentMouseWheel(event) { event.preventDefault(); // Only do something if the panorama is loaded if (!loaded) { return; } latestInteraction = Date.now(); if (event.wheelDeltaY) { // WebKit setHfov(config.hfov - event.wheelDeltaY * 0.05); zoomSpeed = event.wheelDelta < 0 ? 1 : -1; } else if (event.wheelDelta) { // Opera / Explorer 9 setHfov(config.hfov - event.wheelDelta * 0.05); zoomSpeed = event.wheelDelta < 0 ? 1 : -1; } else if (event.detail) { // Firefox setHfov(config.hfov + event.detail * 1.5); zoomSpeed = event.detail > 0 ? 1 : -1; } animateInit(); } /** * Event handler for key presses. Updates list of currently pressed keys. * @private * @param {KeyboardEvent} event - Document key press event. */ function onDocumentKeyPress(event) { // Override default action event.preventDefault(); // Turn off auto-rotation if enabled config.autoRotate = false; // Record key pressed var keynumber = event.keycode; if (event.which) { keynumber = event.which; } // If escape key is pressed if (keynumber == 27) { // If in fullscreen mode if (fullscreenActive) { toggleFullscreen(); } } else { // Change key changeKey(keynumber, true); } } /** * Clears list of currently pressed keys. * @private */ function clearKeys() { for (var i = 0; i < 10; i++) { keysDown[i] = false; } } /** * Event handler for key releases. Updates list of currently pressed keys. * @private * @param {KeyboardEvent} event - Document key up event. */ function onDocumentKeyUp(event) { // Override default action event.preventDefault(); // Record key released var keynumber = event.keycode; if (event.which) { keynumber = event.which; } // Change key changeKey(keynumber, false); } /** * Updates list of currently pressed keys. * @private * @param {number} keynumber - Key number. * @param {boolean} value - Whether or not key is pressed. */ function changeKey(keynumber, value) { var keyChanged = false; switch(keynumber) { // If minus key is released case 109: case 189: case 17: if (keysDown[0] != value) { keyChanged = true; } keysDown[0] = value; break; // If plus key is released case 107: case 187: case 16: if (keysDown[1] != value) { keyChanged = true; } keysDown[1] = value; break; // If up arrow is released case 38: if (keysDown[2] != value) { keyChanged = true; } keysDown[2] = value; break; // If "w" is released case 87: if (keysDown[6] != value) { keyChanged = true; } keysDown[6] = value; break; // If down arrow is released case 40: if (keysDown[3] != value) { keyChanged = true; } keysDown[3] = value; break; // If "s" is released case 83: if (keysDown[7] != value) { keyChanged = true; } keysDown[7] = value; break; // If left arrow is released case 37: if (keysDown[4] != value) { keyChanged = true; } keysDown[4] = value; break; // If "a" is released case 65: if (keysDown[8] != value) { keyChanged = true; } keysDown[8] = value; break; // If right arrow is released case 39: if (keysDown[5] != value) { keyChanged = true; } keysDown[5] = value; break; // If "d" is released case 68: if (keysDown[9] != value) { keyChanged = true; } keysDown[9] = value; } if (keyChanged && value) { if (typeof performance !== 'undefined' && performance.now()) { prevTime = performance.now(); } else { prevTime = Date.now(); } animateInit(); } } /** * Pans and/or zooms panorama based on currently pressed keys. Also handles * panorama "inertia" and auto rotation. * @private */ function keyRepeat() { // Only do something if the panorama is loaded if (!loaded) { return; } var prevPitch = config.pitch; var prevYaw = config.yaw; var prevZoom = config.hfov; var newTime; if (typeof performance !== 'undefined' && performance.now()) { newTime = performance.now(); } else { newTime = Date.now(); } if (prevTime === undefined) { prevTime = newTime; } var diff = (newTime - prevTime) * config.hfov / 1700; diff = Math.min(diff, 1.0); // If minus key is down if (keysDown[0] && config.keyboardZoom === true) { setHfov(config.hfov + (zoomSpeed * 0.8 + 0.5) * diff); } // If plus key is down if (keysDown[1] && config.keyboardZoom === true) { setHfov(config.hfov + (zoomSpeed * 0.8 - 0.2) * diff); } // If up arrow or "w" is down if (keysDown[2] || keysDown[6]) { // Pan up config.pitch += (pitchSpeed * 0.8 + 0.2) * diff; } // If down arrow or "s" is down if (keysDown[3] || keysDown[7]) { // Pan down config.pitch += (pitchSpeed * 0.8 - 0.2) * diff; } // If left arrow or "a" is down if (keysDown[4] || keysDown[8]) { // Pan left config.yaw += (yawSpeed * 0.8 - 0.2) * diff; } // If right arrow or "d" is down if (keysDown[5] || keysDown[9]) { // Pan right config.yaw += (yawSpeed * 0.8 + 0.2) * diff; } // If auto-rotate var inactivityInterval = Date.now() - latestInteraction; if (config.autoRotate && inactivityInterval > config.autoRotateInactivityDelay && config.autoRotateStopDelay !== false) { // Pan if (diff > 0.000001) { config.yaw -= config.autoRotate / 60 * diff; } // Deal with stopping auto rotation after a set delay if (config.autoRotateStopDelay) { config.autoRotateStopDelay -= newTime - prevTime; if (config.autoRotateStopDelay <= 0) { config.autoRotateStopDelay = false; } } } // "Inertia" if (diff > 0) { // "Friction" var friction = 0.85; // Yaw if (!keysDown[4] && !keysDown[5] && !keysDown[8] && !keysDown[9]) { config.yaw += yawSpeed * diff * friction; } // Pitch if (!keysDown[2] && !keysDown[3] && !keysDown[6] && !keysDown[7]) { config.pitch += pitchSpeed * diff * friction; } // Zoom if (!keysDown[0] && !keysDown[1]) { setHfov(config.hfov + zoomSpeed * diff * friction); } } prevTime = newTime; if (diff > 0) { yawSpeed = yawSpeed * 0.8 + (config.yaw - prevYaw) / diff * 0.2; pitchSpeed = pitchSpeed * 0.8 + (config.pitch - prevPitch) / diff * 0.2; zoomSpeed = zoomSpeed * 0.8 + (config.hfov - prevZoom) / diff * 0.2; // Limit speed var maxSpeed = 5; yawSpeed = Math.min(maxSpeed, Math.max(yawSpeed, -maxSpeed)); pitchSpeed = Math.min(maxSpeed, Math.max(pitchSpeed, -maxSpeed)); zoomSpeed = Math.min(maxSpeed, Math.max(zoomSpeed, -maxSpeed)); } // Stop movement if opposite controls are pressed if (keysDown[0] && keysDown[0]) { zoomSpeed = 0; } if ((keysDown[2] || keysDown[6]) && (keysDown[3] || keysDown[7])) { pitchSpeed = 0; } if ((keysDown[4] || keysDown[8]) && (keysDown[5] || keysDown[9])) { yawSpeed = 0; } } /** * Event handler for document resizes. Updates viewer size and rerenders view. * @private */ function onDocumentResize() { // Resize panorama renderer renderer.resize(); animateInit(); // Kludge to deal with WebKit regression: https://bugs.webkit.org/show_bug.cgi?id=93525 onFullScreenChange(); } /** * Initializes animation. * @private */ function animateInit() { if (animating) { return; } animating = true; animate(); } /** * Animates view, using requestAnimationFrame to trigger rendering. * @private */ function animate() { render(); if (isUserInteracting) { requestAnimationFrame(animate); } else if (keysDown[0] || keysDown[1] || keysDown[2] || keysDown[3] || keysDown[4] || keysDown[5] || keysDown[6] || keysDown[7] || keysDown[8] || keysDown[9] || config.autoRotate || Math.abs(yawSpeed) > 0.01 || Math.abs(pitchSpeed) > 0.01 || Math.abs(zoomSpeed) > 0.01) { keyRepeat(); requestAnimationFrame(animate); } else if (renderer && (renderer.isLoading() || (config.dynamic === true && update))) { requestAnimationFrame(animate); } else { animating = false; } } /** * Renders panorama view. * @private */ function render() { var tmpyaw; if (loaded) { if (config.yaw > 180) { config.yaw -= 360; } else if (config.yaw < -180) { config.yaw += 360; } // Keep a tmp value of yaw for autoRotate comparison later tmpyaw = config.yaw; // Ensure the yaw is within min and max allowed config.yaw = Math.max(config.minYaw, Math.min(config.maxYaw, config.yaw)); // Check if we autoRotate in a limited by min and max yaw // If so reverse direction if (config.autoRotate !== false && tmpyaw != config.yaw) { config.autoRotate *= -1; } // Ensure the calculated pitch is within min and max allowed config.pitch = Math.max(config.minPitch, Math.min(config.maxPitch, config.pitch)); renderer.render(config.pitch * Math.PI / 180, config.yaw * Math.PI / 180, config.hfov * Math.PI / 180); renderHotSpots(); // Update compass if (config.compass) { compass.style.transform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; compass.style.webkitTransform = 'rotate(' + (-config.yaw - config.northOffset) + 'deg)'; } } } /** * Initializes renderer. * @private */ function renderInit() { try { renderer.init(config.haov * Math.PI / 180, config.vaov * Math.PI / 180, config.vOffset * Math.PI / 180, renderInitCallback); } catch(event) { // Panorama not loaded // Display error if there is a bad texture if (event.type == 'webgl error' || event.type == 'no webgl') { anError(); } else if (event.type == 'webgl size error') { anError('This panorama is too big for your device! It\'s ' + event.width + 'px wide, but your device only supports images up to ' + event.maxWidth + 'px wide. Try another device.' + ' (If you\'re the author, try scaling down the image.)'); } } } /** * Triggered when render initialization finishes. Handles fading between * scenes as well as showing the compass and hotspots and hiding the loading * display. * @private */ function renderInitCallback() { if (oldRenderer !== undefined) { oldRenderer.destroy(); // Fade if specified if (config.sceneFadeDuration && oldRenderer.fadeImg !== undefined) { oldRenderer.fadeImg.style.opacity = 0; // Remove image var fadeImg = oldRenderer.fadeImg; oldRenderer = undefined; setTimeout(function() { renderContainer.removeChild(fadeImg); }, config.sceneFadeDuration); } } // Show compass if applicable if (config.compass) { compass.style.display = 'inline'; } else { compass.style.display = 'none'; } // Show hotspots createHotSpots(); // Hide loading display infoDisplay.load.box.style.display = 'none'; if (preview !== undefined) { renderContainer.removeChild(preview); preview = undefined; } loaded = true; animateInit(); } /** * Creates hot spot elements for the current scene. * @private */ function createHotSpots() { if (hotspotsCreated) return; if (!config.hotSpots) { config.hotSpots = []; } else { // Sort by pitch so tooltip is never obscured by another hot spot config.hotSpots = config.hotSpots.sort(function(a, b) { return a.pitch < b.pitch; }); config.hotSpots.forEach(function(hs) { var div = document.createElement('div'); div.className = 'pnlm-hotspot pnlm-tooltip pnlm-sprite pnlm-' + escapeHTML(hs.type); var span = document.createElement('span'); if (hs.text) span.innerHTML = escapeHTML(hs.text); var a; if (hs.URL) { a = document.createElement('a'); a.href = encodeURI(hs.URL); a.target = '_blank'; renderContainer.appendChild(a); div.style.cursor = 'pointer'; span.style.cursor = 'pointer'; a.appendChild(div); } else if (hs.video) { var video = document.createElement('video'); video.src = encodeURI(hs.video); video.controls = true; video.style.width = hs.width + 'px'; renderContainer.appendChild(div); span.appendChild(video); } else if (hs.image) { a = document.createElement('a'); a.href = encodeURI(hs.image); a.target = '_blank'; span.appendChild(a); var image = document.createElement('img'); image.src = encodeURI(hs.image); image.style.width = hs.width + 'px'; image.style.paddingTop = '5px'; renderContainer.appendChild(div); a.appendChild(image); span.style.maxWidth = 'initial'; } else { if (hs.sceneId) { div.onclick = function() { loadScene(hs.sceneId, hs.targetPitch, hs.targetYaw); return false; }; div.ontouchend = function() { loadScene(hs.sceneId, hs.targetPitch, hs.targetYaw); return false; }; div.style.cursor = 'pointer'; span.style.cursor = 'pointer'; } renderContainer.appendChild(div); } div.appendChild(span); span.style.width = span.scrollWidth - 20 + 'px'; span.style.marginLeft = -(span.scrollWidth - 26) / 2 + 'px'; span.style.marginTop = -span.scrollHeight - 12 + 'px'; hs.div = div; }); } hotspotsCreated = true; renderHotSpots(); } /** * Destroys currently create hot spot elements. * @private */ function destroyHotSpots() { if (config.hotSpots) { for (var i = 0; i < config.hotSpots.length; i++) { var current = config.hotSpots[i].div; while(current.parentNode != renderContainer) { current = current.parentNode; } renderContainer.removeChild(current); delete config.hotSpots[i].div; } } hotspotsCreated = false; delete config.hotSpots; } /** * Renders hot spots, updating their positions and visibility. * @private */ function renderHotSpots() { config.hotSpots.forEach(function(hs) { var hsPitchSin = Math.sin(hs.pitch * Math.PI / 180); var hsPitchCos = Math.cos(hs.pitch * Math.PI / 180); var configPitchSin = Math.sin(config.pitch * Math.PI / 180); var configPitchCos = Math.cos(config.pitch * Math.PI / 180); var yawCos = Math.cos((-hs.yaw + config.yaw) * Math.PI / 180); var hfovTan = Math.tan(config.hfov * Math.PI / 360); var z = hsPitchSin * configPitchSin + hsPitchCos * yawCos * configPitchCos; if ((hs.yaw <= 90 && hs.yaw > -90 && z <= 0) || ((hs.yaw > 90 || hs.yaw <= -90) && z <= 0)) { hs.div.style.visibility = 'hidden'; } else { hs.div.style.visibility = 'visible'; // Subpixel rendering doesn't work in Firefox // https://bugzilla.mozilla.org/show_bug.cgi?id=739176 var canvas = renderer.getCanvas(), canvasWidth = canvas.width / (window.devicePixelRatio || 1), canvasHeight = canvas.height / (window.devicePixelRatio || 1); var transform = 'translate(' + (-canvasWidth / hfovTan * Math.sin((-hs.yaw + config.yaw) * Math.PI / 180) * hsPitchCos / z / 2 + canvasWidth / 2 - 13) + 'px, ' + (-canvasWidth / hfovTan * (hsPitchSin * configPitchCos - hsPitchCos * yawCos * configPitchSin) / z / 2 + canvasHeight / 2 - 13) + 'px) translateZ(9999px)'; hs.div.style.webkitTransform = transform; hs.div.style.MozTransform = transform; hs.div.style.transform = transform; } }); } /** * Merges a scene configuration into the current configuration. * @private * @param {string} sceneId - Identifier of scene configuration to merge in. */ function mergeConfig(sceneId) { config = {}; var k; var photoSphereExcludes = ['haov', 'vaov', 'vOffset', 'northOffset']; // Merge default config for (k in defaultConfig) { if (defaultConfig.hasOwnProperty(k)) { config[k] = defaultConfig[k]; } } // Merge default scene config for (k in initialConfig.default) { if (initialConfig.default.hasOwnProperty(k)) { config[k] = initialConfig.default[k]; if (photoSphereExcludes.indexOf(k) >= 0) { config.ignoreGPanoXMP = true; } } } // Merge current scene config if ((sceneId !== null) && (sceneId !== '') && (initialConfig.scenes) && (initialConfig.scenes[sceneId])) { var scene = initialConfig.scenes[sceneId]; for (k in scene) { if (scene.hasOwnProperty(k)) { config[k] = scene[k]; if (photoSphereExcludes.indexOf(k) >= 0) { config.ignoreGPanoXMP = true; } } } config.activeScene = sceneId; } // Merge initial config for (k in initialConfig) { if (initialConfig.hasOwnProperty(k)) { config[k] = initialConfig[k]; if (photoSphereExcludes.indexOf(k) >= 0) { config.ignoreGPanoXMP = true; } } } } /** * Processes configuration options. * @private */ function processOptions() { // Process preview first so it always loads before the browser hits its // maximum number of connections to a server as can happen with cubic // panoramas if ('preview' in config) { var p = config.preview; if (config.basePath) { p = config.basePath + p; } preview = document.createElement('div'); preview.className = 'pnlm-preview-img'; preview.style.backgroundImage = "url('" + encodeURI(p) + "')"; renderContainer.appendChild(preview); } // Process other options for (var key in config) { if (config.hasOwnProperty(key)) { switch(key) { case 'title': infoDisplay.title.innerHTML = escapeHTML(config[key]); infoDisplay.container.style.display = 'inline'; break; case 'author': infoDisplay.author.innerHTML = 'by ' + escapeHTML(config[key]); infoDisplay.container.style.display = 'inline'; break; case 'fallback': infoDisplay.errorMsg.innerHTML = 'Your browser does not support WebGL.
Click here to view this panorama in an alternative viewer.