diff options
Diffstat (limited to 'official_climb/matcher.go')
| -rw-r--r-- | official_climb/matcher.go | 84 |
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 } |
