diff --git a/bincio/segments/detect.py b/bincio/segments/detect.py index 828d879..57f24d4 100644 --- a/bincio/segments/detect.py +++ b/bincio/segments/detect.py @@ -25,10 +25,17 @@ CONFORMANCE_MAX_FRAC = 0.30 # max fraction of interior points allowed to deviat # Rejects false matches from long circuit rides where the track passes the # segment start early and the segment end hours later. _MIN_SPEED_MS: dict[str, float] = { - 'cycling': 1.0, # ~3.6 km/h — slowest realistic climb - 'running': 0.5, # ~1.8 km/h + 'cycling': 2.0, # ~7.2 km/h — below any realistic cyclist even on brutal climbs + 'running': 0.8, # ~2.9 km/h } -_MIN_SPEED_DEFAULT = 0.2 # hiking / walking / unknown +_MIN_SPEED_DEFAULT = 0.3 # hiking / walking / unknown + +# Maximum geometric speed per sport in m/s — rejects GPS glitch matches. +_MAX_SPEED_MS: dict[str, float] = { + 'cycling': 30.0, # ~108 km/h + 'running': 12.0, # ~43 km/h +} +_MAX_SPEED_DEFAULT = 20.0 # ── fast distance approximation ─────────────────────────────────────────────── @@ -207,7 +214,10 @@ def _extract_effort( ) -> SegmentEffort: elapsed_s = track.times[j] - track.times[i] started_at = (track.started_at + timedelta(seconds=track.times[i])).replace(microsecond=0) - avg_speed = _avg_nonnull(track.speeds, i, j) + # Always derive avg speed from segment distance / elapsed time. Device-recorded + # speed is unreliable across formats (m/s vs km/h in older FIT files) and + # averaging instantaneous GPS speed over a slice gives different results anyway. + avg_speed = (seg.distance_m / elapsed_s * 3.6) if elapsed_s > 0 else None avg_hr_raw = _avg_nonnull(track.hrs, i, j) avg_hr = int(round(avg_hr_raw)) if avg_hr_raw is not None else None avg_pwr_raw = _avg_nonnull(track.powers, i, j) @@ -259,12 +269,13 @@ def detect_one(track: ActivityTrack, seg: Segment) -> list[SegmentEffort]: # No end found — no more efforts possible starting at or after start_idx. break - # Reject implausibly slow matches (e.g. circuit rides matching start - # early and end hours later on a second pass through the area). + # Reject implausibly slow or fast matches. elapsed = track.times[end_idx] - track.times[start_idx] if elapsed > 0: + geo_speed = seg.distance_m / elapsed min_speed = _MIN_SPEED_MS.get(track.sport, _MIN_SPEED_DEFAULT) - if seg.distance_m / elapsed < min_speed: + max_speed = _MAX_SPEED_MS.get(track.sport, _MAX_SPEED_DEFAULT) + if geo_speed < min_speed or geo_speed > max_speed: search_from = start_idx + 1 continue