1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
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
}
|