diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-27 11:21:17 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-27 11:21:17 +0200 |
| commit | 825cf388b55a6c8441e02149749bb6e2968477c0 (patch) | |
| tree | e3394cef97aa43df9775a1465d3c99f386ce3a9b /web | |
| parent | 366e7b0d671395c2ad32f85ee7706a592db69d15 (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 'web')
| -rw-r--r-- | web/ride-filters.go | 3 | ||||
| -rw-r--r-- | web/ride-filters_test.go | 3 | ||||
| -rw-r--r-- | web/rides.go | 7 | ||||
| -rw-r--r-- | web/server_test.go | 13 | ||||
| -rw-r--r-- | web/views.go | 24 |
5 files changed, 28 insertions, 22 deletions
diff --git a/web/ride-filters.go b/web/ride-filters.go index 0bb97d8..8535756 100644 --- a/web/ride-filters.go +++ b/web/ride-filters.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/biking_home/ride" ) @@ -417,7 +418,7 @@ func rideMatchesFilters(item ride.StoredRide, filters RideFilters) bool { } if filters.CotacolMinPoints != nil || filters.CotacolMaxPoints != nil { cotacol, ready := item.CotacolScore() - if !ready || item.CotacolAlgorithmVersion() != ride.CotacolAlgorithmVersion || item.DistanceM <= 0 { + if !ready || item.CotacolAlgorithmVersion() != climb.CotacolAlgorithmVersion || item.DistanceM <= 0 { return false } if filters.CotacolMinPoints != nil && cotacol < float64(*filters.CotacolMinPoints) { diff --git a/web/ride-filters_test.go b/web/ride-filters_test.go index e4efc02..84c6417 100644 --- a/web/ride-filters_test.go +++ b/web/ride-filters_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/biking_home/ride" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -209,7 +210,7 @@ func TestFilterRidesByCotacolExcludesUnavailableScores(t *testing.T) { WHEN ? THEN 'old' END WHERE external_id IN (?, ?) - `, current.ExternalID, stale.ExternalID, current.ExternalID, ride.CotacolAlgorithmVersion, stale.ExternalID, current.ExternalID, stale.ExternalID) + `, current.ExternalID, stale.ExternalID, current.ExternalID, climb.CotacolAlgorithmVersion, stale.ExternalID, current.ExternalID, stale.ExternalID) require.NoError(t, err) items, err := ride.List(db) require.NoError(t, err) diff --git a/web/rides.go b/web/rides.go index 71ab3fd..d3f5634 100644 --- a/web/rides.go +++ b/web/rides.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/biking_home/mountain_pass" "github.com/martinlehoux/biking_home/official_climb" "github.com/martinlehoux/biking_home/ride" @@ -65,7 +66,7 @@ func (s *Server) handleRide(w http.ResponseWriter, r *http.Request) { }), w) return } - passes, err := mountain_pass.LoadMountainPassesAroundRide(s.db, parsed.Coords(), rideDetailMountainPassMarginM) + passes, err := mountain_pass.LoadMountainPassesAroundRide(s.db, parsed.Coords, rideDetailMountainPassMarginM) if err != nil { slog.Warn("Failed to load nearby mountain passes for ride detail", "ride_id", id, "error", err) } @@ -144,7 +145,7 @@ func (s *Server) handleCreateOfficialClimb(w http.ResponseWriter, r *http.Reques http.Error(w, "the recorded route is unavailable", http.StatusBadRequest) return } - bounds := parsed.Coords() + bounds := parsed.Coords if startIndex >= endIndex || endIndex >= len(bounds) { http.Error(w, "the official climb boundaries must be ordered route points", http.StatusBadRequest) return @@ -192,7 +193,7 @@ func buildRideViews(items []ride.StoredRide) []RideView { func buildRideView(item ride.StoredRide) RideView { view := RideView{Ride: item, Cotacol: "-", CotacolPer100Km: "-", MovingTime: formatDuration(item.EffectiveMovingTimeS())} cotacol, ready := item.CotacolScore() - if ready && item.CotacolAlgorithmVersion() == ride.CotacolAlgorithmVersion && item.DistanceM > 0 { + if ready && item.CotacolAlgorithmVersion() == climb.CotacolAlgorithmVersion && item.DistanceM > 0 { view.Cotacol = formatCotacol(cotacol) view.CotacolPer100Km = formatCotacolPer100Km(cotacol, item.DistanceM) } diff --git a/web/server_test.go b/web/server_test.go index 6a1c13f..a60d742 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/jftuga/geodist" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/biking_home/config" "github.com/martinlehoux/biking_home/internal/dbtest" "github.com/martinlehoux/biking_home/internal/ridetest" @@ -225,8 +226,8 @@ func TestBuildRideProfileIncludesOfficialClimbMatch(t *testing.T) { profile := buildRideProfile(parsed, nil, []official_climb.OfficialClimb{{ ID: 42, Name: "Col de Test", - StartCoord: parsed.Coords()[0], - EndCoord: parsed.Coords()[1], + StartCoord: parsed.Coords[0], + EndCoord: parsed.Coords[1], }}, official_climb.DefaultMatchPolicy()) require.Len(t, profile.Climbs, 1) @@ -242,8 +243,8 @@ func TestBuildRideProfileUsesOfficialClimbBoundaries(t *testing.T) { profile := buildRideProfile(parsed, nil, []official_climb.OfficialClimb{{ ID: 42, Name: "Col de Test", - StartCoord: parsed.Coords()[0], - EndCoord: parsed.Coords()[2], + StartCoord: parsed.Coords[0], + EndCoord: parsed.Coords[2], }}, official_climb.DefaultMatchPolicy()) require.Len(t, profile.Climbs, 1) @@ -370,7 +371,7 @@ func TestHandlerRendersAnalyticsChart(t *testing.T) { SET cotacol_score = CASE external_id WHEN ? THEN 20 ELSE 120 END, cotacol_algo_version = ?, average_speed_mps = CASE external_id WHEN ? THEN 7.0 ELSE 4.0 END - `, first.ExternalID, ride.CotacolAlgorithmVersion, second.ExternalID) + `, first.ExternalID, climb.CotacolAlgorithmVersion, second.ExternalID) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/analytics", nil) @@ -416,7 +417,7 @@ func TestHandlerFiltersAnalyticsByDistance(t *testing.T) { SET cotacol_score = CASE external_id WHEN ? THEN 20 ELSE 120 END, cotacol_algo_version = ?, average_speed_mps = CASE external_id WHEN ? THEN 7.0 ELSE 5.0 END - `, short.ExternalID, ride.CotacolAlgorithmVersion, long.ExternalID) + `, short.ExternalID, climb.CotacolAlgorithmVersion, long.ExternalID) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/analytics?distance_min=60", nil) diff --git a/web/views.go b/web/views.go index b3cab8b..4f769cc 100644 --- a/web/views.go +++ b/web/views.go @@ -5,6 +5,7 @@ import ( "net/url" "time" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/biking_home/mountain_pass" "github.com/martinlehoux/biking_home/official_climb" "github.com/martinlehoux/biking_home/ride" @@ -47,7 +48,7 @@ func buildAnalyticsPoints(items []ride.StoredRide) []AnalyticsPoint { points := make([]AnalyticsPoint, 0, len(items)) for _, item := range items { cotacol, ready := item.CotacolScore() - if !ready || item.CotacolAlgorithmVersion() != ride.CotacolAlgorithmVersion || item.DistanceM <= 0 { + if !ready || item.CotacolAlgorithmVersion() != climb.CotacolAlgorithmVersion || item.DistanceM <= 0 { continue } points = append(points, AnalyticsPoint{ @@ -286,7 +287,7 @@ func officialClimbCreateURL(id int64) string { } func buildRideDetailView(item ride.StoredRide, parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) RideDetailView { - coords := parsed.Coords() + coords := parsed.Coords coordinates := make([][]float64, len(coords)) for i, coordinate := range coords { coordinates[i] = []float64{coordinate.Lon, coordinate.Lat} @@ -327,9 +328,9 @@ func buildRideMapPasses(passes []mountain_pass.MountainPass) []RideMapPass { } func buildRideProfile(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) RideProfile { - coords := parsed.Coords() - distances := parsed.Distances() - elevations := parsed.Elevations() + coords := parsed.Coords + distances := parsed.Distances + elevations := parsed.Elevations profile := RideProfile{ Points: make([]RideProfilePoint, len(coords)), Climbs: make([]RideProfileClimb, 0), @@ -346,12 +347,13 @@ func buildRideProfile(parsed ride.Ride, passes []mountain_pass.MountainPass, off analysis := rideanalysis.Analyze(parsed, passes, officialClimbs, matchPolicy) for _, analyzedClimb := range analysis.Climbs { segment := analyzedClimb.Segment + topIndex, topElevationM := segment.Top() climb := RideProfileClimb{ Index: len(profile.Climbs), - StartKm: segment.StartDistanceM() / 1000, - EndKm: segment.EndDistanceM() / 1000, - TopKm: segment.TopDistanceM() / 1000, - TopElevationM: segment.TopElevationM(), + StartKm: segment.Distances[0] / 1000, + EndKm: segment.Distances[len(segment.Distances)-1] / 1000, + TopKm: segment.Distances[topIndex] / 1000, + TopElevationM: topElevationM, Name: analyzedClimb.Name, Category: analyzedClimb.Category, DistanceKm: analyzedClimb.DistanceM / 1000, @@ -359,8 +361,8 @@ func buildRideProfile(parsed ride.Ride, passes []mountain_pass.MountainPass, off Cotacol: analyzedClimb.Cotacol, OfficialClimbID: analyzedClimb.OfficialClimbID, OfficialName: analyzedClimb.OfficialName, - StartIndex: segment.StartIndex(), - EndIndex: segment.EndIndex(), + StartIndex: analyzedClimb.Interval.Start, + EndIndex: analyzedClimb.Interval.End, } profile.Climbs = append(profile.Climbs, climb) } |
