baldav/docs/plans/2026-09-10-brouter-ux-gapfill-planner.md
4aiman cfe0212bbc BRouter UX: gap-fill preview+reversible save; per-segment route planner
Gap-fill (track detail): GPS.fillGaps now returns {points,gaps} with per-gap
status; the 🧩 control previews ONLY the filled dropouts (cyan casing; unfilled
= red dashed) with a Save/Cancel panel. Save is a reversible in-place patch
(estimated points added, original untouched; DB.saveGapFill/revertGapFill +
estimated/gap_filled columns), then recalculateSession. Revert removes them.

Route planner: renamed menu 'Нарисовать маршрут' → 'Нарисовать тренировку'
(it makes a workout session, not a route); routes empty-state now points at the
on-phone planner. showRouteEditor rewritten per-leg: each leg road (BRouter) or
straight independently, road by default, tap a leg to toggle it, long-press to
insert, silent straight fallback where no road; the global button sets all legs.
2026-09-10 15:26:05 +03:00

13 KiB
Raw Blame History

План: BRouter на телефоне — понятный гэп-филл и планировщик маршрутов

Context (зачем)

BRouter уже встроен и роутит, но две фичи на телефоне непонятны в использовании:

  1. Заполнение GPS-пропусков (кнопка 🧩 на детали трека). Сейчас GPS.fillGaps возвращает ВЕСЬ трек с дорисованными пропусками, а GapCtl рисует его целиком (main.js:4784) — поэтому голубая линия ложится поверх всего трека, даже там, где GPS был. Не видно, где именно BRouter достроил, непонятно, что это станет треком, и НЕТ сохранения/отмены. Нужно: тап кнопки → предпросмотр ТОЛЬКО достроенных пропусков → явная кнопка сохранить/отмена. Сохранение — патч на месте, обратимо (не новый трек).

  2. Планирование маршрута по точкам. Оно уже есть: «Сохранённые маршруты» → «Новый маршрут» → showRouteEditor с тумблером «🚲 По дорогам». Но пользователь его не нашёл: в меню есть отдельный пункт «Нарисовать маршрут», который на деле рисует ТРЕНИРОВКУ (сессию с калориями, showDrawRouteScreen), а пустой список маршрутов советует импортировать GPX/рисовать в Яндексе. Плюс «По дорогам» — глобальный тумблер на весь маршрут. Нужно: понятный вход + по-сегментное решение (по дорогам по умолчанию, где дороги нет — прямая, ручной тумблер влияет на текущий отрезок).

Итог решений с пользователем: (1) патч на месте обратимо; (2) по-сегментный роутинг, дорога по умолчанию, прямая где нет дороги, ручной тумблер на отрезок; (3) пункт меню переименовать (функцию оставить).

Часть A — Гэп-филл: предпросмотр только пропусков + обратимый патч на месте

A1. GPS.fillGaps → богатый результат (frontend/js/gps.js:1400)

Сейчас возвращает плоский массив. Вернуть { points, gaps }:

  • gaps: [{ aIdx, bIdx, routed:[{lat,lon}], addedM, chordM, status }], где status = "filled" | "skipped-long" | "skipped-noroute" (сейчас пропуск молча скипается — теперь причина известна для отчёта в UI).
  • Достроенным точкам присваивать интерполированные ts по отрезку [a.ts, b.ts] (сейчас всем ставится a.ts, что ломает порядок при чтении из БД) и флаг estimated: true.
  • Обратная совместимость: единственный вызов — GapCtl (main.js:4783), правится тут же.

A2. GapCtl — предпросмотр только пропусков + панель сохранить/отмена (frontend/js/main.js:4772-4791, showSessionDetail)

  • По тапу: посчитать fillGaps(points), нарисовать только достроенные отрезки (каждый routed + его две реальные концевые точки) отдельным ярким стилем (голубая линия с бело-чёрной оковкой, как маршрут в brouter-web) поверх исходного трека. Весь трек НЕ перерисовывать.
  • Пропуски со status !== "filled" пометить иначе (красный пунктир по хорде) — видно, что их достроить не вышло.
  • Показать панель-баннер: «Достроено N пропусков (+X.X км). M не удалось.» + подпись «Так трек будет выглядеть после сохранения» + кнопки [Сохранить] и [Отмена].
  • [Отмена] — убрать оверлей и панель, ничего не менять.
  • [Сохранить] — DB.saveGapFill(sessionId, filledPoints) → GPS.recalculateSession(id, {noDelete:true}) → перерисовать деталь. Если трек уже достроен — вместо этого показывать [Убрать достройку] → DB.revertGapFill(id).
  • Найти в showSessionDetail имя переменной сессии/sessionId (рядом уже есть points).

A3. БД: обратимое хранение (frontend/js/db.js)

  • Миграции идемпотентным паттерном (как db.js:1422-1433): if (!gpsCols.includes("estimated")) ALTER TABLE gps_points ADD COLUMN estimated INTEGER DEFAULT 0; if (!gsCols.includes("gap_filled")) ALTER TABLE gps_sessions ADD COLUMN gap_filled INTEGER DEFAULT 0.
  • saveGapFill(sessionId, filledPoints): вставить только достроенные точки с estimated=1, seq = seq предыдущей реальной точки (порядок держит ORDER BY COALESCE(seq,999999), ts в db.js:866 — совпадающий seq + интерполированный ts ставит их между концами), батч- вставкой по образцу importGpxSession/addGpsPointsBulk (db.js:751,874). Выставить gps_sessions.gap_filled=1. Оригинальные точки не трогаются.
  • revertGapFill(sessionId): DELETE FROM gps_points WHERE session_id=? AND estimated=1; gap_filled=0. Затем пересчёт статы вызывающей стороной.
  • Загрузчик db.js:866 уже SELECT * — колонку estimated подхватит сам; убедиться, что она мапится в point.estimated. computeStats уже трактует .estimated (gps.js:241) — доп. правок в расчёте не нужно.
  • Продуктовый seed_meta.version НЕ трогаем (это пользовательская БД, не products_seed).

A4. Отрисовка трека и стиль (frontend/js/main.js:~4912, frontend/css/styles.css)

  • В отрисовке трека детали пометить estimated-точки/отрезки отдельным стилем, чтобы после сохранения было видно, какие участки достроены.
  • CSS: стиль голубой линии достройки с бело-чёрной оковкой + стиль панели предпросмотра.

Часть B — Планировщик: понятный вход + по-сегментный роутинг

B1. Переименовать пункт меню (frontend/js/main.js:2733)

«Нарисовать маршрут» → «Нарисовать тренировку» (он сохраняет сессию через showDrawRouteScreen, а не маршрут). Функцию оставить.

B2. Подсказка в пустом списке маршрутов (frontend/js/main.js:3261)

Текст сейчас зовёт импортировать GPX/рисовать в Яндексе. Дописать, что «Новый маршрут» — это планирование прямо в телефоне по дорогам (BRouter), тапами по карте.

B3. showRouteEditor — по-сегментный роутинг (frontend/js/main.js:3486-3649)

Заменить глобальные brouterMode + единый routeGeom на модель по отрезкам:

  • Состояние: waypoints[], legMode[i] ("road" по умолчанию | "straight"), legGeom[i] (кэш дорожной геометрии отрезка).
  • Каждый отрезок между соседними точками роутить BRouter.route([wp[i],wp[i+1]], "trekking") (debounce). Если отрезок не проложился (нет дороги/сегментов/сети) — этот отрезок падает на прямую линию молча (без тост-спама; можно тонко пометить).
  • Тап по отрезку переключает режим ИМЕННО этого отрезка (дорога ↔ прямая) — это и есть «ручной тумблер на текущий отрезок». (Тап по точке — удалить, как сейчас; тап по карте — добавить точку в конец; тап по линии сейчас вставляет точку — развести с «переключить отрезок»: вставка по долгому тапу либо отдельная явная механика; уточнить при реализации.)
  • Рисовать конкатенацию всех отрезков; сохранять итоговую геометрию (как сейчас, DB.addRoute/updateRoutePoints). Переиспользовать parseRouteGPX, на детали — routeElevStats (профиль высот).
  • Глобальную кнопку «🚲 По дорогам» превратить в «все отрезки: по дорогам/прямые» (задаёт дефолт для новых отрезков), по-сегментный тап её переопределяет. Подсказка внизу: «тап карты — точка · тап отрезка — дорога/прямая · тап точки — удалить».

Файлы

  • frontend/js/gps.js — fillGaps (A1).
  • frontend/js/main.js — GapCtl/showSessionDetail (A2, A4), меню (B1), showRoutesScreen (B2), showRouteEditor (B3).
  • frontend/js/db.js — миграции + saveGapFill/revertGapFill (A3).
  • frontend/css/styles.css — стили достройки/панели (A4).

Переиспользование (не изобретать заново)

  • DB.importGpxSession/addGpsPointsBulk (db.js:874,751) — батч-вставка точек.
  • GPS.recalculateSession(id,{noDelete:true}) (gps.js:1697) — пересчёт статы без удаления.
  • computeStats уже понимает estimated (gps.js:241); загрузчик точек db.js:866.
  • BRouter.route (router.js), GPS.parseRouteGPX (gps.js:1369), routeElevStats.
  • Образец обратимости — сглаживание (gps_sessions.smoothed + lat_smoothed/lon_smoothed, saveSmoothedTrack db.js:802).

Верификация (десктоп, Xvfb :99 + домашний RouteServer; потом на теле)

  1. Гэп-филл. Открыть трек с пропуском (сессия 1782115144011, два дропаута): 🧩 → подсвечены ТОЛЬКО пропуски + панель «Достроено N (+X км), M не удалось» → Сохранить → деталь показывает трек с помеченными достроенными участками, статистика пересчитана → Убрать достройку возвращает исходный трек. Скрины на вирт. экране.
  2. Планировщик. «Сохранённые маршруты» → «Новый маршрут» → 3 точки: отрезки легли по дорогам; тап по отрезку переключает его на прямую и обратно; сохранить → деталь с профилем высот. Скрин.
  3. Меню: пункт называется «Нарисовать тренировку»; подсказка в пустом списке зовёт планировать на телефоне.
  4. Коммит перед тестом, затем на устройстве ./build.sh install -d — гэп-филл и планировщик встроенным движком офлайн (докачать сегмент региона, если ещё не докачан).

Явно не делаем

  • Не создаём новый трек-копию (выбран патч на месте).
  • Не трогаем общий список городов-пресетов, координаты, pois.json, домен (privacy-скраб был отдельной задачей).
  • Не переписываем showDrawRouteScreen (только переименование пункта меню).