summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2024-07-28 15:52:12 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2024-07-28 15:52:12 +0200
commit20043af3c289052e239773257cbbd961e4397394 (patch)
tree2f39f251961bbbebf0a86ef0481759e0401be436
parent45844ae231b7afe68a1cd5b7c0d09fd77e806d0f (diff)
perf: Precompute distance
-rw-r--r--README.md8
-rw-r--r--go.mod16
-rw-r--r--go.sum40
-rw-r--r--main.go133
4 files changed, 148 insertions, 49 deletions
diff --git a/README.md b/README.md
index 34070e6..7d7274d 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,8 @@
- Storing GPX
- Export mountain pass from OSM and auto detect when pass is crossed
- Auto detect climbs from GPX
+- Compute estimated power
+- Export data from Strava, Garmin
## Implementation
@@ -20,11 +22,14 @@
- Django bike
- https://github.com/paulmach/osm
- https://github.com/tkrajina/gpxgo
+- https://github.com/brendangregg/FlameGraph
+- `go tool pprof -http=":8081"`
## TODO
- CI / Tooling
- Tests
+- linter for calls to `make([]..., n)` that then use append
## Examples
@@ -41,4 +46,5 @@
| 99.0km | 109.5km |
| 128.2km | 141.8km |
-- add min slope to prevent long false up
+- add min slope to prevent long false up ?
+- check score of 60km-76km vs 32km-76km
diff --git a/go.mod b/go.mod
index d63967b..cfb39fd 100644
--- a/go.mod
+++ b/go.mod
@@ -1,14 +1,24 @@
module github.com/martinlehoux/biking_home
-go 1.21.12
+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/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/gofrs/uuid v4.4.0+incompatible // indirect
+ github.com/kataras/i18n v0.0.8 // indirect
+ github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
- golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
- golang.org/x/text v0.3.6 // indirect
+ golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
+ golang.org/x/net v0.26.0 // indirect
+ golang.org/x/text v0.16.0 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 2661cf6..a232fe1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,25 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+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/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=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/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=
@@ -7,18 +27,28 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6
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=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+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/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
+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/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/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=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
index 5a2edc6..a3fa527 100644
--- a/main.go
+++ b/main.go
@@ -1,24 +1,44 @@
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"
)
-func Score(points []gpx.GPXPoint) float64 {
- distance := Distance(points)
+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
- slope := (points[len(points)-1].Elevation.Value() - points[0].Elevation.Value()) / distance * 100.0
+ dElevation := points[end].Elevation.Value() - points[start].Elevation.Value()
- return math.Abs(slope) * slope * distance / 1000.0
+ return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0
}
func Category(score float64) string {
@@ -43,23 +63,45 @@ type Climb struct {
end int
}
-func (c Climb) Score(points []gpx.GPXPoint) float64 {
- return Score(points[c.start:c.end])
+type Point struct {
+ gpx.GPXPoint
+ distance float64
}
-func BestUpHill(points []gpx.GPXPoint, start int, end int) Climb {
- bestScore := Score(points[start:end])
+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])
+ score := Score(points, i, end)
if score > bestScore {
bestStart = i
bestScore = score
}
}
bestEnd := end
- for i := bestStart; i < end; i++ {
- score := Score(points[bestStart:i])
+ for i := end; i > bestStart; i-- {
+ score := Score(points, bestStart, i)
if score > bestScore {
bestEnd = i
bestScore = score
@@ -68,19 +110,20 @@ func BestUpHill(points []gpx.GPXPoint, start int, end int) Climb {
return Climb{bestStart, bestEnd}
}
-func BestDownHill(points []gpx.GPXPoint, start int, end int) Climb {
- bestScore := Score(points[start:end])
+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])
+ score := Score(points, i, end)
if score < bestScore {
bestStart = i
bestScore = score
}
}
bestEnd := end
- for i := bestStart; i < end; i++ {
- score := Score(points[bestStart:i])
+ for i := end; i > bestStart; i-- {
+ score := Score(points, bestStart, i)
if score < bestScore {
bestEnd = i
bestScore = score
@@ -89,26 +132,15 @@ func BestDownHill(points []gpx.GPXPoint, start int, end int) Climb {
return Climb{bestStart, bestEnd}
}
-func Distance(points []gpx.GPXPoint) float64 {
- distance := 0.0
- for i, point := range points {
- if i > 0 {
- distance += point.Distance2D(&points[i-1])
- }
- }
- return distance
-}
-
-func FindAllClimbs(points []gpx.GPXPoint, start int, end int) []Climb {
- from := Distance(points[:start])
- to := from + Distance(points[start:end])
- fmt.Printf("Searching climbs between %.1fkm and %.1fkm (%d-%d)\n", from/1000, to/1000, start, end)
+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)
- climbs := []Climb{}
+ Assert(climb.start < climb.end, "empty climb")
- if climb.Score(points) < 35 {
- if descent.Score(points) < -35 {
+ 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)...)
}
@@ -118,6 +150,7 @@ func FindAllClimbs(points []gpx.GPXPoint, start int, end int) []Climb {
}
} 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)...)
}
@@ -129,18 +162,38 @@ func FindAllClimbs(points []gpx.GPXPoint, start int, end int) []Climb {
return climbs
}
+var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
+
func main() {
- example, err := gpx.ParseFile("examples/2022-07-21.Pogacar.gpx")
- if err != nil {
- panic(err)
+ flag.Parse()
+ if *cpuprofile != "" {
+ f, err := os.Create(*cpuprofile)
+ kcore.Expect(err, "failed to create CPU profile")
+ pprof.StartCPUProfile(f)
+ defer pprof.StopCPUProfile()
}
- segment := example.Tracks[0].Segments[0]
- climbs := FindAllClimbs(segment.Points, 0, len(segment.Points)-1)
+ 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 })
table := tablewriter.NewWriter(os.Stdout)
- table.SetHeader([]string{"Score", "Distance", "Category", "Slope", "From", "To"})
+ table.SetHeader([]string{"Score", "Distance", "Category", "Slope", "From", "To", "Point span"})
for _, climb := range climbs {
- table.Append([]string{fmt.Sprintf("%d", int(climb.Score(segment.Points))), fmt.Sprintf("%.1fkm", Distance(segment.Points[climb.start:climb.end])/1000), Category(climb.Score(segment.Points)), fmt.Sprintf("%.1f%%", (segment.Points[climb.end].Elevation.Value()-segment.Points[climb.start].Elevation.Value())/Distance(segment.Points[climb.start:climb.end])*100), fmt.Sprintf("%.1fkm", Distance(segment.Points[:climb.start])/1000), fmt.Sprintf("%.1fkm", Distance(segment.Points[:climb.end])/1000)})
+ table.Append([]string{fmt.Sprintf("%d", int(Score(ride.points, climb.start, climb.end))), fmt.Sprintf("%.1fkm", (ride.points[climb.end].distance-ride.points[climb.start].distance)/1000), Category(Score(ride.points, climb.start, climb.end)), fmt.Sprintf("%.1f%%", (ride.points[climb.end].Elevation.Value()-ride.points[climb.start].Elevation.Value())/(ride.points[climb.end].distance-ride.points[climb.start].distance)*100), 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.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)
}