diff options
Diffstat (limited to 'official_climb/matcher.go')
| -rw-r--r-- | official_climb/matcher.go | 42 |
1 files changed, 25 insertions, 17 deletions
diff --git a/official_climb/matcher.go b/official_climb/matcher.go index 00f7e9b..657d2ef 100644 --- a/official_climb/matcher.go +++ b/official_climb/matcher.go @@ -4,22 +4,22 @@ import ( "math" "github.com/jftuga/geodist" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/biking_home/geo" ) -// MatchedClimb is an official climb that lies on a route, together with the -// route point indexes of its start and end. It lets the climb detector know -// which segment of the route is already claimed by an official climb. +// MatchedClimb is an official climb lying on a route, with the route indexes of its start and end: the segment the climb detector must not re-report. type MatchedClimb struct { OfficialClimb StartIndex int EndIndex int } -// MatchClimb matches an official climb against a route purely by -// coordinates: both the start and end coordinates must be within radiusM of a -// route point, and the matched start point must precede the matched end point. -func MatchClimb(route []geodist.Coord, official OfficialClimb, radiusM float64) (MatchedClimb, bool) { +// SegmentSampleBandRatio is the half-width of the band a candidate passage must land in, relative to the climb's sample. +const SegmentSampleBandRatio = 0.5 + +// MatchClimb matches an official climb against a route: endpoints within radiusM, start before end, and the passage inside the climb's sample band. +func MatchClimb(route climb.Route, official OfficialClimb, radiusM float64) (MatchedClimb, bool) { matches := MatchClimbs(route, official, radiusM) if len(matches) == 0 { return MatchedClimb{}, false @@ -27,26 +27,22 @@ func MatchClimb(route []geodist.Coord, official OfficialClimb, radiusM float64) return matches[0], true } -// MatchClimbs returns all non-overlapping occurrences of an official climb on -// a route. Each occurrence is defined by a start and end point both within -// radiusM, with start strictly before end and direction preserved. Loops are -// handled by clustering contiguous approaches into single candidates and pairing -// each start cluster with the first end cluster inside its lap window. -func MatchClimbs(route []geodist.Coord, official OfficialClimb, radiusM float64) []MatchedClimb { +// MatchClimbs returns every occurrence of an official climb on a route: each start cluster pairs with the first end cluster in its lap window, and pairings that contradict the recorded sample are dropped — what a loop ridden the other way produces. +func MatchClimbs(route climb.Route, official OfficialClimb, radiusM float64) []MatchedClimb { if math.IsNaN(radiusM) || math.IsInf(radiusM, 0) || radiusM <= 0 { return nil } - if len(route) == 0 { + if len(route.Coords) == 0 { return nil } - startClusters := clustersInRadius(route, official.StartCoord, radiusM) - endClusters := clustersInRadius(route, official.EndCoord, radiusM) + startClusters := clustersInRadius(route.Coords, official.StartCoord, radiusM) + endClusters := clustersInRadius(route.Coords, official.EndCoord, radiusM) if len(startClusters) == 0 || len(endClusters) == 0 { return nil } matches := make([]MatchedClimb, 0) for i, start := range startClusters { - nextStartIdx := len(route) + nextStartIdx := len(route.Coords) if i+1 < len(startClusters) { nextStartIdx = startClusters[i+1].Index } @@ -61,6 +57,9 @@ func MatchClimbs(route []geodist.Coord, official OfficialClimb, radiusM float64) if matchedEnd == nil { continue } + if !withinSampleBand(official.Sample, route.Sample(start.Index, matchedEnd.Index)) { + continue + } matches = append(matches, MatchedClimb{ OfficialClimb: official, StartIndex: start.Index, @@ -70,6 +69,15 @@ func MatchClimbs(route []geodist.Coord, official OfficialClimb, radiusM float64) return matches } +// withinSampleBand reports whether a candidate passage matches both axes of the climb's sample. +func withinSampleBand(sample, candidate climb.SegmentSample) bool { + return inSampleBand(candidate.DistanceM, sample.DistanceM) && inSampleBand(candidate.ElevationGainM, sample.ElevationGainM) +} + +func inSampleBand(value, reference float64) bool { + return value >= reference*(1-SegmentSampleBandRatio) && value <= reference*(1+SegmentSampleBandRatio) +} + type clusterCandidate struct { Index int Distance float64 |
