Skip to content

Location-aware panoramas: a Pano2VR experiment

In this 2021 experiment I tested whether a Pano2VR tour could choose a panorama according to a device’s geographic position. Possible uses included opening a nearby view or providing a location-based prompt within a wider presentation.

The approach

The script read the panorama coordinates from the tour’s XML data, compared them with a position and selected a scene. It could look for the nearest panorama, target a panorama identified in the URL, or compare the panoramas with supplied coordinates. A distance threshold controlled whether a scene would open.

Limits of the example

This is an archived prototype, not a maintained installation guide or navigation tool. It depends on the exported tour structure, its coordinate data and older script assumptions. Distance to a point also does not establish a walkable route or safe access.

Browser location requires the visitor’s permission and a secure context. See the Geolocation API documentation. Accuracy and update frequency depend on the device and conditions.

Original code

The original example is retained below without a claim that it works with current Pano2VR exports. The distance function returns kilometres, while the displayed result is converted to metres; a threshold must therefore be checked against those units.

var checkTimer;
const urlParams = new URLSearchParams(window.location.search);
const panoParam = urlParams.get('panoramaid');
const panolon = urlParams.get('givenlon');
const panolat = urlParams.get('givenlat');
var smallerdistance = urlParams.get('threshold');
function readXML(position) {
	var xhttp = new XMLHttpRequest();
	xhttp.onreadystatechange = function() {
	   if (this.readyState == 4 && this.status == 200) {
		   initXML(this, position);
	   }
	};
	xhttp.open("GET", "pano.xml", true);
	xhttp.send();
};
function initXML(xml, position) {
    var panorama, panoramaid, i, xmlDoc, panolongitude, panolatitude, userlongitude, userlatitude, panodistance=[], message;
	if (!smallerdistance) {
		smallerdistance = 2000;
	}
    xmlDoc = xml.responseXML;
	userlatitude =  position.coords.latitude;
    panolongitude	 = "";
	panolatitude	 = "";
	userlongitude = position.coords.longitude;
    panorama = xmlDoc.getElementsByTagName('panorama');
	// If none of the url parameters are set, then check for the closest panorama
	// Case 1 - This is done by checking all the panoramas, measuring the distance to the user coordinates and sorting them
	if (!panoParam && !panolon && !panolat) {
		for (i = 0; i < panorama.length; i++) { 
			userdata = xmlDoc.getElementsByTagName('userdata');
			panoramaid = panorama[i].getAttribute('id');
			panolongitude = parseFloat(userdata[i].getAttribute('longitude'));
			panolatitude = parseFloat(userdata[i].getAttribute('latitude'));
			panodistance.push([panoramaid, distance(panolongitude, panolatitude, userlongitude, userlatitude)]);
		}
		var sortedpanos = panodistance.sort(compareDistances);
		var closestpanoid = sortedpanos[0][0];
		var closestpanodistance = sortedpanos[0][1];
		message = "The closest panorama is ";
	// Case 3 - If the url panorama parameter is set then check the distance of the user to this panorama	
	} else if (panoParam) {
		for (i = 0; i < panorama.length; i++) { 
			userdata = xmlDoc.getElementsByTagName('userdata');
			panoramaid = panorama[i].getAttribute('id');
			if (panoramaid == panoParam) {
				panolongitude = parseFloat(userdata[i].getAttribute('longitude'));
				panolatitude = parseFloat(userdata[i].getAttribute('latitude'));
				panodistance.push([panoramaid, distance(panolongitude, panolatitude, userlongitude, userlatitude)]);
				break;
			}
		}
		//var sortedpanos = panodistance.sort(compareDistances);
		var closestpanoid = panodistance[0][0];
		var closestpanodistance = panodistance[0][1];
		message = "This panorama is ";
	// case 5 - If the coordinates url parameters are given then check the closest panorama to this point
	} else if (panolon && panolat) {
		for (i = 0; i < panorama.length; i++) { 
			userdata = xmlDoc.getElementsByTagName('userdata');
			panoramaid = panorama[i].getAttribute('id');
			panolongitude = parseFloat(userdata[i].getAttribute('longitude'));
			panolatitude = parseFloat(userdata[i].getAttribute('latitude'));
			panodistance.push([panoramaid, distance(panolongitude, panolatitude, parseFloat(panolon), parseFloat(panolat))]);
		}
		var sortedpanos = panodistance.sort(compareDistances);
		var closestpanoid = sortedpanos[0][0];
		var closestpanodistance = sortedpanos[0][1];
		message = "The closest panorama to the given coordinate is ";
	}	
	var bearing = checkBearing(userlongitude, userlatitude, panolongitude, panolatitude);
	var currentPano = pano.getCurrentNode();
	if (currentPano !== closestpanoid) {
		// case 2, 4 and 6 (open the panorama if the distance is lower that the smaller distance threshold)
		if (closestpanodistance < smallerdistance) {
			pano.openNext("{" + closestpanoid + "}");
		}
	//alert("This panorama is " + Math.round(closestpanodistance * 100) / 100 + " kilometers away from you."); 
	}
	$(".outputdistance").fadeOut();
	$(".outputdistance").fadeIn();
	$(".outputdistance").html(message + Math.round(closestpanodistance * 1000) + " meters away and the bearing is " + Math.round(bearing));
}
function stopcheck() {
	clearInterval(checkTimer);
	$(".outputdistance").fadeOut();
}
function compareDistances(a, b) {
    if (a[1] === b[1]) {
        return 0;
    }
    else {
        return (a[1] < b[1]) ? -1 : 1;
    }
}
function checkBearing(startLng, startLat, destLng, destLat){
  startLat = toRadians(startLat);
  startLng = toRadians(startLng);
  destLat = toRadians(destLat);
  destLng = toRadians(destLng);
  y = Math.sin(destLng - startLng) * Math.cos(destLat);
  x = Math.cos(startLat) * Math.sin(destLat) -
        Math.sin(startLat) * Math.cos(destLat) * Math.cos(destLng - startLng);
  brng = Math.atan2(y, x);
  brng = toDegrees(brng);
  return (brng + 360) % 360;
}
// Converts from degrees to radians.
function toRadians(degrees) {
  return degrees * Math.PI / 180;
}
function toDegrees(radians) {
  return radians * 180 / Math.PI;
}
if (navigator.geolocation) {    
    // Browser supports it, we're good to go!     
    } else {    
    alert('Sorry your browser doesn't support the Geolocation API');    
    }
function execcheck() {
	navigator.geolocation.getCurrentPosition(readXML, errorPosition);
}
function checkdistance() {    
	execcheck();
    checkTimer = setInterval(execcheck, 10000);
}
function errorPosition() {  
    alert('Sorry couldn't find your location');                 
    pretext.show();         
}
function distance(lon1, lat1, lon2, lat2) {
  var R = 6371; // Radius of the earth in km
  var dLat = (lat2-lat1).toRad();  // Javascript functions in radians
  var dLon = (lon2-lon1).toRad(); 
  var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) * Math.sin(dLon/2) * Math.sin(dLon/2); 
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
  var d = R * c; // Distance in km
  return d;
}
/** Converts numeric degrees to radians */
if (typeof(Number.prototype.toRad) === "undefined") {
  Number.prototype.toRad = function() {
    return this * Math.PI / 180;
  }
}

For the wider work, see the Psiloritis project and the maps and GIS service.

Back to top