From 708f5428968b7c1526f257b683d0146ef5dae418 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Tue, 25 Aug 2026 11:32:57 +0200 Subject: Make mountain pass and web consumers take ride components instead of a whole Ride rideBounds, nearestCrossing and DetectCrossings now accept the coordinate, distance and elevation component slices they read. LoadMountainPassesAroundRide takes the coords slice only. Web ride view builders and the official climb creation path iterate ride components (Coords, Distances, Elevations) rather than per-index accessors on Ride. --- mountain_pass/detection.go | 30 +++++++++++++++--------------- mountain_pass/detection_test.go | 10 +++++----- rideanalysis/analysis.go | 2 +- web/rides.go | 9 +++++---- web/views.go | 18 ++++++++++-------- 5 files changed, 36 insertions(+), 33 deletions(-) diff --git a/mountain_pass/detection.go b/mountain_pass/detection.go index 09792e3..139bc60 100644 --- a/mountain_pass/detection.go +++ b/mountain_pass/detection.go @@ -29,8 +29,8 @@ 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)) +func LoadMountainPassesAroundRide(db *sql.DB, routeCoords []geodist.Coord, marginM float64) ([]MountainPass, error) { + return loadMountainPasses(db, rideBounds(routeCoords, marginM)) } type coordinateBounds struct { @@ -40,11 +40,11 @@ type coordinateBounds struct { 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) +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) @@ -95,13 +95,13 @@ func loadMountainPasses(db *sql.DB, bounds *coordinateBounds) ([]MountainPass, e return mountainPasses, rows.Err() } -func DetectCrossings(ride ride.Ride, passes []MountainPass, radiusM, elevationToleranceM float64) []Crossing { +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(ride, mountainPass) + crossing, found := nearestCrossing(coords, distances, elevations, mountainPass) if found && crossing.DistanceToM <= radiusM && crossing.ElevationDiff <= elevationToleranceM { crossings = append(crossings, crossing) } @@ -138,16 +138,16 @@ func MatchClimb(climb ride.Climb, passes []MountainPass, radiusM, elevationToler return best, found } -func nearestCrossing(ride ride.Ride, mountainPass MountainPass) (Crossing, bool) { +func nearestCrossing(coords []geodist.Coord, distances, elevations []float64, 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) + 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 = ride.DistanceM(i) - best.RideElevation = ride.ElevationM(i) - best.ElevationDiff = absFloat64(ride.ElevationM(i) - float64(mountainPass.Elevation)) + best.RideDistanceM = distances[i] + best.RideElevation = elevations[i] + best.ElevationDiff = absFloat64(elevations[i] - float64(mountainPass.Elevation)) } } if best.DistanceToM > 1e17 { diff --git a/mountain_pass/detection_test.go b/mountain_pass/detection_test.go index f55532b..39bb9ae 100644 --- a/mountain_pass/detection_test.go +++ b/mountain_pass/detection_test.go @@ -18,7 +18,7 @@ func TestDetectCrossingsFindsPass(t *testing.T) { Name: "Pas de Magnan", Elevation: 460, Coord: &geodist.Coord{Lat: 43.62, Lon: 5.43}, } - crossings := mountain_pass.DetectCrossings(r, []mountain_pass.MountainPass{pass}, 100, 25) + crossings := mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{pass}, 100, 25) assert.Len(t, crossings, 1) assert.Equal(t, "Pas de Magnan", crossings[0].Pass.Name) assert.Less(t, crossings[0].DistanceToM, 10.0) @@ -46,7 +46,7 @@ func TestLoadMountainPassesAroundRideUsesExpandedBounds(t *testing.T) { []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) + passes, err := mountain_pass.LoadMountainPassesAroundRide(db, route.Coords(), 10_000) require.NoError(t, err) require.Len(t, passes, 3) assert.Equal(t, "On route edge", passes[0].Name) @@ -81,13 +81,13 @@ func TestDetectCrossingsIgnoresDistantPass(t *testing.T) { Name: "Col de la Faucille", Elevation: 1320, Coord: &geodist.Coord{Lat: 46.37, Lon: 6.02}, } - assert.Empty(t, mountain_pass.DetectCrossings(r, []mountain_pass.MountainPass{far}, 100, 25)) + assert.Empty(t, mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{far}, 100, 25)) } func TestDetectCrossingsIgnoresPassWithoutCoordinates(t *testing.T) { r := rideFromPoint(t, 43.62, 5.43, 447) noCoord := mountain_pass.MountainPass{Name: "Col de la Gatasse", Elevation: 120} - assert.Empty(t, mountain_pass.DetectCrossings(r, []mountain_pass.MountainPass{noCoord}, 100, 25)) + assert.Empty(t, mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{noCoord}, 100, 25)) } func TestDetectCrossingsRequiresElevationAgreement(t *testing.T) { @@ -96,7 +96,7 @@ func TestDetectCrossingsRequiresElevationAgreement(t *testing.T) { Name: "Pas de Magnan", Elevation: 900, Coord: &geodist.Coord{Lat: 43.62, Lon: 5.43}, } - assert.Empty(t, mountain_pass.DetectCrossings(r, []mountain_pass.MountainPass{pass}, 100, 25)) + assert.Empty(t, mountain_pass.DetectCrossings(r.Coords(), r.Distances(), r.Elevations(), []mountain_pass.MountainPass{pass}, 100, 25)) } func rideFromPoint(t *testing.T, latitude, longitude, elevation float64) ride.Ride { diff --git a/rideanalysis/analysis.go b/rideanalysis/analysis.go index 55f77b8..5e08e65 100644 --- a/rideanalysis/analysis.go +++ b/rideanalysis/analysis.go @@ -59,7 +59,7 @@ func Analyze(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClim return a.Segment.StartIndex() - b.Segment.StartIndex() }) result.Climbs = climbs - for _, crossing := range mountain_pass.DetectCrossings(parsed, passes, passCrossingRadiusM, passCrossingElevationToleranceM) { + for _, crossing := range mountain_pass.DetectCrossings(parsed.Coords(), parsed.Distances(), parsed.Elevations(), passes, passCrossingRadiusM, passCrossingElevationToleranceM) { result.Crossings = append(result.Crossings, crossing) } return result diff --git a/web/rides.go b/web/rides.go index 81017d9..71ab3fd 100644 --- a/web/rides.go +++ b/web/rides.go @@ -65,7 +65,7 @@ func (s *Server) handleRide(w http.ResponseWriter, r *http.Request) { }), w) return } - passes, err := mountain_pass.LoadMountainPassesAroundRide(s.db, parsed, rideDetailMountainPassMarginM) + passes, err := mountain_pass.LoadMountainPassesAroundRide(s.db, parsed.Coords(), rideDetailMountainPassMarginM) if err != nil { slog.Warn("Failed to load nearby mountain passes for ride detail", "ride_id", id, "error", err) } @@ -144,14 +144,15 @@ func (s *Server) handleCreateOfficialClimb(w http.ResponseWriter, r *http.Reques http.Error(w, "the recorded route is unavailable", http.StatusBadRequest) return } - if startIndex >= endIndex || endIndex >= parsed.Len() { + bounds := parsed.Coords() + if startIndex >= endIndex || endIndex >= len(bounds) { http.Error(w, "the official climb boundaries must be ordered route points", http.StatusBadRequest) return } created, err := official_climb.Create(s.db, official_climb.OfficialClimb{ Name: r.FormValue("name"), - StartCoord: parsed.Coord(startIndex), - EndCoord: parsed.Coord(endIndex), + StartCoord: bounds[startIndex], + EndCoord: bounds[endIndex], }) if err != nil { slog.Warn("Failed to create official climb", "ride_id", id, "error", err) diff --git a/web/views.go b/web/views.go index cfb31e5..8df860c 100644 --- a/web/views.go +++ b/web/views.go @@ -280,9 +280,9 @@ func officialClimbCreateURL(id int64) string { } func buildRideDetailView(item ride.StoredRide, parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) RideDetailView { - coordinates := make([][]float64, parsed.Len()) - for i := 0; i < parsed.Len(); i++ { - coordinate := parsed.Coord(i) + coords := parsed.Coords() + coordinates := make([][]float64, len(coords)) + for i, coordinate := range coords { coordinates[i] = []float64{coordinate.Lon, coordinate.Lat} } profile := buildRideProfile(parsed, passes, officialClimbs, matchPolicy) @@ -321,16 +321,18 @@ func buildRideMapPasses(passes []mountain_pass.MountainPass) []RideMapPass { } func buildRideProfile(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) RideProfile { + coords := parsed.Coords() + distances := parsed.Distances() + elevations := parsed.Elevations() profile := RideProfile{ - Points: make([]RideProfilePoint, parsed.Len()), + Points: make([]RideProfilePoint, len(coords)), Climbs: make([]RideProfileClimb, 0), Crossings: make([]RideProfileCrossing, 0), } - for i := 0; i < parsed.Len(); i++ { - coordinate := parsed.Coord(i) + for i, coordinate := range coords { profile.Points[i] = RideProfilePoint{ - DistanceKm: parsed.DistanceM(i) / 1000, - ElevationM: parsed.ElevationM(i), + DistanceKm: distances[i] / 1000, + ElevationM: elevations[i], Latitude: coordinate.Lat, Longitude: coordinate.Lon, } -- cgit v1.3