From 7351c558ce3ec15ffac86dd15146d04022629517 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Sat, 14 Mar 2026 00:29:47 +0000 Subject: [PATCH] Update wizard: populate category from activity type, add cycling mode --- .../wizards/wt_import_timeline_wizard.py | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/work_trace/wizards/wt_import_timeline_wizard.py b/work_trace/wizards/wt_import_timeline_wizard.py index dd9361e4..ac50379b 100644 --- a/work_trace/wizards/wt_import_timeline_wizard.py +++ b/work_trace/wizards/wt_import_timeline_wizard.py @@ -7,10 +7,26 @@ 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'} + +CATEGORY_LABELS = { + 'IN_PASSENGER_VEHICLE': 'In Vehicle', + 'IN_VEHICLE': 'In Vehicle', + 'IN_ROAD_VEHICLE': 'In Vehicle', + 'IN_RAIL_VEHICLE': 'Rail / Transit', + 'IN_TWO_WHEELER_VEHICLE': 'Motorcycle / Scooter', + 'WALKING': 'Walking', + 'ON_FOOT': 'Walking', + 'RUNNING': 'Running', + 'ON_BICYCLE': 'Cycling', + 'STILL': 'Stationary', + 'UNKNOWN': 'Unknown', + 'EXITING_VEHICLE': 'In Vehicle', + 'TILTING': 'Unknown', +} -# Positions within this distance (meters) are considered the same location PROXIMITY_METERS = 200 @@ -31,16 +47,18 @@ def _get_travel_mode(activity_type): return 'driving' if activity_type in WALKING_ACTIVITIES: return 'walking' + if activity_type in CYCLING_ACTIVITIES: + return 'cycling' return 'unknown' -def _dominant_travel_mode(activities, start_ts, end_ts): - """Get dominant travel mode from activity records between two timestamps.""" +def _dominant_activity(activities, start_ts, end_ts): + """Get dominant activity type between two timestamps.""" window = [a for a in activities if start_ts <= a['ts'] <= end_ts] if not window: - return 'unknown' + return 'UNKNOWN' counts = Counter(a['type'] for a in window) - return _get_travel_mode(counts.most_common(1)[0][0]) + return counts.most_common(1)[0][0] class WtImportTimelineWizard(models.TransientModel): @@ -99,7 +117,7 @@ class WtImportTimelineWizard(models.TransientModel): stop['distance_from_previous'] = 0.0 stop['travel_time_from_previous'] = 0.0 - # Get existing arrived_at timestamps to avoid duplicates + # Skip duplicates LocationLog = self.env['wt.location.log'] existing = set( r.arrived_at.strftime('%Y-%m-%d %H:%M:%S') @@ -125,6 +143,7 @@ class WtImportTimelineWizard(models.TransientModel): 'latitude': stop['lat'], 'longitude': stop['lng'], 'travel_mode': stop.get('travel_mode', 'unknown'), + 'category': stop.get('category', ''), 'distance_from_previous': stop['distance_from_previous'], 'travel_time_from_previous': stop['travel_time_from_previous'], 'source': 'google_timeline', @@ -141,7 +160,7 @@ class WtImportTimelineWizard(models.TransientModel): return { 'type': 'ir.actions.act_window', - 'name': _('Imported %d new stops (%d skipped as duplicates)') % (len(created_ids), skipped), + 'name': _('Imported %d new stops (%d skipped)') % (len(created_ids), skipped), 'res_model': 'wt.location.log', 'view_mode': 'list,form', 'domain': [('id', 'in', created_ids)], @@ -151,11 +170,8 @@ class WtImportTimelineWizard(models.TransientModel): def _parse_timeline(self, data, proximity_meters=200): """ Parse Google Timeline Edits JSON into location stops. - - Google records positions primarily when the device is stationary. - We cluster consecutive positions within proximity_meters into a single stop. - The gap between clusters = travel time. - Activity records between clusters determine the travel mode. + Positions represent stationary moments — cluster by proximity. + Activity records between clusters give travel category/mode. """ positions = [] activities = [] @@ -188,7 +204,7 @@ class WtImportTimelineWizard(models.TransientModel): positions.sort(key=lambda x: x['ts']) activities.sort(key=lambda x: x['ts']) - # Cluster positions by proximity + # Cluster consecutive positions within proximity_meters stops = [] current_cluster = [positions[0]] @@ -197,21 +213,17 @@ class WtImportTimelineWizard(models.TransientModel): dist = _distance_meters(prev['lat'], prev['lng'], pos['lat'], pos['lng']) if dist <= proximity_meters: - # Same location — extend current cluster current_cluster.append(pos) else: - # New location — save current cluster as a stop avg_lat = sum(p['lat'] for p in current_cluster) / len(current_cluster) avg_lng = sum(p['lng'] for p in current_cluster) / len(current_cluster) - # Departed = last position in cluster - # Next arrived = first position in new cluster - # Travel mode = dominant activity between the two - travel_mode = _dominant_travel_mode( - activities, - current_cluster[-1]['ts'], - pos['ts'] + # Activity between this stop and next = travel mode + act_type = _dominant_activity( + activities, current_cluster[-1]['ts'], pos['ts'] ) + travel_mode = _get_travel_mode(act_type) + category = CATEGORY_LABELS.get(act_type, act_type.replace('_', ' ').title()) stops.append({ 'arrived_at': current_cluster[0]['ts'], @@ -219,10 +231,11 @@ class WtImportTimelineWizard(models.TransientModel): 'lat': avg_lat, 'lng': avg_lng, 'travel_mode': travel_mode, + 'category': category, }) current_cluster = [pos] - # Handle last cluster + # Last cluster if current_cluster: avg_lat = sum(p['lat'] for p in current_cluster) / len(current_cluster) avg_lng = sum(p['lng'] for p in current_cluster) / len(current_cluster) @@ -232,14 +245,13 @@ class WtImportTimelineWizard(models.TransientModel): 'lat': avg_lat, 'lng': avg_lng, 'travel_mode': 'unknown', + 'category': '', }) - # For single-position stops (arrived == departed), estimate duration - # using half the gap to the next stop + # Estimate duration for single-position stops for i, stop in enumerate(stops): - if stop['arrived_at'] == stop['departed_at']: - if i + 1 < len(stops): - gap = (stops[i + 1]['arrived_at'] - stop['arrived_at']).total_seconds() - stop['departed_at'] = stop['arrived_at'] + timedelta(seconds=gap / 2) + if stop['arrived_at'] == stop['departed_at'] and i + 1 < len(stops): + gap = (stops[i + 1]['arrived_at'] - stop['arrived_at']).total_seconds() + stop['departed_at'] = stop['arrived_at'] + timedelta(seconds=gap / 2) return stops