14 KiB
type, work_item_id, adr_id, title, status, created_at, authors, supersedes, superseded_by, labels
| type | work_item_id | adr_id | title | status | created_at | authors | supersedes | superseded_by | labels | |
|---|---|---|---|---|---|---|---|---|---|---|
| adr | ET-005 | adr-0001 | ADR-0001: Переключение единиц измерения (км/мили) — клиентское решение с централизованным форматтером | accepted | 2026-05-21 |
|
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.
-
Только клиент. Backend, БД, API, тайл-сервер, OSRM, инфраструктура — без изменений. Расстояния продолжают приходить с backend в метрах.
-
Новый модуль
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">. -
Глобальный API модуля — неймспейс
window.Units(соответствует стилюapp.js, где функции глобальны):Units.getUnit()→'km' | 'mi';Units.setUnit(unit)— валидирует, пишетlocalStorage, диспатчитunitchange;Units.formatDistance(meters, { precision })— возвращает строку с корректной единицей в текущем режиме;Units.KM_TO_MI = 0.621371— единственная константа коэффициента.
-
Каноническая единица — метры. Конвертация выполняется только в момент форматирования. Запрещено хранить или пересчитывать значения расстояний в милях во внутреннем состоянии (
route.distance_m,rulerTotalи т.п. остаются метрическими) — это исключает накопление ошибок округления при многократном переключении (ФТ-3, AC-2/TP-04). -
Персистентность —
localStorage, ключdistance_unit(как в ТЗ), значения'km'/'mi'. При отсутствии или некорректном значении — дефолт'km'(AC-3, TP-01). Чтение — при инициализации модуля. -
Пересчёт — единый оркестратор.
setUnit()диспатчитunitchangeнаdocument. Вapp.jsрегистрируется ровно один обработчикonUnitChange(), пере-вызывающий существующие функции отрисовки видимых поверхностей с расстояниями (лист маршрута, список точек, карточки маршрутов, линейка, scale-bar). Рассыпать подписки по компонентам запрещено. -
Все 13 мест форматирования
(m/1000).toFixed(N)+' км'вapp.jsпереводятся наUnits.formatDistance(). Список call-sites и риск пропуска зафиксированы в10-tech-risks.md(R1). Форматтер принимаетprecision, чтобы сохранить текущую точность карточек (toFixed(0)) и при этом по умолчанию давать 1 знак (AC-2, R5). -
UI-элемент — сегментированный переключатель
km | miна готовом компоненте.seg-control/.seg-btn, размещаемый в попапе#terrain-popup(та же «панель настроек», что и чекбоксы слоёв и POI-чекбокс из ET-002). Новый CSS-компонент не вводится. -
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.mdR2, 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.mddocs/work-items/ET-005/02-trz.mddocs/work-items/ET-005/03-acceptance-criteria.mddocs/work-items/ET-005/04-test-plan.yamldocs/work-items/ET-005/07-infra-requirements.mddocs/work-items/ET-005/08-data-requirements.mddocs/work-items/ET-005/10-tech-risks.mddocs/architecture/README.md