summaryrefslogtreecommitdiff
path: root/mountain_pass
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-17 13:05:50 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-17 13:05:50 +0200
commitb626f15b6e40311143cdec91728046dc79e0d328 (patch)
tree9c7ff4f9a879785d5a63b3410e0aa02be1a5219d /mountain_pass
parent151853c458348a4afccd7677deba6366ee4181eb (diff)
feat(web): display nearby passes on ride maps
Diffstat (limited to 'mountain_pass')
-rw-r--r--mountain_pass/detection.go54
-rw-r--r--mountain_pass/detection_test.go30
2 files changed, 80 insertions, 4 deletions
diff --git a/mountain_pass/detection.go b/mountain_pass/detection.go
index 4be2f94..4c54493 100644
--- a/mountain_pass/detection.go
+++ b/mountain_pass/detection.go
@@ -3,6 +3,7 @@ package mountain_pass
import (
"database/sql"
"fmt"
+ "math"
"github.com/jftuga/geodist"
"github.com/martinlehoux/biking_home/ride"
@@ -17,11 +18,56 @@ type Crossing struct {
}
func LoadMountainPasses(db *sql.DB) ([]MountainPass, error) {
- rows, err := db.Query(`
+ 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
- ORDER BY elevation
- `)
+ 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
}
diff --git a/mountain_pass/detection_test.go b/mountain_pass/detection_test.go
index d6cba9f..d86e67b 100644
--- a/mountain_pass/detection_test.go
+++ b/mountain_pass/detection_test.go
@@ -5,9 +5,11 @@ import (
"time"
"github.com/jftuga/geodist"
+ "github.com/martinlehoux/biking_home/internal/dbtest"
"github.com/martinlehoux/biking_home/mountain_pass"
"github.com/martinlehoux/biking_home/ride"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestDetectCrossingsFindsPass(t *testing.T) {
@@ -23,6 +25,34 @@ func TestDetectCrossingsFindsPass(t *testing.T) {
assert.Less(t, crossings[0].ElevationDiff, 25.0)
}
+func TestLoadMountainPassesAroundRideUsesExpandedBounds(t *testing.T) {
+ db := dbtest.New(t)
+ _, err := db.Exec(`
+ INSERT INTO mountain_passes (external_id, name, country_code, department_code, elevation, latitude, longitude)
+ VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?)
+ `,
+ "550e8400-e29b-41d4-a716-446655440000", "On route", "FR", "13", 400, 43.05, 5.05,
+ "6ba7b810-9dad-41d1-80b4-00c04fd430c8", "Within margin", "FR", "13", 500, 43.18, 5.20,
+ "6ba7b811-9dad-41d1-80b4-00c04fd430c8", "Outside margin", "FR", "13", 600, 43.21, 5.20,
+ "6ba7b812-9dad-41d1-80b4-00c04fd430c8", "Missing coordinates", "FR", "13", 700, nil, nil,
+ "6ba7b813-9dad-41d1-80b4-00c04fd430c8", "On route edge", "FR", "13", 300, 43.10, 5.10,
+ )
+ require.NoError(t, err)
+
+ route := ride.FromColumns(
+ []float64{0, 1000},
+ []float64{100, 200},
+ []geodist.Coord{{Lat: 43.0, Lon: 5.0}, {Lat: 43.1, Lon: 5.1}},
+ make([]time.Time, 2),
+ )
+ passes, err := mountain_pass.LoadMountainPassesAroundRide(db, route, 10_000)
+ require.NoError(t, err)
+ require.Len(t, passes, 3)
+ assert.Equal(t, "On route edge", passes[0].Name)
+ assert.Contains(t, []string{passes[1].Name, passes[2].Name}, "On route")
+ assert.Contains(t, []string{passes[1].Name, passes[2].Name}, "Within margin")
+}
+
func TestDetectCrossingsIgnoresDistantPass(t *testing.T) {
r := rideFromPoint(t, 43.62, 5.43, 447)
far := mountain_pass.MountainPass{