Skip to content

Instantly share code, notes, and snippets.

@grantstephens
Created September 8, 2021 06:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grantstephens/fa50198b308f1acb262b22eeb0721aec to your computer and use it in GitHub Desktop.
Save grantstephens/fa50198b308f1acb262b22eeb0721aec to your computer and use it in GitHub Desktop.
Go Script for converting gpx and fit files from strava archive to GeoJson
package main
import (
"archive/zip"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math"
"os"
"strings"
"github.com/beyoung/fit"
"github.com/twpayne/go-gpx"
)
type geoJson struct {
Type string `json:"type,omitempty"`
Features []geoFeature `json:"features,omitempty"`
}
type geoFeature struct {
Type string `json:"type,omitempty"`
Properties struct{} `json:"properties,omitempty"`
Geometry geometry `json:"geometry,omitempty"`
}
type geometry struct {
Type string `json:"type,omitempty"`
Coordinates [][]float64 `json:"coordinates,omitempty"`
}
type ErrLog struct {
FileName string `json:"fileName,omitempty"`
Error string `json:"error,omitempty"`
}
type ErrorFile struct {
Errors []ErrLog `json:"errors,omitempty"`
}
func main() {
fn := os.Args[1]
zf, err := zip.OpenReader(fn)
if err != nil {
panic(err)
}
defer zf.Close()
geo := geoJson{Type: "FeatureCollection"}
features := make([]geoFeature, 0)
errF := 0
sucF := 0
for _, file := range zf.File {
if !strings.Contains(file.Name, "activities/") {
continue
}
geoF, err := processFile(file)
if err != nil {
fmt.Println(file.Name, err)
errF++
continue
}
if len(geoF.Geometry.Coordinates) > 2 {
features = append(features, geoF)
sucF++
}
}
geo.Features = features
file, err := json.Marshal(geo)
if err != nil {
panic(err)
}
_ = ioutil.WriteFile(fn+".geojson", file, 0644)
fmt.Println(float32(errF)/float32(sucF+errF), errF, sucF)
}
func processFile(f *zip.File) (geoFeature, error) {
var rc, orc io.ReadCloser
geoF := geoFeature{
Type: "Feature",
}
geo := geometry{Type: "LineString"}
var err error
var coords [][]float64
rc, err = f.Open()
if err != nil {
return geoF, err
}
defer rc.Close()
switch {
case strings.HasSuffix(f.Name, "fit.gz"):
orc, err = gzip.NewReader(rc)
if err != nil {
return geoF, fmt.Errorf("error unzipping %s with error: %v", f.Name, err)
}
default:
orc = rc
}
switch {
case strings.HasSuffix(f.Name, ".fit"), strings.HasSuffix(f.Name, ".fit.gz"):
coords, err = processFit(orc)
case strings.HasSuffix(f.Name, ".gpx"), strings.HasSuffix(f.Name, ".gpx.gz"):
coords, err = processGpx(orc)
default:
return geoF, fmt.Errorf("cannot process")
}
if err != nil {
fmt.Println(f.Name, err)
}
if len(coords) > 0 {
coords := checkCoords(coords)
geo.Coordinates = coords
} else {
return geoF, fmt.Errorf("no valid coords")
}
geoF.Geometry = geo
return geoF, nil
}
func processGpx(rc io.ReadCloser) ([][]float64, error) {
coords := [][]float64{}
t, err := gpx.Read(rc)
if err != nil {
return coords, err
}
for _, tk := range t.Trk {
for _, sg := range tk.TrkSeg {
for _, tp := range sg.TrkPt {
coords = append(coords, []float64{tp.Lon, tp.Lat})
}
}
}
return coords, nil
}
func processFit(rc io.ReadCloser) ([][]float64, error) {
fit, err := fit.Decode(rc)
if err != nil {
return [][]float64{}, fmt.Errorf("fit decording error: %v", err)
}
activity, err := fit.Activity()
if err != nil {
return [][]float64{}, fmt.Errorf("fit activities error: %v", err)
}
coords := make([][]float64, 0, len(activity.Records))
for _, record := range activity.Records {
if !record.PositionLat.Invalid() && !record.PositionLong.Invalid() {
coords = append(coords, []float64{record.PositionLong.Degrees(), record.PositionLat.Degrees()})
}
}
return coords, nil
}
func checkCoords(coords [][]float64) [][]float64 {
var delete []int
rerun := false
for i := 1; i < len(coords); i++ {
d := distance(coords[i], coords[i-1])
if d > 0.1 {
delete = append(delete, i)
continue
}
if coords[i][0] == coords[i-1][0] && coords[i][1] == coords[i-1][1] {
delete = append(delete, i)
continue
}
}
for i := len(delete); i > 0; i-- {
coords = append(coords[:delete[i-1]], coords[delete[i-1]+1:]...)
rerun = true
}
if rerun {
return checkCoords(coords)
}
return coords
}
func distance(p1, p2 []float64) float64 {
return math.Sqrt(math.Pow(p1[0]-p2[0], 2) + math.Pow(p1[1]-p2[1], 2))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment