diff options
Diffstat (limited to 'ride/ride.go')
| -rw-r--r-- | ride/ride.go | 127 |
1 files changed, 123 insertions, 4 deletions
diff --git a/ride/ride.go b/ride/ride.go index 311e933..ef91272 100644 --- a/ride/ride.go +++ b/ride/ride.go @@ -2,9 +2,12 @@ package ride import ( "errors" + "fmt" "io" "math" "os" + "strconv" + "strings" "time" "github.com/jftuga/geodist" @@ -16,11 +19,16 @@ import ( "gonum.org/v1/plot/vg" ) +const garminTrackPointExtensionNamespace = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1" + type Ride struct { distances []float64 elevations []float64 coords []geodist.Coord timestamps []time.Time + heartRates []float64 + cadences []float64 + powers []float64 } func (r *Ride) check() { @@ -28,6 +36,11 @@ func (r *Ride) check() { kcore.Assert(len(r.distances) == len(r.elevations), "ride columns have different lengths") kcore.Assert(len(r.distances) == len(r.coords), "ride columns have different lengths") kcore.Assert(len(r.distances) == len(r.timestamps), "ride columns have different lengths") + for _, column := range [][]float64{r.heartRates, r.cadences, r.powers} { + if len(column) > 0 { + kcore.Assert(len(r.distances) == len(column), "ride columns have different lengths") + } + } } func (r Ride) Len() int { @@ -42,6 +55,25 @@ func (r Ride) Coord(i int) geodist.Coord { return r.coords[i] } func (r Ride) Timestamp(i int) time.Time { return r.timestamps[i] } +func (r Ride) HeartRateBpm(i int) (float64, bool) { + return optionalMetric(r.heartRates, i) +} + +func (r Ride) CadenceRpm(i int) (float64, bool) { + return optionalMetric(r.cadences, i) +} + +func (r Ride) PowerW(i int) (float64, bool) { + return optionalMetric(r.powers, i) +} + +func optionalMetric(column []float64, i int) (float64, bool) { + if len(column) == 0 { + return 0, false + } + return column[i], true +} + type RideParser interface { Parse(reader io.Reader) (Ride, error) } @@ -73,6 +105,12 @@ func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) { elevations := make([]float64, 0, len(segment.Points)) coords := make([]geodist.Coord, 0, len(segment.Points)) timestamps := make([]time.Time, 0, len(segment.Points)) + heartRates := make([]float64, 0, len(segment.Points)) + cadences := make([]float64, 0, len(segment.Points)) + powers := make([]float64, 0, len(segment.Points)) + heartRateComplete := true + cadenceComplete := true + powerComplete := true distance := 0.0 previous := segment.Points[0] if previous.Elevation.Null() { @@ -82,6 +120,13 @@ func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) { elevations = append(elevations, previous.Elevation.Value()) coords = append(coords, geodist.Coord{Lat: previous.Latitude, Lon: previous.Longitude}) timestamps = append(timestamps, previous.Timestamp) + sample, err := metricSampleForPoint(previous) + if err != nil { + return Ride{}, err + } + appendMetric(&heartRates, &heartRateComplete, sample.heartRate) + appendMetric(&cadences, &cadenceComplete, sample.cadence) + appendMetric(&powers, &powerComplete, sample.power) for i := 1; i < len(segment.Points); i++ { p := segment.Points[i] distance += p.Distance2D(&previous) @@ -96,21 +141,95 @@ func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) { elevations = append(elevations, p.Elevation.Value()) coords = append(coords, geodist.Coord{Lat: p.Latitude, Lon: p.Longitude}) timestamps = append(timestamps, p.Timestamp) + sample, err = metricSampleForPoint(p) + if err != nil { + return Ride{}, err + } + appendMetric(&heartRates, &heartRateComplete, sample.heartRate) + appendMetric(&cadences, &cadenceComplete, sample.cadence) + appendMetric(&powers, &powerComplete, sample.power) } if len(distances) < 2 { return Ride{}, errors.New("zero distance") } - ride := Ride{distances: distances, elevations: elevations, coords: coords, timestamps: timestamps} - ride.check() - return ride, nil + var heartRatesColumn, cadencesColumn, powersColumn []float64 + if heartRateComplete { + heartRatesColumn = heartRates + } + if cadenceComplete { + cadencesColumn = cadences + } + if powerComplete { + powersColumn = powers + } + return fromColumns(distances, elevations, coords, timestamps, heartRatesColumn, cadencesColumn, powersColumn), nil } func FromColumns(distances []float64, elevations []float64, coords []geodist.Coord, timestamps []time.Time) Ride { - ride := Ride{distances: distances, elevations: elevations, coords: coords, timestamps: timestamps} + return fromColumns(distances, elevations, coords, timestamps, nil, nil, nil) +} + +func fromColumns(distances []float64, elevations []float64, coords []geodist.Coord, timestamps []time.Time, heartRates, cadences, powers []float64) Ride { + ride := Ride{ + distances: distances, + elevations: elevations, + coords: coords, + timestamps: timestamps, + heartRates: heartRates, + cadences: cadences, + powers: powers, + } ride.check() return ride } +type metricSample struct { + heartRate *float64 + cadence *float64 + power *float64 +} + +func metricSampleForPoint(point gpx.GPXPoint) (metricSample, error) { + heartRate, err := metricValue(point, "hr") + if err != nil { + return metricSample{}, err + } + cadence, err := metricValue(point, "cad") + if err != nil { + return metricSample{}, err + } + power, err := metricValue(point, "watts") + if err != nil { + return metricSample{}, err + } + return metricSample{heartRate: heartRate, cadence: cadence, power: power}, nil +} + +func appendMetric(column *[]float64, complete *bool, value *float64) { + if value == nil { + *complete = false + *column = append(*column, 0) + return + } + *column = append(*column, *value) +} + +func metricValue(point gpx.GPXPoint, name string) (*float64, error) { + trackPointExtension, found := point.Extensions.GetNode(gpx.NamespaceURL(garminTrackPointExtensionNamespace), "TrackPointExtension") + if !found { + return nil, nil + } + node, found := trackPointExtension.GetNode(name) + if !found { + return nil, nil + } + value, err := strconv.ParseFloat(strings.TrimSpace(node.Data), 64) + if err != nil { + return nil, fmt.Errorf("invalid Garmin %s value %q: %w", name, node.Data, err) + } + return &value, nil +} + func (r *Ride) ScoreFromKm(start, end float64) float64 { i := 0 j := 0 |