Virtual Earth Workout Tracker - Part II

by Gong Liu March 31, 2009 00:45

In Part I, I decribed what the Virtual Earth Workout Tracker was, and how it could be a useful tool for tracking one's workout and motivating him to keep going. In this part, I'll describe how to implement the Workout Tracker with Virtual Earth SDK.

Generating the Route

Normally, you want to generate a route that is closely resembling the physical thing on the ground, whether it's a highway or things like the Greate Wall of China. There are basically two ways to generate a route: trace the route manually with the aid of some mapping tool, or generate it automatically with a route engine. The following screenshot shows how you could trace a stretch of highway, say Route 66, using Google Earth.

Google Earth will save the route you are tracing in KML format, which is just XML understandable by any XML parser. As to how many points you need, depending on the length and geometry of the route, a few hundread to a couple of thousand points seem to be a good number. You need more points to trace the route more closely, but it will take longer time to get it done and longer computing time to render the route.

If your route is along drivable roads, you could probably generate the route automatically using some route engine, such as the one in Virtual Earth or Google Earth. One issue with a route engine is that it has its own mind. The route it genetares may not include all the roads you want. In this case, you will need to set up a multi-waypoint route. The waypoints provide some sort of a guideline on how the route should be generated. In Virtual Earth, once a route is generated, you can get all the points that describe the shape of the route with the VERoute.ShapePoints property. Note that this property is only available to licensed Virtual Earth developers.

If you generate the route with Google Earth, you get all the details about the route in a KML file, something like:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="">
 <Style id="roadStyle">
   <![CDATA[Distance: 3,034 mi Map data 2009 Maponics, Sanborn, Tele Atlas]]>
      -118.38541,33.83817,0 -118.38331,33.83788,0 -118.38157,33.83755000000001,0 -118.38086,33.83751,0 ......

Polyline Reduction and Douglas-Peucker Algorithm

For a long route generated with a route engine, the number of points can be huge. For example, a coast-to-coast cross country route generated with Google Earth contains some 12,500 points. To draw the route with so many points would cause significant delay when the Workout Tracker is started. To improve the performance, we need to reduce the number of points, but at the same time maintain the general shape of the route. This type of problem is called polyline reduction or polyline simplification. One of the best-known algorithms for polyline reduction is the Douglas-Peucker algorithm. The following series of diagrams from McNeel Wiki best illustrate how the algorithm works:   




I found a C# implementation of the algorithm here. I modified the code to accept latitude/longitude points instead of integer points. I also added to the algorithm an interface that reads a polyline (route) from a KML file and writes the reduced polyline to a javascript file in form of a javascript array: 

var route = new Array(
new VELatLong(33.83817,-118.38541),
new VELatLong(33.83748,-118.35352),
new VELatLong(33.85775,-118.35359),

The screenshot below is the interface to the polyline reduction algorithm:

The single parameter that controls which points are removed and how many points are removed is the tolerance, which is the maximum allowable distance from a point to the nearest line segment (the dashed blue lines in the above series of diagrams). The total route distance can also serve as a crude measurement about how well a reduction is. The following table shows the point count and total distance vs. the tolerance for my cross country route:

The data in the table shows that as the tolerance increases, the number of points needed to describe the shape of the route decreases. In extreme, when the tolerance is big enough, only 2 points, the starting point and the ending point, are needed. Notice that as the tolerance increases, the route distance becomes shorter and shorter as compared to the original route distance. In my case, I use the route with 614 points (tolerance = 0.005°). This represents a 95% point reduction, while the route distance is only about 32 miles shorter or 1% shorter than the original route. Even at this level of reduction, you won't notice the discrepancy between the orginal route and reduced route up to about zoom level 9 (Virtual Earth has total 19 zoom levels, with the zoom level 19 representing the closest level to the ground). If you zoom in closer, you will see the difference, though. 

You may prefer a route with less reduction and thus less discrepancy, but you need to strike a balance between accuracy and the loading speed.


There are 4 layers of data on the map. The first layer contains the route (cyan color). The second layer contains the progress polyline (red color). The third layer contains the workout day markers (red little requares). The fourth layer contains the starting point (green pushpin) and the ending point (red pushpin).

The route is loaded on the map with this script:

function LoadRoute()
    var layer = new VEShapeLayer();   //layer 1
    var shape = new VEShape(VEShapeType.Polyline, route);
    shape.SetLineColor(new VEColor(0,255,255,0.5));
    shape.SetFillColor(new VEColor(0,255,255,0.5));

Here, route is the javascript array generated by the polyline reduction program mentioned earlier.

For simplicity, we use the following 2D javascript array to hold our workout data:

var data = new Array(
//Date, Comment, Fitness Machine, Mileage, Calories, Fitness Machine, Mileage, Calories, ...
new Array("2/24/2009", "A Long Journey Begins with the First Step", "Stationary Bike", 9, 150, "Stair Master", 2.3, 280),
new Array("2/25/2009", "", "Stationary Bike", 10, 170, "Elliptical Trainer", 1.7, 168, "Treadmill", 3, 340),
new Array("2/26/2009", "", "Stationary Bike", 9, 150, "Stair Master", 2.3, 280),
new Array("2/27/2009", "", "Stationary Bike", 9, 150, "Treadmill", 3, 350),

Each row in the above array represents a workout day, and each workout day has a date field, a comment field, and one or more workout items with each workout item containing such information as fitness machine used, distance covered, and calories burned. Of course, in a real application the workout data is most likely stored in a backend database.

To facilitate the manipulation of the workout data, we further define the following two javascript objects, WorkoutDay and WorkoutItem:

function WorkoutDay(dt, comment, items) {
    this.Date = dt;
    this.Comment = comment;
    this.Items = items;
    this.Point = null;
    this.CumulatedDistance = 0;
    this.CumulatedCalories = 0;
    this.GetDayMiles = function() {
        var m = 0;
        for(var i = 0; i < items.length; i++)
            m += items[i].Miles;
        return m;
    this.GetDayCalories = function() {
        var c = 0;
        for(var i = 0; i < items.length; i++)
            c += items[i].Calories;
        return c;
    this.GetHTML = function() {
        var s = "<table cellspacing='0' cellpadding='2' border='0' width='200'>";
        for(var i = 0; i < items.length; i++)
            s += "<tr>";
            s += "<td>Type:</td>";
            s += "<td><img src='Images/" + items[i].GetTypeImage() + "' alt='" + items[i].Type + "'></td>";
            s += "</tr>";
            s += "<tr>";
            s += "<td>Distance:</td>";
            s += "<td>" + items[i].Miles + " miles</td>";
            s += "</tr>";
            s += "<tr>";
            s += "<td>Calories:</td>";
            s += "<td>" + items[i].Calories + "</td>";
            s += "</tr>";
            s += "<tr>";
            s += "<td colspan='2'><hr></td>";
            s += "</tr>";
        s += "</table>";
        s += "<table cellspacing='0' cellpadding='2' border='0' width='200'>";
        s += "<tr><td>Day total dist:</td><td>" + this.GetDayMiles() + " miles</td></tr>";
        s += "<tr><td>Day total calories:</td><td>" + this.GetDayCalories() + "</td></tr>";
        s += "<tr><td>Cumulated dist:</td><td>" + this.CumulatedDistance.toFixed(1) + " miles</td></tr>";
        s += "<tr><td>Cumulated calories:</td><td>" + this.CumulatedCalories.toFixed(1) + "</td></tr>";
        if (this.Comment != "")
            s += "<tr><td colspan='2'><hr></td></tr>";
            s += "<tr><td colspan='2'>" + this.Comment + "</td></tr>";
        s += "</table>";
        return s;

function WorkoutItem(type, miles, calories) {
    this.Type = type;
    this.Miles = miles;
    this.Calories = calories;
    this.GetTypeImage = function() {
        // Replace " " with ""
        var re = / /g;
        var r = type.replace(re, "");
        return r.toLowerCase() + ".gif";

The WorkoutDay.Items property is an array of WorkoutItem objects. The WorkoutDay.GetHTML method generates a HTML snip to be presented in the InfoBox of the workout day marker. It includes such info as the workout activities and statistics for the current workout day. The WorkoutDay.CumulatedDistance property represents the cumulated workout mileage from all previous workout days through the current workout day. The WorkoutDay.Point property is a VELatLong object that marks the position of the current workout day along the route.

Referring to the above diagram, P0 ... Pn are route points. Q is the WorkoutDay.Point, and its coordinates can be determined in the following way:

  • Calculate WorkoutDay.CumulatedDistance, which is denoted by cd
  • Calculate the cumulated route distance from P0 through Pi-1, which is denoted by cdr
  • Calculate the distance between Pi-1 and Pi, which is denoted by D
  • Whether Q is in between Pi-1 and Pi is determined by this condition: cdr < cd < cdr + D 
  • Calculate d = cd - cdr
  • Calculate the coordinates of Q by linear interpolation: 

Finally, we are ready to show the script for loading the second, third and fourth layers of data:

function LoadData()

    //populate an array of WorkoutDay objects with workout data
    workoutdays = new Array(data.length);
    for (var i = 0; i < data.length; i++)
        var items = new Array();
        for (var j = 2; j < data[i].length; j += 3)
            var item = new WorkoutItem(data[i][j], data[i][j+1], data[i][j+2]);
        var dt = new Date(Date.parse(data[i][0]));
        var comment = data[i][1];
        var wd = new WorkoutDay(dt, comment, items);
        workoutdays[i] = wd;
    //Calculate cumulated mileage, calories and workout day position
    for (var i = 0; i < workoutdays.length; i++)
        if (i == 0)
            workoutdays[i].CumulatedDistance = workoutdays[i].GetDayMiles();
            workoutdays[i].CumulatedCalories = workoutdays[i].GetDayCalories();
            workoutdays[i].CumulatedDistance = workoutdays[i-1].CumulatedDistance + workoutdays[i].GetDayMiles();
            workoutdays[i].CumulatedCalories = workoutdays[i-1].CumulatedCalories + workoutdays[i].GetDayCalories();
        workoutdays[i].Point = GetProgressPoint(workoutdays[i].CumulatedDistance);
    //load progress polyline to the second layer
    var layer2 = new VEShapeLayer(); 
    var points = new Array();
    for (var i = 0; i < lastDayIndex; i++)
        points[i] = route[i];
    var shape = new VEShape(VEShapeType.Polyline, points);
    shape.SetLineColor(new VEColor(255,0,0,0.5));
    shape.SetFillColor(new VEColor(255,0,0,0.5));
    //load workout day markers to the third layer
    var layer3 = new VEShapeLayer(); 
    var shapes = new Array();
    for (var i = 0; i < workoutdays.length; i++)
        var sh = new VEShape(VEShapeType.Pushpin, workoutdays[i].Point);
        sh.SetTitle(workoutdays[i].Date.toDateString() + " - Day: " + (i + 1));
        sh.SetCustomIcon("<div style='margin-top: 11px; margin-left: 10px'><img src='images/redsquare.gif' width='5' height='5'></div>");
        shapes[i] = sh;
    //load starting and ending points to the fourth layer
    var layer4 = new VEShapeLayer(); 
    var startPt = new VEShape(VEShapeType.Pushpin, route[0]);
    startPt.SetCustomIcon("<div style='margin-top: -31px; margin-left: -5px'><img src='images/" + od[0][1] + "'></div>");
    var endPt = new VEShape(VEShapeType.Pushpin, route[route.length - 1]);
    endPt.SetCustomIcon("<div style='margin-top: -32px; margin-left: 6px'><img src='images/" + od[1][1] + "'></div>");

function GetProgressPoint(cd)
    var pt1 = route[0];
    var pt2;
    var cdr = 0;
    for (var i = 1; i < route.length; i++)
        pt2 = route[i];
        var D = GetDistance(pt1, pt2);
        if (cdr + D > cd)
            var d = cd - cdr;
            var k = d/D;
            lastDayIndex = i;
            return new VELatLong(pt1.Latitude + k * (pt2.Latitude - pt1.Latitude), pt1.Longitude + k * (pt2.Longitude - pt1.Longitude));
        cdr += D;
        pt1 = pt2;
    lastDayIndex = route.length - 1;
    return pt2; //last route point or destination

function GetDistance(pt1, pt2)
    var EarthRadiusInMiles = 3963.23;
    var RadiansPerDegree = 0.017453292519943295;
    return EarthRadiusInMiles * RadiansPerDegree * Math.sqrt((pt2.Latitude - pt1.Latitude) * (pt2.Latitude - pt1.Latitude) +
            Math.pow((pt2.Longitude - pt1.Longitude) * Math.cos(RadiansPerDegree * 0.5 * (pt1.Latitude + pt2.Latitude)), 2));

The above formula is used in the function GetProgressPoint(). The distance formula in GetDistance() was discussed in this post.

The touring/slide show feature was discussed in this post.

Further Improvements

Here are some ideas for further improvements on the Workout Tracker:

  • Use a database to store the workout data
  • Provide a web interface to allow a user to enter his workout data into the database
  • Provide a mobile interface so that a user can capture his workout data in the gym and send it out to the server via his cell phone
  • Support a group of users


A seasoned computer professional. A tofu culture evangelist...
more >>

Tag Cloud


<<  April 2017  >>

View posts in large calendar
Copyright © 2008-2011 Gong Liu. All rights reserved. | credits | contact me
The content on this site represents my own personal opinions, and does not reflect those of my employer in any way.