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="http://earth.google.com/kml/2.2">
<Document>
 <name>KmlFile</name>
 <Style id="roadStyle">
  <LineStyle>
   <color>7fcf0064</color>
   <width>6</width>
  </LineStyle>
 </Style>
 <Placemark>
  <name>Route</name>
  <description>
   <![CDATA[Distance: 3,034 mi Map data 2009 Maponics, Sanborn, Tele Atlas]]>
  </description>
  <styleUrl>#roadStyle</styleUrl>
  <MultiGeometry>
   <LineString>
    <coordinates>
      -118.38541,33.83817,0 -118.38331,33.83788,0 -118.38157,33.83755000000001,0 -118.38086,33.83751,0 ......
    </coordinates> 
   </LineString>
  </MultiGeometry>
 </Placemark>
</Document>
</kml>

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.

Implementation

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
    map.AddShapeLayer(layer); 
    var shape = new VEShape(VEShapeType.Polyline, route);
    shape.HideIcon();
    shape.SetLineColor(new VEColor(0,255,255,0.5));
    shape.SetFillColor(new VEColor(0,255,255,0.5));
    shape.SetLineWidth(5);
    layer.AddShape(shape);        

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]);
            items.push(item);
        }
        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();
        }
        else
        {
            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(); 
    map.AddShapeLayer(layer2);
    var points = new Array();
    for (var i = 0; i < lastDayIndex; i++)
        points[i] = route[i];
    points.push(workoutdays[workoutdays.length-1].Point);
    var shape = new VEShape(VEShapeType.Polyline, points);
    shape.HideIcon();
    shape.SetLineColor(new VEColor(255,0,0,0.5));
    shape.SetFillColor(new VEColor(255,0,0,0.5));
    shape.SetLineWidth(7);
    layer2.AddShape(shape);      
   
    //load workout day markers to the third layer
    var layer3 = new VEShapeLayer(); 
    map.AddShapeLayer(layer3);
    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>");
        sh.SetDescription(workoutdays[i].GetHTML());
        shapes[i] = sh;
    }
    layer3.AddShape(shapes);
   
    //load starting and ending points to the fourth layer
    var layer4 = new VEShapeLayer(); 
    map.AddShapeLayer(layer4);
    var startPt = new VEShape(VEShapeType.Pushpin, route[0]);
    startPt.SetTitle(od[0][0]);
    startPt.SetCustomIcon("<div style='margin-top: -31px; margin-left: -5px'><img src='images/" + od[0][1] + "'></div>");
    startPt.SetDescription(od[0][2]);
    startPt.SetMoreInfoURL(od[0][3]);
    layer4.AddShape(startPt);
    var endPt = new VEShape(VEShapeType.Pushpin, route[route.length - 1]);
    endPt.SetTitle(od[1][0]);
    endPt.SetCustomIcon("<div style='margin-top: -32px; margin-left: 6px'><img src='images/" + od[1][1] + "'></div>");
    endPt.SetDescription(od[1][2]);
    endPt.SetMoreInfoURL(od[1][3]);
    layer4.AddShape(endPt);
}

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

Virtual Earth Workout Tracker - Part I

by Gong Liu March 26, 2009 11:27

I picked up a 24 Hour Fitness membership last Thanksgiving, but I realized that I would need something to motivate myself to go to the gym as often as I could. I remembered when I was in junior high school we had a long-distance running event. The goal of the event was to cumulate the running mileage of all students in a class to reach a number that symbolized something. Of course, in that era of China, every event had to be political. We symbolized our running as the Long March led by Chairman Mao in 1934-35, which covered some 8000 miles. I remembered we had a paper map that highlighted our symbolized route. Each day our class monitor would advance a little flag pin along the route to show our collective progress. We were encouraged to compare our map with that of other classes throughout the school. Man, that little map motivated me so much that I would run 10 kilometers a day almost every day for the entire event!

So I set out to create an electronic version of the paper map I had in junior high. It will serve the purposes of tracking my workout and motivating me by showing my daily progress against my goals. The result is the Virtual Earth Workout Tracker, as shown below.

Main Features

The workout tracker has these features:

  • It clearly shows the symbolized route and my workout progress on Virtual Earth. I can zoom and pan the map any way I want.
  • At the bottom is a bar graph that shows my progress against my goals in terms of days passed (which include workout days and resting days), cumulated distance, and cumulated calories .
  • Each workout day is marked as a little red square along the route. Clicking a workout day marker will show me the workout activities and statistics for that day.
  • I can play a slide show about all my workout days with the controls at the bottom-left corner.

Goal Setting 

My primary goal is to go to the gym or do some kind of outdoor activities as often as I can so that fitness becomes part of my life style. My plan is to cross the US coast to coast in one year, symbolicly that is. The total distance is about 3000 miles. I can make it if I workout 300 days of the year at the rate of 10 miles per day. That leaves me one day of rest per week, plus some holidays. Of course running 10 miles per day is unrealistic for my condition. So I will have to "cheat" a little bit - every activity by any means counts. All the cardio machines in the gym count, treadmill, elliptical trainer, stairmaster, stationary bike, etc. All outdoor activities count, jogging, biking, hiking, swimming, etc. I'm allowed to collect about 9 miles a day simply by biking to and from my workplace, for example.

My secondary goal is to lost some weight and build strength. I intend to lost 20 pounds to reach my desired body weight, which means I need to burn at least 70,000 calories in one year. All cardio machines in the gym have calorie readings, even though they are not terribly accurate. By my experience elliptical trainers tend to over estimate calories burned, while treadmills and stairmasters tend to do the oppsite. Counting calories burned druing weight lifting is too tedious, so I'll simply ignore it. For outdoor activities, I will use the following table as reference:

   

These numbers are based on the Health Calculators found here. Calorie-intake and basic metabolic rate are also very important. But again they are too tedious to count. I'll simply multiply the original calorie goal by a factor of 2 to accomadate all the uncertainties. So my final calorie goal is 140,000 160,000 175,000 calories per year.

About the Route

The route I choose has no particular meaning other than the fact that it is cross country and about 3000 miles. It starts from Redondo Beach, CA and ends at Plymouth, MA. The route is actually a drivable route generated by Vertual Earth route engine. Of course, if you want, you can choose some famous historical route or a route that is meanful to you. Here are some ideas:

In Part II, I will disciss the technical aspect of the Virtual Earth Workout Tracker.

About

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

Tag Cloud

Calendar

<<  April 2017  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

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.