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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
package mountain_pass
import (
"database/sql"
"fmt"
"math"
"github.com/jftuga/geodist"
"github.com/martinlehoux/biking_home/ride"
)
type Crossing struct {
Pass MountainPass
DistanceToM float64
RideDistanceM float64
RideElevation float64
ElevationDiff float64
}
func LoadMountainPasses(db *sql.DB) ([]MountainPass, error) {
return loadMountainPasses(db, nil)
}
func LoadMountainPassesAroundRide(db *sql.DB, route ride.Ride, marginM float64) ([]MountainPass, error) {
return loadMountainPasses(db, rideBounds(route, marginM))
}
type coordinateBounds struct {
minLatitude float64
minLongitude float64
maxLatitude float64
maxLongitude float64
}
func rideBounds(route ride.Ride, marginM float64) *coordinateBounds {
minLatitude, maxLatitude := route.Coord(0).Lat, route.Coord(0).Lat
minLongitude, maxLongitude := route.Coord(0).Lon, route.Coord(0).Lon
for index := 1; index < route.Len(); index++ {
coordinate := route.Coord(index)
minLatitude = math.Min(minLatitude, coordinate.Lat)
maxLatitude = math.Max(maxLatitude, coordinate.Lat)
minLongitude = math.Min(minLongitude, coordinate.Lon)
maxLongitude = math.Max(maxLongitude, coordinate.Lon)
}
latitudeMargin := marginM / 111_320
longitudeScale := math.Cos((minLatitude + maxLatitude) / 2 * math.Pi / 180)
longitudeMargin := marginM / (111_320 * math.Max(longitudeScale, 0.01))
return &coordinateBounds{
minLatitude: minLatitude - latitudeMargin,
minLongitude: minLongitude - longitudeMargin,
maxLatitude: maxLatitude + latitudeMargin,
maxLongitude: maxLongitude + longitudeMargin,
}
}
func loadMountainPasses(db *sql.DB, bounds *coordinateBounds) ([]MountainPass, error) {
query := `
SELECT external_id, name, country_code, department_code, elevation, latitude, longitude
FROM mountain_passes`
args := make([]any, 0, 4)
if bounds != nil {
query += `
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
AND latitude BETWEEN ? AND ?
AND longitude BETWEEN ? AND ?`
args = append(args, bounds.minLatitude, bounds.maxLatitude, bounds.minLongitude, bounds.maxLongitude)
}
query += `
ORDER BY elevation`
rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
mountainPasses := make([]MountainPass, 0)
for rows.Next() {
var mountainPass MountainPass
var latitude, longitude sql.NullFloat64
if err := rows.Scan(&mountainPass.ExternalID, &mountainPass.Name, &mountainPass.CountryCode, &mountainPass.DepartmentCode, &mountainPass.Elevation, &latitude, &longitude); err != nil {
return nil, err
}
if latitude.Valid && longitude.Valid {
mountainPass.Coord = &geodist.Coord{Lat: latitude.Float64, Lon: longitude.Float64}
}
mountainPasses = append(mountainPasses, mountainPass)
}
return mountainPasses, rows.Err()
}
func DetectCrossings(ride ride.Ride, passes []MountainPass, radiusM, elevationToleranceM float64) []Crossing {
crossings := make([]Crossing, 0)
for _, mountainPass := range passes {
if mountainPass.Coord == nil {
continue
}
crossing, found := nearestCrossing(ride, mountainPass)
if found && crossing.DistanceToM <= radiusM && crossing.ElevationDiff <= elevationToleranceM {
crossings = append(crossings, crossing)
}
}
return crossings
}
// MatchClimb returns the pass whose coordinates lie within radiusM of the
// climb's highest point and whose elevation is within elevationToleranceM of
// that point's elevation, nearest first. Found is false when no pass matches.
func MatchClimb(climb ride.Climb, passes []MountainPass, radiusM, elevationToleranceM float64) (MountainPass, bool) {
topCoord := climb.TopCoord()
topElevation := climb.TopElevationM()
var best MountainPass
bestDistanceM := radiusM
found := false
for _, mountainPass := range passes {
if mountainPass.Coord == nil {
continue
}
distanceKm, _ := geodist.HaversineDistance(topCoord, *mountainPass.Coord)
distanceM := distanceKm * 1000
if distanceM > bestDistanceM {
continue
}
elevationDiff := absFloat64(topElevation - float64(mountainPass.Elevation))
if elevationDiff > elevationToleranceM {
continue
}
best = mountainPass
bestDistanceM = distanceM
found = true
}
return best, found
}
func nearestCrossing(ride ride.Ride, mountainPass MountainPass) (Crossing, bool) {
best := Crossing{Pass: mountainPass, DistanceToM: 1e18}
for i := 0; i < ride.Len(); i++ {
distanceKm, _ := geodist.HaversineDistance(ride.Coord(i), *mountainPass.Coord)
distanceM := distanceKm * 1000
if distanceM < best.DistanceToM {
best.DistanceToM = distanceM
best.RideDistanceM = ride.DistanceM(i)
best.RideElevation = ride.ElevationM(i)
best.ElevationDiff = absFloat64(ride.ElevationM(i) - float64(mountainPass.Elevation))
}
}
if best.DistanceToM > 1e17 {
return best, false
}
return best, true
}
func absFloat64(value float64) float64 {
if value < 0 {
return -value
}
return value
}
func (crossing Crossing) String() string {
return fmt.Sprintf("%s (%dm) at %.1fkm, %.0fm away, Δelev %.0fm",
crossing.Pass.Name, crossing.Pass.Elevation, crossing.RideDistanceM/1000, crossing.DistanceToM, crossing.ElevationDiff)
}
|