metrics: fall back to GPS-derived speed in compute_best_efforts when device speed absent

FIT files from older devices (and GPX/TCX files) often omit the speed field.
The sliding-window best-effort algorithm was treating all such points as speed=0,
so no records were ever produced for these activities.

Fix: when p.speed_kmh is None but consecutive lat/lon are available, compute
haversine segment speed and spread it evenly across the 1Hz interval slots.
This mirrors what _gps_stats already does for avg/max speed computation.
This commit is contained in:
Davide Scaini
2026-05-14 09:16:01 +02:00
parent c85d2edf39
commit a10164b932
+18 -2
View File
@@ -181,16 +181,32 @@ def compute_best_efforts(
# Build dense 1 Hz speed (km/h) and elevation (m) arrays with gap zero-filling. # Build dense 1 Hz speed (km/h) and elevation (m) arrays with gap zero-filling.
# Zero-filling speed gaps (0 km/h) prevents best-effort windows from spanning # Zero-filling speed gaps (0 km/h) prevents best-effort windows from spanning
# recording pauses and producing artificially fast times. # recording pauses and producing artificially fast times.
# When the device didn't record speed (common in older FIT files), fall back to
# GPS-derived speed: spread the haversine segment speed evenly across the interval
# so the sliding window accumulates the correct distance.
sparse_speed: dict[int, float] = {} sparse_speed: dict[int, float] = {}
sparse_ele: dict[int, Optional[float]] = {} sparse_ele: dict[int, Optional[float]] = {}
last_t = -1 last_t = -1
_prev: Optional[DataPoint] = None
for p in pts: for p in pts:
t = int((p.timestamp - started_at).total_seconds()) t = int((p.timestamp - started_at).total_seconds())
if t < 0 or t == last_t: if t < 0 or t == last_t:
continue continue
last_t = t
sparse_speed[t] = p.speed_kmh if p.speed_kmh is not None else 0.0
sparse_ele[t] = p.elevation_m sparse_ele[t] = p.elevation_m
if p.speed_kmh is not None:
sparse_speed[t] = p.speed_kmh
elif (_prev is not None
and _prev.lat is not None and _prev.lon is not None
and p.lat is not None and p.lon is not None):
dt_s = t - last_t
seg_m = _haversine_m(_prev.lat, _prev.lon, p.lat, p.lon)
seg_kmh = (seg_m / dt_s) * 3.6
for slot in range(last_t, t):
sparse_speed[slot] = seg_kmh
else:
sparse_speed[t] = 0.0
last_t = t
_prev = p
if not sparse_speed: if not sparse_speed:
return None, None return None, None