Using Linq to XML with C# to Read Gpx Files

GPX is the standardized file format for GPS file exchanges. A GPX file can contain a lot of different kinds of information. Take a look at the schema here. In general, the major things that you will work with are:

Waypoints

A waypoint is a specific position that is manually marked by a user for future reference. So when you get to the suspension bridge, mark a waypoint and you can find it again later as well as tell everyone else about it.

Tracks

Tracks are where you've been. When I want to mark out a trail for users of my application, I set up my GPS on my bike and just go for a ride. GPS antennae have come a long way in the last few years and my inexpensive Garmin eTrex keeps pretty accurate markers even when I'm in a deep draw. When I get home I have a complete listing of a few hundred points on my route, depending on how far apart or how long to wait I've preset my GPS to mark between saved track points.

Routes

A route is what you load into your GPS. It's essentially a list of positions you build by looking at a map or a file you get from someone else's track. When loaded, it will direct you to each point along the route in the appropriate order.

My Garmin saves files in a GDB format which is proprietary for the product. I load this file onto a machine with Garmin MapSource and immediately save the file as a GPX. This gets it into the standardized format that nearly all other GPS units and mapping software can use and I'm ready to load my data. At the end of this post is a well formed (but incomplete) GPX file. The original file had about 6,500 lines in it.

The Code

It's really pretty straight forward once you realize that you need to pull in the namespace object and then include it in each call to an element. My initial run at this netted attribute values but no element values which was really frustrating. Also, when working with Xml, remember that an element that doesn't exist results in a null object reference so you'll see in the code how I handled that for each element. The biggest issue for me with Linq is the inability to debug line-by-line. Still it's crazy fast and I'm loading a lot of data in only a few lines of code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;

namespace LinqXMLTester
{
  public class GPXLoader
  {
    /// <summary>
    /// Load the Xml document for parsing
    /// </summary>
    /// <param name="sFile">Fully qualified file name (local)</param>
    /// <returns>XDocument</returns>
    private XDocument GetGpxDoc(string sFile)
    {
      XDocument gpxDoc = XDocument.Load(sFile);
      return gpxDoc;
    }

    /// <summary>
    /// Load the namespace for a standard GPX document
    /// </summary>
    /// <returns></returns>
    private XNamespace GetGpxNameSpace()
    {
      XNamespace gpx = XNamespace.Get("http://www.topografix.com/GPX/1/1");
      return gpx;
    }

    /// <summary>
    /// When passed a file, open it and parse all waypoints from it.
    /// </summary>
    /// <param name="sFile">Fully qualified file name (local)</param>
    /// <returns>string containing line delimited waypoints from
    /// the file (for test)
</returns>
    /// <remarks>Normally, this would be used to populate the
    /// appropriate object model
</remarks>
    public string LoadGPXWaypoints(string sFile)
    {
      XDocument gpxDoc = GetGpxDoc(sFile);
      XNamespace gpx = GetGpxNameSpace();

      var waypoints = from waypoint in gpxDoc.Descendants(gpx + "wpt")
              select new
              {
                Latitude = waypoint.Attribute("lat").Value,
                Longitude = waypoint.Attribute("lon").Value,
                Elevation = waypoint.Element(gpx + "ele") != null ?
                    waypoint.Element(gpx + "ele").Value : null,
                Name = waypoint.Element(gpx + "name") != null ?
                    waypoint.Element(gpx + "name").Value : null,
                Dt = waypoint.Element(gpx + "cmt") != null ?
                    waypoint.Element(gpx + "cmt").Value : null
              };

      StringBuilder sb = new StringBuilder();
      foreach (var wpt in waypoints)
      {
        // This is where we'd instantiate data
        // containers for the information retrieved.
        sb.Append(
          string.Format("Name:{0} Latitude:{1} Longitude:{2} Elevation:{3} Date:{4}\n",
          wpt.Name,wpt.Latitude,wpt.Longitude,
          wpt.Elevation, wpt.Dt));
      }

      return sb.ToString();
    }

    /// <summary>
    /// When passed a file, open it and parse all tracks
    /// and track segments from it.
    /// </summary>
    /// <param name="sFile">Fully qualified file name (local)</param>
    /// <returns>string containing line delimited waypoints from the
    /// file (for test)</returns>
    public string LoadGPXTracks(string sFile)
    {
      XDocument gpxDoc = GetGpxDoc(sFile);
      XNamespace gpx = GetGpxNameSpace();
      var tracks = from track in gpxDoc.Descendants(gpx + "trk")
             select new
             {
               Name = track.Element(gpx + "name") != null ?
                track.Element(gpx + "name").Value : null,
               Segs = (
                    from trackpoint in track.Descendants(gpx + "trkpt")
                    select new
                    {
                      Latitude = trackpoint.Attribute("lat").Value,
                      Longitude = trackpoint.Attribute("lon").Value,
                      Elevation = trackpoint.Element(gpx + "ele") != null ?
                        trackpoint.Element(gpx + "ele").Value : null,
                      Time = trackpoint.Element(gpx + "time") != null ?
                        trackpoint.Element(gpx + "time").Value : null
                    }
                  )
             };

      StringBuilder sb = new StringBuilder();
      foreach (var trk in tracks)
      {
        // Populate track data objects.
        foreach (var trkSeg in trk.Segs)
        {
          // Populate detailed track segments
          // in the object model here.
          sb.Append(
            string.Format("Track:{0} - Latitude:{1} Longitude:{2} " +
                         "Elevation:{3} Date:{4}\n"
,
            trk.Name, trkSeg.Latitude,
            trkSeg.Longitude, trkSeg.Elevation,
            trkSeg.Time));
        }
      }
      return sb.ToString();
    }
  }
}


 

GPX Sample File

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<
gpx xmlns="http://www.topografix.com/GPX/1/1"
   creator="MapSource 6.13.7"
   version="1.1"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.garmin.com/xmlschemas/GpxExtensions/v3
       http://www.garmin.com/xmlschemas/GpxExtensions/v3/GpxExtensionsv3.xsd
       http://www.topografix.com/GPX/1/1
       http://www.topografix.com/GPX/1/1/gpx.xsd
">

  <
metadata>
    <
link href="http://www.garmin.com">
      <
text>Garmin International</text>
    </
link>
    <
time>2009-03-08T20:11:54Z</time>
    <
bounds
      maxlat="39.2971185"
      maxlon="-76.6951826"
      minlat="39.2035537"
      minlon="-76.8203088"/>
  </
metadata>

  <
wpt lat="39.2445616" lon="-76.7194497">
    <
ele>88.8067627</ele>
    <
name>001</name>
    <
cmt>16-SEP-08 8:50:11AM</cmt>
    <
desc>16-SEP-08 8:50:11AM</desc>
    <
sym>Residence</sym>
    <
extensions>
       <
gpxx:WaypointExtension
       xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
         <
gpxx:DisplayMode>SymbolAndName</gpxx:DisplayMode>
      </
gpxx:WaypointExtension>
    </
extensions>
  </
wpt>

  <
wpt lat="39.2422711" lon="-76.7213488">
    <
ele>38.3380127</ele>
    <
name>009</name>
    <
cmt>16-SEP-08 9:40:46AM</cmt>
    <
desc>16-SEP-08 9:40:46AM</desc>
    <
sym>Residence</sym>
    <
extensions>
      <
gpxx:WaypointExtension
      xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
        <
gpxx:DisplayMode>SymbolAndName</gpxx:DisplayMode>
      </
gpxx:WaypointExtension>
    </
extensions>
  </
wpt>

  <
trk>
    <
name>Vinyard Springs Loop</name>

    <
extensions>
      <
gpxx:TrackExtension
     
xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
        <
gpxx:DisplayColor>Transparent</gpxx:DisplayColor>
      </
gpxx:TrackExtension>
    </
extensions>

    <
trkseg>
      <
trkpt lat="39.2446415" lon="-76.7199907">
        <
ele>60.2197266</ele>
        <
time>2009-03-08T13:18:25Z</time>
      </
trkpt>
      <
trkpt lat="39.2445078" lon="-76.7193838">
        <
ele>88.0980225</ele>
        <
time>2009-03-08T13:19:23Z</time>
      </
trkpt>
      <
trkpt lat="39.2440145" lon="-76.7200792">
        <
ele>81.3687744</ele>
        <
time>2009-03-08T13:19:43Z</time>
      </
trkpt>
      <
trkpt lat="39.2435182" lon="-76.7208523">
        <
ele>82.3300781</ele>
        <
time>2009-03-08T13:20:04Z</time>
      </
trkpt>
      <
trkpt lat="39.2427701" lon="-76.7212304">
        <
ele>78.9654541</ele>
        <
time>2009-03-08T13:20:52Z</time>
      </
trkpt>
      <
trkpt lat="39.2417241" lon="-76.7208448">
        <
ele>65.0263672</ele>
        <
time>2009-03-08T13:21:43Z</time>
      </
trkpt>
    </
trkseg>
  </
trk>

  <
trk>
    <
name>Patapsco Boundry Ref</name>

    <
extensions>
      <
gpxx:TrackExtension
      xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
        <
gpxx:DisplayColor>DarkRed</gpxx:DisplayColor>
      </
gpxx:TrackExtension>
    </
extensions>
   
    <
trkseg>
      <
trkpt lat="39.2253216" lon="-76.7073387">
        <
ele>46.2806396</ele>
      </
trkpt>
      <
trkpt lat="39.2248415" lon="-76.7065894">
        <
ele>43.3966064</ele>
      </
trkpt>
      <
trkpt lat="39.2194385" lon="-76.7042182">
        <
ele>17.4411621</ele>
      </
trkpt>
      <
trkpt lat="39.2188285" lon="-76.7043919">
        <
ele>15.0377197</ele>
      </
trkpt>
      <
trkpt lat="39.2183665" lon="-76.7051846">
        <
ele>14.0764160</ele>
      </
trkpt>
      <
trkpt lat="39.2180424" lon="-76.7061647">
        <
ele>12.6343994</ele>
      </
trkpt>
      <
trkpt lat="39.2177812" lon="-76.7070982">
        <
ele>11.1925049</ele>
      </
trkpt>
      <
trkpt lat="39.2485097" lon="-76.8098993">
        <
ele>127.5119629</ele>
      </
trkpt>
    </
trkseg>
  </
trk>

</
gpx>


 

5 Comments

  • Eh...wouldn't this be a good candidate for some Data Transfer Objects and Serialization?

  • Yes, if you notice the comments in the code, the spots where I append text to show the values that are pulled from the file are changed in my application to load my business objects. These objects are then sent to the data layer via WCF to update my database.
    My primary purpose in this post was to show how fast it is to use Linq to XML to get data out of a file and into an object model. Deserializing the file into a model built from the schema would work too but I like the ability to dynamically filter it.

  • The XElement class has an explicit cast to various types, which handles null elements.

    For example, you can replace:
    waypoint.Element(gpx + "ele") != null ? waypoint.Element(gpx + "ele").Value : null

    with:
    (string)waypoint.Element(gpx + "ele")

    If the "ele" element exists, the cast will return the "Value" property; otherwise, it will return null.

  • Did you already give Linq to XSD a try? You can find the toolset on codeplex.
    Using it, it *should* be possible to get objectoriented access to the gpx files through linq like that:
    var myCache = gpx.Load("data.gpx");
    var waypoints =
    &nbsp; &nbsp;from w in myCache.wpt
    &nbsp; &nbsp;select w;
    foreach (var waypoint in waypoints)
    {
    &nbsp; &nbsp;Console.WriteLine(waypoint.desc);
    }
    However, I for instance, am looking for a way of accessing geocaching gpx files downloadable from Groundspeak's geocaching.com website. The guys extended the gpx standard by own namespaces and I simply don't know how to get those xsd's merged in a way, that I can access all data from within the classes generated by LinqToXsd.
    Can you help?

  • I haven't actually looked into LinqToXsd. Lack of time and I've been working on other things. My process is to load gpx (1.0 and 1.1) along with kml files from the site into my database for manipulation there so once I get a piece working, I don't really have time to touch it again.

    I'd say off hand that you can load multiple namespaces for an xml file so if he has a link for his new namespaces you should be ok. Shouldn't the link for the xsd be in the files you're downloading?

Comments have been disabled for this content.