aboutsummaryrefslogtreecommitdiff
path: root/web/climbs.go
diff options
context:
space:
mode:
Diffstat (limited to 'web/climbs.go')
-rw-r--r--web/climbs.go81
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 {