Convert Garmin 705 Navigator TCX Format using LINQ to XML and Data Objects – Article 1

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.

xxx

If you look at any of these files, you’ll see an xml formatted file that begins like the following:

ttt

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.

z1

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.

About Peter Kellner

Follow me:


Comments

  1. Peter Kellner says:

    Is the source not attached to the article? It was at one point.

  2. Great article but the formatting appears to be mucked up, and the implementation in the comments appears to be incomplete.

    Would there be anyway to get the source for this awesome codebase?

    THANKS!
    Ron

  3. Awesome code, well done mate

  4. Youre so cool! I dont suppose Ive read something like this before. So nice to seek out someone with some unique thoughts on this subject. realy thank you for starting this up. this website is one thing that is needed on the internet, someone with a bit originality. helpful job for bringing something new to the web!

  5. Meant to ask…am I seeing different perf than anyone else or is this expected w/above approach?

  6. I was happy to find this sample, I’d like to graph the data my own way. However, it seems for any TCX that has lots of TrackPoints ( …the performance of this approach is a non-starter ( in the matter of minutes…if you don’t get a OutOfMemory exception thrown ).

  7. i have created a program that will enable me to pick any lat/long point and have it sort through all of my workouts to tell me exactly quickly i passed between the two points – used to race against myself. the program also interacts with google earth with KML files and with Excel to export all of your workout activities to a spreadsheet.

    if you are interested in seeing the program email me at hockeydave26@hotmail.com and i will send you the code.

    sorry that this didn’t cut and paste well – but over time i have expanded on the first piece of code. this will now take a list of .tcx files and create ‘activity’ objects – although my objects differ from the first example i think you can get the idea (if you paste this back into Visual Studia and format it).

    private void extractDataFromXmlFile(string[] fileNames)
    {
    _myActivities.Clear();

    foreach (string fileName in fileNames)
    {
    try
    {
    XElement root = XElement.Load(fileName);
    XNamespace ns1 = “http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2″;

    var activities = from activityElement in root.Elements(ns1 + “Activities”).Elements(ns1 + “Activity”)
    select new Activity
    {
    Sport = activityElement.FirstAttribute.Name.ToString(),

    Id = activityElement.Element(ns1 + “Id”) != null ? activityElement.Element(ns1 + “Id”).Value : “ID Empty”,

    Notes = activityElement.Element(ns1 + “Notes”) != null ? activityElement.Element(ns1 + “Notes”).Value : “Notes Empty”,

    Laps = (from lapElement in activityElement.Descendants(ns1 + “Lap”)
    select new Lap
    {
    ID = lapElement.LastAttribute.Value != null ? Convert.ToString(lapElement.LastAttribute.Value) : DateTime.Now.ToString(),

    TotalTimeSeconds = lapElement.Element(ns1 + “TotalTimeSeconds”) != null
    ? Convert.ToDouble((string)lapElement.Element(ns1 + “TotalTimeSeconds”).Value) : 0.00,

    DistanceMeters = lapElement.Element(ns1 + “DistanceMeters”) != null ? Convert.ToDouble((string)lapElement.Element(ns1 + “DistanceMeters”).Value) : 0.00,
    MaximumSpeed = lapElement.Element(ns1 + “MaximumSpeed”) != null ? Convert.ToDouble((string)lapElement.Element(ns1 + “MaximumSpeed”).Value) : 0.00,
    Calories = lapElement.Element(ns1 + “Calories”) != null ? Convert.ToInt16((string)lapElement.Element(ns1 + “Calories”).Value) : 0,
    AverageHeartRateBpm = lapElement.Element(ns1 + “AverageHeartRateBpm”) != null ? Convert.ToInt16((string)lapElement.Element(ns1 + “AverageHeartRateBpm”).Value) : 0,
    MaximumHeartRateBpm = lapElement.Element(ns1 + “MaximumHeartRateBpm”) != null ? Convert.ToInt16((string)lapElement.Element(ns1 + “MaximumHeartRateBpm”).Value) : 0,
    Intensity = lapElement.Element(ns1 + “Intensity”) != null ? lapElement.Element(ns1 + “Intensity”).Value : “”,
    Cadence = lapElement.Element(ns1 + “Cadence”) != null ? Convert.ToInt16((string)lapElement.Element(ns1 + “Cadence”).Value) : 0,
    TriggerMethod = lapElement.Element(ns1 + “TriggerMethod”) != null ? lapElement.Element(ns1 + “TriggerMethod”).Value : “”,
    Notes = lapElement.Element(ns1 + “Notes”) != null ? lapElement.Element(ns1 + “Notes”).Value : “”,

    Tracks = (from trackElement in lapElement.Descendants(ns1 + “Track”)
    select new Track
    {
    TrackPoints = (from trackPointElement in trackElement.Descendants(ns1 + “Trackpoint”)
    select new TrackPoint
    {
    TimeAt = trackPointElement.Element(ns1 + “Time”) != null ? Convert.ToString((string)trackPointElement.Element(ns1 + “Time”).Value) : “”,
    AltitudeMeters = trackPointElement.Element(ns1 + “AltitudeMeters”) != null ? Convert.ToDouble((string)trackPointElement.Element(ns1 + “AltitudeMeters”).Value) : 0.0,
    DistanceMeters = trackPointElement.Element(ns1 + “DistanceMeters”) != null ? Convert.ToDouble((string)trackPointElement.Element(ns1 + “DistanceMeters”).Value) : 0.0,
    HeartRateBpm = trackPointElement.Element(ns1 + “HeartRateBpm”) != null ? Convert.ToInt16((string)trackPointElement.Element(ns1 + “HeartRateBpm”).Value) : 0,
    Cadence = trackPointElement.Element(ns1 + “Cadence”) != null ? Convert.ToInt16((string)trackPointElement.Element(ns1 + “Cadence”).Value) : 0,
    SensorState = trackPointElement.Element(ns1 + “SensorState”) != null ? trackPointElement.Element(ns1 + “SensorState”).Value : “”,

    Positionx = ((from positionElement in trackPointElement.Descendants(ns1 + “Position”)
    select new Position
    {
    LatitudeDegrees = Convert.ToDouble((string)positionElement.Element(ns1 + “LatitudeDegrees”).Value),
    LongitudeDegrees = Convert.ToDouble((string)positionElement.Element(ns1 + “LongitudeDegrees”).Value)

    }).ToList())

    }).ToList()

    }).ToList()

    }).ToList()

    };

    foreach (Activity act in activities)
    {
    foreach (Lap lap in act.Laps)
    {
    lap.ElevationGainMeters = 0;

    for (int i = 0; i < lap.Tracks.Count; i++)
    {
    lap.Tracks[i] = processPoints(lap.ID, lap.Tracks[i]);

    // this probably doesn't work either
    lap.AvgMPH = lap.Tracks[i].AvgMPH;
    // not sure this works with multi-tracks
    // seem to get multi-tracks from full backup or training center not from garmin connect.
    lap.ElevationGainMeters = lap.ElevationGainMeters + lap.Tracks[i].ElevationGainMeters;
    lap.ElevationLossMeters = lap.ElevationLossMeters + lap.Tracks[i].ElevationLossMeters;
    }

    Console.WriteLine("Lap " + lap.DistanceInMiles + " " + lap.ElevationGainMeters);

    act.Miles = act.Miles + lap.DistanceInMiles;

    act.ElevationGainMeters = act.ElevationGainMeters + lap.ElevationGainMeters;
    act.ElevationLossMeters = act.ElevationLossMeters + lap.ElevationLossMeters;

    act.TotalTimeSeconds = act.TotalTimeSeconds + lap.TotalTimeSeconds;
    }

    _myActivities.Add(act);

    }
    }
    catch (System.ArgumentException ex1)
    {
    MessageBox.Show(ex1.InnerException.Message);
    }
    }

    }

  8. Roger download the latest Garmin Training Center and connect your 305 – it will upload all the activities – you can then export the ones you want to deal with to a .tcx file

  9. You’ve probably long since forgotten about this but you are the “I’m Feeling Lucky” for the Google query “C# tcx file parser” so I figured I’d post a quick bug. Your initial query looks for the Activities node (root.Descendants(ns1 + “Activities”)) when it should actually be iterating the Activity nodes. That way if the user has the GPS track a multi-sport activity, like a triathlon, the laps get broken out into multiple Activity objects. Of course you would need to have the method return the generic List and not the SingleOrDefault.

  10. Thank you for a great program, I am a beginner in Linq, and I see how to get the information in Laps, but how do you get at the information in Tracks and Positionx? How does one list lead to the next?

    Mark

  11. dutchnomad says:

    Is the zip file still somewhere available?

  12. Very interesting article,

    I’m in search of something a bit more unique. Every search I’ve done point to the same thing.

    I’m trying to find devices, similarly to what the API is doing, but from code behind. Seeing that plug in is an ActiveX object I assume that wrapping the assembly in a managed class would give me the required functionality to do this. However, I cannot seem to either “unlock” the plugin in order to start the FindDevices() methods.

    Have you tried this by any chance? Or know of any link I can point to, to help me out?

    Regards,

    Eric

  13. Thanks for the post – wish I’d stumbled across it 2 months ago. I went the “xsd /classes” route to parsing tcx files instead – but this looks cleaner.

    I wonder which works faster…

  14. Thank you for been generous

  15. HI,

    very good idea and i think the code is also very smart, but sorry i dont speak c#.net.

    I for myself tried to do a tcx lap combiner and the first tests where not that bad, but the code was very unhandy.

    Now i want to take a look at your solution with LINQ. I tried to convert to vb.net (witch i speak) but stuck in garmin.cs line 41…

    COULD you help or even better do you see a way how to combine c#.net and a vb.net development, because i think you intension to do a tcx tool is also in my interests. I´ve got enough ideas what else we can do with the Edge files.

    That the output i´v got form converting…..
    Public Class GarminUtils
    Public Shared Function ConvertTCS(fileName As String) As Activity
    Dim root As XElement = XElement.Load(fileName)
    Dim ns1 As XNamespace = “http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2″

    Dim activities As IEnumerable(Of Activity) = From activityElement In root.Descendants(Convert.ToString(ns1) & “Activities”) _
    Select New Activity()

    Return activities.SingleOrDefault()
    End Function
    End Class

Follow

Get every new post delivered to your Inbox

Join other followers: