Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Build an Exercise Tracking App: Persistence & Graphing

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

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.

Home page

"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

history

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.

track info page

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!

Advertisement