fix high priority issues

This commit is contained in:
Davide Scaini
2026-03-31 22:53:50 +02:00
parent e2870c3344
commit f8abab2c23
3 changed files with 40 additions and 16 deletions
+26 -11
View File
@@ -112,10 +112,11 @@ def compute_mmp(pts: list[DataPoint], started_at: datetime) -> Optional[list[lis
[duration_s, avg_watts] pairs (integers), or None when the activity has no [duration_s, avg_watts] pairs (integers), or None when the activity has no
power data. Only durations shorter than the total activity are included. power data. Only durations shorter than the total activity are included.
""" """
# 1 Hz downsample: at most one sample per second, skip sub-second duplicates. # Build a dense 1 Hz power array with gaps zero-filled.
# Seconds without a recorded sample are omitted (not zero-filled) so that # Zero-filling is the standard approach (matches GoldenCheetah / WKO):
# paused-recording gaps don't silently lower power averages. # a recording gap counts as 0 W so windows cannot silently span pauses
power_1hz: list[int] = [] # and inflate MMP values.
sparse: dict[int, int] = {}
last_t = -1 last_t = -1
for p in pts: for p in pts:
t = int((p.timestamp - started_at).total_seconds()) t = int((p.timestamp - started_at).total_seconds())
@@ -123,11 +124,15 @@ def compute_mmp(pts: list[DataPoint], started_at: datetime) -> Optional[list[lis
continue continue
last_t = t last_t = t
if p.power_w is not None: if p.power_w is not None:
power_1hz.append(p.power_w) sparse[t] = p.power_w
if len(power_1hz) < 2: if len(sparse) < 2:
return None return None
t_min = min(sparse)
t_max = max(sparse)
power_1hz: list[int] = [sparse.get(t, 0) for t in range(t_min, t_max + 1)]
n = len(power_1hz) n = len(power_1hz)
results: list[list[int]] = [] results: list[list[int]] = []
@@ -166,17 +171,27 @@ def compute_best_efforts(
""" """
targets = BEST_EFFORT_DISTANCES.get(sport, []) targets = BEST_EFFORT_DISTANCES.get(sport, [])
# Build 1 Hz speed (km/h) and elevation (m) arrays — same downsampling as timeseries.py # Build dense 1 Hz speed (km/h) and elevation (m) arrays with gap zero-filling.
speed_1hz: list[float] = [] # Zero-filling speed gaps (0 km/h) prevents best-effort windows from spanning
ele_1hz: list[Optional[float]] = [] # recording pauses and producing artificially fast times.
sparse_speed: dict[int, float] = {}
sparse_ele: dict[int, Optional[float]] = {}
last_t = -1 last_t = -1
for p in pts: for p in pts:
t = int((p.timestamp - started_at).total_seconds()) t = int((p.timestamp - started_at).total_seconds())
if t < 0 or t == last_t: if t < 0 or t == last_t:
continue continue
last_t = t last_t = t
speed_1hz.append(p.speed_kmh if p.speed_kmh is not None else 0.0) sparse_speed[t] = p.speed_kmh if p.speed_kmh is not None else 0.0
ele_1hz.append(p.elevation_m) sparse_ele[t] = p.elevation_m
if not sparse_speed:
return None, None
t_min = min(sparse_speed)
t_max = max(sparse_speed)
speed_1hz: list[float] = [sparse_speed.get(t, 0.0) for t in range(t_min, t_max + 1)]
ele_1hz: list[Optional[float]] = [sparse_ele.get(t) for t in range(t_min, t_max + 1)]
best_efforts: Optional[list[list[float]]] = None best_efforts: Optional[list[list[float]]] = None
if targets and speed_1hz: if targets and speed_1hz:
+7
View File
@@ -83,6 +83,13 @@ def write_activity(
} }
json_path = acts_dir / f"{activity_id}.json" json_path = acts_dir / f"{activity_id}.json"
# Collision guard: if a *different* activity already has this ID, append a
# short hash suffix to disambiguate (same hash = idempotent re-extract).
if json_path.exists():
existing = json.loads(json_path.read_text(encoding="utf-8"))
if existing.get("source_hash") != activity.source_hash:
activity_id = f"{activity_id}-{activity.source_hash[-6:]}"
json_path = acts_dir / f"{activity_id}.json"
json_path.write_text(json.dumps(detail, indent=2, ensure_ascii=False)) json_path.write_text(json.dumps(detail, indent=2, ensure_ascii=False))
# ── GeoJSON track ──────────────────────────────────────────────────────── # ── GeoJSON track ────────────────────────────────────────────────────────
+7 -5
View File
@@ -85,13 +85,15 @@
$: if (map && MarkerClass && timeseries && !markersAdded) { $: if (map && MarkerClass && timeseries && !markersAdded) {
markersAdded = true; markersAdded = true;
const add = () => { const add = () => {
const lats = (timeseries!.lat ?? []).filter(v => v != null) as number[]; // Filter lat/lon together so indices stay aligned
const lons = (timeseries!.lon ?? []).filter(v => v != null) as number[]; const pts = (timeseries!.lat ?? [])
if (!lats.length) return; .map((lat, i) => ({ lat, lon: (timeseries!.lon ?? [])[i] }))
.filter(p => p.lat != null && p.lon != null) as { lat: number; lon: number }[];
if (!pts.length) return;
new MarkerClass({ element: makeDot('#4ade80'), anchor: 'center' }) new MarkerClass({ element: makeDot('#4ade80'), anchor: 'center' })
.setLngLat([lons[0], lats[0]]).addTo(map); .setLngLat([pts[0].lon, pts[0].lat]).addTo(map);
new MarkerClass({ element: makeDot('#f87171'), anchor: 'center' }) new MarkerClass({ element: makeDot('#f87171'), anchor: 'center' })
.setLngLat([lons[lons.length - 1], lats[lats.length - 1]]).addTo(map); .setLngLat([pts[pts.length - 1].lon, pts[pts.length - 1].lat]).addTo(map);
}; };
map.loaded() ? add() : map.once('load', add); map.loaded() ? add() : map.once('load', add);
} }