aboutsummaryrefslogtreecommitdiff
path: root/strava
diff options
context:
space:
mode:
Diffstat (limited to 'strava')
-rw-r--r--strava/sync.go107
-rw-r--r--strava/sync_test.go17
2 files changed, 117 insertions, 7 deletions
diff --git a/strava/sync.go b/strava/sync.go
index b8fa428..4630a1d 100644
--- a/strava/sync.go
+++ b/strava/sync.go
@@ -1,12 +1,15 @@
package strava
import (
+ "bytes"
"errors"
"fmt"
"net/url"
"strconv"
"time"
+ "github.com/jftuga/geodist"
+ "github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/kagamigo/kcore"
"github.com/tkrajina/gpxgo/gpx"
)
@@ -68,20 +71,24 @@ func (c *Client) list(from, to time.Time, types ...string) ([]Activity, error) {
return all, nil
}
-func (c *Client) Get(id int64) (Activity, []byte, error) {
+func (c *Client) Get(id int64) (Activity, []byte, []byte, error) {
var activity Activity
if err := c.GetJSON(fmt.Sprintf("/activities/%d", id), &activity); err != nil {
- return Activity{}, nil, kcore.Wrap(err, "failed to get Strava activity")
+ return Activity{}, nil, nil, kcore.Wrap(err, "failed to get Strava activity")
}
streams, err := c.activityStreams(id)
if err != nil {
- return Activity{}, nil, err
+ return Activity{}, nil, nil, err
}
gpxData, err := activityGPX(activity, streams)
if err != nil {
- return activity, nil, err
+ return activity, nil, nil, err
}
- return activity, gpxData, nil
+ fitData, err := activityFIT(activity, streams)
+ if err != nil {
+ return activity, nil, nil, err
+ }
+ return activity, gpxData, fitData, nil
}
type activityStreams struct {
@@ -139,6 +146,96 @@ func (c *Client) activityStreams(id int64) (activityStreams, error) {
return result, nil
}
+func activityFIT(activity Activity, streams activityStreams) ([]byte, error) {
+ if len(streams.LatLng) == 0 {
+ return nil, ErrNoTrackPoints
+ }
+ n := len(streams.LatLng)
+ distances := make([]float64, 0, n)
+ elevations := make([]float64, 0, n)
+ coords := make([]geodist.Coord, 0, n)
+ timestamps := make([]time.Time, 0, n)
+ heartRates := make([]float64, 0, n)
+ cadences := make([]float64, 0, n)
+ powers := make([]float64, 0, n)
+ heartRateComplete := true
+ cadenceComplete := true
+ powerComplete := true
+
+ distance := 0.0
+ var previous *geodist.Coord
+ for i := 0; i < n; i++ {
+ coord := geodist.Coord{Lat: streams.LatLng[i][0], Lon: streams.LatLng[i][1]}
+ if previous != nil {
+ _, km := geodist.HaversineDistance(*previous, coord)
+ d := km * 1000
+ if d == 0 {
+ continue
+ }
+ distance += d
+ } else {
+ distance = 0
+ }
+ previous = &coord
+
+ var elev float64
+ if i < len(streams.Altitude) {
+ elev = streams.Altitude[i]
+ }
+ // If altitude missing and we have no value, use 0 to keep ride valid.
+ // The GPX path requires elevation; this mirrors that requirement.
+ distances = append(distances, distance)
+ elevations = append(elevations, elev)
+ coords = append(coords, coord)
+ var ts time.Time
+ if i < len(streams.Seconds) {
+ ts = activity.StartDate.Add(time.Duration(streams.Seconds[i]) * time.Second)
+ } else {
+ ts = activity.StartDate.Add(time.Duration(i) * time.Second)
+ }
+ timestamps = append(timestamps, ts)
+
+ if hr := streamValue(streams.HeartRate, i); hr == nil {
+ heartRateComplete = false
+ heartRates = append(heartRates, 0)
+ } else {
+ heartRates = append(heartRates, *hr)
+ }
+ if cad := streamValue(streams.Cadence, i); cad == nil {
+ cadenceComplete = false
+ cadences = append(cadences, 0)
+ } else {
+ cadences = append(cadences, *cad)
+ }
+ if pow := streamValue(streams.Power, i); pow == nil {
+ powerComplete = false
+ powers = append(powers, 0)
+ } else {
+ powers = append(powers, *pow)
+ }
+ }
+ if len(distances) < 2 {
+ return nil, errors.New("zero distance")
+ }
+ var hrCol, cadCol, powCol []float64
+ if heartRateComplete {
+ hrCol = heartRates
+ }
+ if cadenceComplete {
+ cadCol = cadences
+ }
+ if powerComplete {
+ powCol = powers
+ }
+ r := ride.FromColumnsWithMetrics(distances, elevations, coords, timestamps, hrCol, cadCol, powCol)
+
+ var buf bytes.Buffer
+ if err := ride.EncodeFIT(r, &buf); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
func activityGPX(activity Activity, streams activityStreams) ([]byte, error) {
if len(streams.LatLng) == 0 {
return nil, ErrNoTrackPoints
diff --git a/strava/sync_test.go b/strava/sync_test.go
index abc3d0e..6d28e47 100644
--- a/strava/sync_test.go
+++ b/strava/sync_test.go
@@ -70,12 +70,13 @@ func TestGet(t *testing.T) {
}
})
- activity, gpxData, err := client.Get(42)
+ activity, gpxData, fitData, err := client.Get(42)
require.NoError(t, err)
assert.Equal(t, int64(42), activity.ID)
assert.Equal(t, 42195.0, activity.DistanceM)
assert.Equal(t, int64(7200), activity.MovingTimeS)
assert.NotEmpty(t, gpxData)
+ assert.NotEmpty(t, fitData)
parsed, err := gpx.ParseBytes(gpxData)
require.NoError(t, err)
require.Len(t, parsed.Tracks[0].Segments[0].Points, 2)
@@ -113,6 +114,18 @@ func TestGet(t *testing.T) {
require.True(t, ok)
powerValue := powers[1]
assert.Equal(t, 0.0, powerValue)
+
+ parsedFIT, err := (ride.FITRideParser{}).Parse(bytes.NewReader(fitData))
+ require.NoError(t, err)
+ assert.Equal(t, 2, parsedFIT.Len())
+ heartRates, ok = parsedFIT.HeartRates()
+ require.True(t, ok)
+ assert.Equal(t, 91.0, heartRates[1])
+ _, ok = parsedFIT.Cadences()
+ assert.False(t, ok)
+ powers, ok = parsedFIT.Powers()
+ require.True(t, ok)
+ assert.Equal(t, 0.0, powers[1])
}
func TestGetRejectsActivityWithoutTrackPoints(t *testing.T) {
@@ -130,7 +143,7 @@ func TestGetRejectsActivityWithoutTrackPoints(t *testing.T) {
}
})
- activity, _, err := client.Get(14701658670)
+ activity, _, _, err := client.Get(14701658670)
assert.Equal(t, int64(14701658670), activity.ID)
require.EqualError(t, err, "activity has no track points")