From 7c65a34fce4ff3ff2089fd40be7e0cd1c0957233 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Sat, 14 Mar 2026 02:59:53 +0000 Subject: [PATCH] fix: extract travel_mode from activity segments preceding each visit in semanticSegments --- .../wizards/wt_import_timeline_wizard.py | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/work_trace/wizards/wt_import_timeline_wizard.py b/work_trace/wizards/wt_import_timeline_wizard.py index b6f83779..11f4ddf6 100644 --- a/work_trace/wizards/wt_import_timeline_wizard.py +++ b/work_trace/wizards/wt_import_timeline_wizard.py @@ -8,8 +8,12 @@ from datetime import datetime, timedelta from math import radians, sin, cos, sqrt, atan2 from collections import Counter -VEHICLE_ACTIVITIES = {'IN_VEHICLE', 'IN_ROAD_VEHICLE', 'IN_RAIL_VEHICLE', 'IN_TWO_WHEELER_VEHICLE'} -WALKING_ACTIVITIES = {'WALKING', 'ON_FOOT', 'RUNNING', 'ON_BICYCLE'} +VEHICLE_ACTIVITIES = { + 'IN_VEHICLE', 'IN_ROAD_VEHICLE', 'IN_RAIL_VEHICLE', + 'IN_TWO_WHEELER_VEHICLE', 'IN_PASSENGER_VEHICLE', +} +WALKING_ACTIVITIES = {'WALKING', 'ON_FOOT', 'RUNNING'} +CYCLING_ACTIVITIES = {'ON_BICYCLE', 'CYCLING'} SEMANTIC_TYPE_CATEGORY = { 'HOME': 'Home', @@ -33,11 +37,19 @@ def _distance_meters(lat1, lon1, lat2, lon2): return _haversine_miles(lat1, lon1, lat2, lon2) * 1609.34 -def _get_travel_mode(activity_type): - if activity_type in VEHICLE_ACTIVITIES: +def _activity_type_to_travel_mode(activity_type): + """Map Google activity type string to Odoo travel_mode selection value.""" + if not activity_type: + return 'unknown' + t = activity_type.upper() + if t in VEHICLE_ACTIVITIES: return 'driving' - if activity_type in WALKING_ACTIVITIES: + if t == 'IN_RAIL_VEHICLE': + return 'transit' + if t in WALKING_ACTIVITIES: return 'walking' + if t in CYCLING_ACTIVITIES: + return 'cycling' return 'unknown' @@ -147,7 +159,7 @@ class WtImportTimelineWizard(models.TransientModel): stop['distance_from_previous'] = 0.0 stop['travel_time_from_previous'] = 0.0 - # Skip existing records + # Skip existing records (deduplicate by arrived_at) LocationLog = self.env['wt.location.log'] existing = set( r.arrived_at.strftime('%Y-%m-%d %H:%M:%S') @@ -195,22 +207,47 @@ class WtImportTimelineWizard(models.TransientModel): } def _parse_semantic_timeline(self, data): + """Parse Timeline.json (semanticSegments format from Android export). + + Iterates segments in order. Activity segments (travel between stops) + are captured to provide travel_mode for the following visit segment. + """ stops = [] - for seg in data.get('semanticSegments', []): + segments = data.get('semanticSegments', []) + pending_travel_mode = 'unknown' + + for seg in segments: + # --- Activity / travel segment --- + activity = seg.get('activity') + if activity: + top = activity.get('topCandidate', {}) + activity_type = top.get('type', '') + mode = _activity_type_to_travel_mode(activity_type) + if mode != 'unknown': + pending_travel_mode = mode + continue + + # --- Visit / stop segment --- visit = seg.get('visit') if not visit: + # timelinePath or unknown — ignore but keep pending travel mode continue + start_ts = _parse_ts(seg.get('startTime')) end_ts = _parse_ts(seg.get('endTime')) if not start_ts or not end_ts: + pending_travel_mode = 'unknown' continue + candidate = visit.get('topCandidate', {}) semantic_type = candidate.get('semanticType', '') latlng_str = candidate.get('placeLocation', {}).get('latLng', '') lat, lng = _parse_latlng(latlng_str) if latlng_str else (None, None) + category = SEMANTIC_TYPE_CATEGORY.get(semantic_type, '') if not category and semantic_type: category = semantic_type.replace('_', ' ').title() + stops.append({ 'arrived_at': start_ts, 'departed_at': end_ts, @@ -218,11 +255,16 @@ class WtImportTimelineWizard(models.TransientModel): 'lng': lng, 'place_name': '', 'category': category, - 'travel_mode': 'unknown', + 'travel_mode': pending_travel_mode, }) + + # Reset after consuming for the next visit + pending_travel_mode = 'unknown' + return stops def _parse_raw_timeline(self, data, proximity_meters=200): + """Parse Timeline Edits.json (raw GPS signal format from Google Takeout).""" positions = [] activities = [] for entry in data.get('timelineEdits', []): @@ -256,7 +298,7 @@ class WtImportTimelineWizard(models.TransientModel): if not window: return 'unknown' counts = Counter(a['type'] for a in window) - return _get_travel_mode(counts.most_common(1)[0][0]) + return _activity_type_to_travel_mode(counts.most_common(1)[0][0]) stops = [] current_cluster = [positions[0]]