diff options
Diffstat (limited to 'web/climbs.go')
| -rw-r--r-- | web/climbs.go | 81 |
1 files changed, 21 insertions, 60 deletions
diff --git a/web/climbs.go b/web/climbs.go index 5cd6db9..02f6eb6 100644 --- a/web/climbs.go +++ b/web/climbs.go @@ -2,8 +2,6 @@ package web import ( "database/sql" - "github.com/jftuga/geodist" - "github.com/martinlehoux/biking_home/climb" "log/slog" "math" "net/http" @@ -12,8 +10,10 @@ import ( "strconv" "strings" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/biking_home/official_climb" "github.com/martinlehoux/biking_home/ride" + "github.com/martinlehoux/biking_home/rideanalysis" "github.com/martinlehoux/kagamigo/kcore" ) @@ -21,15 +21,9 @@ func climbDetailURL(id int64) string { return "/climbs/" + strconv.FormatInt(id, 10) } -type routeCandidate struct { - coords []geodist.Coord - distance float64 -} - type ClimbProfileData struct { - DistanceKm float64 `json:"distanceKm"` - Elevations []float64 `json:"elevations"` - Route [][]float64 `json:"route,omitempty"` + Sections []ProfileSectionView `json:"sections"` + Route [][]float64 `json:"route,omitempty"` } type ClimbAttemptView struct { RideID int64 @@ -116,15 +110,22 @@ func (s *Server) handleClimb(w http.ResponseWriter, r *http.Request) { }), w) } -// buildClimbProfile computes the median normalized elevation profile from the linked rides, resampling every occurrence onto a shared axis. +// buildClimbProfile merges the recorded passages of a climb into one chart profile. func (s *Server) buildClimbProfile(officialClimb official_climb.OfficialClimb, aggregate official_climb.Aggregate, attempts []official_climb.Attempt) ClimbProfileData { - profile := ClimbProfileData{DistanceKm: aggregate.DistanceM / 1000} - if len(attempts) == 0 { - return profile + profile := rideanalysis.MergeClimbProfile(officialClimb, s.climbAttemptRides(attempts), aggregate, s.officialClimbMatchPolicy(), climb.DefaultProfilePolicy()) + data := ClimbProfileData{Sections: profileSectionsView(profile.Sections)} + if len(profile.Route) > 1 { + data.Route = make([][]float64, len(profile.Route)) + for index, coord := range profile.Route { + data.Route[index] = []float64{coord.Lon, coord.Lat} + } } - matchPolicy := s.officialClimbMatchPolicy() - elevationSamples := make([][]float64, 0, len(attempts)) - routeCandidates := make([]routeCandidate, 0, len(attempts)) + return data +} + +// climbAttemptRides loads and parses the rides that recorded the attempts, once per ride. +func (s *Server) climbAttemptRides(attempts []official_climb.Attempt) []ride.Ride { + parsedRides := make([]ride.Ride, 0, len(attempts)) seen := make(map[int64]bool) for _, attempt := range attempts { if seen[attempt.RideID] { @@ -137,52 +138,12 @@ func (s *Server) buildClimbProfile(officialClimb official_climb.OfficialClimb, a } parsed, err := ride.ParseStoredRide(item) if err != nil { - slog.Warn("Failed to parse ride for climb profile", "climb_id", officialClimb.ID, "ride_id", attempt.RideID, "error", err) + slog.Warn("Failed to parse ride for climb profile", "ride_id", attempt.RideID, "error", err) continue } - for _, matched := range official_climb.MatchClimbs(parsed.Route, officialClimb, matchPolicy.EndpointRadiusM) { - segment := climb.Climb{ - Distances: parsed.Distances[matched.StartIndex : matched.EndIndex+1], - Elevations: parsed.Elevations[matched.StartIndex : matched.EndIndex+1], - Coords: parsed.Coords[matched.StartIndex : matched.EndIndex+1]} - if sample := official_climb.NormalizeProfile(segment); len(sample) > 0 { - elevationSamples = append(elevationSamples, sample) - } - routeCandidates = append(routeCandidates, routeCandidate{ - coords: segment.Coords, - distance: segment.Distances[len(segment.Distances)-1] - segment.Distances[0], - }) - } - } - profile.Elevations = official_climb.MedianProfile(elevationSamples) - if route := medianRoute(routeCandidates); len(route) > 1 { - profile.Route = make([][]float64, len(route)) - for i, coord := range route { - profile.Route[i] = []float64{coord.Lon, coord.Lat} - } - } - return profile -} - -// medianRoute picks the recorded route of the attempt whose distance is the median: full precision, no resampling, robust to one drifted ride. -func medianRoute(candidates []routeCandidate) []geodist.Coord { - if len(candidates) == 0 { - return nil - } - distances := make([]float64, len(candidates)) - for i, candidate := range candidates { - distances[i] = candidate.distance - } - median := official_climb.Median(distances) - best := candidates[0] - bestDiff := math.Abs(best.distance - median) - for _, candidate := range candidates[1:] { - if diff := math.Abs(candidate.distance - median); diff < bestDiff { - best = candidate - bestDiff = diff - } + parsedRides = append(parsedRides, parsed) } - return best.coords + return parsedRides } func buildClimbStatsView(aggregate official_climb.Aggregate) ClimbStatsView { |
