diff options
Diffstat (limited to 'strava')
| -rw-r--r-- | strava/sync.go | 107 | ||||
| -rw-r--r-- | strava/sync_test.go | 17 |
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") |
