1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
package strava
import (
"fmt"
"net/url"
"os"
"strconv"
"time"
"github.com/martinlehoux/kagamigo/kcore"
"github.com/tkrajina/gpxgo/gpx"
)
type Activity struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
SportType string `json:"sport_type"`
StartDate time.Time `json:"start_date"`
}
func (c *Client) ListActivities(after time.Time, types ...string) ([]Activity, error) {
allowed := map[string]bool{}
for _, t := range types {
allowed[t] = true
}
var all []Activity
for page := 1; ; page++ {
q := url.Values{}
q.Set("per_page", "200")
q.Set("page", strconv.Itoa(page))
q.Set("after", strconv.FormatInt(after.Unix(), 10))
var batch []Activity
err := c.GetJSON("/athlete/activities?"+q.Encode(), &batch)
if err != nil {
return nil, kcore.Wrap(err, "failed to list Strava activities")
}
if len(batch) == 0 {
break
}
for _, activity := range batch {
if len(allowed) == 0 || allowed[activity.Type] || allowed[activity.SportType] {
all = append(all, activity)
}
}
if len(batch) < 200 {
break
}
}
return all, nil
}
func (c *Client) ActivityStreams(id int64) (latlng [][2]float64, altitude []float64, seconds []float64, err error) {
path := fmt.Sprintf("/activities/%d/streams?keys=latlng,altitude,time&key_by_type=true", id)
var streams struct {
LatLng *struct {
Data [][2]float64 `json:"data"`
} `json:"latlng"`
Altitude *struct {
Data []float64 `json:"data"`
} `json:"altitude"`
Time *struct {
Data []float64 `json:"data"`
} `json:"time"`
}
err = c.GetJSON(path, &streams)
if err != nil {
return nil, nil, nil, kcore.Wrap(err, "failed to fetch Strava streams")
}
if streams.LatLng != nil {
latlng = streams.LatLng.Data
}
if streams.Altitude != nil {
altitude = streams.Altitude.Data
}
if streams.Time != nil {
seconds = streams.Time.Data
}
return latlng, altitude, seconds, nil
}
func WriteActivityGPX(file string, activity Activity, latlng [][2]float64, altitude, seconds []float64) error {
points := make([]gpx.GPXPoint, len(latlng))
for i := range latlng {
point := gpx.GPXPoint{}
point.Latitude = latlng[i][0]
point.Longitude = latlng[i][1]
if i < len(altitude) {
point.Elevation = *gpx.NewNullableFloat64(altitude[i])
}
if i < len(seconds) {
point.Timestamp = activity.StartDate.Add(time.Duration(seconds[i]) * time.Second)
}
points[i] = point
}
doc := gpx.GPX{
Version: "1.1",
Creator: "biking_home",
Name: activity.Name,
Tracks: []gpx.GPXTrack{{
Name: activity.Name,
Segments: []gpx.GPXTrackSegment{{Points: points}},
}},
}
xmlData, err := doc.ToXml(gpx.ToXmlParams{})
if err != nil {
return kcore.Wrap(err, "failed to build GPX XML")
}
return os.WriteFile(file, xmlData, 0o644)
}
|