Extract and analyze GPS data with Go
Programming Snapshot – GPS Analysis with Go
For running statistics on his recorded hiking trails, Mike Schilli turns to Go to extract the GPS data while relying on plotters and APIs for a bit of geoanalysis.
The GPX data of my hiking trails, which I recorded with the help of geotrackers and apps like Komoot [1], hold some potential for statistical analysis. On which days was I on the move, and when was I lazy? Which regions were my favorites for hiking, and in which regions on the world map did I cover the most miles?
No matter where the GPX files come from – whether recorded by a Garmin tracker or by an app like Komoot that lets you download the data from its website [2] – the recorded data just screams to be put through more or less intelligent analysis programs. For each hike or bike ride, the tours/
directory (Figure 1) contains one file in XML format (Figure 2). Each of these GPX files consists of a series of geodata recorded with timestamps. In each case, the data shows the longitude and latitude determined using GPS, from which, in turn, you can determine a point on the Earth's surface, visited at a given time.
Nabbed from GitHub
It would be a tedious task to read the XML data manually with Go because its internal structure, with separate tracks, segments, and points, dictates that you have matching structures in Go. Fortunately, someone already solved that problem with the gpxgo project, which is available on GitHub. The program from Listing 1 [3] retrieves it in line 4 and completes the job in one fell swoop using ParseFile()
in line 26. Any analysis program presented later on just needs to call gpxPoints()
from Listing 1 with the name of a GPX file to retrieve Go structures with all the geopoints in the file and the matching timestamps.
Listing 1
gpxread.go
01 package main 02 03 import ( 04 "github.com/tkrajina/gpxgo/gpx" 05 "os" 06 "path/filepath" 07 ) 08 09 func gpxFiles() []string { 10 tourDir := "tours" 11 files := []string{} 12 13 entries, err := os.ReadDir(tourDir) 14 if err != nil { 15 panic(err) 16 } 17 18 for _, entry := range entries { 19 gpxPath := filepath.Join(tourDir, entry.Name()) 20 files = append(files, gpxPath) 21 } 22 return files 23 } 24 25 func gpxPoints(path string) []gpx.GPXPoint { 26 gpxData, err := gpx.ParseFile(path) 27 points := []gpx.GPXPoint{} 28 29 if err != nil { 30 panic(err) 31 } 32 33 for _, trk := range gpxData.Tracks { 34 for _, seg := range trk.Segments { 35 for _, pt := range seg.Points { 36 points = append(points, pt) 37 } 38 } 39 } 40 return points 41 } 42 43 func gpxAvg(path string)(float64, float64, int) { 44 nofPoints := 0 45 latSum,longSum := 0.0, 0.0 46 for _, pt := range gpxPoints(path) { 47 latSum += pt.Latitude 48 longSum += pt.Longitude 49 nofPoints++ 50 } 51 return latSum/float64(nofPoints), 52 longSum/float64(nofPoints), nofPoints 53 }
To calculate the average of all geopoints in a GPX file, say, to determine where the whole trail is located, gpxAvg()
first calls gpxPoints()
starting in line 43, uses pt.Longitude
and pt.Latitude
from the Point structure to pick up the values for longitude and latitude, and adds them up to create two float64 sums. Also, for each geopoint processed, the nofPoints
counter is incremented by one, and the averaging function only has to divide the total by the number of points at the end to return the mean value.
Time for an Overview
To get a brief overview of the contents of all collected GPX files, Listing 2 walks through all the files in the tours/
directory using gpxFiles()
from Listing 1. It reads the files' XML data and uses gpxPoints()
to return a list of all the geopoints it contains along with matching timestamps.
Listing 2
tourstats.go
01 package main 02 03 import ("fmt") 04 05 func main() { 06 for _, path := range gpxFiles() { 07 lat, lon, pts := gpxAvg(path) 08 fmt.Printf("%s %.2f,%.2f (%d points)\n", 09 path, lat, lon, pts) 10 } 11 }
The output in Figure 3 shows that the trails were recorded all over the place. For example, the intersection of the latitude of 37 degrees north and the longitude of -122 degrees west is my adopted home of San Francisco. On the other hand, the latitude of 48 degrees north and the longitude of 10 degrees east represents my former home of Augsburg, Germany, which I visited as an American tourist last summer.
Out and About or Lazy?
A recording's GPX points also come with timestamps; in other words, a collection of GPX files reveals the calendar days on which I recorded walks. From this data, Listing 3 generates a time-based activity curve. To accumulate the number of all track points recorded during a given calendar day, it sets the hour, minute, and second values of all timestamps it finds to zero and uses time.Date()
to set the recording date, valid for all points sampled during a specific calendar day. In the perday
hash map, line 17 then increments the respective day entry by one with each matching timestamp it finds. All that remains to do then is to sort the keys of the hash map (i.e., the date values) in ascending order and to draw them on a chart with values corresponding to the assigned counters.
Listing 3
activity.go
01 package main 02 03 import ( 04 "fmt" 05 "github.com/wcharczuk/go-chart/v2" 06 "os" 07 "sort" 08 "time" 09 ) 10 11 func main() { 12 perday := map[time.Time]int{} 13 14 for _, path := range gpxFiles() { 15 for _, pt := range gpxPoints(path) { 16 t := time.Date(pt.Timestamp.Year(), pt.Timestamp.Month(), pt.Timestamp.Day(), 0, 0, 0, 0, time.Local) 17 perday[t]++ 18 } 19 } 20 21 keys := []time.Time{} 22 for day, _ := range perday { 23 keys = append(keys, day) 24 } 25 sort.Slice(keys, func(i, j int) bool { 26 return keys[i].Before(keys[j]) 27 }) 28 29 xVals := []time.Time{} 30 yVals := []float64{} 31 for _, key := range keys { 32 xVals = append(xVals, key) 33 yVals = append(yVals, float64(perday[key])) 34 } 35 36 mainSeries := chart.TimeSeries{ 37 Name: "GPS Activity", 38 Style: chart.Style{ 39 StrokeColor: chart.ColorBlue, 40 FillColor: chart.ColorBlue.WithAlpha(100), 41 }, 42 XValues: xVals, 43 YValues: yVals, 44 } 45 46 graph := chart.Chart{ 47 Width: 1280, 48 Height: 720, 49 Series: []chart.Series{mainSeries}, 50 } 51 52 f, _ := os.Create("activity.png") 53 defer f.Close() 54 55 graph.Render(chart.PNG, f) 56 }
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
![Learn More](https://www.linux-magazine.com/var/linux_magazin/storage/images/media/linux-magazine-eng-us/images/misc/learn-more/834592-1-eng-US/Learn-More_medium.png)
News
-
NVIDIA Released Driver for Upcoming NVIDIA 560 GPU for Linux
Not only has NVIDIA released the driver for its upcoming CPU series, it's the first release that defaults to using open-source GPU kernel modules.
-
OpenMandriva Lx 24.07 Released
If you’re into rolling release Linux distributions, OpenMandriva ROME has a new snapshot with a new kernel.
-
Kernel 6.10 Available for General Usage
Linus Torvalds has released the 6.10 kernel and it includes significant performance increases for Intel Core hybrid systems and more.
-
TUXEDO Computers Releases InfinityBook Pro 14 Gen9 Laptop
Sporting either AMD or Intel CPUs, the TUXEDO InfinityBook Pro 14 is an extremely compact, lightweight, sturdy powerhouse.
-
Google Extends Support for Linux Kernels Used for Android
Because the LTS Linux kernel releases are so important to Android, Google has decided to extend the support period beyond that offered by the kernel development team.
-
Linux Mint 22 Stable Delayed
If you're anxious about getting your hands on the stable release of Linux Mint 22, it looks as if you're going to have to wait a bit longer.
-
Nitrux 3.5.1 Available for Install
The latest version of the immutable, systemd-free distribution includes an updated kernel and NVIDIA driver.
-
Debian 12.6 Released with Plenty of Bug Fixes and Updates
The sixth update to Debian "Bookworm" is all about security mitigations and making adjustments for some "serious problems."
-
Canonical Offers 12-Year LTS for Open Source Docker Images
Canonical is expanding its LTS offering to reach beyond the DEB packages with a new distro-less Docker image.
-
Plasma Desktop 6.1 Released with Several Enhancements
If you're a fan of Plasma Desktop, you should be excited about this new point release.