Build an Exercise Tracking App: Persistence & Graphing

Build an Exercise Tracking App: Persistence & Graphing

Tutorial Details
  • Completion Time: 30 - 60 Minutes
  • Difficulty: Beginner
  • Technology: PhoneGap/HTML5/CSS/JavaScript

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!

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://godsofart.com S3bY

    Very cool! Two of my friends are preparing for a marathon! They would love this app!

  • Kevin

    Hey Logan. I followed this tutorial. I got it to install on my android phone. But after tracking a new workout, I am not able to view its track info page. When i click on the workout on the history page the app gets stuck on that info page which is blank and even after clicking on home or any of the other buttons, the screen does not change. However if i click on the sample workout, the output is as expected. It would be great if you could help me out. Thanks a lot.

  • http://amzacatalin.co.cc Amza Catalin

    Same here, Kevin.

  • Kevin

    The problem is the following lines in the #startTracking_stop:
    var watch_id = null;
    var tracking_data = null;

    Remove var from both the lines :)

    • Jens

      To be able to track more than one time, replace:

      var watch_id = null;
      var tracking_data = null;

      with:

      watch_id = null;
      tracking_data = [];

      The important part is resetting the tracking_data to an empty array instead of null, just as it is when initiated (var tracking_data = []).
      Otherwise there will be no array to push the geoloaction-data to, and you’ll end up storing a stringified ‘null’ as the tracking_data for that run.

      Hope this helps :)

  • Gene Smith

    Hey guys, great tutorial, I am learning alot. However, i have installed on my iphone4 and I get a weird problem. About every minute or so, I get a notification to “redo typing” or cancel. In other words, I start tracking a new workout, it responds, “tracking……” and about 30 to 60 secs later, I get the noticification. I usually hit cancel and 30 to 6o seconds later the same thing.

    It seems to me that when the geolocation gathers a location point and tries to move it into the database, something is going on??????

    Help?

  • Dav

    I´ve the same problem that Kevin and Amza… and it´s no solved removing the “var” from the lines that Kevin said…
    Any ideas?
    Thanks

    • http://youtube.nl Help

      bump

  • Dav

    Another question, I´ve installed the app in my cell and I realize that the GPS works bad… I don´t know why, but it show me that I´ve walked many kilometers when I only have walked a few meters… it is like the GPS takes the first position and then uses the distance between this first position and each ones of the new positions, and it add all of them at the end… Is it possible? Any solutions?
    Please, help me… I´m using the PhoneGap for a final project in my degree and it´s being very hard!!!

  • Arrie

    Hey Logan!, thanks for your insight and sharing your knowledge i learned alot!!… but i am uber noob when it comes to any webbased app… all the jquery, and javascript you did? did you put it all into your exercisetracker.js file? because all my UI looks awsome but it does not track anything or save the history when i run it on the emulator for WP7… please help! :)

  • Fernando

    It’s a good job, but it goes badly!! I have the same problem that Dav, Kevin and Amza.
    Please, somebody has an idea about this problem?

    PROBLEM: When i click on the workout on the history page the app gets stuck on that info page which is blank and even after clicking on home or any of the other buttons, the screen does not change. However if i click on the sample workout, the output is as expected. It would be great if you could help me out.

  • Puneet srivastva

    Hi I am getting the same problem as Fernando.Any help with this regard would be greatly appreciated

  • Jens

    Although Kevin’s solution with removing the var’s fixes the problem with tracking _once_, there is still a problem when trying to track the next run.

    So, to fix the problem experienced by Dav, Kevin, Amza, Fernando, Puneet, and others, and to be able to track more than one time, replace:

    var watch_id = null;
    var tracking_data = null;

    with:

    watch_id = null;
    tracking_data = [];

    The important part here is resetting the tracking_data to an empty array instead of null, just as it is when initiated (var tracking_data = []).
    Otherwise there will be no array to push the geoloaction-data to, and you’ll end up storing a stringified ‘null’ as the tracking_data for that run.

    I made a jsbin with a working example here: http://jsbin.com/ufuled/12
    Note: this example actually works in plain html5 too, if your device supports geolocation :)

    Hope this helps :)

    • Johnny

      HI Jens

      When I open this link in chrome browser on desktop it looks like a Mobile site, but when i open this on my iphone it does not work. Either with Safari or with google chrome for iphone

      Any ideas ?

      Thanks
      Johnny

  • Jamie

    Why don’t the source files work? I want to check this out – Can anyone tell me why the source files don’t even work in the browser? Or when buidling in Xcode or Eclipse? It tracks the location, saves my workout – but does not show the google maps (I have added my correct API key)

    Any help with this would be greatly appreciated. Thanks :)

  • Christoffer

    Very cool app!!! But, can this run in background and still log GPS positions on iPhone?