206 lines
14 KiB
Markdown
206 lines
14 KiB
Markdown
---
|
||
type: adr
|
||
work_item_id: ET-005
|
||
adr_id: adr-0001
|
||
title: "ADR-0001: Переключение единиц измерения (км/мили) — клиентское решение с централизованным форматтером"
|
||
status: accepted
|
||
created_at: 2026-05-21
|
||
authors:
|
||
- "agent:architect"
|
||
supersedes: []
|
||
superseded_by: []
|
||
labels: []
|
||
---
|
||
|
||
# ADR-0001 — Переключение единиц измерения (км/мили): клиентское решение с централизованным форматтером
|
||
|
||
## Статус
|
||
|
||
Accepted
|
||
|
||
## Контекст
|
||
|
||
ET-005 добавляет переключатель единиц измерения расстояний (км/мили) в
|
||
панель настроек карты. Выбор сохраняется в `localStorage`, по умолчанию —
|
||
километры, при переключении все видимые расстояния пересчитываются за
|
||
< 100 мс (см. `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`).
|
||
|
||
Существующее состояние кодовой базы (`src/web/`):
|
||
|
||
- Фронтенд **плоский, без сборщика и без модульной системы**:
|
||
`index.html`, `app.js`, `app.css`, `style.json`, `style-dark.json`.
|
||
`app.js` (≈ 3036 строк) подключается как **классический скрипт**
|
||
(`<script src="app.js"></script>`, `index.html:405`), а не ES-модуль.
|
||
Каталога `src/web/static/js/` в репозитории **нет**.
|
||
- Форматирование расстояний **не централизовано** — захардкоженный
|
||
паттерн `(m / 1000).toFixed(N) + ' км'` встречается минимум в **13
|
||
местах** `app.js`: `formatDist()` (стр. 187–189), карточки сегментов
|
||
(638), карточки маршрута (1157, 1191, 2202, 2212, 2357, 2370, 2605),
|
||
scale-bar (1416–1440), всплывающие подсказки (1478), линейка/ruler
|
||
(1837, 1875, 1885, 1931). Единого форматтера нет.
|
||
- Внутренняя каноническая единица расстояния — **метры**
|
||
(`route.distance_m`), для линейки — километры (`rulerTotal`).
|
||
- Сложился устойчивый паттерн персистентности UI-настроек в
|
||
`localStorage`: ключи `enduro-theme-mode`, `terrain-hillshade`,
|
||
`terrain-tri`, `trails-track`, `trails-path`, `poi-visible` (ET-002),
|
||
`MARKERS_KEY`.
|
||
- «Панель настроек карты» из BRD/ТЗ де-факто реализована как попап
|
||
`#terrain-popup` (заголовок «Эндуро»), открываемый кнопкой «Рельеф».
|
||
В ET-002 в этот же попап добавлен чекбокс POI. В `app.css` уже есть
|
||
готовый компонент сегментированного переключателя `.seg-control` /
|
||
`.seg-btn` (стр. 360–363).
|
||
|
||
Backend (FastAPI), БД (SQLite/Spatialite), тайл-сервер и OSRM к выбору
|
||
единиц измерения отношения **не имеют**: расстояния приходят с backend
|
||
в метрах, перевод — исключительно вопрос представления на клиенте.
|
||
|
||
## Рассматриваемые варианты
|
||
|
||
### Вариант A — Централизованный модуль-форматтер + единый оркестратор (выбран)
|
||
|
||
Новый модуль `src/web/units.js`, подключаемый как **классический скрипт
|
||
до `app.js`**, экспортирует глобальный неймспейс `window.Units` с
|
||
функциями состояния (`getUnit()` / `setUnit()`) и форматирования
|
||
(`formatDistance(meters, {precision})`). Все 13 мест форматирования в
|
||
`app.js` переводятся на вызов `Units.formatDistance(...)`. При смене
|
||
единицы `setUnit()` пишет в `localStorage` и диспатчит `unitchange` на
|
||
`document`; в `app.js` регистрируется **один** обработчик-оркестратор
|
||
`onUnitChange()`, который пере-вызывает существующие функции отрисовки
|
||
видимых поверхностей с расстояниями.
|
||
|
||
- **Плюсы:** единственный источник истины по форматированию убирает
|
||
риск рассинхрона; нулевые изменения backend/БД/инфраструктуры;
|
||
пересчёт мгновенный (нет ни сети, ни перезагрузки тайлов) — НФТ
|
||
«< 100 мс» выполняется тривиально; согласуется с принципом «минимум
|
||
зависимостей»; модуль покрывается unit-тестами изолированно.
|
||
- **Минусы:** требует рефакторинга 13 мест в `app.js` (риск пропустить
|
||
вызов — вынесен в `10-tech-risks.md`, R1); глобальный неймспейс вместо
|
||
ES-import — но это сознательное соответствие текущему стилю `app.js`.
|
||
|
||
### Вариант B — Рассыпанные подписчики `unitchange` по компонентам (буквальный тех-дизайн ТЗ)
|
||
|
||
Каждый компонент с расстоянием самостоятельно подписывается на
|
||
`unitchange` и сам себя перерисовывает.
|
||
|
||
- **Плюсы:** формально совпадает с разделом «Технический дизайн» ТЗ.
|
||
- **Минусы:** N подписок вместо одной — высокая связность, легко
|
||
забыть подписку у нового компонента; дублирование логики перерисовки;
|
||
не решает корневую проблему — захардкоженное `+ ' км'` остаётся
|
||
размазанным. Отклонён: оркестрация в одном месте надёжнее.
|
||
|
||
### Вариант C — Вынести `units.js` в ES-модуль `src/web/static/js/units.js`
|
||
|
||
Буквально следовать пути из ТЗ: создать `src/web/static/js/units.js` как
|
||
ES-модуль.
|
||
|
||
- **Плюсы:** «правильная» модульность.
|
||
- **Минусы:** в проекте нет каталога `static/js/`, нет сборщика, а
|
||
`app.js` подключён как классический скрипт. Полноценный `import`
|
||
потребовал бы перевода `app.js` в `type="module"` и реструктуризации
|
||
≈ 3000 строк — несопоставимо со scope ET-005 и противоречит принципу
|
||
«минимум зависимостей». Отклонён.
|
||
|
||
## Решение
|
||
|
||
Принимается **Вариант A**.
|
||
|
||
1. **Только клиент.** Backend, БД, API, тайл-сервер, OSRM, инфраструктура
|
||
— без изменений. Расстояния продолжают приходить с backend в метрах.
|
||
|
||
2. **Новый модуль `src/web/units.js`** (НЕ `src/web/static/js/units.js`).
|
||
Раздел «Технический дизайн» ТЗ носит рекомендательный характер;
|
||
финальное размещение файлов — мандат архитектуры. Путь скорректирован
|
||
под фактическую плоскую структуру `src/web/`. Модуль подключается в
|
||
`index.html` как классический скрипт **строго перед `app.js`**:
|
||
`<script src="units.js"></script>` затем `<script src="app.js">`.
|
||
|
||
3. **Глобальный API модуля** — неймспейс `window.Units` (соответствует
|
||
стилю `app.js`, где функции глобальны):
|
||
- `Units.getUnit()` → `'km' | 'mi'`;
|
||
- `Units.setUnit(unit)` — валидирует, пишет `localStorage`, диспатчит
|
||
`unitchange`;
|
||
- `Units.formatDistance(meters, { precision })` — возвращает строку с
|
||
корректной единицей в текущем режиме;
|
||
- `Units.KM_TO_MI = 0.621371` — единственная константа коэффициента.
|
||
|
||
4. **Каноническая единица — метры.** Конвертация выполняется **только в
|
||
момент форматирования**. Запрещено хранить или пересчитывать значения
|
||
расстояний в милях во внутреннем состоянии (`route.distance_m`,
|
||
`rulerTotal` и т.п. остаются метрическими) — это исключает накопление
|
||
ошибок округления при многократном переключении (ФТ-3, AC-2/TP-04).
|
||
|
||
5. **Персистентность** — `localStorage`, ключ `distance_unit` (как в ТЗ),
|
||
значения `'km'` / `'mi'`. При отсутствии или некорректном значении —
|
||
дефолт `'km'` (AC-3, TP-01). Чтение — при инициализации модуля.
|
||
|
||
6. **Пересчёт — единый оркестратор.** `setUnit()` диспатчит `unitchange`
|
||
на `document`. В `app.js` регистрируется ровно **один** обработчик
|
||
`onUnitChange()`, пере-вызывающий существующие функции отрисовки
|
||
видимых поверхностей с расстояниями (лист маршрута, список точек,
|
||
карточки маршрутов, линейка, scale-bar). Рассыпать подписки по
|
||
компонентам запрещено.
|
||
|
||
7. **Все 13 мест форматирования** `(m/1000).toFixed(N)+' км'` в `app.js`
|
||
переводятся на `Units.formatDistance()`. Список call-sites и риск
|
||
пропуска зафиксированы в `10-tech-risks.md` (R1). Форматтер принимает
|
||
`precision`, чтобы сохранить текущую точность карточек (`toFixed(0)`)
|
||
и при этом по умолчанию давать 1 знак (AC-2, R5).
|
||
|
||
8. **UI-элемент** — сегментированный переключатель `km | mi` на готовом
|
||
компоненте `.seg-control` / `.seg-btn`, размещаемый в попапе
|
||
`#terrain-popup` (та же «панель настроек», что и чекбоксы слоёв и
|
||
POI-чекбокс из ET-002). Новый CSS-компонент не вводится.
|
||
|
||
9. **C4 без изменений.** Состав компонентов системы не меняется,
|
||
обновление `docs/architecture/c4-*.mmd` не требуется (диаграммы C4 в
|
||
репозитории отсутствуют — только `docs/architecture/README.md`).
|
||
|
||
## Последствия
|
||
|
||
### Положительные
|
||
|
||
- Изменение полностью клиентское: деплой без миграций и без изменения
|
||
состава контейнеров (штатный релиз фронтенда).
|
||
- Пересчёт < 100 мс (НФТ ТЗ) — нет ни сетевых вызовов, ни перезагрузки
|
||
тайлов.
|
||
- Появляется единственная точка форматирования расстояний — снижается
|
||
будущий риск рассинхрона и упрощается поддержка (положительный
|
||
побочный эффект рефакторинга 13 call-sites).
|
||
- Модуль `units.js` изолирован и тестируется юнит-тестами отдельно от
|
||
`app.js`.
|
||
|
||
### Отрицательные / ограничения
|
||
|
||
- Настройка локальна для браузера: при очистке `localStorage` или смене
|
||
устройства сбрасывается на дефолт `km`. Принято осознанно — вне scope
|
||
BRD (синхронизация между устройствами не требуется).
|
||
- Требуется рефакторинг 13 мест в `app.js` — операционный риск пропуска
|
||
call-site (митигация — `10-tech-risks.md`, R1; grep-аудит + e2e).
|
||
- Имперский эквивалент суб-километровых значений («м») и единиц
|
||
scale-bar требует политики — см. `10-tech-risks.md` R2, R3.
|
||
|
||
### Технический долг
|
||
|
||
- `units.js` подключается как глобальный классический скрипт. Если в
|
||
будущих фазах появится сборщик/модульность фронтенда, модуль легко
|
||
переводится в ES-модуль без изменения публичного контракта. На ET-005
|
||
перевод `app.js` в модули — вне scope.
|
||
|
||
## Классификация изменения
|
||
|
||
**Minor change.** Новые сервисы, БД, API-эндпоинты и контейнеры не
|
||
вводятся; состав компонентов C4 не меняется; backend и инфраструктура не
|
||
затрагиваются. Лейбл `arch:major-change` **не требуется**. Обязательного
|
||
архитектурного approve не требуется.
|
||
|
||
## Связанные документы
|
||
|
||
- `docs/work-items/ET-005/01-brd.md`
|
||
- `docs/work-items/ET-005/02-trz.md`
|
||
- `docs/work-items/ET-005/03-acceptance-criteria.md`
|
||
- `docs/work-items/ET-005/04-test-plan.yaml`
|
||
- `docs/work-items/ET-005/07-infra-requirements.md`
|
||
- `docs/work-items/ET-005/08-data-requirements.md`
|
||
- `docs/work-items/ET-005/10-tech-risks.md`
|
||
- `docs/architecture/README.md`
|