diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-03 08:25:07 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-03 08:25:07 +0200 |
| commit | 9707f9ad09db935af7d06b5de08bc52de13ba6a7 (patch) | |
| tree | 34bcd01e5eda302a8730bf0817d5dd7e82186359 /chart.go | |
| parent | f06a10982ab9d5aaa7d31cea31964431fa75f253 (diff) | |
feat: Detect mountain pass crossings, name climbs after passes, render ride charts
- osmpass: extract mountain_pass=yes nodes from an OSM PBF, enrich centcols passes with OSM coordinates, resumable -fetch-osm download
- mountain_pass: DetectCrossings for a ride, MatchClimb to name a climb after the pass it tops
- ride: expose Points(), add Climb.Top() and Climb.Name
- chart: -chart renders elevation profile with climb bands and pass markers
- commands: -fetch-osm, -extract-osm, -import-cached, -enrich
Diffstat (limited to 'chart.go')
| -rw-r--r-- | chart.go | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/chart.go b/chart.go new file mode 100644 index 0000000..063b495 --- /dev/null +++ b/chart.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "image/color" + + "github.com/martinlehoux/biking_home/mountain_pass" + "github.com/martinlehoux/biking_home/ride" + "github.com/martinlehoux/kagamigo/kcore" + "gonum.org/v1/plot" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/vg" +) + +func renderChart(r ride.Ride, climbs []ride.Climb, crossings []mountain_pass.Crossing, output string) { + p := plot.New() + p.Title.Text = "Ride profile" + p.X.Label.Text = "Distance (km)" + 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 + } + } + + for _, climb := range climbs { + x0 := climb.Start().DistanceM / 1000 + x1 := climb.End().DistanceM / 1000 + fill, err := plotter.NewPolygon(plotter.XYs{ + {X: x0, Y: 0}, + {X: x1, Y: 0}, + {X: x1, Y: maxElev}, + {X: x0, Y: maxElev}, + }) + kcore.Expect(err, "failed to create climb band") + 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))) + } + + for _, crossing := range crossings { + x := crossing.RideDistanceM / 1000 + line, err := plotter.NewLine(plotter.XYs{ + {X: x, Y: 0}, + {X: x, Y: maxElev}, + }) + kcore.Expect(err, "failed to create pass marker") + line.LineStyle.Dashes = []vg.Length{vg.Points(4), vg.Points(2)} + p.Add(line) + p.Add(plotLabels(plotter.XY{X: x, Y: maxElev * 0.96}, + fmt.Sprintf("%s %dm", crossing.Pass.Name, crossing.Pass.Elevation))) + } + + elevLine, err := plotter.NewLine(elevation) + kcore.Expect(err, "failed to create elevation line") + p.Add(elevLine) + + kcore.Expect(p.Save(10*vg.Inch, 4*vg.Inch, output), "failed to save chart") +} + +func climbLabel(climb ride.Climb) string { + if climb.Name != "" { + return climb.Name + } + return fmt.Sprintf("%s %d", ride.Category(climb.Score()), int(climb.Score())) +} + +func plotLabels(xy plotter.XY, label string) *plotter.Labels { + labels, err := plotter.NewLabels(plotter.XYLabels{ + XYs: plotter.XYs{xy}, + Labels: []string{label}, + }) + kcore.Expect(err, "failed to create labels") + return labels +} |