From 22a58e96dddd326f4c331c2a1bdea46f808ae3ad Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Fri, 7 Aug 2026 14:58:02 +0200 Subject: refactor: Store rides in columns --- chart/chart.go | 18 +++--- mountain_pass/detection.go | 17 +++--- mountain_pass/detection_test.go | 46 +++++++------- ride/climb.go | 104 +++++++++++++++++-------------- ride/climb_test.go | 17 +++--- ride/difficulty_test.go | 2 +- ride/index.go | 4 +- ride/parser_test.go | 4 +- ride/ride.go | 131 +++++++++++++++++++++++----------------- 9 files changed, 190 insertions(+), 153 deletions(-) diff --git a/chart/chart.go b/chart/chart.go index a185378..7f5a4c7 100644 --- a/chart/chart.go +++ b/chart/chart.go @@ -19,18 +19,18 @@ func Render(r ride.Ride, climbs []ride.Climb, crossings []mountain_pass.Crossing p.Y.Label.Text = "Elevation (m)" maxElev := 0.0 - elevation := make(plotter.XYs, len(r.Points())) - for i, point := range r.Points() { - elevation[i].X = point.DistanceM / 1000 - elevation[i].Y = point.ElevationM - if point.ElevationM > maxElev { - maxElev = point.ElevationM + elevation := make(plotter.XYs, r.Len()) + for i := 0; i < r.Len(); i++ { + elevation[i].X = r.DistanceM(i) / 1000 + elevation[i].Y = r.ElevationM(i) + if r.ElevationM(i) > maxElev { + maxElev = r.ElevationM(i) } } for _, climb := range climbs { - x0 := climb.Start().DistanceM / 1000 - x1 := climb.End().DistanceM / 1000 + x0 := climb.StartDistanceM() / 1000 + x1 := climb.EndDistanceM() / 1000 fill, err := plotter.NewPolygon(plotter.XYs{ {X: x0, Y: 0}, {X: x1, Y: 0}, @@ -41,7 +41,7 @@ func Render(r ride.Ride, climbs []ride.Climb, crossings []mountain_pass.Crossing fill.Color = color.RGBA{R: 230, G: 160, B: 0, A: 70} fill.LineStyle.Width = 0 p.Add(fill) - p.Add(plotLabels(plotter.XY{X: (x0 + x1) / 2, Y: climb.Top().ElevationM * 1.01}, climbLabel(climb))) + p.Add(plotLabels(plotter.XY{X: (x0 + x1) / 2, Y: climb.TopElevationM() * 1.01}, climbLabel(climb))) } for _, crossing := range crossings { diff --git a/mountain_pass/detection.go b/mountain_pass/detection.go index e30fddc..4be2f94 100644 --- a/mountain_pass/detection.go +++ b/mountain_pass/detection.go @@ -60,7 +60,8 @@ func DetectCrossings(ride ride.Ride, passes []MountainPass, radiusM, elevationTo // climb's highest point and whose elevation is within elevationToleranceM of // that point's elevation, nearest first. Found is false when no pass matches. func MatchClimb(climb ride.Climb, passes []MountainPass, radiusM, elevationToleranceM float64) (MountainPass, bool) { - top := climb.Top() + topCoord := climb.TopCoord() + topElevation := climb.TopElevationM() var best MountainPass bestDistanceM := radiusM found := false @@ -68,12 +69,12 @@ func MatchClimb(climb ride.Climb, passes []MountainPass, radiusM, elevationToler if mountainPass.Coord == nil { continue } - distanceKm, _ := geodist.HaversineDistance(top.Coord, *mountainPass.Coord) + distanceKm, _ := geodist.HaversineDistance(topCoord, *mountainPass.Coord) distanceM := distanceKm * 1000 if distanceM > bestDistanceM { continue } - elevationDiff := absFloat64(top.ElevationM - float64(mountainPass.Elevation)) + elevationDiff := absFloat64(topElevation - float64(mountainPass.Elevation)) if elevationDiff > elevationToleranceM { continue } @@ -86,14 +87,14 @@ func MatchClimb(climb ride.Climb, passes []MountainPass, radiusM, elevationToler func nearestCrossing(ride ride.Ride, mountainPass MountainPass) (Crossing, bool) { best := Crossing{Pass: mountainPass, DistanceToM: 1e18} - for _, point := range ride.Points() { - distanceKm, _ := geodist.HaversineDistance(point.Coord, *mountainPass.Coord) + for i := 0; i < ride.Len(); i++ { + distanceKm, _ := geodist.HaversineDistance(ride.Coord(i), *mountainPass.Coord) distanceM := distanceKm * 1000 if distanceM < best.DistanceToM { best.DistanceToM = distanceM - best.RideDistanceM = point.DistanceM - best.RideElevation = point.ElevationM - best.ElevationDiff = absFloat64(point.ElevationM - float64(mountainPass.Elevation)) + best.RideDistanceM = ride.DistanceM(i) + best.RideElevation = ride.ElevationM(i) + best.ElevationDiff = absFloat64(ride.ElevationM(i) - float64(mountainPass.Elevation)) } } if best.DistanceToM > 1e17 { diff --git a/mountain_pass/detection_test.go b/mountain_pass/detection_test.go index 3208061..d6cba9f 100644 --- a/mountain_pass/detection_test.go +++ b/mountain_pass/detection_test.go @@ -2,6 +2,7 @@ package mountain_pass_test import ( "testing" + "time" "github.com/jftuga/geodist" "github.com/martinlehoux/biking_home/mountain_pass" @@ -48,19 +49,20 @@ func TestDetectCrossingsRequiresElevationAgreement(t *testing.T) { func rideFromPoint(t *testing.T, latitude, longitude, elevation float64) ride.Ride { t.Helper() - points := []ride.Point{ - {DistanceM: 0, ElevationM: 100, Coord: geodist.Coord{Lat: latitude - 0.01, Lon: longitude - 0.01}}, - {DistanceM: 1000, ElevationM: elevation, Coord: geodist.Coord{Lat: latitude, Lon: longitude}}, - } - return ride.FromPoints(points) + return ride.FromColumns( + []float64{0, 1000}, + []float64{100, elevation}, + []geodist.Coord{{Lat: latitude - 0.01, Lon: longitude - 0.01}, {Lat: latitude, Lon: longitude}}, + make([]time.Time, 2), + ) } func TestMatchClimbFindsPassAtTop(t *testing.T) { - climb := climbFromPoints([]ride.Point{ - {DistanceM: 0, ElevationM: 300, Coord: geodist.Coord{Lat: 43.61, Lon: 5.42}}, - {DistanceM: 1000, ElevationM: 447, Coord: geodist.Coord{Lat: 43.62, Lon: 5.43}}, - {DistanceM: 2000, ElevationM: 380, Coord: geodist.Coord{Lat: 43.63, Lon: 5.44}}, - }) + climb := climbFromColumns( + []float64{0, 1000, 2000}, + []float64{300, 447, 380}, + []geodist.Coord{{Lat: 43.61, Lon: 5.42}, {Lat: 43.62, Lon: 5.43}, {Lat: 43.63, Lon: 5.44}}, + ) pass := mountain_pass.MountainPass{ Name: "Pas de Magnan", Elevation: 440, Coord: &geodist.Coord{Lat: 43.62, Lon: 5.43}, @@ -71,11 +73,11 @@ func TestMatchClimbFindsPassAtTop(t *testing.T) { } func TestMatchClimbRequiresElevationAgreement(t *testing.T) { - climb := climbFromPoints([]ride.Point{ - {DistanceM: 0, ElevationM: 300, Coord: geodist.Coord{Lat: 43.61, Lon: 5.42}}, - {DistanceM: 1000, ElevationM: 447, Coord: geodist.Coord{Lat: 43.62, Lon: 5.43}}, - {DistanceM: 2000, ElevationM: 380, Coord: geodist.Coord{Lat: 43.63, Lon: 5.44}}, - }) + climb := climbFromColumns( + []float64{0, 1000, 2000}, + []float64{300, 447, 380}, + []geodist.Coord{{Lat: 43.61, Lon: 5.42}, {Lat: 43.62, Lon: 5.43}, {Lat: 43.63, Lon: 5.44}}, + ) pass := mountain_pass.MountainPass{ Name: "Pas de Magnan", Elevation: 900, Coord: &geodist.Coord{Lat: 43.62, Lon: 5.43}, @@ -85,11 +87,11 @@ func TestMatchClimbRequiresElevationAgreement(t *testing.T) { } func TestMatchClimbNearestOfTwo(t *testing.T) { - climb := climbFromPoints([]ride.Point{ - {DistanceM: 0, ElevationM: 300, Coord: geodist.Coord{Lat: 43.61, Lon: 5.42}}, - {DistanceM: 1000, ElevationM: 447, Coord: geodist.Coord{Lat: 43.62, Lon: 5.43}}, - {DistanceM: 2000, ElevationM: 380, Coord: geodist.Coord{Lat: 43.63, Lon: 5.44}}, - }) + climb := climbFromColumns( + []float64{0, 1000, 2000}, + []float64{300, 447, 380}, + []geodist.Coord{{Lat: 43.61, Lon: 5.42}, {Lat: 43.62, Lon: 5.43}, {Lat: 43.63, Lon: 5.44}}, + ) near := mountain_pass.MountainPass{ Name: "Pas de Magnan", Elevation: 440, Coord: &geodist.Coord{Lat: 43.62, Lon: 5.43}, @@ -103,7 +105,7 @@ func TestMatchClimbNearestOfTwo(t *testing.T) { assert.Equal(t, "Pas de Magnan", matched.Name) } -func climbFromPoints(points []ride.Point) ride.Climb { - ride := ride.FromPoints(points) +func climbFromColumns(distances, elevations []float64, coords []geodist.Coord) ride.Climb { + ride := ride.FromColumns(distances, elevations, coords, make([]time.Time, len(distances))) return ride.ClimbFromDist(0, 2000) } diff --git a/ride/climb.go b/ride/climb.go index d5a1718..9f38038 100644 --- a/ride/climb.go +++ b/ride/climb.go @@ -7,51 +7,67 @@ import ( "slices" "time" + "github.com/jftuga/geodist" "github.com/martinlehoux/kagamigo/kcore" ) const ClimbDistanceMinimum = 500 type Climb struct { + ride Ride rideStart int rideEnd int - points []Point Name string } func (climb Climb) Duration() time.Duration { - return climb.points[len(climb.points)-1].Timestamp.Sub(climb.points[0].Timestamp) + return climb.ride.Timestamp(climb.rideEnd).Sub(climb.ride.Timestamp(climb.rideStart)) } func (climb Climb) Speed() float64 { - return (climb.End().DistanceM - climb.Start().DistanceM) / climb.Duration().Seconds() + return (climb.EndDistanceM() - climb.StartDistanceM()) / climb.Duration().Seconds() } -func (climb Climb) Start() Point { - return climb.points[0] +func (climb Climb) StartIndex() int { return climb.rideStart } + +func (climb Climb) EndIndex() int { return climb.rideEnd } + +func (climb Climb) StartDistanceM() float64 { + return climb.ride.DistanceM(climb.rideStart) } -func (climb Climb) End() Point { - return climb.points[len(climb.points)-1] +func (climb Climb) EndDistanceM() float64 { + return climb.ride.DistanceM(climb.rideEnd) } -// Top returns the highest point inside the climb, used as the crest anchor -// when naming the climb after a pass. -func (climb Climb) Top() Point { - top := climb.points[0] - for _, point := range climb.points { - if point.ElevationM > top.ElevationM { - top = point +// TopIndex returns the highest point inside the climb, used as the crest anchor. +func (climb Climb) TopIndex() int { + top := climb.rideStart + for i := climb.rideStart; i <= climb.rideEnd; i++ { + if climb.ride.ElevationM(i) > climb.ride.ElevationM(top) { + top = i } } return top } +func (climb Climb) TopDistanceM() float64 { + return climb.ride.DistanceM(climb.TopIndex()) +} + +func (climb Climb) TopElevationM() float64 { + return climb.ride.ElevationM(climb.TopIndex()) +} + +func (climb Climb) TopCoord() geodist.Coord { + return climb.ride.Coord(climb.TopIndex()) +} + func (climb Climb) String() string { - start := climb.points[0] - end := climb.points[len(climb.points)-1] - score := Score(climb.points, 0, len(climb.points)-1) - body := fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", start.DistanceM/1000, end.DistanceM/1000, (end.DistanceM-start.DistanceM)/1000, Slope(start, end)*100, int(score), Category(score)) + startDistance := climb.StartDistanceM() + endDistance := climb.EndDistanceM() + score := climb.Score() + body := fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", startDistance/1000, endDistance/1000, (endDistance-startDistance)/1000, Slope(climb.ride, climb.rideStart, climb.rideEnd)*100, int(score), Category(score)) if climb.Name == "" { return body } @@ -59,24 +75,24 @@ func (climb Climb) String() string { } func (climb Climb) Score() float64 { - return Score(climb.points, 0, len(climb.points)-1) + return Score(climb.ride, climb.rideStart, climb.rideEnd) } func (climb Climb) DifficultyScore() float64 { - return difficultyScore(climb.points) + return difficultyScore(climb.ride, climb.rideStart, climb.rideEnd) } -func Slope(start, end Point) float64 { - return (end.ElevationM - start.ElevationM) / (end.DistanceM - start.DistanceM) +func Slope(r Ride, start, end int) float64 { + return (r.ElevationM(end) - r.ElevationM(start)) / (r.DistanceM(end) - r.DistanceM(start)) } -func Score(points []Point, start int, end int) float64 { +func Score(r Ride, start, end int) float64 { kcore.Assert(end > start, "no points for score") - distance := points[end].DistanceM - points[start].DistanceM + distance := r.DistanceM(end) - r.DistanceM(start) if distance == 0 { return 0 } - dElevation := points[end].ElevationM - points[start].ElevationM + dElevation := r.ElevationM(end) - r.ElevationM(start) return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0 } @@ -98,13 +114,13 @@ func Category(score float64) string { } } -func bestClimbBetween(points []Point, start int, end int) Climb { +func bestClimbBetween(r Ride, start, end int) Climb { kcore.Assert(end > start, "empty points") - bestScore := Score(points, start, end) + bestScore := Score(r, start, end) bestStart := start for i := start; i < end; i++ { - score := Score(points, i, end) + score := Score(r, i, end) if score > bestScore { bestStart = i bestScore = score @@ -112,54 +128,52 @@ func bestClimbBetween(points []Point, start int, end int) Climb { } bestEnd := end for i := end; i > bestStart; i-- { - score := Score(points, bestStart, i) + score := Score(r, bestStart, i) if score > bestScore { bestEnd = i bestScore = score } } for i := bestStart; i < bestEnd; i++ { - score := Score(points, i, bestEnd) + score := Score(r, i, bestEnd) if score > bestScore { bestStart = i bestScore = score } } kcore.Assert(bestStart < bestEnd, "empty climb") - climb := Climb{rideStart: bestStart, rideEnd: bestEnd, points: points[bestStart : bestEnd+1]} - - return climb + return Climb{ride: r, rideStart: bestStart, rideEnd: bestEnd} } -func climbsBetween(points []Point, start int, end int) []Climb { +func climbsBetween(r Ride, start, end int) []Climb { climbs := []Climb{} - if points[end].DistanceM-points[start].DistanceM < ClimbDistanceMinimum { + if r.DistanceM(end)-r.DistanceM(start) < ClimbDistanceMinimum { return climbs } - slog.Debug("Searching climbs between", slog.Int("start", int(points[start].DistanceM)), slog.Int("end", int(points[end].DistanceM))) + slog.Debug("Searching climbs between", slog.Int("start", int(r.DistanceM(start))), slog.Int("end", int(r.DistanceM(end)))) highest := start for i := start; i <= end; i++ { - if points[i].ElevationM > points[highest].ElevationM { + if r.ElevationM(i) > r.ElevationM(highest) { highest = i } } // TODO: Use descent to reduce recursion - if points[highest].DistanceM-points[start].DistanceM < ClimbDistanceMinimum { - return climbsBetween(points, start+1, end) + if r.DistanceM(highest)-r.DistanceM(start) < ClimbDistanceMinimum { + return climbsBetween(r, start+1, end) } - climb := bestClimbBetween(points, start, highest) - if climb.Score() >= 35 && climb.End().DistanceM-climb.Start().DistanceM >= ClimbDistanceMinimum { - slog.Debug("Found climb between", slog.Int("start", int(climb.Start().DistanceM)), slog.Int("end", int(climb.End().DistanceM))) + climb := bestClimbBetween(r, start, highest) + if climb.Score() >= 35 && climb.EndDistanceM()-climb.StartDistanceM() >= ClimbDistanceMinimum { + slog.Debug("Found climb between", slog.Int("start", int(climb.StartDistanceM())), slog.Int("end", int(climb.EndDistanceM()))) climbs = append(climbs, climb) } - climbs = append(climbs, climbsBetween(points, start, climb.rideStart)...) - climbs = append(climbs, climbsBetween(points, climb.rideEnd, end)...) + climbs = append(climbs, climbsBetween(r, start, climb.rideStart)...) + climbs = append(climbs, climbsBetween(r, climb.rideEnd, end)...) return climbs } func (ride *Ride) AllClimbs() []Climb { - climbs := climbsBetween(ride.points, 0, len(ride.points)-1) + climbs := climbsBetween(*ride, 0, ride.Len()-1) slices.SortFunc(climbs, climbCmpStart) return climbs } diff --git a/ride/climb_test.go b/ride/climb_test.go index ad00d65..3dd6e53 100644 --- a/ride/climb_test.go +++ b/ride/climb_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/jftuga/geodist" "github.com/martinlehoux/biking_home/ride" "github.com/martinlehoux/kagamigo/kcore" "github.com/stretchr/testify/assert" @@ -40,7 +41,10 @@ func (b RideBuilder) WithSection(input string) RideBuilder { } func (b RideBuilder) Build() ride.Ride { - points := []ride.Point{{}} + distances := []float64{0} + elevations := []float64{0} + coords := []geodist.Coord{{}} + timestamps := []time.Time{{}} distance := 0.0 elevation := 0.0 for _, section := range b.sections { @@ -48,15 +52,14 @@ func (b RideBuilder) Build() ride.Ride { for curSecDist < section.distance { curSecDist += b.precision elevation += b.precision * section.slope - points = append(points, ride.Point{ - DistanceM: distance + curSecDist, - ElevationM: elevation, - Timestamp: points[len(points)-1].Timestamp.Add(time.Second), - }) + distances = append(distances, distance+curSecDist) + elevations = append(elevations, elevation) + coords = append(coords, geodist.Coord{}) + timestamps = append(timestamps, timestamps[len(timestamps)-1].Add(time.Second)) } distance += curSecDist } - return ride.FromPoints(points) + return ride.FromColumns(distances, elevations, coords, timestamps) } var parser = ride.GPXRideParser{} diff --git a/ride/difficulty_test.go b/ride/difficulty_test.go index 5ba14c7..4b72037 100644 --- a/ride/difficulty_test.go +++ b/ride/difficulty_test.go @@ -54,6 +54,6 @@ func TestDifficultyScoreHautacamClimbfinderReference(t *testing.T) { climbs := r.AllClimbs() assert.Len(t, climbs, 6) hautacam := climbs[5] - assert.InDelta(t, 128.6, hautacam.Start().DistanceM/1000, 0.1) + assert.InDelta(t, 128.6, hautacam.StartDistanceM()/1000, 0.1) assert.InDelta(t, 930.0, hautacam.DifficultyScore(), 10) } diff --git a/ride/index.go b/ride/index.go index 79f9212..17564ca 100644 --- a/ride/index.go +++ b/ride/index.go @@ -26,8 +26,8 @@ func (index *FlatRideClimbsIndex) Insert(ride Ride) { func (index *FlatRideClimbsIndex) Similar(ride Ride, climb Climb) []Climb { results := make([]Climb, 0) for _, c := range index.climbs { - _, startDistance := geodist.HaversineDistance(c.Start().Coord, climb.Start().Coord) - _, endDistance := geodist.HaversineDistance(c.End().Coord, climb.End().Coord) + _, startDistance := geodist.HaversineDistance(c.ride.Coord(c.StartIndex()), climb.ride.Coord(climb.StartIndex())) + _, endDistance := geodist.HaversineDistance(c.ride.Coord(c.EndIndex()), climb.ride.Coord(climb.EndIndex())) if startDistance <= index.sensitivity/1000 && endDistance <= index.sensitivity/1000 { results = append(results, c) } diff --git a/ride/parser_test.go b/ride/parser_test.go index db4907d..96c837e 100644 --- a/ride/parser_test.go +++ b/ride/parser_test.go @@ -22,8 +22,8 @@ func TestGPXRideParserSkipsStationaryPoints(t *testing.T) { parsed, err := (ride.GPXRideParser{}).Parse(strings.NewReader(data)) require.NoError(t, err) - assert.Len(t, parsed.Points(), 2) - assert.Greater(t, parsed.Points()[1].DistanceM, 0.0) + assert.Equal(t, 2, parsed.Len()) + assert.Greater(t, parsed.DistanceM(1), 0.0) } func TestGPXRideParserRejectsStationaryRide(t *testing.T) { diff --git a/ride/ride.go b/ride/ride.go index 9ba644e..311e933 100644 --- a/ride/ride.go +++ b/ride/ride.go @@ -16,25 +16,32 @@ import ( "gonum.org/v1/plot/vg" ) -type Point struct { - DistanceM float64 - ElevationM float64 - Coord geodist.Coord - Timestamp time.Time -} - type Ride struct { - points []Point + distances []float64 + elevations []float64 + coords []geodist.Coord + timestamps []time.Time } func (r *Ride) check() { - kcore.Assert(len(r.points) > 0, "no points in ride") + kcore.Assert(r.Len() > 0, "no points in ride") + 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") } -func (r *Ride) Points() []Point { - return r.points +func (r Ride) Len() int { + return len(r.distances) } +func (r Ride) DistanceM(i int) float64 { return r.distances[i] } + +func (r Ride) ElevationM(i int) float64 { return r.elevations[i] } + +func (r Ride) Coord(i int) geodist.Coord { return r.coords[i] } + +func (r Ride) Timestamp(i int) time.Time { return r.timestamps[i] } + type RideParser interface { Parse(reader io.Reader) (Ride, error) } @@ -62,13 +69,19 @@ func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) { if len(segment.Points) == 0 { return Ride{}, errors.New("ride has no track points") } - points := make([]Point, 0, len(segment.Points)) + distances := make([]float64, 0, len(segment.Points)) + elevations := make([]float64, 0, len(segment.Points)) + coords := make([]geodist.Coord, 0, len(segment.Points)) + timestamps := make([]time.Time, 0, len(segment.Points)) distance := 0.0 previous := segment.Points[0] if previous.Elevation.Null() { return Ride{}, errors.New("points without elevation") } - points = append(points, Point{DistanceM: 0, ElevationM: previous.Elevation.Value(), Coord: geodist.Coord{Lat: previous.Latitude, Lon: previous.Longitude}, Timestamp: previous.Timestamp}) + distances = append(distances, 0) + elevations = append(elevations, previous.Elevation.Value()) + coords = append(coords, geodist.Coord{Lat: previous.Latitude, Lon: previous.Longitude}) + timestamps = append(timestamps, previous.Timestamp) for i := 1; i < len(segment.Points); i++ { p := segment.Points[i] distance += p.Distance2D(&previous) @@ -79,18 +92,21 @@ func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) { if p.Elevation.Null() { return Ride{}, errors.New("points without elevation") } - points = append(points, Point{DistanceM: distance, ElevationM: p.Elevation.Value(), Coord: geodist.Coord{Lat: p.Latitude, Lon: p.Longitude}, Timestamp: p.Timestamp}) + distances = append(distances, distance) + elevations = append(elevations, p.Elevation.Value()) + coords = append(coords, geodist.Coord{Lat: p.Latitude, Lon: p.Longitude}) + timestamps = append(timestamps, p.Timestamp) } - if len(points) < 2 { + if len(distances) < 2 { return Ride{}, errors.New("zero distance") } - ride := Ride{points} + ride := Ride{distances: distances, elevations: elevations, coords: coords, timestamps: timestamps} ride.check() return ride, nil } -func FromPoints(points []Point) Ride { - ride := Ride{points} +func FromColumns(distances []float64, elevations []float64, coords []geodist.Coord, timestamps []time.Time) Ride { + ride := Ride{distances: distances, elevations: elevations, coords: coords, timestamps: timestamps} ride.check() return ride } @@ -98,42 +114,43 @@ func FromPoints(points []Point) Ride { func (r *Ride) ScoreFromKm(start, end float64) float64 { i := 0 j := 0 - for k, p := range r.points { - if i == 0 && p.DistanceM >= start*1000 { + for k, distance := range r.distances { + if i == 0 && distance >= start*1000 { i = k } - if j == 0 && p.DistanceM >= end*1000 { + if j == 0 && distance >= end*1000 { j = k break } } - return Score(r.points, i, j) + return Score(*r, i, j) } func (r *Ride) DifficultyScore() float64 { - return difficultyScore(r.points) + return difficultyScore(*r, 0, r.Len()-1) } -func difficultyScore(points []Point) float64 { - if len(points) < 2 { +func difficultyScore(r Ride, startIndex, endIndex int) float64 { + if endIndex-startIndex < 1 { return 0 } - last := points[len(points)-1] - if last.DistanceM <= 0 { + startDistance := r.DistanceM(startIndex) + lastDistance := r.DistanceM(endIndex) + if lastDistance <= startDistance { return 0 } score := 0.0 - i := 0 - for start := points[0].DistanceM; start < last.DistanceM; start += 100 { - end := math.Min(start+100, last.DistanceM) - for i < len(points)-1 && points[i+1].DistanceM <= start { + i := startIndex + for start := startDistance; start < lastDistance; start += 100 { + end := math.Min(start+100, lastDistance) + for i < endIndex && r.DistanceM(i+1) <= start { i++ } - startElevation := interpolateElevation(points, i, start) - for i < len(points)-1 && points[i+1].DistanceM < end { + startElevation := interpolateElevation(r, i, start) + for i < endIndex && r.DistanceM(i+1) < end { i++ } - endElevation := interpolateElevation(points, i, end) + endElevation := interpolateElevation(r, i, end) slope := (endElevation - startElevation) / (end - start) if slope > 0 { score += (end - start) / 1000 * (slope * 100) * (slope * 100) @@ -142,37 +159,37 @@ func difficultyScore(points []Point) float64 { return score } -func interpolateElevation(points []Point, i int, distance float64) float64 { - if i+1 >= len(points) { - return points[i].ElevationM +func interpolateElevation(r Ride, i int, distance float64) float64 { + if i+1 >= r.Len() { + return r.ElevationM(i) } - start := points[i] - end := points[i+1] - t := (distance - start.DistanceM) / (end.DistanceM - start.DistanceM) - return start.ElevationM + t*(end.ElevationM-start.ElevationM) + startDistance := r.DistanceM(i) + endDistance := r.DistanceM(i + 1) + t := (distance - startDistance) / (endDistance - startDistance) + return r.ElevationM(i) + t*(r.ElevationM(i+1)-r.ElevationM(i)) } func (r *Ride) ClimbFromDist(startDist, endDist float64) Climb { start, end := 0, 0 - for i, p := range r.points { - if start == 0 && p.DistanceM >= startDist { + for i, distance := range r.distances { + if start == 0 && distance >= startDist { start = i } - if end == 0 && p.DistanceM >= endDist { + if end == 0 && distance >= endDist { end = i break } } - return Climb{rideStart: start, rideEnd: end, points: r.points[start : end+1]} + return Climb{ride: *r, rideStart: start, rideEnd: end} } func Plot(r *Ride, outputFile string) { r.check() - pts := make(plotter.XYs, len(r.points)) - for i, p := range r.points { - pts[i].X = p.DistanceM / 1000 // Convert distance to kilometers - pts[i].Y = p.ElevationM + pts := make(plotter.XYs, r.Len()) + for i := 0; i < r.Len(); i++ { + pts[i].X = r.DistanceM(i) / 1000 // Convert distance to kilometers + pts[i].Y = r.ElevationM(i) } p := plot.New() @@ -192,8 +209,8 @@ func PlotScore(r *Ride, startKm, endKm float64, outputFile string) { r.check() startIndex := 0 - for i, p := range r.points { - if p.DistanceM >= startKm*1000 { + for i := 0; i < r.Len(); i++ { + if r.DistanceM(i) >= startKm*1000 { startIndex = i break } @@ -201,22 +218,22 @@ func PlotScore(r *Ride, startKm, endKm float64, outputFile string) { pts1 := make(plotter.XYs, 0) endIndex := 0 - for i := startIndex + 1; i < len(r.points); i++ { - if r.points[i].DistanceM > endKm*1000 { + for i := startIndex + 1; i < r.Len(); i++ { + if r.DistanceM(i) > endKm*1000 { endIndex = i break } - score1 := Score(r.points, startIndex, i) + score1 := Score(*r, startIndex, i) pts1 = append(pts1, plotter.XY{ - X: r.points[i].DistanceM / 1000, // Convert distance to kilometers + X: r.DistanceM(i) / 1000, // Convert distance to kilometers Y: score1, }) } pts2 := make(plotter.XYs, 0) for i := startIndex; i < endIndex; i++ { - score2 := Score(r.points, i, endIndex) + score2 := Score(*r, i, endIndex) pts2 = append(pts2, plotter.XY{ - X: r.points[i].DistanceM / 1000, // Convert distance to kilometers + X: r.DistanceM(i) / 1000, // Convert distance to kilometers Y: score2, }) } -- cgit v1.2.3