Tuesday, March 19, 2024

HTML 5 Geolocation: An Advanced Example

In our article HTML 5 Geolocation: A Practical Example we created a scenario for a Store Locator to illustrate all of the most commonly used HTML 5 Geolocation properties and methods. We are now going to step beyond the common to the uncommon and illustrate some of the less used but arguably more interesting properties and methods like coords.altitude, coords.heading and watchPosition().

Note: The common properties and methods like coords.latitude and coords.longitude are used in the example below but will not be covered. Other topics not covered include calculating distance between two points, showing a location with Google Maps, and error handling. If you are not familiar with these topics please refer to our article HTML 5 Geolocation: A Practical Example.

Setting up the Scenario

We are building a new website for mountain climbers. As part of our new application we are offering a web page that will track a climber’s movement giving them up-to-date information such as their current location, distance to the mountain peak, altitude, speed and heading.

The Markup and Script

<html >

<head>

<title>HTML 5 Geolocation – An Advanced Example</title>

</head>

<body onload=”checkGeolocationSupport()”>

<div id=”ClimberTrackerHeader”>

<h3>Mountain Climber Tracker</h3>

</div>

<div id=”MountainClimberTrackerInputElements”>

Select your mountain destination:<br />

<div id=”InputElementIndent” style=”padding-left:10px;”>

<input id=”MtElbert” type=”radio” onclick=”startTracking(0)” name=”mountainSelect” title=”Mount Elbert, Colorado” />&nbsp;Mount Elbert, Colorado<br />

<input id=”MtMitchell” type=”radio” onclick=”startTracking(1)” name=”mountainSelect” title=”Mount Mitchell, North Carolina” />&nbsp;Mount Mitchell, North Carolina<br />

<input id=”MtRainier” type=”radio” onclick=”startTracking(2)” name=”mountainSelect” title=”Mount Rainier, Washington” />&nbsp;Mount Rainier, Washington<br />

</div>

</div>

<div id=”Error”>

<em>We tried but we can’t calculate your location.</em>

</div>

<div id=”TrackingTitle”>

<h4>Tracking your progress:</h4>

</div>

<div id=”TrackingResults” style=”padding-left:20px;”>

</div>

<div id=”GoogleMap” style=”padding-left:20px;”>

Map image.

</div>

<script type=”text/javascript”>

// Elements that we will need to manipulate

var InputElements = document.getElementById(“MountainClimberTrackerInputElements”);

var locationError = document.getElementById(“Error”);

var resultsTitle = document.getElementById(“TrackingTitle”);

var resultsContent = document.getElementById(“TrackingResults”);

var googleMap = document.getElementById(“GoogleMap”);

// Create a two dimensional array for keeping our store data

var mountainData = new Array(3)

createTwoDimensionalArray(3);

// Variable to track the selected mountain index in our array

var mountainDataIndex = 0;

// Variable to track position watch ID

var watchPositionId = 0;

// Mountain information

mountainData[0][0] = “Mount Elbert, Colorado”;

mountainData[1][0] = “Mount Mitchell, North Carolina”;

mountainData[2][0] = “Mount Rainier, Washington”;

// Mountain latitude

mountainData[0][1] = “39.1178512”;

mountainData[1][1] = “35.7649612”;

mountainData[2][1] = “46.8529129”;

// Mountain longitude

mountainData[0][2] = “-106.4451599”;

mountainData[1][2] = “-82.26511”;

mountainData[2][2] = “-121.7604446”;

// This function creates our two dimensional array

function createTwoDimensionalArray(arraySize) {

for (i = 0; i < mountainData.length; ++i)

mountainData[i] = new Array(arraySize);

}

// Function to determine if browser supports geolocation

function checkGeolocationSupport() {

if (navigator.geolocation) {

// Show mountain selection options – radio buttons –

// otherwise send to function that shows the specific reason why location cannot be determined

InputElements.style.display = ‘block’;

locationError.style.display = ‘none’;

resultsTitle.style.display = ‘none’;

resultsContent.style.display = ‘none’;

googleMap.style.display = ‘none’;

}

else {

// Browser does not support geolocation

InputElements.style.display = ‘none’;

locationError.style.display = ‘block’;

locationError.innerHTML = ‘<em>We cannot show your location because your browser does not support HTML 5 geolocation.</em>’;

resultsTitle.style.display = ‘none’;

resultsContent.style.display = ‘none’;

googleMap.style.display = ‘none’;

}

}

function startTracking(mountainIndex) {

// Set index tracking variable to selected mountain data array index

mountainDataIndex = mountainIndex;

// Clear position watch

navigator.geolocation.clearWatch(watchPositionId);

// Start watching location

watchPositionId = navigator.geolocation.watchPosition(showClimberLocation, showError, { enableHighAccuracy: false, maximumAge: 60000, timeout: 30000 });

}

function showClimberLocation(position) {

// Hide/show appropriate elements

InputElements.style.display = ‘block’;

locationError.style.display = ‘none’;

resultsTitle.style.display = ‘block’;

resultsContent.style.display = ‘block’;

googleMap.style.display = ‘block’;

// Determine current distance to mountain

var currentDistance;

currentDistance = calculateDistance(position.coords.latitude, position.coords.longitude, mountainData[mountainDataIndex][1], mountainData[mountainDataIndex][2]);

var timestampDate = new Date(position.timestamp);

// Build results

var geoloactionResults = ”;

geoloactionResults = ‘<strong>’ + mountainData[mountainDataIndex][0] + ‘</strong><br />&nbsp;Distance to peak (miles): ‘ + Math.round(currentDistance * 10) / 10 + ‘<br />’;

geoloactionResults = geoloactionResults + ‘&nbsp;Position (latitude, longitude): ‘ + position.coords.latitude + ‘, ‘ + position.coords.longitude + ‘<br />’;

geoloactionResults = geoloactionResults + ‘&nbsp;(position accurate to within ‘ + Math.round(convertMetersToFeet(position.coords.accuracy)) + ‘ feet)<br />’;

if ((position.coords.altitude == null) || ((position.coords.altitude == 0) && (position.coords.altitudeAccuracy == 0))) {

// Altitude cannot be determined — generate appropriate message

geoloactionResults = geoloactionResults + ‘&nbsp;Altitude could not be determined.<br />’;

}

else {

// Calculate and display results normally

geoloactionResults = geoloactionResults + ‘&nbsp;Current Altitude: ‘ + position.coords.altitude + ‘<br />’;

geoloactionResults = geoloactionResults + ‘&nbsp;(altitude accurate to within ‘ + Math.round(convertMetersToFeet(position.coords.altitudeAccuracy)) + ‘ feet)<br />’;

}

if ((position.coords.heading == null) || (isNaN(position.coords.heading))) {

// Heading cannot be determined — generate appropriate message

geoloactionResults = geoloactionResults + ‘&nbsp;Heading could not be determined.<br />’;

}

else {

// Calculate and display results normally

geoloactionResults = geoloactionResults + ‘&nbsp;Heading: ‘ + position.coords.heading + ‘ (360 degree heading with North being 0)<br />’;

}

if ((position.coords.speed == null) || (isNaN(position.coords.speed))) {

// Heading cannot be determined — generate appropriate message

geoloactionResults = geoloactionResults + ‘&nbsp;Speed could not be determined.<br />’;

}

else {

// Calculate and display results normally

geoloactionResults = geoloactionResults + ‘&nbsp;Speed: ‘ + position.coords.speed + convertMetersPerSecondToMilesPerHour(position.coords.speed) + ‘ (converted from meters per second to miles per hour)<br />’;

}

geoloactionResults = geoloactionResults + ‘&nbsp;Last Updated: ‘ + timestampDate.toLocaleString();

// Show results

resultsContent.innerHTML = geoloactionResults;

// Show map

var googleLatitudeLongitude = position.coords.latitude + “,” + position.coords.longitude;

var mapImageUrl = “http://maps.googleapis.com/maps/api/staticmap?center=” + googleLatitudeLongitude + “&zoom=12&size=300×300&sensor=false”;

googleMap.innerHTML = “<img src='” + mapImageUrl + “‘ />”;

}

function calculateDistance(latitude1,longitude1,latitude2,longitude2) {

// Calculate distance between mountain peak and current location

// using the Haversine formula

var earthRadius = 3961.3; // Radius of the earth in miles

var dLatitude = convertToRadian(latitude2 – latitude1);

var dLongitude = convertToRadian(longitude2 – longitude1);

var a = Math.sin(dLatitude / 2) * Math.sin(dLatitude / 2) + Math.cos(convertToRadian(latitude1)) * Math.cos(convertToRadian(latitude2)) * Math.sin(dLongitude / 2) * Math.sin(dLongitude / 2);

var greatCircleDistance = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 – a));

var distance = earthRadius * greatCircleDistance; // distance converted to miles from radians

return distance;

}

function convertMetersToFeet(meters) {

// Convert meters to feet and return result

var feet;

feet = meters * 3.2808399;

return feet;

}

function convertMetersPerSecondToMilesPerHour(mps) {

// Convert meters per second to miles per hour

var mph;

feet = mph * 2.2369362920544;

return mph;

}

// Function to convert degrees to radians

function convertToRadian(numericDegree) {

return numericDegree * Math.PI / 180;

}

// Handle all error messages and display appropriate elements

function showError(error) {

// Hide/show appropriate elements

InputElements.style.display = ‘none’;

locationError.style.display = ‘block’;

resultsTitle.style.display = ‘none’;

resultsContent.style.display = ‘none’;

googleMap.style.display = ‘none’;

// Determine appropriate eeror message

switch (error.code) {

case error.PERMISSION_DENIED:

locationError.innerHTML = “We tried but we can’t calculate your location because you denied the request for Geolocation.”

break;

case error.POSITION_UNAVAILABLE:

locationError.innerHTML = “We tried but we can’t calculate your location because the location information is unavailable.”

break;

case error.TIMEOUT:

locationError.innerHTML = “We tried but we can’t calculate your location because the request to get your location timed out.”

break;

case error.UNKNOWN_ERROR:

locationError.innerHTML = “We tried but we can’t calculate your location because an unknown error occurred.”

break;

}

}

</script>

</body>

</html>

Examining clearWatch() and watchPosition()

These two methods are what allow you to begin and end watching a position. What that simply means is that watchPostiton() starts getting position data at intervals while the clearWatch() method stops a specific watchPosition() instance. The specifics of how each of these methods work is explained below.

clearWatch()

The clearWatch() method does not clear all instances of watchPosition() as you might think. Instead it stops only a specific instance. In the script excerpt below notice that we must provide clearWatch() with the ID of the instance of watchPosition() that we want to stop. If you do not provide an ID an error will be thrown.

navigator.geolocation.clearWatch(watchPositionId);

watchPosition()

This method is what kicks off the watching process. There are three different parameters that can be included to more precisely define how watchPosition() does its work. They are the name of the function that gets called each time position data is successfully obtained, the name of the function that is called when an error is encountered when retrieving position data, and finally a set of position options that further define how the method functions. Each parameter is explained below:

Success Function – This function is called each time position data is successfully retrieved. It can be any function name that you like and it is automatically sent the position object as a parameter when it is called. In our case we call function showClimberLocation(position) which handles generating our results set.

Error Function – When something goes wrong the function that you name for this parameter gets called. Like the success function, an object (specifically an error object) is automatically sent as part of the function call. The name of the function can also be anything that you like. In our case we call function showError(error) which handles displaying our error messages.

Position Options – This is a set of attributes that further defines how watchPosition() functions. Each of the attributes is explained below:

enableHighAccuracy – This lets the method know how much effort to put in on obtaining an accurate position. Setting the value to true may result in a more accurate position at the expense of additional power consumption and a slower response time. A false value will likely use less power and have a faster response time but be less accurate. Just remember that setting this attribute to true does not necessarily ensure that a more accurate position can be obtained.

maximumAge – This attribute lets the method know how long it can keep using the cached position results before trying to obtain new position data. Essentially, this allows the developer to more or less define the interval for retrieving updated positional data. This is defined in milliseconds.

timeout – This attribute simply allows the developer to define how long to wait for new position data before throwing a timeout error. This is defined in milliseconds.

An example of a watchPosition() method call using all of the parameters and attributes looks like:

navigator.geolocation.watchPosition(showClimberLocation, showError, { enableHighAccuracy: false, maximumAge: 60000, timeout: 30000 })

Examining coords.accuracy, coords.altitude, coords.altitudeAccuracy, coords.heading, coords.speed and timestamp

In the showClimberLocation function you will notice that we use all of the position properties. We have defined each of the more advanced properties here:

coords.accuracy

This property defines in meters the accuracy of the position data. What that simply means is that if the accuracy rating were 3000 you could draw a circle on a map with a radius of 3000 meters centered on the coords.latitude and coords.longitude coordinates, the actual position will be somewhere inside that circle. The bigger the number, the less accurate the coordinates.

coords.altitude

This property provides the altitude of the position in meters above or below sea level. In our example we convert this value from meters to feet.

coords.altitudeAccuracy

Like coords.accuracy, this property provides a distance in meters that reflects how far off the altitude position could be from the altitude value obtained. Again, the bigger the number, the less accurate the altitude position.

coords.heading

This property provides 360 degree heading information. A value of 0 would indicate north, a value of 90 would indicate east, a value of 225 would indicate southwest, etc.

coords.speed

This property indicates the relative speed in meters per second. In our example we have converted the meters per second value to miles per hour.

timestamp

This property indicates the date and time when the position data was retrieved. The value is in the form of a DOMTimeStamp, so you will need to use .toLocaleString() or some other method to convert the value to a readable date and time.

What to Expect

First, running this example from your computer versus a mobile device will yield very different results. Devices like modern cell phones have GPS capabilities built in and provide much better position data. In addition, mobile devices will provide much more data like speed and heading that you can’t get when using a PC.

You will also find that the browsers still have a way to go in implementing HTML 5 Geolocation. Even though all of the modern major browsers support HTML 5 Geolocation, they do not all support it fully or in the same way. In our example, you will notice that we check for both (position.coords.heading == null) and (isNaN(position.coords.heading)). The reason for this is due to different browsers assigning different values to properties when they can’t discern a value. In this case (position.coords.heading == null) is used for Opera and (isNaN(position.coords.heading)) is used for Firefox.

Lastly, expect things to change. As HTML 5 evolves, expect that implementations will get better and more consistent across the major browsers.

Conclusion

In the short term try to stick to the basics when developing using HTML 5 Geolocation. Support for getCurrentLocation(), coords.latitude, coords.longitude and coords.accuracy is fairly consistent across the major browsers, so stick with using them as much as possible for now. The other properties and methods are very interesting and useful in specific situations but will require much more planning and testing in order to get consistent results.

Happy coding!

 

 

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured