---
type: adr
work_item_id: ET-006
adr_id: ADR-002
title: "GPX-фича как отдельный модуль gpx.js"
status: accepted
date: 2026-05-22
authors:
  - "agent:architect"
supersedes: null
superseded_by: null
---

# ADR-002: GPX-фича как отдельный модуль `gpx.js`

## Контекст

ET-006 добавляет самодостаточную фичу: парсинг GPX, внутренняя модель,
управление source/layer/маркерами карты, bottom sheet `sheet-gpx`,
canvas-профиль высот, расчёт статистики. Оценка объёма — ~600–900 строк JS.

ТЗ (`02-trz.md` §6) оставляет структуру файлов открытой и предлагает два
варианта: дописать в `app.js` либо вынести в отдельный `gpx.js`. ТЗ ссылается
на `units.js` как на прецедент.

Фактическое состояние кодовой базы (проверено):

- `src/web/units.js` **не существует**. `app.js` — единственный JS-файл
  фронтенда: 113 КБ, ~2900 строк.
- `app.js` подключён как **классический скрипт** (`<script src="app.js">`),
  **не** как ES-модуль. Все функции — глобальные; обработчики событий
  навешаны через инлайновые `onclick="..."` в `index.html`.
- Сборщика (bundler) нет. Статика раздаётся как есть через FastAPI
  `StaticFiles` (mount `/`), Docker копирует каталог целиком
  (`COPY src/web/ ./src/web/`).

То есть «прецедент `units.js`» в ТЗ — иллюстративная неточность, но решение
о структуре файлов ТЗ явно делегирует этому этапу. Принимается архитектурно.

## Решение

Реализовать GPX-фичу в **новом файле `src/web/gpx.js`**, подключённом
**классическим** `<script>` после `app.js` в `index.html`.

`gpx.js` владеет:

- парсингом GPX (см. ADR-003);
- внутренней моделью `window.gpxTracks` (см. `08-data-requirements.md`);
- управлением объектами карты: source / line layer / waypoint layers / маркеры;
- логикой `sheet-gpx` (список треков, активный трек);
- canvas-профилем высот и расчётом статистики (Haversine, набор/сброс высот).

### Контракт интеграции (единственная поверхность связности)

`gpx.js` **потребляет** глобали, объявленные в `app.js`:

| Символ | Назначение |
|---|---|
| `window._map` | экземпляр MapLibre |
| `openSheet(id)` / `closeSheet(id)` | управление bottom sheet |
| `showToast(msg)` | уведомления об ошибках (см. ниже) |

`app.js` получает **ровно одну** новую строку — хук в `rebuildMapOverlays()`:

```js
// в конце rebuildMapOverlays()
if (typeof rebuildGpxOverlays === 'function') rebuildGpxOverlays();
```

Хук защищён `typeof`, поэтому `app.js` остаётся валидным и без `gpx.js`.
Это закрывает REQ-F-13 (восстановление GPX-слоёв после `map.setStyle()`).

### Toast-хелпер

Сейчас существует только `#ruler-toast` + `showRulerToast()` — частный случай.
ET-006 требует несколько разных сообщений (REQ-F-02, REQ-F-03).
Рекомендуется **обобщить** до переиспользуемого `showToast(message)`
(элемент-контейнер + автоскрытие 4 с, стиль как у `#ruler-toast` — TRZ §3.4).
Конкретная реализация — на этапе разработки; дизайн toast — за дизайнером.

### Изменения подключения

- `index.html`: добавить `<script src="gpx.js"></script>` после строки 400
  (`<script src="app.js">`), плюс разметку (кнопка `#btn-gpx-upload`,
  `#tb-gpx`, `#sheet-gpx`) — см. TRZ §3.
- Новый статический ассет `gpx.js` подхватывается автоматически:
  Docker уже копирует весь `src/web/`, FastAPI `StaticFiles` отдаёт по `/gpx.js`.
  Инфраструктурных изменений нет (см. `07-infra-requirements.md`).

## Рассмотренные альтернативы

### Альтернатива A: дописать в `app.js` (отклонена)

- `app.js` уже 113 КБ — добавление ~800 строк ухудшает читаемость и время ревью.
- Смешение несвязанных доменов в одном файле.
- Невозможно ревьюить/тестировать GPX-логику изолированно.

### Альтернатива B: ES-модуль (`<script type="module">`) (отклонена)

- Потребовала бы перевода всех инлайновых `onclick`-обработчиков на
  `addEventListener` и экспорт/импорт — большой blast radius на чужой код.
- `app.js` не является модулем; смешивать module и classic-скрипты с общими
  глобалями хрупко.
- Вне scope ET-006.

### Альтернатива C: отдельный классический скрипт `gpx.js` (выбрана)

- Изоляция домена, минимальная поверхность связности.
- Совместимо с текущей моделью «всё глобальное, без сборщика».
- Ревью и тесты ET-006 локализованы: `gpx.js` + 3 правки разметки в
  `index.html` + 1 строка в `app.js`.

## Последствия

### Положительные

- Первое разбиение фронтенда на несколько файлов — задаёт лёгкий паттерн
  «одна фича = один классический скрипт + глобали» для будущих задач.
- Поверхность ревью/тестирования ET-006 изолирована.

### Отрицательные / митигации

| Последствие | Митигация |
|---|---|
| Порядок подключения: `gpx.js` после `app.js` | Межфайловые вызовы происходят в момент события — функции уже определены. Единственный вызов `app.js → gpx.js` защищён `typeof`. |
| Связность через глобали (нет инкапсуляции) | Контракт интеграции зафиксирован выше и узок (3 потребляемых символа + 1 хук). |
| Нет `showToast` — нужно обобщать `#ruler-toast` | Небольшой шаред-утиль; реализация на этапе разработки. |

### Влияние на компоненты

- **Frontend** — новый файл `gpx.js`, правки `index.html`, 1 строка в `app.js`.
- **Backend / API / OSRM / БД** — без изменений.
- **C4-диаграммы** — состав компонентов не меняется (фронтенд остаётся одним
  логическим компонентом) → обновление C4 не требуется. Отдельных `.mmd`
  в репозитории нет.

## Связанные

- ТЗ: `docs/work-items/ET-006/02-trz.md` (§6, REQ-F-13)
- ADR-003: стратегия парсинга GPX
- Данные: `docs/work-items/ET-006/08-data-requirements.md`
- Инфра: `docs/work-items/ET-006/07-infra-requirements.md`
- Риски: `docs/work-items/ET-006/10-tech-risks.md`
