diff --git a/PROJECT_MAP.md b/PROJECT_MAP.md index c0c32fb..7395159 100644 --- a/PROJECT_MAP.md +++ b/PROJECT_MAP.md @@ -142,7 +142,7 @@ ### Экраны - **Сегодня** — баланс ккал, еда (продукты + блюда), GPS тренировки, активность, вес, сон, добавки, прогресс-бар жидкости, нутриенты (приход + запасы) - **Продукты** — подтабы [Продукты | Рецепты], поиск, карточки с КБЖУ, добавление в дневник -- **Трек** — GPS тренировки (запись с HR), карта с компасом (leaflet-rotate). Навигационный «пак» позиции (SVG divIcon): круг цвета трека (на 20% темнее) с белой + 1px чёрной обводкой + треугольник, указывающий ВВЕРХ (карта и так доворачивается активным источником — курс/компас — вверх, поэтому без покадрового поворота: не трясёт). Мини-компас только в режиме компаса. Z-order панелями: тайлы < дороги(`roadsPane` 240) < маршрут(`routePane` 250) < трек(overlay) < пак(marker). course-up по GPS-курсу (карта и панель доворачиваются в ландшафт вместе, `bearing = h`), ландшафтный режим (CSS rotation), графики скорости/высоты/ЧСС, импорт GPX/FTB, dead reckoning, placeholder до GPS, карта дорог (дедуп 2м, цвета по вело/пешие/бег), сохранённые маршруты (оверлей-навигация, кнопка-тоггл «М», предпросмотр с профилем высоты, топо/уклон-обводка, тоггл «∡/⛰», высоты по цепочке GPXZ→open-elevation→open-meteo→OpenTopoData), де-джиттер трека (Савицкий-Голей, кнопка «≈» на детали) +- **Трек** — GPS тренировки (запись с HR), карта с компасом (leaflet-rotate). Навигационный «пак» позиции (SVG divIcon): круг цвета трека (на 20% темнее) с белой + 1px чёрной обводкой + треугольник, указывающий ВВЕРХ (карта и так доворачивается активным источником — курс/компас — вверх, поэтому без покадрового поворота: не трясёт). Мини-компас только в режиме компаса. Z-order панелями: тайлы < дороги(`roadsPane` 240) < маршрут(`routePane` 250) < трек(overlay) < пак(marker). course-up по GPS-курсу (карта и панель доворачиваются в ландшафт вместе, `bearing = h`), ландшафтный режим (CSS rotation), графики скорости/высоты/ЧСС, импорт GPX/FTB, dead reckoning, placeholder до GPS, карта дорог (дедуп 2м, цвета по вело/пешие/бег), сохранённые маршруты (оверлей-навигация, кнопка-тоггл «М», предпросмотр с профилем высоты, топо/уклон-обводка, тоггл «∡/⛰», высоты по цепочке GPXZ→open-elevation→open-meteo→OpenTopoData), де-джиттер трека (Савицкий-Голей, кнопка «≈» на детали), тоггл следования карты за позицией (`followMode`, кнопка `⊕` в `rec-map-btn` стеке; guard вокруг `panTo`/`setView` в `onPoint`/`onBatchUpdate`/`onStateChange`; выкл = свободный просмотр карты на ходу, повторный тап центрирует; не запоминается) - **Добавки** — 41 нутриент (витамины, минералы, жирные кислоты, женьшень, ресвератрол, CoQ10) - **Настройки** — профиль, образ жизни, нормы ЧСС/давления, BLE пульсометр, велосипеды, GPS конфиг (в т.ч. цвет трека `trackColor`, цвета дорог, GPXZ-ключ), OTA обновления, импорт тайлов, карта дорог diff --git a/README.md b/README.md index 4751c44..f9ab8f9 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ - **POI на карте** — точки интереса (кафе, магазины, вода, туалеты, банки/ATM, остановки, укрытия, смотровые и т.д.) круглыми контрастными значками (эмодзи в кружке, читаемо на светлой и тёмной карте). Кнопка `📍` с панелью-фильтром категорий; рендер по видимой области от зума 13. Данные оффлайн (`data/pois.json`, ~100 км вокруг опорной точки, 5662 объекта из OSM/Overpass). На картах записи, деталей трека и дорог. - **Тёмная карта** — тумблер `☾` на экранах записи, деталей трека и дорог: инвертирует ТОЛЬКО слой тайлов (`invert + hue-rotate 180° + brightness`, трек/маршрут/дороги остаются цветными), выбор запоминается. Работает одинаково на офлайн-тайлах и онлайне, без ключей/квот/перепаковки. - **Метки ветра** — кнопка «Ветер» на экране записи: тап открывает интервал встречного ветра, тап закрывает. Метки хранятся с треком (`wind_marks`) как эталон для проверки оценки ветра по треку; на расчёт калорий/мощности не влияют. Голосовые метки — на будущее. +- **Слежение карты (тумблер `⊕`)** — на экране записи карта по умолчанию центрируется на текущей позиции при каждой новой точке. Кнопка `⊕` (подсвечена = слежу) выключает автоцентрирование: карту можно свободно подвигать и осмотреться, когда «где я» непонятно (отклонился от трека или трек ведёт туда, где дороги нет), а новые точки её не возвращают. Повторный тап центрирует на текущей позиции и включает слежение снова. По умолчанию включено при каждом заходе (не запоминается). - **Экспорт/импорт** — свои продукты, рецепты и добавки одним файлом (JSON). - **Вес, сон, добавки** — логирование с историей. diff --git a/docs/plans/2026-09-26-map-follow-toggle.md b/docs/plans/2026-09-26-map-follow-toggle.md new file mode 100644 index 0000000..b97cc14 --- /dev/null +++ b/docs/plans/2026-09-26-map-follow-toggle.md @@ -0,0 +1,76 @@ +# План: кнопка тоггла "следование карты за позицией" на экране записи + +## Context (зачем) +Во время записи карта на экране трекинга постоянно снэпится (`panTo`) на текущую GPS-позицию +при каждой новой точке. Кейс пользователя: отклонился от трека или трек ведёт туда, где дороги +нет, надо осмотреться по карте вокруг - но карта тут же прыгает обратно на позицию, подвигать +её невозможно. Нужна кнопка-тумблер: нажал - карта перестаёт следовать за позицией (можно +свободно двигать/смотреть), нажал снова - карта центрируется на текущей позиции и следование +возобновляется. + +Решения по UX (согласованы с пользователем): +- Кнопка - плитка 32x32 в общем правом ряду кнопок карты, с символом (не словом), активное + состояние подсвечивается (как freeze/roads). Активна = следование включено. +- Только кнопка-тумблер. Драг карты пальцем следование НЕ выключает (строго как в запросе). +- Повторный тап (включение следования) сразу центрирует карту на текущей позиции. +- Следование по умолчанию включено при каждом заходе на экран (в localStorage не храним). + +## Где сейчас следование (frontend/js/main.js, функция renderRecordingInline) +Автоследование = центрирование вида карты на текущей точке. Происходит в: +- `GPS.recording.onPoint`, ~4840: `map.panTo(ll)` - основное следование при записи. +- `GPS.recording.onPoint` состояние waiting_gps, ~4857: `map.setView([p.lat,p.lon],15)`. +- `GPS.recording.onStateChange` "ready", ~4814: `map.setView([p.lat,p.lon],16)` - разовый locate. +- `GPS.recording.onBatchUpdate`, ~5005: `map.panTo(last)` - следование при пакетном апдейте. + +Маркер позиции (`updateMe`/`refreshMe`) вид карты НЕ трогает - при выключенном следовании +точка продолжит обновляться на своём месте, просто карта не будет прыгать. Компас/поворот +(`compassFrozen`, `applyBearing`) ортогональны следованию - `panTo` на bearing не влияет. + +## Фикс (frontend/js/main.js, только renderRecordingInline) +1. Флаг: рядом с `compassFrozen`/`rotOffset` (~4470-4477) добавить `let followMode = true;`. +2. Кнопка: по образцу `freezeBtn`/`roadsBtn` (~4624-4676) создать `followBtn` + (`class="rec-map-btn rec-follow-btn"`, символ-цель, `title="Следить за позицией"`), + добавить в `btnWrap`, сразу выставить `followBtn.classList.add("active")` (followMode=true). + onclick: + - `followMode = !followMode;` + - `followBtn.classList.toggle("active", followMode);` + - при включении - центрировать на текущей точке: взять последнюю точку + `GPS.recording.points` (как в onBatchUpdate: сортировка по ts, последняя) и `map.panTo`. +3. Загардить все 4 места следования условием `if (followMode)`: + - ~4840 `map.panTo(ll)` -> `if (followMode) map.panTo(ll);` + - ~4857 waiting_gps `map.setView(...)` -> обернуть в `if (followMode)` (return оставить). + - ~4814 "ready" `map.setView(...)` -> обернуть в `if (followMode)`. + - ~5005 `map.panTo(last)` -> `if (followMode) map.panTo(last);` + +## CSS (frontend/css/styles.css) +После `.rec-dark-toggle { right:8px; bottom:238px; }` (~1248) добавить позицию для новой кнопки +в следующем свободном слоте стека: `.rec-follow-btn { right: 8px; bottom: 276px; }`. +Базовый вид и `.active` уже есть у `.rec-map-btn` / `.rec-map-btn.active` - переиспользуем. + +## Символ кнопки +Плитка-цель, читается как "центрировать на мне": glyph круг-с-точкой/крест-в-круге +(например "⊕" плюс-в-круге или "◎" bullseye) - выбрать широко отрисовываемый. +Смысл несёт `title` (текст). Активная подсветка = следование включено. + +## Регрессии (проверить рядом) +- Freeze-кнопка компаса (`compassFrozen`) и поворот карты - не задеты (следование = только + центр вида, поворот = bearing). Убедиться, что обе кнопки работают независимо. +- Маркер позиции `updateMe` при выключенном следовании продолжает двигаться (карта стоит). +- Второй, отдельный экран `setupLiveUpdates` (мини-карта gpsMap, ~5030, свой panTo 5039/5055) + НЕ трогаем - это превью в списке, у него нет ряда кнопок. Пользователь просил "экран + трекинга" = renderRecordingInline. + +## Верификация +- Десктоп (Xvfb :99, `DISPLAY=:99 ./build.sh`): открыть экран записи, начать запись + (или подсадить точки), проверить: + 1) следование по умолчанию включено, кнопка подсвечена, карта идёт за точкой; + 2) тап -> подсветка гаснет, карту можно свободно двигать, новые точки её не возвращают; + 3) тап -> карта центрируется на текущей позиции, подсветка загорается, следование снова идёт. + Скрин каждого состояния. +- Сборка APK + телефон (по явной просьбе): в реальной записи проехать/постоять, отжать кнопку, + подвигать карту, вернуть кнопкой. Коммит ДО теста на телефоне, доработки - отдельным коммитом. +- README/PROJECT_MAP + лог сессии обновить вместе с коммитом. + +## Не в этой задаче +Авто-выключение следования при драге карты (пользователь выбрал "только кнопка"). +Хранение состояния следования между сессиями (по умолчанию всегда включено). diff --git a/docs/session-logs/2026-09-26_map-follow-toggle.md b/docs/session-logs/2026-09-26_map-follow-toggle.md new file mode 100644 index 0000000..89e144d --- /dev/null +++ b/docs/session-logs/2026-09-26_map-follow-toggle.md @@ -0,0 +1,47 @@ +# Сессия 2026-09-26 - тумблер "следование карты за позицией" на экране записи + +Работали по утверждённому плану: `docs/plans/2026-09-26-map-follow-toggle.md`. + +## Контекст +Во время записи карта на экране трекинга при каждой новой GPS-точке центрируется на текущей +позиции (`map.panTo`). Кейс: отклонился от трека или трек ведёт туда, где дороги нет, надо +осмотреться по карте - но она тут же прыгает обратно, подвигать её нельзя. Нужен тумблер: +нажал - карта перестаёт следовать (можно двигать), нажал снова - центрируется на позиции и +следование возобновляется. + +Согласовано с пользователем (AskUserQuestion): кнопка - символ-плитка в общем ряду кнопок +карты (не слово), активная подсветка = слежу; только кнопка-тумблер (драг карты следование НЕ +выключает); повторный тап центрирует; состояние не запоминается между сессиями. + +## Действия +- `frontend/js/main.js`, `renderRecordingInline`: + - добавлен флаг `let followMode = true;` рядом с `compassFrozen`/`rotOffset`; + - создана кнопка `followBtn` (`class="rec-map-btn rec-follow-btn active"`, символ `⊕`, + `title="Следить за позицией"`) по образцу `freezeBtn`; onclick тогглит `followMode` и класс + `active`, при включении центрирует `map.panTo` на последней точке `GPS.recording.points` + (сортировка по ts); + - 4 места автоследования загардены `if (followMode)`: `onPoint` `panTo` (~4840), `onPoint` + waiting_gps `setView` (~4857), `onStateChange` "ready" `setView` (~4814), `onBatchUpdate` + `panTo` (~5005). +- `frontend/css/styles.css`: `.rec-follow-btn { right: 8px; bottom: 276px; font-size: 18px; }` + (следующий слот стека после `.rec-dark-toggle` 238px). Базовый вид и `.active` - + переиспользован `.rec-map-btn` / `.rec-map-btn.active`. +- README: добавлен буллет "Слежение карты (тумблер ⊕)". + +## Проверка +- `node -c frontend/js/main.js` - OK. +- Десктоп (Xvfb :99, `DISPLAY=:99 ./build.sh` = tauri dev, без бампа версии): открыт экран + записи (Старт), кнопка `⊕` в правом стеке вверху, по умолчанию подсвечена (слежу). Тап -> + подсветка гаснет (слежу выкл). Тап -> подсветка возвращается (слежу вкл). JS-ошибок в логе + нет. Скрины: s_rec.png / s_rec_off.png / s_rec_on2.png в scratchpad. +- НЕ проверено на десктопе: живое поведение "карта не прыгает на новую точку при выкл / прыгает + при вкл" - под Xvfb настоящего GPS нет, экран в waiting_gps, поток точек не идёт. Логика - + тривиальный guard вокруг уже работающих `panTo`/`setView`. Реальная проверка потока - на + телефоне. + +## Следующие шаги +- Сборка APK + установка на телефон (по явной просьбе пользователя): проверить в живой записи - + проехать/постоять, отжать `⊕`, подвигать карту (новые точки не возвращают), вернуть кнопкой + (центрируется). Доработки - отдельным коммитом. +- Компас (freeze `X`) и поворот карты не задеты (следование = только центр вида, поворот = + bearing) - убедиться на телефоне, что обе кнопки работают независимо. diff --git a/frontend/css/styles.css b/frontend/css/styles.css index fe0ff97..ff5e03c 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1246,6 +1246,7 @@ select { .rec-mode-btn { right: 8px; bottom: 162px; font-size: 11px; } .rec-route-toggle { right: 8px; bottom: 200px; } .rec-dark-toggle { right: 8px; bottom: 238px; } +.rec-follow-btn { right: 8px; bottom: 276px; font-size: 18px; } /* Dark map: invert ONLY the tile layer (overlays — track, route, roads, puck — keep their colours). User-tuned filter on OSM standard tiles. Applies to every Leaflet map. */ body.dark-map .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(1.5); } diff --git a/frontend/js/main.js b/frontend/js/main.js index abae94d..8746172 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -4469,6 +4469,7 @@ async function renderRecordingInline(container) { // BOTH directions (orange = compass, green = GPS course) for visual comparison. let rotOffset = 0; // 0/90/180/270 — whole-screen rotation let compassFrozen = false; // X button: freeze current heading + let followMode = true; // ⊕ button: keep the map centred on the current position let frozenHeading = 0; // heading captured at the moment of freezing let smoothedHeading = null; // EMA of calibrated compass heading (deg, 0=N, CW) let courseHeading = null; // EMA of GPS course over ground (deg, 0=N, CW) @@ -4634,6 +4635,26 @@ async function renderRecordingInline(container) { }; btnWrap.appendChild(freezeBtn); + // Follow button — ⊕: toggle whether the map keeps re-centring on the current position. + // Off = free pan (look around when lost / off-route); tapping it back on recentres now. + let followBtn = document.createElement("button"); + followBtn.className = "rec-map-btn rec-follow-btn active"; + followBtn.textContent = "⊕"; // ⊕ + followBtn.title = "Следить за позицией"; + followBtn.onclick = () => { + followMode = !followMode; + followBtn.classList.toggle("active", followMode); + if (followMode) { + // Re-enabling: jump straight back to where we are now. + let pts = GPS.recording.points; + if (pts && pts.length) { + let last = pts.slice().sort((a, b) => new Date(a.ts) - new Date(b.ts))[pts.length - 1]; + if (last) map.panTo([last.lat, last.lon]); + } + } + }; + btnWrap.appendChild(followBtn); + // Roads toggle button let roadsBtn = document.createElement("button"); roadsBtn.className = "rec-map-btn rec-roads-toggle"; @@ -4811,7 +4832,7 @@ async function renderRecordingInline(container) { if (ph) ph.remove(); statusEl.textContent = "GPS OK. Начните движение..."; statusEl.style.color = "#03dac6"; - map.setView([p.lat, p.lon], 16); + if (followMode) map.setView([p.lat, p.lon], 16); } else if (state === "recording") { statusEl.textContent = ""; statusEl.style.color = ""; @@ -4837,7 +4858,7 @@ async function renderRecordingInline(container) { let ll = [p.lat, p.lon]; borderLine.addLatLng(ll); trailLine.addLatLng(ll); - map.panTo(ll); + if (followMode) map.panTo(ll); // Save position for next session start (avoid black screen) DB.setProfileValue("last_lat", String(p.lat)); DB.setProfileValue("last_lon", String(p.lon)); @@ -4854,7 +4875,7 @@ async function renderRecordingInline(container) { if (recState === "waiting_gps") { statusEl.textContent = "Поиск GPS..."; statusEl.style.color = "#ffb74d"; - map.setView([p.lat, p.lon], 15); + if (followMode) map.setView([p.lat, p.lon], 15); return; } if (recState === "ready") { @@ -5002,7 +5023,7 @@ async function renderRecordingInline(container) { trailLine.setLatLngs(coords); if (coords.length) { let last = coords[coords.length - 1]; - map.panTo(last); + if (followMode) map.panTo(last); updateCourse(sorted[sorted.length - 1]); // course from the last point object (has bearing) updateMe(last); }