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.go84
1 files changed, 76 insertions, 8 deletions
diff --git a/official_climb/matcher.go b/official_climb/matcher.go
index faf51a7..00f7e9b 100644
--- a/official_climb/matcher.go
+++ b/official_climb/matcher.go
@@ -20,16 +20,84 @@ type MatchedClimb struct {
// 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) {
- if math.IsNaN(radiusM) || math.IsInf(radiusM, 0) || radiusM <= 0 {
+ matches := MatchClimbs(route, official, radiusM)
+ if len(matches) == 0 {
return MatchedClimb{}, false
}
- startIndex, startDistance := geo.Nearest(route, official.StartCoord)
- endIndex, endDistance := geo.Nearest(route, official.EndCoord)
- if startDistance > radiusM || endDistance > radiusM {
- return MatchedClimb{}, false
+ 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 {
+ if math.IsNaN(radiusM) || math.IsInf(radiusM, 0) || radiusM <= 0 {
+ return nil
}
- if startIndex >= endIndex {
- return MatchedClimb{}, false
+ if len(route) == 0 {
+ return nil
+ }
+ startClusters := clustersInRadius(route, official.StartCoord, radiusM)
+ endClusters := clustersInRadius(route, official.EndCoord, radiusM)
+ if len(startClusters) == 0 || len(endClusters) == 0 {
+ return nil
+ }
+ matches := make([]MatchedClimb, 0)
+ for i, start := range startClusters {
+ nextStartIdx := len(route)
+ if i+1 < len(startClusters) {
+ nextStartIdx = startClusters[i+1].Index
+ }
+ var matchedEnd *clusterCandidate
+ for j := range endClusters {
+ end := &endClusters[j]
+ if end.Index > start.Index && end.Index < nextStartIdx {
+ matchedEnd = end
+ break
+ }
+ }
+ if matchedEnd == nil {
+ continue
+ }
+ matches = append(matches, MatchedClimb{
+ OfficialClimb: official,
+ StartIndex: start.Index,
+ EndIndex: matchedEnd.Index,
+ })
+ }
+ return matches
+}
+
+type clusterCandidate struct {
+ Index int
+ Distance float64
+}
+
+func clustersInRadius(route []geodist.Coord, target geodist.Coord, radiusM float64) []clusterCandidate {
+ candidates := make([]clusterCandidate, 0)
+ inCluster := false
+ bestIndex := 0
+ bestDistance := 0.0
+ for idx, coord := range route {
+ dist := geo.DistanceM(coord, target)
+ if dist <= radiusM {
+ if !inCluster {
+ inCluster = true
+ bestIndex = idx
+ bestDistance = dist
+ } else if dist < bestDistance {
+ bestIndex = idx
+ bestDistance = dist
+ }
+ } else if inCluster {
+ candidates = append(candidates, clusterCandidate{Index: bestIndex, Distance: bestDistance})
+ inCluster = false
+ }
+ }
+ if inCluster {
+ candidates = append(candidates, clusterCandidate{Index: bestIndex, Distance: bestDistance})
}
- return MatchedClimb{OfficialClimb: official, StartIndex: startIndex, EndIndex: endIndex}, true
+ return candidates
}