diff --git a/docs/session-logs/2026-09-17_repeat-intervals.md b/docs/session-logs/2026-09-17_repeat-intervals.md index f648790..a80bd9e 100644 --- a/docs/session-logs/2026-09-17_repeat-intervals.md +++ b/docs/session-logs/2026-09-17_repeat-intervals.md @@ -43,6 +43,20 @@ совпадает с харнессом, вердикт зелёный 105%, участок подсвечен бирюзой на карте. Скрин. - Далее: сборка APK + проверка на телефоне (реальные интервалы там же). +## Фикс после проверки на теле (трек 16 сен) +- Симптом (пользователь): на 16 сен в повторы попала только часть горки, 729м вместо 1200+, + на карте видно прямую, что идёт дальше. +- Причина: эталон брался с ПЕРВОЙ горячей ячейки. Поездка 16 сен стартует прямо в горячей + ячейке (подъезд частично совпал с коридором), проход с индекса 0 обрывался гэпом на 143й + точке -> 729м, хотя горячих ячеек ~183 (~1830м). +- Фикс: эталон = САМЫЙ ДЛИННЫЙ чистый проход (перебор всех стартов горячих ранов, + `buildTraversal`), не первый. Дёшево (стартов немного). +- Проверка: харнесс 16 сен -> участок 1572м (было 729), 3 полных заезда. 11/17 сен без + изменений (по 5). Контроль (5 обычных) - 0 ложных. Десктоп (Xvfb, подсажена 16 сен, + бэкап+удаление): подсветка на карте покрывает весь верхний коридор, таблица 1.57км/3 заезда, + вердикт 104%. По коридору 4 траверса, но первый 74% (начат выше низа) - отсечён правилом + 80% (как pers.py MIN_FRAC), иначе нормировка длина/время завысила бы его скорость. + ## Не в этой задаче Из того же файла НЕ делалось: поправка модели п.2 (аддитивная мощность вместо max(физика,MET); п.1 про Crr уже был сделан ранее), массовое уточнение высот, ветер (Часть 2 - там вопросы). diff --git a/frontend/js/gps.js b/frontend/js/gps.js index 3031599..c4d7b92 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -130,26 +130,37 @@ GPS.detectRepeats = function(points, opts) { } let isHot = (k) => k != null && (visits.get(k) || 0) >= minPasses; - // Reference segment = ONE clean traversal of the corridor: start at the first hot - // cell, add each new hot cell in order, and STOP at the first revisit (that means the - // rider turned back / started the next lap). Gluing several laps into the reference - // (a long hot run) inflates its length and scrambles the order — measured, gives - // absurd 9-second "passes" over a 4.4 km "segment". One traversal fixes both. - let s0 = 0; - while (s0 < n && !isHot(key[s0])) s0++; - if (s0 >= n) return { found: false }; - let seq = [], order = new Map(), s1 = s0; - for (let k = s0, gap = 0; k < n; k++) { - let c = key[k]; - if (c == null || !isHot(c)) { gap++; if (gap > 5) break; continue; } // left the corridor - gap = 0; - if (seq.length && c === seq[seq.length - 1]) { s1 = k; continue; } // still in the same cell - if (order.has(c)) break; // revisit of an earlier cell → first traversal complete - order.set(c, seq.length); seq.push(c); s1 = k; + // Reference segment = the LONGEST clean traversal of the corridor. One traversal = + // start at a cell, add each NEW hot cell in order, STOP at the first revisit (turned + // back / next lap) or a gap out of the corridor. Gluing laps together inflates length + // and scrambles order (measured: absurd 9 s "passes" over 4.4 km); a single traversal + // fixes that. But picking the FIRST hot cell is also wrong — a ride can START inside a + // hot cell (its approach overlaps the corridor), giving a short gap-truncated seq + // (measured: 729 m of a 1572 m climb). So try a traversal from every hot-run start and + // keep the longest. + let buildTraversal = (start) => { + let seq = [], order = new Map(), end = start; + for (let k = start, gap = 0; k < n; k++) { + let c = key[k]; + if (c == null || !isHot(c)) { gap++; if (gap > 5) break; continue; } // left the corridor + gap = 0; + if (seq.length && c === seq[seq.length - 1]) { end = k; continue; } // still in the same cell + if (order.has(c)) break; // revisit of an earlier cell → traversal complete + order.set(c, seq.length); seq.push(c); end = k; + } + let len = 0; + for (let k = start + 1; k <= end; k++) + len += GPS.distance(points[k - 1].lat, points[k - 1].lon, points[k].lat, points[k].lon); + return { seq, order, len }; + }; + let best = null; + for (let k = 0; k < n; k++) { + if (!isHot(key[k]) || (k > 0 && isHot(key[k - 1]))) continue; // only at a hot-run start + let t = buildTraversal(k); + if (!best || t.len > best.len) best = t; } - let segLength = 0; - for (let k = s0 + 1; k <= s1; k++) - segLength += GPS.distance(points[k - 1].lat, points[k - 1].lon, points[k].lat, points[k].lon); + if (!best) return { found: false }; + let seq = best.seq, order = best.order, segLength = best.len; if (seq.length < 3 || segLength < minLenM) return { found: false }; // Passes: scan the ride; a pass is a run where position along seq rises from the