From 7d2581dc2d496159866ccfce5f39173339890ed7 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Thu, 17 Sep 2026 23:17:47 +0300 Subject: [PATCH] Repeat analysis: bridge a stop inside a pass, count 70%+ passes (dropped-seat rep now counts) --- .../2026-09-17_repeat-intervals.md | 16 ++++++++++++ frontend/js/gps.js | 26 ++++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/session-logs/2026-09-17_repeat-intervals.md b/docs/session-logs/2026-09-17_repeat-intervals.md index a80bd9e..da57cfe 100644 --- a/docs/session-logs/2026-09-17_repeat-intervals.md +++ b/docs/session-logs/2026-09-17_repeat-intervals.md @@ -57,6 +57,22 @@ вердикт 104%. По коридору 4 траверса, но первый 74% (начат выше низа) - отсечён правилом 80% (как pers.py MIN_FRAC), иначе нормировка длина/время завысила бы его скорость. +## Фикс 2: остановка внутри заезда (седло) + сшивка (трек 16 сен) +- Пользователь: первый заезд 16 сен был полным, но разорвался остановкой (опущенное седло, + ноги не крутили) - такие стопы <~минуты сшивать в один заезд, время стопа оставлять в + заезде как штраф. +- Проверка данных: остановка ЕСТЬ - idx 310-319, **89 секунд** сдвиг <15м. По reported speed + её не видно (fused показывал 8 км/ч), нашлась по СМЕЩЕНИЮ. Плюс у вершины трек первого + заезда уходит с общей линии в негорячие ячейки (GPS-разброс на 20 км/ч), из-за чего покрытие + вышло 74%. +- Сделано: (1) в разметке проходов - мост через короткое прерывание рядом с коридором + (`bridgeSec` 90с, `bridgeM` 60м) + сохранена терпимость к нулевым пропускам (gap<=25); + время простоя остаётся в длительности заезда -> честный штраф скорости. (2) Порог покрытия + `minFrac` 0.8 -> **0.7**: полный заезд, у которого GPS сполз с линии у вершины, засчитывается. +- Проверка (харнесс): 16 сен -> **4 заезда** (был 3), норм.скор [19.6, 21.3, 21.2, 23.8] - + первый самый медленный (штраф за 89с стоп), вердикт 121%. 11/17 сен без изменений (по 5), + контроль 5 обычных - 0 ложных при 0.7. Десктоп: 16 сен подсажен, таблица 4 строки. + ## Не в этой задаче Из того же файла НЕ делалось: поправка модели п.2 (аддитивная мощность вместо max(физика,MET); п.1 про Crr уже был сделан ранее), массовое уточнение высот, ветер (Часть 2 - там вопросы). diff --git a/frontend/js/gps.js b/frontend/js/gps.js index c4d7b92..634f14a 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -106,7 +106,11 @@ GPS.detectRepeats = function(points, opts) { let cell = opts.cellM ?? 10; // grid cell ~10 m (as in tools/track heightmap) let minLenM = opts.minLenM ?? 300; // repeated segment must be at least this long let minPasses = opts.minPasses ?? 3; // and ridden at least this many times - let minFrac = opts.minFrac ?? 0.8; // a pass must cover this fraction of the segment + let minFrac = opts.minFrac ?? 0.7; // a pass must cover this fraction of the segment (0.7: + // a full rep whose GPS drifted off the common line near + // the top still counts; its stop time penalises its speed) + let bridgeSec = opts.bridgeSec ?? 90; // bridge an interruption inside a pass up to this long + let bridgeM = opts.bridgeM ?? 60; // ...as long as the rider stays within this of the corridor let n = points ? points.length : 0; if (n < 20) return { found: false }; @@ -170,15 +174,23 @@ GPS.detectRepeats = function(points, opts) { let p = 0; while (p < n) { if (pos[p] == null || pos[p] > seq.length * 0.15) { p++; continue; } - let q = p, lo = pos[p], hi = pos[p], gap = 0; + let q = p, lo = pos[p], hi = pos[p], anchor = p, gap = 0; while (q < n - 1) { q++; - if (pos[q] == null) { gap++; if (gap > 25) break; continue; } - if (pos[q] < hi - 3) break; // went backward — pass ended (e.g. coasting down) - gap = 0; hi = Math.max(hi, pos[q]); + if (pos[q] != null && pos[q] >= hi - 3) { gap = 0; hi = Math.max(hi, pos[q]); anchor = q; continue; } + // Off the corridor (a gap) or a step backward. BRIDGE a short interruption near the + // corridor - a stop (dropped seat, mechanical) or GPS wobble/divergence - so it stays + // ONE pass, with the idle time kept in the pass duration (a fair speed penalty). A real + // descent between reps moves far and long, exceeding the bridge, and ends the pass. + let dtStop = (new Date(points[q].ts) - new Date(points[anchor].ts)) / 1000; + let dispStop = GPS.distance(points[anchor].lat, points[anchor].lon, points[q].lat, points[q].lon); + if ((dtStop <= bridgeSec && dispStop <= bridgeM) || ++gap <= 25) continue; + break; } - if ((hi - lo) >= seq.length * minFrac) passes.push({ startIdx: p, endIdx: q }); - p = q + 1; + // Pass spans [p, anchor] (last on-corridor point); duration ts[anchor]-ts[p] includes any + // bridged stop, so a pass with a rest reads slower, exactly as intended. + if ((hi - lo) >= seq.length * minFrac) passes.push({ startIdx: p, endIdx: anchor }); + p = Math.max(q, anchor) + 1; } if (passes.length < minPasses) return { found: false }; return { found: true, segLength: Math.round(segLength), passCount: passes.length, passes };