aboutsummaryrefslogtreecommitdiff
path: root/rideanalysis
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-21 15:30:51 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-21 15:30:51 +0200
commit5e3d39cda5250cb55bc8324bcf96b0ab4b36c47f (patch)
treef92fe6f938866688b994c99670e1e27863579255 /rideanalysis
parent0e27097e3d706e0e5bd761bf35fe301fd119d2a9 (diff)
feat: bound climb detection by matched official climbs
Detected climbs no longer absorb an official climb's range: official climbs matched to the route by coordinates claim their stored start/end range, and climb detection now runs only in the free regions around them. On ride 263 a single absorbed Cat 2 ascent is split so creating a first-half official climb lets the second half be detected on its own. - ride: add Range, freeRegions, and DetectClimbs(claimed); exclude claimed ranges so a climb never overlaps one - official_climb: MatchClimb now bounds-matches purely by coordinates without needing a detected climb; extract geo.Nearest shared helper - rideanalysis: Analyze builds claimed climbs from matched official climbs and detects only in free regions; render official vs unmatched climbs separately
Diffstat (limited to 'rideanalysis')
-rw-r--r--rideanalysis/analysis.go80
1 files changed, 34 insertions, 46 deletions
diff --git a/rideanalysis/analysis.go b/rideanalysis/analysis.go
index a4c319f..f055676 100644
--- a/rideanalysis/analysis.go
+++ b/rideanalysis/analysis.go
@@ -1,9 +1,8 @@
package rideanalysis
import (
- "math"
+ "slices"
- "github.com/jftuga/geodist"
"github.com/martinlehoux/biking_home/mountain_pass"
"github.com/martinlehoux/biking_home/official_climb"
"github.com/martinlehoux/biking_home/ride"
@@ -37,64 +36,53 @@ func Analyze(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClim
Climbs: make([]AnalyzedClimb, 0),
Crossings: make([]mountain_pass.Crossing, 0),
}
- for _, detectedClimb := range parsed.AllClimbs() {
+ claimedClimbs := make([]AnalyzedClimb, 0)
+ claimedRanges := make([]ride.Range, 0)
+ for _, official := range officialClimbs {
+ matched, found := official_climb.MatchClimb(parsed.Coords, official, matchPolicy.EndpointRadiusM)
+ if !found {
+ continue
+ }
+ claimedRanges = append(claimedRanges, ride.Range{Start: matched.StartIndex, End: matched.EndIndex})
+ claimedClimbs = append(claimedClimbs, analyzedClimb(parsed, parsed.ClimbFromIndexes(matched.StartIndex, matched.EndIndex), official.Name, official.ID))
+ }
+ for _, detectedClimb := range parsed.DetectClimbs(claimedRanges) {
displayClimb := detectedClimb
if matchedPass, ok := mountain_pass.MatchClimb(displayClimb, passes, passMatchRadiusM, passMatchElevationToleranceM); ok {
displayClimb.Name = matchedPass.Name
}
- matchedOfficial, officialFound := official_climb.MatchClimb(parsed, detectedClimb, officialClimbs, matchPolicy)
- if officialFound {
- startIndex := nearestCoordIndex(parsed, matchedOfficial.StartCoord)
- endIndex := nearestCoordIndex(parsed, matchedOfficial.EndCoord)
- if startIndex < endIndex {
- displayClimb = parsed.ClimbFromIndexes(startIndex, endIndex)
- displayClimb.Name = matchedOfficial.Name
- } else {
- officialFound = false
- matchedOfficial = official_climb.OfficialClimb{}
- }
- }
- cotacol := displayClimb.DifficultyScore()
- result.Climbs = append(result.Climbs, AnalyzedClimb{
- Segment: displayClimb,
- Name: displayClimb.Name,
- Category: ride.CategoryOf(cotacol).String(),
- DistanceM: displayClimb.EndDistanceM() - displayClimb.StartDistanceM(),
- SlopePercent: ride.Slope(parsed, displayClimb.StartIndex(), displayClimb.EndIndex()) * 100,
- Cotacol: cotacol,
- OfficialClimbID: officialClimbID(matchedOfficial, officialFound),
- OfficialName: officialClimbName(matchedOfficial, officialFound),
- })
+ result.Climbs = append(result.Climbs, analyzedClimb(parsed, displayClimb, displayClimb.Name, 0))
}
+ climbs := make([]AnalyzedClimb, 0, len(claimedClimbs)+len(result.Climbs))
+ climbs = append(climbs, claimedClimbs...)
+ climbs = append(climbs, result.Climbs...)
+ slices.SortFunc(climbs, func(a, b AnalyzedClimb) int {
+ return a.Segment.StartIndex() - b.Segment.StartIndex()
+ })
+ result.Climbs = climbs
for _, crossing := range mountain_pass.DetectCrossings(parsed, passes, passCrossingRadiusM, passCrossingElevationToleranceM) {
result.Crossings = append(result.Crossings, crossing)
}
return result
}
-func nearestCoordIndex(parsed ride.Ride, target geodist.Coord) int {
- bestIndex := 0
- bestDistance := math.Inf(1)
- for index := 0; index < parsed.Len(); index++ {
- _, distanceKm := geodist.HaversineDistance(parsed.Coord(index), target)
- if distanceKm < bestDistance {
- bestDistance = distanceKm
- bestIndex = index
- }
- }
- return bestIndex
-}
-
-func officialClimbID(climb official_climb.OfficialClimb, found bool) int64 {
- if !found {
- return 0
+func analyzedClimb(parsed ride.Ride, segment ride.Climb, name string, officialID int64) AnalyzedClimb {
+ cotacol := segment.DifficultyScore()
+ return AnalyzedClimb{
+ Segment: segment,
+ Name: name,
+ Category: ride.CategoryOf(cotacol).String(),
+ DistanceM: segment.EndDistanceM() - segment.StartDistanceM(),
+ SlopePercent: ride.Slope(parsed, segment.StartIndex(), segment.EndIndex()) * 100,
+ Cotacol: cotacol,
+ OfficialClimbID: officialID,
+ OfficialName: officialClimbNameForID(name, officialID),
}
- return climb.ID
}
-func officialClimbName(climb official_climb.OfficialClimb, found bool) string {
- if !found {
+func officialClimbNameForID(name string, officialID int64) string {
+ if officialID == 0 {
return ""
}
- return climb.Name
+ return name
}