aboutsummaryrefslogtreecommitdiff
path: root/mountain_pass
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 11:21:17 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 11:21:17 +0200
commit825cf388b55a6c8441e02149749bb6e2968477c0 (patch)
treee3394cef97aa43df9775a1465d3c99f386ce3a9b /mountain_pass
parent366e7b0d671395c2ad32f85ee7706a592db69d15 (diff)
Extract climb detection into a dedicated climb package
Move climb detection, slope, and Cotacol scoring out of ride into a new climb package that operates on a geometry-only Route (distances, elevations, coords). Ride embeds climb.Route and keeps its timing and telemetry columns. - climb.Detect returns []DetectedClimb (Climb + ride-absolute Start). - Climb owns its sliced geometry; indices are local, with Start reported at the detection boundary for absolute position. - Detected bounds use core.Interval; freeRegions claim suppression fixed. - ride no longer builds slices: ClimbFromDist/ClimbFromIndexes/climbFrom and ride.Climb/Interval/Range are removed; rideanalysis, mountain_pass MatchClimb, and web views are rewired to climb types. - New internal/routetest builds climb.Route from the ride DSL; ridetest reuses it and adds timestamps and telemetry. - Tests moved to climb (synthetic detection and difficulty) and kept in ride for GPX-example integration; ride.comparison detection coverage retained. - mountain_pass.nearestCrossing now reuses geo.Nearest.
Diffstat (limited to 'mountain_pass')
-rw-r--r--mountain_pass/detection.go33
-rw-r--r--mountain_pass/detection_test.go24
2 files changed, 28 insertions, 29 deletions
diff --git a/mountain_pass/detection.go b/mountain_pass/detection.go
index 40281f3..79e55e4 100644
--- a/mountain_pass/detection.go
+++ b/mountain_pass/detection.go
@@ -5,7 +5,8 @@ import (
"math"
"github.com/jftuga/geodist"
- "github.com/martinlehoux/biking_home/ride"
+ "github.com/martinlehoux/biking_home/climb"
+ "github.com/martinlehoux/biking_home/geo"
)
// MountainPass is a pass extracted from OpenStreetMap: a node tagged
@@ -112,9 +113,9 @@ func DetectCrossings(coords []geodist.Coord, distances, elevations []float64, pa
// MatchClimb returns the pass whose coordinates lie within radiusM of the
// 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) {
- topCoord := climb.TopCoord()
- topElevation := climb.TopElevationM()
+func MatchClimb(segment climb.Climb, passes []MountainPass, radiusM, elevationToleranceM float64) (MountainPass, bool) {
+ topIndex, topElevation := segment.Top()
+ topCoord := segment.Coords[topIndex]
var best MountainPass
bestDistanceM := radiusM
found := false
@@ -139,21 +140,17 @@ func MatchClimb(climb ride.Climb, passes []MountainPass, radiusM, elevationToler
}
func nearestCrossing(coords []geodist.Coord, distances, elevations []float64, mountainPass MountainPass) (Crossing, bool) {
- best := Crossing{Pass: mountainPass, DistanceToM: 1e18}
- for i := range coords {
- distanceKm, _ := geodist.HaversineDistance(coords[i], *mountainPass.Coord)
- distanceM := distanceKm * 1000
- if distanceM < best.DistanceToM {
- best.DistanceToM = distanceM
- best.RideDistanceM = distances[i]
- best.RideElevation = elevations[i]
- best.ElevationDiff = absFloat64(elevations[i] - float64(mountainPass.Elevation))
- }
- }
- if best.DistanceToM > 1e17 {
- return best, false
+ index, distanceM := geo.Nearest(coords, *mountainPass.Coord)
+ if math.IsInf(distanceM, 1) {
+ return Crossing{Pass: mountainPass}, false
}
- return best, true
+ return Crossing{
+ Pass: mountainPass,
+ DistanceToM: distanceM,
+ RideDistanceM: distances[index],
+ RideElevation: elevations[index],
+ ElevationDiff: absFloat64(elevations[index] - float64(mountainPass.Elevation)),
+ }, true
}
func absFloat64(value float64) float64 {
diff --git a/mountain_pass/detection_test.go b/mountain_pass/detection_test.go
index 6318ca3..6b297b5 100644
--- a/mountain_pass/detection_test.go
+++ b/mountain_pass/detection_test.go
@@ -5,8 +5,10 @@ import (
"testing"
"github.com/jftuga/geodist"
+ "github.com/martinlehoux/biking_home/climb"
"github.com/martinlehoux/biking_home/internal/dbtest"
"github.com/martinlehoux/biking_home/internal/ridetest"
+ "github.com/martinlehoux/biking_home/internal/routetest"
"github.com/martinlehoux/biking_home/mountain_pass"
"github.com/martinlehoux/biking_home/ride"
"github.com/stretchr/testify/assert"
@@ -19,7 +21,7 @@ func TestDetectCrossingsFindsPass(t *testing.T) {
Name: "Pas de Magnan", Elevation: 460,
Coord: &geodist.Coord{Lat: 43.62, Lon: 5.43},
}
- crossings := mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{pass}, 100, 25)
+ crossings := mountain_pass.DetectCrossings(r.Coords, r.Distances, r.Elevations, []mountain_pass.MountainPass{pass}, 100, 25)
assert.Len(t, crossings, 1)
assert.Equal(t, "Pas de Magnan", crossings[0].Pass.Name)
assert.Less(t, crossings[0].DistanceToM, 10.0)
@@ -42,7 +44,7 @@ func TestLoadMountainPassesAroundRideUsesExpandedBounds(t *testing.T) {
require.NoError(t, err)
route := ridetest.MustRide("1km at 10%", ridetest.StartCoord(43.0, 5.0), ridetest.Heading(0.1, 0.1), ridetest.StartElevation(100), ridetest.Sampling(1000))
- passes, err := mountain_pass.LoadMountainPassesAroundRide(db, route.Coords(), 10_000)
+ passes, err := mountain_pass.LoadMountainPassesAroundRide(db, route.Coords, 10_000)
require.NoError(t, err)
require.Len(t, passes, 3)
assert.Equal(t, "On route edge", passes[0].Name)
@@ -77,13 +79,13 @@ func TestDetectCrossingsIgnoresDistantPass(t *testing.T) {
Name: "Col de la Faucille", Elevation: 1320,
Coord: &geodist.Coord{Lat: 46.37, Lon: 6.02},
}
- assert.Empty(t, mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{far}, 100, 25))
+ assert.Empty(t, mountain_pass.DetectCrossings(r.Coords, r.Distances, r.Elevations, []mountain_pass.MountainPass{far}, 100, 25))
}
func TestDetectCrossingsIgnoresPassWithoutCoordinates(t *testing.T) {
r := rideFromPoint(t, 43.62, 5.43, 447)
noCoord := mountain_pass.MountainPass{Name: "Col de la Gatasse", Elevation: 120}
- assert.Empty(t, mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{noCoord}, 100, 25))
+ assert.Empty(t, mountain_pass.DetectCrossings(r.Coords, r.Distances, r.Elevations, []mountain_pass.MountainPass{noCoord}, 100, 25))
}
func TestDetectCrossingsRequiresElevationAgreement(t *testing.T) {
@@ -92,7 +94,7 @@ func TestDetectCrossingsRequiresElevationAgreement(t *testing.T) {
Name: "Pas de Magnan", Elevation: 900,
Coord: &geodist.Coord{Lat: 43.62, Lon: 5.43},
}
- assert.Empty(t, mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{pass}, 100, 25))
+ assert.Empty(t, mountain_pass.DetectCrossings(r.Coords, r.Distances, r.Elevations, []mountain_pass.MountainPass{pass}, 100, 25))
}
// rideFromPoint builds a 1 km ride ending at (latitude, longitude) at the
@@ -144,10 +146,10 @@ func TestMatchClimbNearestOfTwo(t *testing.T) {
// climbFixture builds a 2 km climb from 300 m up to 447 m then back down to
// 380 m, heading northeast from (43.61, 5.42).
-func climbFixture() ride.Climb {
- r := ridetest.MustRide("1km at 14.7% | 1km at -6.7%",
- ridetest.StartCoord(43.61, 5.42),
- ridetest.StartElevation(300),
- ridetest.Sampling(1000))
- return r.ClimbFromDist(0, 2000)
+func climbFixture() climb.Climb {
+ route := routetest.MustRoute("1km at 14.7% | 1km at -6.7%",
+ routetest.StartCoord(43.61, 5.42),
+ routetest.StartElevation(300),
+ routetest.Sampling(1000))
+ return climb.Climb{Route: route}
}