From 5e3d39cda5250cb55bc8324bcf96b0ab4b36c47f Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Fri, 21 Aug 2026 15:30:51 +0200 Subject: 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 --- ride/climb.go | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) (limited to 'ride/climb.go') diff --git a/ride/climb.go b/ride/climb.go index 29a4dcf..88b9d3b 100644 --- a/ride/climb.go +++ b/ride/climb.go @@ -221,8 +221,47 @@ func climbsBetween(r Ride, start, end int) []Climb { return climbs } -func (ride *Ride) AllClimbs() []Climb { - climbs := climbsBetween(*ride, 0, ride.Len()-1) +type Range struct { + Start int + End int +} + +func isValidClaim(claim Range, routeLen int) bool { + return claim.Start >= 0 && claim.End < routeLen && claim.Start < claim.End +} + +// freeRegions returns the maximal sub-ranges of [rStart, rEnd] that are not +// covered by any claimed range, after discarding claims that fall outside. +func freeRegions(rStart, rEnd int, claimed []Range) []Range { + regions := make([]Range, 0, len(claimed)+1) + current := rStart + for _, claim := range claimed { + if !isValidClaim(claim, rEnd+1) { + continue + } + if claim.End < current { + continue + } + if claim.Start > current { + regions = append(regions, Range{Start: current, End: claim.Start - 1}) + } + if claim.End+1 > current { + current = claim.End + 1 + } + } + if current <= rEnd { + regions = append(regions, Range{Start: current, End: rEnd}) + } + return regions +} + +// returns the detected climbs of the whole ride, but never a climb that overlaps +// a claimed range. +func (ride *Ride) DetectClimbs(claimed []Range) []Climb { + climbs := make([]Climb, 0) + for _, region := range freeRegions(0, ride.Len()-1, claimed) { + climbs = append(climbs, climbsBetween(*ride, region.Start, region.End)...) + } slices.SortFunc(climbs, climbCmpStart) return climbs } -- cgit v1.3