diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2024-07-28 17:50:29 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2024-07-28 17:50:29 +0200 |
| commit | 203b9bdf17bd4742b5aab2c35df47b59af0652f1 (patch) | |
| tree | b6bc8ddee21bcaa022ef68b3769f61100117b190 | |
| parent | 171366f337c3422331b84b6c7416e1b1d6bd36a0 (diff) | |
test: Add some test to fix behavior
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | go.mod | 16 | ||||
| -rw-r--r-- | go.sum | 24 | ||||
| -rw-r--r-- | main.go | 209 | ||||
| -rw-r--r-- | ride/climb.go | 113 | ||||
| -rw-r--r-- | ride/climb_test.go | 82 | ||||
| -rw-r--r-- | ride/ride.go | 46 |
7 files changed, 261 insertions, 244 deletions
@@ -35,17 +35,4 @@ **[Pogacar 21 july 2022](https://www.strava.com/activities/7505784085)** -**Expected** - -| From | To | -| ------- | ------- | -| 1.0km | 5.7km | -| 44.0km | 45.2km | -| 60.1km | 76.3km | -| 83.8km | 86.3km | -| 99.0km | 109.5km | -| 128.2km | 141.8km | - -- add min slope to prevent long false up ? -- check score of 60km-76km vs 32km-76km - - as expected, the score is better but not found +- 43.0-45.5 ? @@ -3,22 +3,24 @@ module github.com/martinlehoux/biking_home go 1.22 require ( - github.com/go-echarts/go-echarts/v2 v2.4.1 - github.com/martinlehoux/kagamigo v0.4.0 - github.com/olekukonko/tablewriter v0.0.5 + github.com/martinlehoux/kagamigo v0.4.1 + github.com/stretchr/testify v1.8.4 github.com/tkrajina/gpxgo v1.4.0 ) require ( github.com/BurntSushi/toml v1.4.0 // indirect - github.com/a-h/templ v0.2.707 // indirect + github.com/a-h/templ v0.2.747 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/kataras/i18n v0.0.8 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect - golang.org/x/net v0.26.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/text v0.16.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -1,13 +1,11 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= -github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= +github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-echarts/go-echarts/v2 v2.4.1 h1:imBFGngJ9zv/2zJVjK3k0uLL+LzyPDgzeV7MWzxH0rs= -github.com/go-echarts/go-echarts/v2 v2.4.1/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -16,14 +14,12 @@ github.com/kataras/i18n v0.0.8 h1:thiDRqq4fN2sQOK5CwR5Yh1yT7swP6B3wnadILhqjhk= github.com/kataras/i18n v0.0.8/go.mod h1:M/yRAqQ3Y7z2oSpotxG/+nPrgsJLas6t0kEJfIJk98E= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/martinlehoux/kagamigo v0.4.0 h1:v9iHjh1a7vJP13iGnGR6G5OIhdgU9j/YICjvkOqGcsA= -github.com/martinlehoux/kagamigo v0.4.0/go.mod h1:CDkN/Kx8KQSU0kC/ISfq2gYIhVbddHD3E8r3uAlnnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/martinlehoux/kagamigo v0.4.1 h1:jI4TXyQMdm05H/Pd5RE7pfoMHq9PqdOF21oWAZOxsJ8= +github.com/martinlehoux/kagamigo v0.4.1/go.mod h1:CDkN/Kx8KQSU0kC/ISfq2gYIhVbddHD3E8r3uAlnnMI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -32,11 +28,11 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tkrajina/gpxgo v1.4.0 h1:cSD5uSwy3VZuNFieTEZLyRnuIwhonQEkGPkPGW4XNag= github.com/tkrajina/gpxgo v1.4.0/go.mod h1:BXSMfUAvKiEhMEXAFM2NvNsbjsSvp394mOvdcNjettg= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1,167 +1,13 @@ package main import ( - "errors" "flag" - "fmt" - "io" - "math" "os" "runtime/pprof" - "slices" - "github.com/go-echarts/go-echarts/v2/charts" - "github.com/go-echarts/go-echarts/v2/opts" "github.com/martinlehoux/kagamigo/kcore" - "github.com/olekukonko/tablewriter" - "github.com/tkrajina/gpxgo/gpx" ) -var ErrAssert = errors.New("assertion error") - -func Assert(cond bool, msg string) { - if !cond { - err := ErrAssert - if msg != "" { - err = fmt.Errorf("%w: %s", err, msg) - } - panic(err) - } -} - -func Score(points []Point, start int, end int) float64 { - Assert(end > start, "no points for score") - distance := points[end].distance - points[start].distance - if distance == 0 { - return 0 - } - // TODO: Expect NullableFloat64 - dElevation := points[end].Elevation.Value() - points[start].Elevation.Value() - - return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0 -} - -func Category(score float64) string { - switch { - case score < 35: - return "NO" - case score < 80: - return "Cat 4" - case score < 180: - return "Cat 3" - case score < 250: - return "Cat 2" - case score < 600: - return "Cat 1" - default: - return "HC" - } -} - -type Climb struct { - start int - end int -} - -type Point struct { - gpx.GPXPoint - distance float64 -} - -type Ride struct { - points []Point -} - -func ParseGPX(r io.Reader) Ride { - content, err := gpx.Parse(r) - kcore.Expect(err, "failed to parse GPX") - segment := content.Tracks[0].Segments[0] - points := make([]Point, len(segment.Points)) - distance := 0.0 - for i, p := range segment.Points { - if i != 0 { - distance += p.Distance2D(&segment.Points[i-1]) - } - Assert(i == 0 || distance > 0, "zero distance") - points[i] = Point{p, distance} - } - return Ride{points} -} - -func BestUpHill(points []Point, start int, end int) Climb { - Assert(end > start, "empty points") - bestScore := Score(points, start, end) - bestStart := start - for i := start; i < end; i++ { - score := Score(points, i, end) - if score > bestScore { - bestStart = i - bestScore = score - } - } - bestEnd := end - for i := end; i > bestStart; i-- { - score := Score(points, bestStart, i) - if score > bestScore { - bestEnd = i - bestScore = score - } - } - return Climb{bestStart, bestEnd} -} - -func BestDownHill(points []Point, start int, end int) Climb { - Assert(len(points) > 0, "empty points") - bestScore := Score(points, start, end) - bestStart := start - for i := start; i < end; i++ { - score := Score(points, i, end) - if score < bestScore { - bestStart = i - bestScore = score - } - } - bestEnd := end - for i := end; i > bestStart; i-- { - score := Score(points, bestStart, i) - if score < bestScore { - bestEnd = i - bestScore = score - } - } - return Climb{bestStart, bestEnd} -} - -func FindAllClimbs(points []Point, start int, end int) []Climb { - fmt.Printf("Searching climbs between %.1fkm and %.1fkm\n", points[start].distance/1000, points[end].distance/1000) - climbs := []Climb{} - climb := BestUpHill(points, start, end) - descent := BestDownHill(points, start, end) - Assert(climb.start < climb.end, "empty climb") - - if Score(points, climb.start, climb.end) < 35 { - if Score(points, descent.start, descent.end) < -35 { - if descent.start > start { - climbs = append(climbs, FindAllClimbs(points, start, descent.start)...) - } - if end > descent.end { - climbs = append(climbs, FindAllClimbs(points, descent.end, end)...) - } - } - } else { - climbs = append(climbs, climb) - fmt.Printf("Found climb between %.1fkm and %.1fkm\n", points[climb.start].distance/1000, points[climb.end].distance/1000) - if climb.start > start { - climbs = append(climbs, FindAllClimbs(points, start, climb.start)...) - } - if end > climb.end { - climbs = append(climbs, FindAllClimbs(points, climb.end, end)...) - } - } - - return climbs -} - var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") func main() { @@ -172,59 +18,4 @@ func main() { pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } - gpxFile, err := os.Open("examples/2022-07-21.Pogacar.gpx") - kcore.Expect(err, "failed to open GPX file") - ride := ParseGPX(gpxFile) - Assert(len(ride.points) > 0, "no points in ride") - fmt.Printf("Ride loaded:\t%.1fkm\t%d points\n", ride.points[len(ride.points)-1].distance/1000, len(ride.points)) - climbs := FindAllClimbs(ride.points, 0, len(ride.points)-1) - slices.SortFunc(climbs, func(i, j Climb) int { return i.start - j.start }) - expectedClimb := Climb{start: 0, end: 0} - for i, p := range ride.points { - if expectedClimb.start == 0 && p.distance > 60_000 { - expectedClimb.start = i - } - if expectedClimb.end == 0 && p.distance > 76_400 { - expectedClimb.end = i - break - } - } - println("Expected climb", expectedClimb.start, expectedClimb.end) - - viewer := Viewer(ride) - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Score", "Distance", "Category", "Slope", "From", "To", "Point span"}) - for _, climb := range climbs { - table.Append([]string{viewer.Score(climb), viewer.Distance(climb), Category(Score(ride.points, climb.start, climb.end)), viewer.Slope(climb), fmt.Sprintf("%.1fkm", ride.points[climb.start].distance/1000), fmt.Sprintf("%.1fkm", ride.points[climb.end].distance/1000), fmt.Sprintf("%d", climb.end-climb.start)}) - } - table.Append([]string{viewer.Score(expectedClimb), viewer.Distance(expectedClimb), Category(Score(ride.points, expectedClimb.start, expectedClimb.end)), viewer.Slope(expectedClimb), fmt.Sprintf("%.1fkm", ride.points[expectedClimb.start].distance/1000), fmt.Sprintf("%.1fkm", ride.points[expectedClimb.end].distance/1000), fmt.Sprintf("%d", expectedClimb.end-expectedClimb.start)}) - table.Render() - - charts.NewLine() - scatter := charts.NewLine() - values := make([]opts.LineData, len(ride.points)) - for i, p := range ride.points { - values[i] = opts.LineData{Value: []any{i, p.distance}} - } - scatter.AddSeries("distance", values) - f, err := os.Create("scatter.html") - kcore.Expect(err, "failed to create scatter plot") - defer f.Close() - scatter.Render(f) -} - -type Viewer struct { - points []Point -} - -func (v *Viewer) Score(climb Climb) string { - return fmt.Sprintf("%.0f", Score(v.points, climb.start, climb.end)) -} - -func (v *Viewer) Distance(climb Climb) string { - return fmt.Sprintf("%.1fkm", (v.points[climb.end].distance-v.points[climb.start].distance)/1000) -} - -func (v *Viewer) Slope(climb Climb) string { - return fmt.Sprintf("%.1f%%", (v.points[climb.end].Elevation.Value()-v.points[climb.start].Elevation.Value())/(v.points[climb.end].distance-v.points[climb.start].distance)*100) } diff --git a/ride/climb.go b/ride/climb.go new file mode 100644 index 0000000..a337a15 --- /dev/null +++ b/ride/climb.go @@ -0,0 +1,113 @@ +package ride + +import ( + "fmt" + "math" + "slices" + + "github.com/martinlehoux/kagamigo/kcore" +) + +const ClimbDistanceMinimum = 500 + +type Climb struct { + start int + end int +} + +type Point struct { + distance float64 + elevation float64 +} + +func NewPoint(distance float64, elevation float64) Point { + return Point{ + distance: distance, + elevation: elevation, + } +} + +func Slope(start, end Point) float64 { + return (end.elevation - start.elevation) / (end.distance - start.distance) +} + +func Score(points []Point, start int, end int) float64 { + kcore.Assert(end > start, "no points for score") + distance := points[end].distance - points[start].distance + if distance == 0 { + return 0 + } + dElevation := points[end].elevation - points[start].elevation + + return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0 +} + +func Category(score float64) string { + switch { + case score < 35: + return "NO" + case score < 80: + return "Cat 4" + case score < 180: + return "Cat 3" + case score < 250: + return "Cat 2" + case score < 600: + return "Cat 1" + default: + return "HC" + } +} + +func bestClimbUntilEnd(points []Point, start int, end int) Climb { + kcore.Assert(end > start, "empty points") + + bestScore := Score(points, start, end) + bestStart := start + for i := start; i < end; i++ { + score := Score(points, i, end) + if score > bestScore { + bestStart = i + bestScore = score + } + } + climb := Climb{bestStart, end} + + kcore.Assert(climb.start < climb.end, "empty climb") + return climb +} + +func climbsBetween(points []Point, start int, end int) []Climb { + climbs := []Climb{} + if points[end].distance-points[start].distance < ClimbDistanceMinimum { + return climbs + } + fmt.Printf("Searching climbs between %.1fkm and %.1fkm\n", points[start].distance/1000, points[end].distance/1000) + highest := start + for i := start; i < end; i++ { + if points[i].elevation > points[highest].elevation { + highest = i + } + } + // TODO: Use descent to reduce recursion + if highest == start { + return climbsBetween(points, start+1, end) + } + climb := bestClimbUntilEnd(points, start, highest) + if Score(points, climb.start, climb.end) >= 35 { + fmt.Printf("Found climb between %.1fkm and %.1fkm\n", points[climb.start].distance/1000, points[climb.end].distance/1000) + climbs = append(climbs, climb) + } + climbs = append(climbs, climbsBetween(points, start, climb.start)...) + climbs = append(climbs, climbsBetween(points, climb.end, end)...) + + return climbs +} + +func (ride *Ride) AllClimbs() []Climb { + climbs := climbsBetween(ride.points, 0, len(ride.points)-1) + slices.SortFunc(climbs, climbCmpStart) + return climbs +} + +func climbCmpStart(a, b Climb) int { return a.start - b.start } diff --git a/ride/climb_test.go b/ride/climb_test.go new file mode 100644 index 0000000..5acc3c4 --- /dev/null +++ b/ride/climb_test.go @@ -0,0 +1,82 @@ +package ride_test + +import ( + "strconv" + "strings" + "testing" + + "github.com/martinlehoux/biking_home/ride" + "github.com/martinlehoux/kagamigo/kcore" + "github.com/stretchr/testify/assert" + "github.com/tkrajina/gpxgo/gpx" +) + +type RideBuilderSection struct { + slope float64 + distance float64 +} + +type RideBuilder struct { + precision float64 + sections []RideBuilderSection +} + +// Example: 13.4km at 7.5% +func (b RideBuilder) WithSection(input string) RideBuilder { + inputs := strings.Split(input, " ") + kcore.Assert(strings.HasSuffix(inputs[0], "km"), "wrong unit") + distanceKm, err := strconv.ParseFloat(strings.TrimSuffix(inputs[0], "km"), 64) + kcore.Expect(err, "faile to parse kms") + kcore.Assert(strings.HasSuffix(inputs[2], "%"), "wrong unit") + slopePercent, err := strconv.ParseFloat(strings.TrimSuffix(inputs[2], "%"), 64) + kcore.Expect(err, "failed to parse %") + + b.sections = append(b.sections, RideBuilderSection{ + slope: slopePercent / 100, + distance: distanceKm * 1000, + }) + + return b +} + +func (b RideBuilder) Build() ride.Ride { + points := []ride.Point{ride.NewPoint(0, 0)} + distance := 0.0 + elevation := 0.0 + for _, section := range b.sections { + curSecDist := 0.0 + for curSecDist < section.distance { + curSecDist += b.precision + elevation += b.precision * section.slope + points = append(points, ride.NewPoint( + distance+curSecDist, + elevation, + )) + } + distance += curSecDist + } + return ride.FromPoints(points) +} + +func TestClimbThenFalseFlat(t *testing.T) { + r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("10km at 1%").Build() + climbs := r.AllClimbs() + + assert.Len(t, climbs, 2) + assert.Equal(t, "0.0km-11.9km: 11.9km at 1.8% (40 pts - Cat 4)", r.String(climbs[0])) + assert.Equal(t, "", r.String(climbs[1])) +} + +func TestPogacar20220721(t *testing.T) { + gpxContent, err := gpx.ParseFile("../examples/2022-07-21.Pogacar.gpx") + assert.NoError(t, err) + r := ride.FromGPX(gpxContent) + climbs := r.AllClimbs() + + assert.Len(t, climbs, 5) + assert.Equal(t, "0.6km-5.6km: 5.0km at 3.5% (62 pts - Cat 4)", r.String(climbs[0])) + assert.Equal(t, "60.0km-76.4km: 16.4km at 7.2% (854 pts - HC)", r.String(climbs[1])) + assert.Equal(t, "83.8km-85.9km: 2.1km at 5.4% (59 pts - Cat 4)", r.String(climbs[2])) + assert.Equal(t, "99.2km-109.3km: 10.2km at 8.4% (710 pts - HC)", r.String(climbs[3])) + assert.Equal(t, "128.6km-142.2km: 13.6km at 7.8% (832 pts - HC)", r.String(climbs[4])) +} diff --git a/ride/ride.go b/ride/ride.go new file mode 100644 index 0000000..8206793 --- /dev/null +++ b/ride/ride.go @@ -0,0 +1,46 @@ +package ride + +import ( + "fmt" + + "github.com/martinlehoux/kagamigo/kcore" + "github.com/tkrajina/gpxgo/gpx" +) + +type Ride struct { + points []Point +} + +func (r *Ride) check() { + kcore.Assert(len(r.points) > 0, "no points in ride") +} + +func FromGPX(content *gpx.GPX) Ride { + segment := content.Tracks[0].Segments[0] + points := make([]Point, len(segment.Points)) + distance := 0.0 + for i, p := range segment.Points { + if i != 0 { + distance += p.Distance2D(&segment.Points[i-1]) + } + kcore.Assert(i == 0 || distance > 0, "zero distance") + kcore.Assert(p.Elevation.NotNull(), "points without elevation") + points[i] = Point{distance: distance, elevation: p.Elevation.Value()} + } + ride := Ride{points} + ride.check() + return ride +} + +func FromPoints(points []Point) Ride { + ride := Ride{points} + ride.check() + return ride +} + +func (r *Ride) String(climb Climb) string { + start := r.points[climb.start] + end := r.points[climb.end] + score := Score(r.points, climb.start, climb.end) + return fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", start.distance/1000, end.distance/1000, (end.distance-start.distance)/1000, Slope(start, end)*100, int(score), Category(score)) +} |