GPX

edit

GPX is the XML format of GPS devices and sport apps. Gpx reads GPX 1.0 and 1.1, and writes GPX 1.1. This example reads an activity and computes the length of its track:

GeoFeatureCollection features;
using (var stream = File.OpenRead("activity.gpx"))
{
    features = Gpx.Read(stream);
}

// A track is a multi-line string, with one line per segment
var track = features.Single(feature => (string?)feature.Properties[Gpx.TypePropertyName] is "trk");
var segments = ((GeoMultiLineString)track.Geometry!).LineStrings;
var length = segments.Sum(segment => segment.GetLength());

Assert.Equal("Lunch Run", track.Properties["name"]);
Assert.Equal(173, Math.Round(length, MidpointRounding.AwayFromZero));

What the reader keeps

Each waypoint, route and track is a feature, in the order of the document:

GPXGeometry
wpt, a waypointGeoPoint
rte, a routeGeoLineString
trk, a trackGeoMultiLineString, a line per segment, even when it has one
  • The elevations (ele) are the altitudes. The name of the file (metadata/name) is the Name of the collection.
  • The property named by Gpx.TypePropertyName, gpxType, is wpt, rte or trk.
  • The other properties are name, cmt, desc, src, type and sym as strings, the number of a route or a track as a double, and the time of a waypoint as a DateTimeOffset.
  • The time of each point is the Time of its position. The other data of the points of the routes and tracks, such as the heart rate of their extensions, is dropped. The links and the other data of the waypoints are dropped too.

The times give the speed of a track:

GeoFeatureCollection features;
using (var stream = File.OpenRead("run.gpx"))
{
    features = Gpx.Read(stream);
}

// The time of each point of the track is the Time of its position
var segment = ((GeoMultiLineString)features[0].Geometry!).LineStrings[0];
var start = segment.Positions[0].Time!.Value;
var end = segment.Positions[^1].Time!.Value;
var speed = segment.GetLength() / (end - start).TotalSeconds; // meters per second

Assert.Equal(new DateTimeOffset(2024, 9, 1, 7, 0, 0, TimeSpan.Zero), start);
Assert.Equal(11.6, speed, 1);

Write a GPX file

GeoFeatureCollection features =
[
    // A point is a waypoint, and a multi-line string is a track with one segment per line
    new GeoFeature(new GeoPoint(new GeoPosition(6.8652, 45.8326, 4808)), new Dictionary<string, object?>(StringComparer.Ordinal)
    {
        ["name"] = "Mont Blanc",
        ["time"] = new DateTimeOffset(2024, 7, 1, 12, 0, 0, TimeSpan.Zero),
    }),
    new GeoFeature(new GeoMultiLineString([new GeoLineString([new(6.8693, 45.9237, 1035), new(6.8814, 45.8775, 2372)])]), new Dictionary<string, object?>(StringComparer.Ordinal)
    {
        ["name"] = "Ascent",
    }),
];
features.Name = "Mont Blanc";

using (var stream = File.Create("mont-blanc.gpx"))
{
    Gpx.Write(features, stream, new GpxWriterOptions { Creator = "My application", Indented = true });
}
  • A point is a waypoint, a line is a route, and a multi-line is a track. Set gpxType to trk to write a line as a track of one segment.
  • As GPX requires, the waypoints come first, then the routes, then the tracks.
  • The time of a position is the time of its point, in UTC. A waypoint whose position has no time gets its time property.
  • The writer writes the properties the reader reads; the other properties, the identifiers and the measures are dropped, as GPX has no element for them.
  • GPX has no polygons: writing a polygon, a collection or a feature without a geometry throws an ArgumentException.
  • GpxWriterOptions sets the Creator of the file and whether it is Indented.