fix: extract travel_mode from activity segments preceding each visit in semanticSegments
This commit is contained in:
@@ -8,8 +8,12 @@ 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 = {
|
||||||
WALKING_ACTIVITIES = {'WALKING', 'ON_FOOT', 'RUNNING', 'ON_BICYCLE'}
|
'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 = {
|
SEMANTIC_TYPE_CATEGORY = {
|
||||||
'HOME': 'Home',
|
'HOME': 'Home',
|
||||||
@@ -33,11 +37,19 @@ def _distance_meters(lat1, lon1, lat2, lon2):
|
|||||||
return _haversine_miles(lat1, lon1, lat2, lon2) * 1609.34
|
return _haversine_miles(lat1, lon1, lat2, lon2) * 1609.34
|
||||||
|
|
||||||
|
|
||||||
def _get_travel_mode(activity_type):
|
def _activity_type_to_travel_mode(activity_type):
|
||||||
if activity_type in VEHICLE_ACTIVITIES:
|
"""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'
|
return 'driving'
|
||||||
if activity_type in WALKING_ACTIVITIES:
|
if t == 'IN_RAIL_VEHICLE':
|
||||||
|
return 'transit'
|
||||||
|
if t in WALKING_ACTIVITIES:
|
||||||
return 'walking'
|
return 'walking'
|
||||||
|
if t in CYCLING_ACTIVITIES:
|
||||||
|
return 'cycling'
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
@@ -147,7 +159,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
|
||||||
|
|
||||||
# Skip existing records
|
# Skip existing records (deduplicate by arrived_at)
|
||||||
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')
|
||||||
@@ -195,22 +207,47 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _parse_semantic_timeline(self, data):
|
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 = []
|
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')
|
visit = seg.get('visit')
|
||||||
if not visit:
|
if not visit:
|
||||||
|
# timelinePath or unknown — ignore but keep pending travel mode
|
||||||
continue
|
continue
|
||||||
|
|
||||||
start_ts = _parse_ts(seg.get('startTime'))
|
start_ts = _parse_ts(seg.get('startTime'))
|
||||||
end_ts = _parse_ts(seg.get('endTime'))
|
end_ts = _parse_ts(seg.get('endTime'))
|
||||||
if not start_ts or not end_ts:
|
if not start_ts or not end_ts:
|
||||||
|
pending_travel_mode = 'unknown'
|
||||||
continue
|
continue
|
||||||
|
|
||||||
candidate = visit.get('topCandidate', {})
|
candidate = visit.get('topCandidate', {})
|
||||||
semantic_type = candidate.get('semanticType', '')
|
semantic_type = candidate.get('semanticType', '')
|
||||||
latlng_str = candidate.get('placeLocation', {}).get('latLng', '')
|
latlng_str = candidate.get('placeLocation', {}).get('latLng', '')
|
||||||
lat, lng = _parse_latlng(latlng_str) if latlng_str else (None, None)
|
lat, lng = _parse_latlng(latlng_str) if latlng_str else (None, None)
|
||||||
|
|
||||||
category = SEMANTIC_TYPE_CATEGORY.get(semantic_type, '')
|
category = SEMANTIC_TYPE_CATEGORY.get(semantic_type, '')
|
||||||
if not category and semantic_type:
|
if not category and semantic_type:
|
||||||
category = semantic_type.replace('_', ' ').title()
|
category = semantic_type.replace('_', ' ').title()
|
||||||
|
|
||||||
stops.append({
|
stops.append({
|
||||||
'arrived_at': start_ts,
|
'arrived_at': start_ts,
|
||||||
'departed_at': end_ts,
|
'departed_at': end_ts,
|
||||||
@@ -218,11 +255,16 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
'lng': lng,
|
'lng': lng,
|
||||||
'place_name': '',
|
'place_name': '',
|
||||||
'category': category,
|
'category': category,
|
||||||
'travel_mode': 'unknown',
|
'travel_mode': pending_travel_mode,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Reset after consuming for the next visit
|
||||||
|
pending_travel_mode = 'unknown'
|
||||||
|
|
||||||
return stops
|
return stops
|
||||||
|
|
||||||
def _parse_raw_timeline(self, data, proximity_meters=200):
|
def _parse_raw_timeline(self, data, proximity_meters=200):
|
||||||
|
"""Parse Timeline Edits.json (raw GPS signal format from Google Takeout)."""
|
||||||
positions = []
|
positions = []
|
||||||
activities = []
|
activities = []
|
||||||
for entry in data.get('timelineEdits', []):
|
for entry in data.get('timelineEdits', []):
|
||||||
@@ -256,7 +298,7 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
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 _activity_type_to_travel_mode(counts.most_common(1)[0][0])
|
||||||
|
|
||||||
stops = []
|
stops = []
|
||||||
current_cluster = [positions[0]]
|
current_cluster = [positions[0]]
|
||||||
|
|||||||
Reference in New Issue
Block a user