fix: detect flights from activity segment coords; fix category for UNKNOWN/ALIASED_LOCATION
This commit is contained in:
@@ -21,7 +21,8 @@ SEMANTIC_TYPE_CATEGORY = {
|
|||||||
'WORK': 'Work',
|
'WORK': 'Work',
|
||||||
'INFERRED_WORK': 'Work',
|
'INFERRED_WORK': 'Work',
|
||||||
'SEARCHED_ADDRESS': 'Searched Address',
|
'SEARCHED_ADDRESS': 'Searched Address',
|
||||||
'UNKNOWN': '',
|
'ALIASED_LOCATION': 'Saved Place',
|
||||||
|
'UNKNOWN': 'Unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -45,8 +46,6 @@ def _activity_type_to_travel_mode(activity_type):
|
|||||||
return None
|
return None
|
||||||
if t == 'IN_RAIL_VEHICLE':
|
if t == 'IN_RAIL_VEHICLE':
|
||||||
return 'transit'
|
return 'transit'
|
||||||
if t == 'IN_PASSENGER_VEHICLE' or t == 'FLYING':
|
|
||||||
return 'flying'
|
|
||||||
if t in VEHICLE_ACTIVITIES:
|
if t in VEHICLE_ACTIVITIES:
|
||||||
return 'driving'
|
return 'driving'
|
||||||
if t in WALKING_ACTIVITIES:
|
if t in WALKING_ACTIVITIES:
|
||||||
@@ -57,7 +56,7 @@ def _activity_type_to_travel_mode(activity_type):
|
|||||||
|
|
||||||
|
|
||||||
def _speed_to_travel_mode(max_speed_mph):
|
def _speed_to_travel_mode(max_speed_mph):
|
||||||
"""Infer travel mode from maximum speed observed along a path."""
|
"""Infer travel mode from maximum speed observed."""
|
||||||
if max_speed_mph > 150:
|
if max_speed_mph > 150:
|
||||||
return 'flying'
|
return 'flying'
|
||||||
if max_speed_mph > 15:
|
if max_speed_mph > 15:
|
||||||
@@ -115,6 +114,29 @@ def _max_speed_from_path(timeline_path):
|
|||||||
return max_speed
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
slat, slng = _parse_latlng(start_latlng)
|
||||||
|
elat, elng = _parse_latlng(end_latlng)
|
||||||
|
if slat is None or elat is None:
|
||||||
|
return 0.0
|
||||||
|
if not seg_start_ts or not seg_end_ts:
|
||||||
|
return 0.0
|
||||||
|
dt_hours = (seg_end_ts - seg_start_ts).total_seconds() / 3600.0
|
||||||
|
if dt_hours <= 0:
|
||||||
|
return 0.0
|
||||||
|
dist_miles = _haversine_miles(slat, slng, elat, elng)
|
||||||
|
return dist_miles / dt_hours
|
||||||
|
|
||||||
|
|
||||||
class WtImportTimelineWizard(models.TransientModel):
|
class WtImportTimelineWizard(models.TransientModel):
|
||||||
_name = 'wt.import.timeline.wizard'
|
_name = 'wt.import.timeline.wizard'
|
||||||
_description = 'Import Google Timeline'
|
_description = 'Import Google Timeline'
|
||||||
@@ -250,34 +272,45 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
def _parse_semantic_timeline(self, data):
|
def _parse_semantic_timeline(self, data):
|
||||||
"""Parse Timeline.json (semanticSegments format).
|
"""Parse Timeline.json (semanticSegments format).
|
||||||
|
|
||||||
Travel mode is determined in priority order:
|
Travel mode priority:
|
||||||
1. activity segment topCandidate.type (if not UNKNOWN)
|
1. Explicit activity type (if not UNKNOWN)
|
||||||
2. max speed calculated from preceding timelinePath points
|
2. Speed inferred from activity segment start/end coords + timestamps
|
||||||
3. 'unknown' as fallback
|
(catches flights that have no timelinePath GPS trail)
|
||||||
|
3. Max speed calculated from preceding timelinePath points
|
||||||
|
4. 'unknown' as fallback
|
||||||
"""
|
"""
|
||||||
stops = []
|
stops = []
|
||||||
segments = data.get('semanticSegments', [])
|
segments = data.get('semanticSegments', [])
|
||||||
|
|
||||||
# We scan segments in order, accumulating travel signals between visits
|
|
||||||
pending_mode = 'unknown'
|
pending_mode = 'unknown'
|
||||||
pending_path_points = [] # all timelinePath points seen since last visit
|
pending_path_points = []
|
||||||
|
|
||||||
for seg in segments:
|
for seg in segments:
|
||||||
# --- timelinePath: collect GPS path points for speed inference ---
|
# --- timelinePath: collect GPS points for speed inference ---
|
||||||
if 'timelinePath' in seg:
|
if 'timelinePath' in seg:
|
||||||
pending_path_points.extend(seg['timelinePath'])
|
pending_path_points.extend(seg['timelinePath'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# --- activity segment: try to get explicit travel mode ---
|
# --- activity segment ---
|
||||||
if 'activity' in seg:
|
if 'activity' in seg:
|
||||||
activity = seg['activity']
|
activity = seg['activity']
|
||||||
top = activity.get('topCandidate', {})
|
top = activity.get('topCandidate', {})
|
||||||
mode = _activity_type_to_travel_mode(top.get('type', ''))
|
mode = _activity_type_to_travel_mode(top.get('type', ''))
|
||||||
if mode:
|
if mode:
|
||||||
|
# Explicit type wins (driving, walking, transit, cycling)
|
||||||
pending_mode = mode
|
pending_mode = mode
|
||||||
|
else:
|
||||||
|
# UNKNOWN type — infer from segment distance / elapsed time
|
||||||
|
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
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# --- visit segment: the stop we care about ---
|
# --- visit segment: the stop we record ---
|
||||||
if 'visit' in seg:
|
if 'visit' in seg:
|
||||||
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'))
|
||||||
@@ -287,16 +320,16 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
candidate = seg['visit'].get('topCandidate', {})
|
candidate = seg['visit'].get('topCandidate', {})
|
||||||
semantic_type = candidate.get('semanticType', '')
|
semantic_type = candidate.get('semanticType', 'UNKNOWN')
|
||||||
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(
|
||||||
if not category and semantic_type:
|
semantic_type,
|
||||||
category = semantic_type.replace('_', ' ').title()
|
semantic_type.replace('_', ' ').title() if semantic_type else 'Unknown'
|
||||||
|
)
|
||||||
|
|
||||||
# Determine travel mode: explicit activity type wins,
|
# Determine travel mode
|
||||||
# otherwise infer from max speed along the preceding path
|
|
||||||
travel_mode = pending_mode
|
travel_mode = pending_mode
|
||||||
if travel_mode == 'unknown' and pending_path_points:
|
if travel_mode == 'unknown' and pending_path_points:
|
||||||
max_speed = _max_speed_from_path(pending_path_points)
|
max_speed = _max_speed_from_path(pending_path_points)
|
||||||
@@ -312,12 +345,11 @@ class WtImportTimelineWizard(models.TransientModel):
|
|||||||
'travel_mode': travel_mode,
|
'travel_mode': travel_mode,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Reset for next inter-visit gap
|
|
||||||
pending_mode = 'unknown'
|
pending_mode = 'unknown'
|
||||||
pending_path_points = []
|
pending_path_points = []
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# timelineMemory or other unknown segment types — ignore
|
# timelineMemory or other segment types — ignore
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return stops
|
return stops
|
||||||
|
|||||||
Reference in New Issue
Block a user