Update wizard: populate category from activity type, add cycling mode
This commit is contained in:
@@ -7,10 +7,26 @@ from datetime import datetime, timedelta
|
|||||||
from math import radians, sin, cos, sqrt, atan2
|
from math import radians, sin, cos, sqrt, atan2
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
VEHICLE_ACTIVITIES = {'IN_VEHICLE', 'IN_ROAD_VEHICLE', 'IN_RAIL_VEHICLE', 'IN_TWO_WHEELER_VEHICLE'}
|
VEHICLE_ACTIVITIES = {'IN_VEHICLE', 'IN_ROAD_VEHICLE', 'IN_RAIL_VEHICLE', 'IN_TWO_WHEELER_VEHICLE', 'IN_PASSENGER_VEHICLE'}
|
||||||
WALKING_ACTIVITIES = {'WALKING', 'ON_FOOT', 'RUNNING', 'ON_BICYCLE'}
|
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
|
PROXIMITY_METERS = 200
|
||||||
|
|
||||||
|
|
||||||
@@ -31,16 +47,18 @@ def _get_travel_mode(activity_type):
|
|||||||
return 'driving'
|
return 'driving'
|
||||||
if activity_type in WALKING_ACTIVITIES:
|
if activity_type in WALKING_ACTIVITIES:
|
||||||
return 'walking'
|
return 'walking'
|
||||||
|
if activity_type in CYCLING_ACTIVITIES:
|
||||||
|
return 'cycling'
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
def _dominant_travel_mode(activities, start_ts, end_ts):
|
def _dominant_activity(activities, start_ts, end_ts):
|
||||||
"""Get dominant travel mode from activity records between two timestamps."""
|
"""Get dominant activity type between two timestamps."""
|
||||||
window = [a for a in activities if start_ts <= a['ts'] <= end_ts]
|
window = [a for a in activities if start_ts <= a['ts'] <= end_ts]
|
||||||
if not window:
|
if not window:
|
||||||
return 'unknown'
|
return 'UNKNOWN'
|
||||||
counts = Counter(a['type'] for a in window)
|
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):
|
class WtImportTimelineWizard(models.TransientModel):
|
||||||
@@ -99,7 +117,7 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
stop['distance_from_previous'] = 0.0
|
stop['distance_from_previous'] = 0.0
|
||||||
stop['travel_time_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']
|
LocationLog = self.env['wt.location.log']
|
||||||
existing = set(
|
existing = set(
|
||||||
r.arrived_at.strftime('%Y-%m-%d %H:%M:%S')
|
r.arrived_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
@@ -125,6 +143,7 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
'latitude': stop['lat'],
|
'latitude': stop['lat'],
|
||||||
'longitude': stop['lng'],
|
'longitude': stop['lng'],
|
||||||
'travel_mode': stop.get('travel_mode', 'unknown'),
|
'travel_mode': stop.get('travel_mode', 'unknown'),
|
||||||
|
'category': stop.get('category', ''),
|
||||||
'distance_from_previous': stop['distance_from_previous'],
|
'distance_from_previous': stop['distance_from_previous'],
|
||||||
'travel_time_from_previous': stop['travel_time_from_previous'],
|
'travel_time_from_previous': stop['travel_time_from_previous'],
|
||||||
'source': 'google_timeline',
|
'source': 'google_timeline',
|
||||||
@@ -141,7 +160,7 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'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',
|
'res_model': 'wt.location.log',
|
||||||
'view_mode': 'list,form',
|
'view_mode': 'list,form',
|
||||||
'domain': [('id', 'in', created_ids)],
|
'domain': [('id', 'in', created_ids)],
|
||||||
@@ -151,11 +170,8 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
def _parse_timeline(self, data, proximity_meters=200):
|
def _parse_timeline(self, data, proximity_meters=200):
|
||||||
"""
|
"""
|
||||||
Parse Google Timeline Edits JSON into location stops.
|
Parse Google Timeline Edits JSON into location stops.
|
||||||
|
Positions represent stationary moments — cluster by proximity.
|
||||||
Google records positions primarily when the device is stationary.
|
Activity records between clusters give travel category/mode.
|
||||||
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 = []
|
positions = []
|
||||||
activities = []
|
activities = []
|
||||||
@@ -188,7 +204,7 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
positions.sort(key=lambda x: x['ts'])
|
positions.sort(key=lambda x: x['ts'])
|
||||||
activities.sort(key=lambda x: x['ts'])
|
activities.sort(key=lambda x: x['ts'])
|
||||||
|
|
||||||
# Cluster positions by proximity
|
# Cluster consecutive positions within proximity_meters
|
||||||
stops = []
|
stops = []
|
||||||
current_cluster = [positions[0]]
|
current_cluster = [positions[0]]
|
||||||
|
|
||||||
@@ -197,21 +213,17 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
dist = _distance_meters(prev['lat'], prev['lng'], pos['lat'], pos['lng'])
|
dist = _distance_meters(prev['lat'], prev['lng'], pos['lat'], pos['lng'])
|
||||||
|
|
||||||
if dist <= proximity_meters:
|
if dist <= proximity_meters:
|
||||||
# Same location — extend current cluster
|
|
||||||
current_cluster.append(pos)
|
current_cluster.append(pos)
|
||||||
else:
|
else:
|
||||||
# New location — save current cluster as a stop
|
|
||||||
avg_lat = sum(p['lat'] for p in current_cluster) / len(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)
|
avg_lng = sum(p['lng'] for p in current_cluster) / len(current_cluster)
|
||||||
|
|
||||||
# Departed = last position in cluster
|
# Activity between this stop and next = travel mode
|
||||||
# Next arrived = first position in new cluster
|
act_type = _dominant_activity(
|
||||||
# Travel mode = dominant activity between the two
|
activities, current_cluster[-1]['ts'], pos['ts']
|
||||||
travel_mode = _dominant_travel_mode(
|
|
||||||
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({
|
stops.append({
|
||||||
'arrived_at': current_cluster[0]['ts'],
|
'arrived_at': current_cluster[0]['ts'],
|
||||||
@@ -219,10 +231,11 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
'lat': avg_lat,
|
'lat': avg_lat,
|
||||||
'lng': avg_lng,
|
'lng': avg_lng,
|
||||||
'travel_mode': travel_mode,
|
'travel_mode': travel_mode,
|
||||||
|
'category': category,
|
||||||
})
|
})
|
||||||
current_cluster = [pos]
|
current_cluster = [pos]
|
||||||
|
|
||||||
# Handle last cluster
|
# Last cluster
|
||||||
if current_cluster:
|
if current_cluster:
|
||||||
avg_lat = sum(p['lat'] for p in current_cluster) / len(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)
|
avg_lng = sum(p['lng'] for p in current_cluster) / len(current_cluster)
|
||||||
@@ -232,14 +245,13 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
'lat': avg_lat,
|
'lat': avg_lat,
|
||||||
'lng': avg_lng,
|
'lng': avg_lng,
|
||||||
'travel_mode': 'unknown',
|
'travel_mode': 'unknown',
|
||||||
|
'category': '',
|
||||||
})
|
})
|
||||||
|
|
||||||
# For single-position stops (arrived == departed), estimate duration
|
# Estimate duration for single-position stops
|
||||||
# using half the gap to the next stop
|
|
||||||
for i, stop in enumerate(stops):
|
for i, stop in enumerate(stops):
|
||||||
if stop['arrived_at'] == stop['departed_at']:
|
if stop['arrived_at'] == stop['departed_at'] and i + 1 < len(stops):
|
||||||
if i + 1 < len(stops):
|
gap = (stops[i + 1]['arrived_at'] - stop['arrived_at']).total_seconds()
|
||||||
gap = (stops[i + 1]['arrived_at'] - stop['arrived_at']).total_seconds()
|
stop['departed_at'] = stop['arrived_at'] + timedelta(seconds=gap / 2)
|
||||||
stop['departed_at'] = stop['arrived_at'] + timedelta(seconds=gap / 2)
|
|
||||||
|
|
||||||
return stops
|
return stops
|
||||||
|
|||||||
Reference in New Issue
Block a user