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
163
164
|
package mountain_pass
import (
"database/sql"
"math"
"github.com/jftuga/geodist"
"github.com/martinlehoux/biking_home/ride"
)
// MountainPass is a pass extracted from OpenStreetMap: a node tagged
// mountain_pass=yes (or legacy natural=mountain_pass) with a name and an
// elevation, as filtered by the loaders.
type MountainPass struct {
Name string
Elevation int
Coord *geodist.Coord
}
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, routeCoords []geodist.Coord, marginM float64) ([]MountainPass, error) {
return loadMountainPasses(db, rideBounds(routeCoords, marginM))
}
type coordinateBounds struct {
minLatitude float64
minLongitude float64
maxLatitude float64
maxLongitude float64
}
func rideBounds(coords []geodist.Coord, marginM float64) *coordinateBounds {
minLatitude, maxLatitude := coords[0].Lat, coords[0].Lat
minLongitude, maxLongitude := coords[0].Lon, coords[0].Lon
for index := 1; index < len(coords); index++ {
coordinate := coords[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 name, elevation, latitude, longitude
FROM mountain_passes
WHERE elevation IS NOT NULL
AND name IS NOT NULL AND name != ''`
args := make([]any, 0, 4)
if bounds != nil {
query += `
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 float64
if err := rows.Scan(&mountainPass.Name, &mountainPass.Elevation, &latitude, &longitude); err != nil {
return nil, err
}
mountainPass.Coord = &geodist.Coord{Lat: latitude, Lon: longitude}
mountainPasses = append(mountainPasses, mountainPass)
}
return mountainPasses, rows.Err()
}
func DetectCrossings(coords []geodist.Coord, distances, elevations []float64, passes []MountainPass, radiusM, elevationToleranceM float64) []Crossing {
crossings := make([]Crossing, 0)
for _, mountainPass := range passes {
if mountainPass.Coord == nil {
continue
}
crossing, found := nearestCrossing(coords, distances, elevations, 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(coords []geodist.Coord, distances, elevations []float64, mountainPass MountainPass) (Crossing, bool) {
best := Crossing{Pass: mountainPass, DistanceToM: 1e18}
for i := 0; i < len(coords); i++ {
distanceKm, _ := geodist.HaversineDistance(coords[i], *mountainPass.Coord)
distanceM := distanceKm * 1000
if distanceM < best.DistanceToM {
best.DistanceToM = distanceM
best.RideDistanceM = distances[i]
best.RideElevation = elevations[i]
best.ElevationDiff = absFloat64(elevations[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
}
|