Solution File For VisualStudio 2008 GarminWebConversionOnly.zip
Several months ago, I bought a Garmin 705 Navigator for my bike. As a gadget junkie, I always want to have the latest stuff. For years, I've used Polar Heartrate monitors, but the idea of having maps on my handle bars was just to much to pass up. Since the Garmin's output is an XML file I figure I have an obligation to unravel it, and what better tool to do this than Microsoft's LINQ to XML. Many months ago I posted a similar article using LINQ to XML for showing the amount of space that was used on my TIVO by category. This article is not quite an end to end solution like that one. It's just a first step. What we will show is the steps necessary to convert the TCS file in data objects which we can use in future posts for display with technologies such as windows live maps.
So, let's begin. The first thing you need to do is plug your Garmin 705 into your USB port and you will see all the TCX files nicely arranged on the drive as follows.
If you look at any of these files, you'll see an xml formatted file that begins like the following:
You will notice it has a schema associated with it (xmlns="http://.."). The schema docs can be found on Garmin's web site here: http://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd
Looking at the schema, it's clear that we need to build several c# classes to use that we will extract the xml data into. Below is a class diagram built with Visual Studio 2008 that shows the classes.
I don't quite know the tricks for using the associated relationships necessary with the class builder to make this look pretty, but what we have is an Activity class that holds Laps by using a generic list of the Lap. Lap's contain Track's, Track's contain many TrackPoint's which are the locations, and each Trackpoint represents a geographical position.
Here are what the classes look like.
using System.Collections.Generic;
public class Activity
{
public string Id { set; get; }
public string Sport { set; get; }
public List<Lap> Laps { set; get; }
}
public class Lap
{
public double TotalTimeSeconds { set; get; }
public double DistanceMeters { set; get; }
public double MaximumSpeed { set; get; }
public int Calories { set; get; }
public string TriggerMethod { set; get; }
public int AverageHeartRateBpm { set; get; }
public int MaximumHeartRateBpm { set; get; }
public string Intensity { set; get; }
public int Cadence { set; get; }
public string Notes { set; get; }
public List<Track> Tracks { set; get; }
}
public class Track
{
public List<TrackPoint> TrackPoints { set; get; }
}
public class TrackPoint
{
public string Timex { set; get; }
public double AltitudeMeters { get; set; }
public double DistanceMeters { get; set; }
public int HeartRateBpm { get; set; }
public int Cadence { get; set; }
public string SensorState { get; set; }
public List<Position> Positionx { get; set; }
}
public class Position
{
public double LatitudeDegrees { set; get; }
public double LongitudeDegrees { set; get; }
}
And now, the LINQ that loads the Activity object which in turn references everything else.
The LINQ To Data Object
The first thing that needs to happen is that a XNamespace has to be declared and a root defined of the XML tree. Let's assume we have a filename of the tcx file already. Then, to do these things, we issue the following lines of code:
XElement root = XElement.Load(fileName);
XNamespace ns1 = "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2";
The technique we will use to load these objects is borrowed from one of my favorite LINQ books, LINQ in Action, page 391. Basically, we are creating an instance of the Activity class which in turns loads all the other classes. That is, we have a top level element named Activities and from that we create an instance of the Activity class. It begins like this:
1: IEnumerable<Activity> activities = from activityElement in root.Descendants(ns1 + "Activities")
2: select new Activity
3: {
4: Laps =
5: (from lapElement in
6: activityElement.Descendants(ns1 + "Lap")
7: select new Lap
8: {
9: TotalTimeSeconds =
10: lapElement.Element(ns1 +
11: "TotalTimeSeconds") != null
12: ? Convert.ToDouble(
13: (string) lapElement.Element(ns1 + "TotalTimeSeconds")
14: .Value)
15: : 0.00,
16: DistanceMeters =
17: lapElement.Element(ns1 +
18: "DistanceMeters") != null
19: ? Convert.ToDouble(
20: (string) lapElement.Element(ns1 + "DistanceMeters")
21: .Value)
22: : 0.00,
23: MaximumSpeed =
24: lapElement.Element(ns1 + "MaximumSpeed") !=...
Line 1 creates an enumeration of activities. Since an activity inclues a generic list of Laps, we need to generate another enumeration of laps on line 4. Lines 9 through 23 basically pull the individual element values from the laps. What is not shown is there is similar details which drills down from Laps to Track's, then to TrackPoint's, then finally to position. Since what is stored in class instances is actually generic Lists, at the end of each select, there is the function ToList() called which in turn creates that generic list. The source code for this is all included at the top of the post. Take a close look at the file GarminUtils.cs for how this really works. It currently has minimal error checking, but for my cycling files, it seems to do the job.
Looking Forward
I'm planning on doing several more posts in this series to actually use this data. Beth Massi has examples she has written that use Live Maps with LINQ. I'm hoping I can do something similar to what she has done.
Feel free to add to this and let me know what you've done. Maybe we can even make this a codeplex project at some point.
Bye for now.