Fix sleep score parsing, dashboard body battery, segment direction
- Garmin sync: read sleepScores from dailySleepDTO (Garmin nests it there), so sleep score is actually stored instead of always null - Dashboard: pass YYYY-MM-DD to the intraday endpoint (was a full ISO timestamp), so the body-battery tile populates - Segment matching: follow the segment in its created direction with a path-length sanity check, so out-and-back routes no longer match an early start pass to a late finish (the >1h bogus segment times) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -102,49 +102,65 @@ def match_segment_in_activity(
|
||||
tol_m: float = 30.0,
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
Determine whether an activity track traverses a segment's GPS geometry, and if so
|
||||
how long it took. Works even when the activity's overall route differs — only the
|
||||
overlapping stretch matters.
|
||||
Determine whether an activity track traverses a segment's GPS geometry in the
|
||||
segment's own direction, and if so how long the fastest such traversal took.
|
||||
Works even when the activity's overall route differs — only the overlapping
|
||||
stretch matters.
|
||||
|
||||
seg_coords: [(lat, lon), ...] segment geometry (start → end).
|
||||
act_coords: [(lat, lon), ...] activity track, in time order.
|
||||
act_times: parallel list of datetimes for act_coords.
|
||||
|
||||
Strategy: anchor on the activity point nearest the segment start, then the nearest
|
||||
point (at/after it) to the segment end, then verify a few intermediate segment
|
||||
points are each passed within tolerance between those anchors. Returns the time
|
||||
between the start and end anchors, or None if the activity doesn't follow the segment.
|
||||
Strategy: for every pass of the activity near the segment START, walk forward
|
||||
accumulating path length; accept the traversal only if the activity reaches the
|
||||
segment END after covering roughly the segment's own length (so an out-and-back
|
||||
route can't match an early start to a late finish), and the intermediate segment
|
||||
points are passed in order. Returns the shortest valid traversal time, or None.
|
||||
"""
|
||||
n = len(act_coords)
|
||||
if n < 2 or len(seg_coords) < 2:
|
||||
m = len(seg_coords)
|
||||
if n < 2 or m < 2:
|
||||
return None
|
||||
|
||||
start_pt, end_pt = seg_coords[0], seg_coords[-1]
|
||||
|
||||
si, sd = None, tol_m
|
||||
for i in range(n):
|
||||
d = haversine_m(act_coords[i], start_pt)
|
||||
if d < sd:
|
||||
sd, si = d, i
|
||||
if si is None:
|
||||
seg_len = sum(haversine_m(seg_coords[k], seg_coords[k + 1]) for k in range(m - 1))
|
||||
if seg_len <= 0:
|
||||
return None
|
||||
|
||||
ei, ed = None, tol_m
|
||||
for i in range(si + 1, n):
|
||||
d = haversine_m(act_coords[i], end_pt)
|
||||
if d < ed:
|
||||
ed, ei = d, i
|
||||
if ei is None or ei <= si:
|
||||
return None
|
||||
near_start = lambda i: haversine_m(act_coords[i], start_pt) <= tol_m
|
||||
|
||||
# Verify the activity actually follows the segment shape between the anchors.
|
||||
for frac in (0.25, 0.5, 0.75):
|
||||
sp = seg_coords[int(frac * (len(seg_coords) - 1))]
|
||||
if not any(haversine_m(act_coords[i], sp) <= tol_m for i in range(si, ei + 1)):
|
||||
return None
|
||||
# One candidate entry per pass through the start region (first point of each run).
|
||||
entries = [i for i in range(n) if near_start(i) and (i == 0 or not near_start(i - 1))]
|
||||
|
||||
dur = (act_times[ei] - act_times[si]).total_seconds()
|
||||
return dur if dur > 0 else None
|
||||
best = None
|
||||
for si in entries:
|
||||
path = 0.0
|
||||
ei = None
|
||||
for i in range(si + 1, n):
|
||||
path += haversine_m(act_coords[i - 1], act_coords[i])
|
||||
if path > seg_len * 1.5: # wandered too far without finishing → wrong pass/direction
|
||||
break
|
||||
if path >= seg_len * 0.6 and haversine_m(act_coords[i], end_pt) <= tol_m:
|
||||
ei = i
|
||||
break
|
||||
if ei is None:
|
||||
continue
|
||||
|
||||
# Confirm the activity follows the segment shape in order between the anchors.
|
||||
ok = True
|
||||
for frac in (0.25, 0.5, 0.75):
|
||||
sp = seg_coords[int(frac * (m - 1))]
|
||||
if not any(haversine_m(act_coords[k], sp) <= tol_m for k in range(si, ei + 1)):
|
||||
ok = False
|
||||
break
|
||||
if not ok:
|
||||
continue
|
||||
|
||||
dur = (act_times[ei] - act_times[si]).total_seconds()
|
||||
if dur > 0 and (best is None or dur < best):
|
||||
best = dur
|
||||
|
||||
return best
|
||||
|
||||
|
||||
def find_best_split_time(
|
||||
|
||||
Reference in New Issue
Block a user