Created
September 8, 2021 06:38
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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