From b626f15b6e40311143cdec91728046dc79e0d328 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Mon, 17 Aug 2026 13:05:50 +0200 Subject: feat(web): display nearby passes on ride maps --- mountain_pass/detection.go | 54 ++++++++++++++++++++++++++++++++++++++--- mountain_pass/detection_test.go | 30 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) (limited to 'mountain_pass') 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{ -- cgit v1.2.3