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