Welcome to the second and final part in this series of tutorials on developing an Exercise Tracker application with PhoneGap. In this tutorial, we will finish the Track Workout page and complete the app by creating the History and Track Info pages.
Saving the GPS Data
When the user clicks the Stop Tracking button, we need to stop following their GPS location and save all of the GPS points that were recorded (tracking_data
) into the database. We'll also reset the text input box (in case they want to record another workout straight away) and we'll display a message that we have stopped location tracking.
PhoneGap provides both browser-based Local Storage and a SQLite database as methods of storing data on the phone. The SQL database is a lot more powerful (due to the fact you can specify table schemas), but comes at the cost of code complexity. Local Storage is a simple key/value store that is easy to setup and use. Data is stored using the setItem(key, value)
method, and retrieved using the getItem(key)
method.
In the ExerciseTracker app, we need to store tracking_data
(the array of Position objects). We set the key to be track_id
(the text/ID the user entered for their exercise) and the value to be a string representation of a JSON object of tracking_data
. We are forced to convert this array to JSON because Local Storage can only store strings.
$("#startTracking_stop").live('click', function(){ // Stop tracking the user navigator.geolocation.clearWatch(watch_id); // Save the tracking data window.localStorage.setItem(track_id, JSON.stringify(tracking_data)); // Reset watch_id and tracking_data var watch_id = null; var tracking_data = null; // Tidy up the UI $("#track_id").val("").show(); $("#startTracking_status").html("Stopped tracking workout: <strong>" + track_id + "</strong>"); });
Your application can now track the user's workouts and store where they went on the phone!
Useful Development Shortcuts
Now we will add a couple of features to the app which help to reduce development time. On the Home page of ExerciseTracker, you will remember the "Clear Local Storage" and "Load Seed GPS Data" buttons. In the first tutorial, we only declared the markup for them. Now we will code the functionality.
"Clear Local Storage" and "Load Seed GPS Data" buttons on the Home page.
Like all of our event handling in ExerciseTracker, we use the jQuery live()
function to listen for the click event. If the "Clear Local Storage" button is fired, then we call the window.localStorage.clear()
method which deletes all entries in the local storage. If the "Load Seed GPS Data" button is fired, then we insert some dummy GPS data into the database.
$("#home_clearstorage_button").live('click', function(){ window.localStorage.clear(); }); $("#home_seedgps_button").live('click', function(){ window.localStorage.setItem('Sample block', '[{"timestamp":1335700802000,"coords":{"heading":null,"altitude":null,"longitude":170.33488333333335,"accuracy":0,"latitude":-45.87475166666666,"speed":null,"altitudeAccuracy":null}},{"timestamp":1335700803000,"coords":{"heading":null,"altitude":null,"longitude":170.33481666666665,"accuracy":0,"latitude":-45.87465,"speed":null,"altitudeAccuracy":null}},{"timestamp":1335700804000,"coords":{"heading":null,"altitude":null,"longitude":170.33426999999998,"accuracy":0,"latitude":-45.873708333333326,"speed":null,"altitudeAccuracy":null}},{"timestamp":1335700805000,"coords":{"heading":null,"altitude":null,"longitude":170.33318333333335,"accuracy":0,"latitude":-45.87178333333333,"speed":null,"altitudeAccuracy":null}},{"timestamp":1335700806000,"coords":{"heading":null,"altitude":null,"longitude":170.33416166666666,"accuracy":0,"latitude":-45.871478333333336,"speed":null,"altitudeAccuracy":null}},{"timestamp":1335700807000,"coords":{"heading":null,"altitude":null,"longitude":170.33526833333332,"accuracy":0,"latitude":-45.873394999999995,"speed":null,"altitudeAccuracy":null}},{"timestamp":1335700808000,"coords":{"heading":null,"altitude":null,"longitude":170.33427333333336,"accuracy":0,"latitude":-45.873711666666665,"speed":null,"altitudeAccuracy":null}},{"timestamp":1335700809000,"coords":{"heading":null,"altitude":null,"longitude":170.33488333333335,"accuracy":0,"latitude":-45.87475166666666,"speed":null,"altitudeAccuracy":null}}]'); });
History Page
Completed History Page
The history page lists all of the workouts the user has recorded. When they click on a workout, we open the Track Info page which contains detailed information (such as distance travelled, time taken, and route plotted on a Google Map). Below is the markup for the history page.
<div data-role="page" id="history"> <div data-role="header"> <h1>History</h1> <div data-role="navbar"> <ul> <li><a href="#home" data-transition="none" data-icon="home">Home</a></li> <li><a href="#startTracking" data-transition="none" data-icon="plus">Track Workout</a></li> <li><a href="#history" data-transition="none" data-icon="star">History</a></li> </ul> </div> </div> <div data-role="content"> <p id="tracks_recorded"></p> <ul data-role="listview" id="history_tracklist"> </ul> </div> </div>
Now we need to code the functionality. When the user loads the page, we need to generate an HTML list containing all of the recorded workouts. Because window.localStorage
is just another Javascript object, we can call the length()
method on it to find out how many workouts the user has recorded. We can then iterate over our database calling the window.localStorage.key()
method (which returns a key for a given index) to find the names of all of the workouts.
// When the user views the history page $('#history').live('pageshow', function () { // Count the number of entries in localStorage and display this information to the user tracks_recorded = window.localStorage.length; $("#tracks_recorded").html("<strong>" + tracks_recorded + "</strong> workout(s) recorded"); // Empty the list of recorded tracks $("#history_tracklist").empty(); // Iterate over all of the recorded tracks, populating the list for(i=0; i<tracks_recorded; i++){ $("#history_tracklist").append("<li><a href='#track_info' data-ajax='false'>" + window.localStorage.key(i) + "</a></li>"); } // Tell jQueryMobile to refresh the list $("#history_tracklist").listview('refresh'); });
Viewing the History page should now show all tracked workouts.
Track Info Page
The Track Info page displays information about an individual workout the user has completed. We will calculate the distance they travelled, the time it took them to complete their workout, and also the route taken on a Google Map.
Completed Track Info Page
<div data-role="page" id="track_info"> <div data-role="header"> <h1>Viewing Single Workout</h1> <div data-role="navbar"> <ul> <li><a href="#home" data-transition="none" data-icon="home">Home</a></li> <li><a href="#startTracking" data-transition="none" data-icon="plus">Track Workout</a></li> <li><a href="#history" data-transition="none" data-icon="star">History</a></li> </ul> </div> </div> <div data-role="content"> <p id="track_info_info"></p> <div id="map_canvas" style="position:absolute;bottom:0;left:0;width:100%;height:300px;"></div> </div> </div>
The Track Info page displays dynamic, not static, information. The content of the page depends on what workout the user clicked on from the History page. So, we need some way to communicate what workout was clicked to the Track Info page.
When the user clicks a workout link, we set a track_id
attribute to the <div id="track_info"></div>
element. Then, when the Track Info page is loaded, we retrieve that track_id
and display the appropriate workout information.
$("#history_tracklist li a").live('click', function(){ $("#track_info").attr("track_id", $(this).text()); }); // When the user views the Track Info page $('#track_info').live('pageshow', function(){ // Find the track_id of the workout they are viewing var key = $(this).attr("track_id"); // Update the Track Info page header to the track_id $("#track_info div[data-role=header] h1").text(key); // Get all the GPS data for the specific workout var data = window.localStorage.getItem(key); // Turn the stringified GPS data back into a JS object data = JSON.parse(data);
Calculating Distance of the Workout
Chris Veness has written a great explanation on how to calculate the distance between two GPS coordinates. I used his code as a base for the gps_distance
function.
function gps_distance(lat1, lon1, lat2, lon2) { // http://www.movable-type.co.uk/scripts/latlong.html var R = 6371; // km var dLat = (lat2-lat1) * (Math.PI / 180); var dLon = (lon2-lon1) * (Math.PI / 180); var lat1 = lat1 * (Math.PI / 180); var lat2 = lat2 * (Math.PI / 180); var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2); var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); var d = R * c; return d; }
Now that we have a function to calculate the distance between two GPS coordinates, and an array full of GPS coordinates the user recorded, we can sum all of the individual distances between adjacent points to calculate the total distance the user travelled.
// Calculate the total distance travelled total_km = 0; for(i = 0; i < data.length; i++){ if(i == (data.length - 1)){ break; } total_km += gps_distance(data[i].coords.latitude, data[i].coords.longitude, data[i+1].coords.latitude, data[i+1].coords.longitude); } total_km_rounded = total_km.toFixed(2);
Calculating Workout Duration
Each of the GPS Position
objects has a timestamp
attribute. We simply subtract the timestamp of the first recorded GPS Position from the last recorded GPS Position to give us the total time taken for the workout in milliseconds. We then do some conversions to calculate the total time in both minutes and seconds.
// Calculate the total time taken for the track start_time = new Date(data[0].timestamp).getTime(); end_time = new Date(data[data.length-1].timestamp).getTime(); total_time_ms = end_time - start_time; total_time_s = total_time_ms / 1000; final_time_m = Math.floor(total_time_s / 1000); final_time_s = total_time_s - (final_time_m * 60); // Display total distance and time $("#track_info_info").html('Travelled <strong>' + total_km_rounded + '</strong> km in <strong>' + final_time_m + 'm</strong> and <strong>' + final_time_s + 's</strong>');
Plotting the Route on the Google Map
Finally, we need to plot the workout route on a Google Map. We start off by setting the intial latitude and longitude that the Google Map will be centered on as the coordinates of the first GPS point. We then declare the options object which contains various settings for the Google Map. We then create the map, specifying that we want the HTML element with the ID map_canvas
to hold the map.
// Set the initial Lat and Long of the Google Map var myLatLng = new google.maps.LatLng(data[0].coords.latitude, data[0].coords.longitude); // Google Map options var myOptions = { zoom: 15, center: myLatLng, mapTypeId: google.maps.MapTypeId.ROADMAP }; // Create the Google Map, set options var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
If your map isn't loading, be sure to check that you are providing the correct API key in the <script src="">
of the Google Map API in index.html. With our map created, we can then plot the user's route. We create an array and fill it with instances of google.maps.LatLng
substituting the values of each of the GPS points. We then create a google.maps.PolyLine
based off of those coordinates and apply the line to the map.
var trackCoords = []; // Add each GPS entry to an array for(i=0; i<data.length; i++){ trackCoords.push(new google.maps.LatLng(data[i].coords.latitude, data[i].coords.longitude)); } // Plot the GPS entries as a line on the Google Map var trackPath = new google.maps.Polyline({ path: trackCoords, strokeColor: "#FF0000", strokeOpacity: 1.0, strokeWeight: 2 }); // Apply the line to the map trackPath.setMap(map);
Conclusion
This concludes the tutorial on building the PhoneGap app ExerciseTracker. I hope you have learned a lot about the various technologies we used. If you have any questions please post them in the comments below!
Comments