Recording map: follow toggle to free-pan while riding
New ⊕ button on the recording screen toggles map auto-follow. When off, the map stays put so you can look around when unsure where you are (off the track, or the track leads where there is no road); new GPS points no longer recentre it. Tapping it back on recentres on the current position and resumes following. Default on per screen entry (not persisted). Guards followMode around panTo/setView in onPoint/onBatchUpdate/ onStateChange. Independent of the compass freeze/rotate controls.
This commit is contained in:
parent
5da02c598a
commit
d6fe47a924
6 changed files with 151 additions and 5 deletions
|
|
@ -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 обновления, импорт тайлов, карта дорог
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
- **POI на карте** — точки интереса (кафе, магазины, вода, туалеты, банки/ATM, остановки, укрытия, смотровые и т.д.) круглыми контрастными значками (эмодзи в кружке, читаемо на светлой и тёмной карте). Кнопка `📍` с панелью-фильтром категорий; рендер по видимой области от зума 13. Данные оффлайн (`data/pois.json`, ~100 км вокруг опорной точки, 5662 объекта из OSM/Overpass). На картах записи, деталей трека и дорог.
|
||||
- **Тёмная карта** — тумблер `☾` на экранах записи, деталей трека и дорог: инвертирует ТОЛЬКО слой тайлов (`invert + hue-rotate 180° + brightness`, трек/маршрут/дороги остаются цветными), выбор запоминается. Работает одинаково на офлайн-тайлах и онлайне, без ключей/квот/перепаковки.
|
||||
- **Метки ветра** — кнопка «Ветер» на экране записи: тап открывает интервал встречного ветра, тап закрывает. Метки хранятся с треком (`wind_marks`) как эталон для проверки оценки ветра по треку; на расчёт калорий/мощности не влияют. Голосовые метки — на будущее.
|
||||
- **Слежение карты (тумблер `⊕`)** — на экране записи карта по умолчанию центрируется на текущей позиции при каждой новой точке. Кнопка `⊕` (подсвечена = слежу) выключает автоцентрирование: карту можно свободно подвигать и осмотреться, когда «где я» непонятно (отклонился от трека или трек ведёт туда, где дороги нет), а новые точки её не возвращают. Повторный тап центрирует на текущей позиции и включает слежение снова. По умолчанию включено при каждом заходе (не запоминается).
|
||||
- **Экспорт/импорт** — свои продукты, рецепты и добавки одним файлом (JSON).
|
||||
- **Вес, сон, добавки** — логирование с историей.
|
||||
|
||||
|
|
|
|||
76
docs/plans/2026-09-26-map-follow-toggle.md
Normal file
76
docs/plans/2026-09-26-map-follow-toggle.md
Normal file
|
|
@ -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 + лог сессии обновить вместе с коммитом.
|
||||
|
||||
## Не в этой задаче
|
||||
Авто-выключение следования при драге карты (пользователь выбрал "только кнопка").
|
||||
Хранение состояния следования между сессиями (по умолчанию всегда включено).
|
||||
47
docs/session-logs/2026-09-26_map-follow-toggle.md
Normal file
47
docs/session-logs/2026-09-26_map-follow-toggle.md
Normal file
|
|
@ -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) - убедиться на телефоне, что обе кнопки работают независимо.
|
||||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue