package official_climb import ( "math" "github.com/jftuga/geodist" "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. 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) { matches := MatchClimbs(route, official, radiusM) if len(matches) == 0 { 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 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 candidates }