diff --git a/work_trace/wizards/wt_import_timeline_wizard.py b/work_trace/wizards/wt_import_timeline_wizard.py index f9c2a722..689d77c9 100644 --- a/work_trace/wizards/wt_import_timeline_wizard.py +++ b/work_trace/wizards/wt_import_timeline_wizard.py @@ -114,27 +114,33 @@ def _max_speed_from_path(timeline_path): return max_speed -def _speed_from_activity_segment(activity, seg_start_ts, seg_end_ts): - """Calculate implied speed from an activity segment's start/end coords and timestamps. +MIN_FLIGHT_DISTANCE_MILES = 50 # Shorter than this cannot be a flight - Flights appear as UNKNOWN_ACTIVITY_TYPE with valid start/end latLng but no - timelinePath GPS trail, so we derive speed directly from the segment geometry. +def _speed_from_activity_segment(activity, seg_start_ts, seg_end_ts): + """Detect flights from an activity segment's start/end coords and timestamps. + + Flights show up as UNKNOWN_ACTIVITY_TYPE with widely-separated start/end latLng. + We require at least 50 miles distance to avoid mis-classifying GPS drift as flying + (short jumps can imply absurdly high speeds due to imprecise timestamps). + + Returns (distance_miles, speed_mph) or (0, 0) if not usable. """ start_latlng = activity.get('start', {}).get('latLng', '') end_latlng = activity.get('end', {}).get('latLng', '') if not start_latlng or not end_latlng: - return 0.0 + return 0.0, 0.0 slat, slng = _parse_latlng(start_latlng) elat, elng = _parse_latlng(end_latlng) if slat is None or elat is None: - return 0.0 + return 0.0, 0.0 if not seg_start_ts or not seg_end_ts: - return 0.0 + return 0.0, 0.0 dt_hours = (seg_end_ts - seg_start_ts).total_seconds() / 3600.0 if dt_hours <= 0: - return 0.0 + return 0.0, 0.0 dist_miles = _haversine_miles(slat, slng, elat, elng) - return dist_miles / dt_hours + speed = dist_miles / dt_hours + return dist_miles, speed class WtImportTimelineWizard(models.TransientModel): @@ -300,14 +306,13 @@ class WtImportTimelineWizard(models.TransientModel): # Explicit type wins (driving, walking, transit, cycling) pending_mode = mode else: - # UNKNOWN type — infer from segment distance / elapsed time + # UNKNOWN type — only infer flying if distance is large enough + # to rule out GPS drift (short jumps can look fast due to imprecise timestamps) seg_start = _parse_ts(seg.get('startTime')) seg_end = _parse_ts(seg.get('endTime')) - speed = _speed_from_activity_segment(activity, seg_start, seg_end) - if speed > 0: - inferred = _speed_to_travel_mode(speed) - if inferred != 'unknown': - pending_mode = inferred + dist, speed = _speed_from_activity_segment(activity, seg_start, seg_end) + if dist >= MIN_FLIGHT_DISTANCE_MILES and speed > 150: + pending_mode = 'flying' continue # --- visit segment: the stop we record ---