290 lines
16 KiB
Markdown
290 lines
16 KiB
Markdown
---
|
||
type: trz
|
||
work_item_id: ET-006
|
||
title: "ТЗ: Загрузка и визуализация GPX-треков"
|
||
version: 2
|
||
status: approved
|
||
created_at: 2026-05-22
|
||
updated_at: 2026-05-22
|
||
authors:
|
||
- "agent:analyst"
|
||
---
|
||
|
||
# ТЗ — ET-006: Загрузка и визуализация GPX-треков
|
||
|
||
## 1. Функциональные требования
|
||
|
||
### REQ-F-01: Кнопка загрузки GPX
|
||
|
||
- В правой панели кнопок карты (`#map-controls-r`) добавляется кнопка «GPX» с иконкой загрузки (стрелка вверх + документ).
|
||
- Позиция: между кнопкой «Компас» и «Моё местоположение» (верхняя часть панели).
|
||
- По нажатию открывается системный диалог выбора файла (`<input type="file" accept=".gpx">`).
|
||
- Допускается множественный выбор (`multiple`).
|
||
|
||
### REQ-F-02: Парсинг GPX
|
||
|
||
- Парсинг выполняется на клиенте через `DOMParser` (XML → DOM → GeoJSON).
|
||
- Поддерживается GPX 1.1 (namespace `http://www.topografix.com/GPX/1/1`).
|
||
- Извлекаются:
|
||
- `<trk>` → массив треков, каждый `<trkseg>` → массив точек `[lon, lat, ele?, time?]`
|
||
- `<wpt>` → waypoints `{lon, lat, name?, ele?}`
|
||
- `<rte>` → route points (трактуются как трек)
|
||
- Если файл содержит несколько `<trk>`, каждый трек — отдельная сущность.
|
||
- При ошибке парсинга — показать toast-уведомление: «Не удалось прочитать GPX-файл».
|
||
|
||
### REQ-F-03: Валидация
|
||
|
||
- Максимальный размер файла: 50 МБ. При превышении — toast: «Файл слишком большой (макс. 50 МБ)».
|
||
- Если файл не содержит ни одного трека и ни одного waypoint — toast: «GPX-файл не содержит данных».
|
||
|
||
### REQ-F-04: Отрисовка трека на карте
|
||
|
||
- Каждый трек отрисовывается как `line` layer в MapLibre.
|
||
- Source: GeoJSON (`LineString` или `MultiLineString`).
|
||
- Цвет: из палитры 8 цветов, циклически. Палитра отличается от цветов роутинга (синий/зелёный/оранжевый).
|
||
- Предлагаемая палитра: `#e6194b`, `#3cb44b`, `#ffe119`, `#4363d8`, `#f58231`, `#911eb4`, `#42d4f4`, `#f032e6`.
|
||
- Толщина линии: 4px.
|
||
- Opacity: 0.85.
|
||
- Z-index: выше базовых слоёв, ниже маршрута OSRM (если активен).
|
||
|
||
### REQ-F-05: Отображение waypoints
|
||
|
||
- Каждый `<wpt>` отображается как маркер (circle layer + symbol layer для имени).
|
||
- Цвет маркера: совпадает с цветом трека из того же файла (или нейтральный, если waypoints без трека).
|
||
- Имя waypoint (`<name>`) отображается как label рядом с маркером.
|
||
- Если имя отсутствует — маркер без подписи.
|
||
|
||
### REQ-F-06: Fit bounds
|
||
|
||
- После загрузки файла карта выполняет `fitBounds` по bbox всех точек загруженного файла.
|
||
- Padding: 50px со всех сторон.
|
||
- Если загружено несколько файлов подряд — fit bounds только по последнему загруженному.
|
||
|
||
### REQ-F-07: Множественная загрузка
|
||
|
||
- Треки из разных файлов накапливаются в сессии.
|
||
- Каждый файл получает следующий цвет из палитры.
|
||
- Максимальное количество одновременных треков: не ограничено (разумный предел — производительность браузера).
|
||
|
||
### REQ-F-08: Удаление трека
|
||
|
||
- В панели управления треками (REQ-F-09) у каждого трека есть кнопка удаления (иконка ✕).
|
||
- При удалении: убирается line layer, source, маркеры waypoints с карты.
|
||
- Если удалён активный (выбранный) трек — панель профиля высот скрывается.
|
||
|
||
### REQ-F-09: Панель управления треками (GPX Sheet)
|
||
|
||
- Реализуется как bottom sheet (`#sheet-gpx`), аналогично существующим sheet-route, sheet-recon.
|
||
- Открывается автоматически при загрузке первого трека.
|
||
- Содержит:
|
||
- Заголовок «GPX-треки» с иконкой и кнопкой свернуть.
|
||
- Список загруженных треков: цветной кружок + имя файла (без расширения) + кнопка удаления.
|
||
- По тапу на трек в списке — он становится «активным» (выделяется), показывается его статистика и профиль высот.
|
||
- Кнопка в тулбаре нижнего toolbar (`#toolbar`): «GPX» — переключает видимость sheet.
|
||
|
||
### REQ-F-10: Профиль высот
|
||
|
||
- Отображается в нижней части sheet-gpx (под списком треков) для активного трека.
|
||
- График: canvas-элемент, ширина 100% sheet, высота 120px.
|
||
- Ось X: расстояние от начала трека (км).
|
||
- Ось Y: высота (м).
|
||
- Линия графика: цвет трека.
|
||
- Заливка под линией: цвет трека с opacity 0.2.
|
||
- Если данные высот отсутствуют (`<ele>` нет) — показать текст: «Данные высот отсутствуют».
|
||
- При наведении/тапе на график — показать tooltip с высотой и расстоянием, и подсветить соответствующую точку на карте (маркер-курсор).
|
||
|
||
### REQ-F-11: Статистика трека
|
||
|
||
- Отображается над профилем высот в sheet-gpx для активного трека.
|
||
- Формат: компактная сетка (аналогично recon-grid).
|
||
- Поля:
|
||
- Длина (км) — сумма расстояний между точками (Haversine).
|
||
- Набор высоты (м) — сумма положительных дельт `ele`.
|
||
- Сброс высоты (м) — сумма отрицательных дельт `ele` (абсолютное значение).
|
||
- Мин. высота (м).
|
||
- Макс. высота (м).
|
||
- Если данные высот отсутствуют — показать только длину, остальные поля: «—».
|
||
|
||
### REQ-F-12: Интерактивность трека на карте
|
||
|
||
- При клике на линию трека на карте — этот трек становится активным в панели (показывается статистика + профиль).
|
||
- Курсор при наведении на трек: pointer.
|
||
|
||
### REQ-F-13: Сохранение треков при переключении стиля карты
|
||
|
||
- При переключении стиля карты (тёмная тема, восстановление слоёв рельефа) вызывается `map.setStyle()`, который удаляет **все** пользовательские source и layer.
|
||
- После смены стиля все загруженные GPX-треки должны быть автоматически восстановлены: линии треков, source, waypoints-маркеры.
|
||
- Восстановление выполняется в функции `rebuildMapOverlays()` (`src/web/app.js`) — по аналогии с уже реализованными там маршрутом OSRM, разведкой и scenic-маршрутами.
|
||
- Данные треков (`window.gpxTracks`) хранятся в памяти и при `setStyle()` не теряются — пересоздаются только объекты карты (source / layer / маркеры).
|
||
- Активный трек, его статистика и профиль высот должны сохраняться после переключения стиля.
|
||
- Z-order GPX-слоёв (см. REQ-F-04) корректно восстанавливается и после смены стиля.
|
||
|
||
## 2. Нефункциональные требования
|
||
|
||
### REQ-NF-01: Производительность
|
||
|
||
- Парсинг файла 50 МБ: ≤ 5 секунд на устройстве с 4 ГБ RAM.
|
||
- Рендеринг трека 500K точек: без видимых фризов при pan/zoom (MapLibre оптимизирует GeoJSON line layers).
|
||
- Во время парсинга показывать индикатор загрузки (spinner или moto-wheel).
|
||
|
||
### REQ-NF-02: Совместимость
|
||
|
||
- Работает в Chrome 90+, Firefox 90+, Safari 15+.
|
||
- Работает на мобильных (touch events для профиля высот).
|
||
|
||
### REQ-NF-03: UX
|
||
|
||
- Кнопка загрузки доступна всегда, независимо от активного режима (роутинг, разведка и т.д.).
|
||
- GPX-треки не конфликтуют с активным маршрутом OSRM — отображаются одновременно.
|
||
- При ошибках — toast-уведомления (не alert/confirm).
|
||
|
||
### REQ-NF-04: Хранение
|
||
|
||
- Данные треков хранятся только в памяти (JS-переменные).
|
||
- При перезагрузке страницы — все треки теряются.
|
||
- Не используется localStorage/sessionStorage для данных треков (слишком большие).
|
||
|
||
## 3. UI-спецификация
|
||
|
||
### 3.1 Кнопка в правой панели (#map-controls-r)
|
||
|
||
```
|
||
┌──────────┐
|
||
│ ↑ GPX │ ← новая кнопка (между Компас и Геолокация)
|
||
└──────────┘
|
||
```
|
||
|
||
- Класс: `map-btn`
|
||
- ID: `btn-gpx-upload`
|
||
- Иконка: стрелка вверх из документа (upload file)
|
||
- Title: «Загрузить GPX»
|
||
|
||
### 3.2 Кнопка в нижнем тулбаре (#toolbar)
|
||
|
||
```
|
||
[ Маршрут | Связка | Красивый | Разведка | Линейка | Поиск | Метка | GPX ]
|
||
```
|
||
|
||
- Класс: `tb-btn`
|
||
- ID: `tb-gpx`
|
||
- Иконка: файл с линией (track)
|
||
- Label: «GPX»
|
||
- Действие: `toggleGpxSheet()`
|
||
|
||
### 3.3 Bottom sheet (#sheet-gpx)
|
||
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ ═══ (handle) │
|
||
│ 📄 GPX-треки [свернуть]│
|
||
├─────────────────────────────────────┤
|
||
│ 🔴 track_morning.gpx [✕] │
|
||
│ 🔵 weekend_ride.gpx ✓ [✕] │ ← активный (выделен)
|
||
│ 🟢 test_route.gpx [✕] │
|
||
├─────────────────────────────────────┤
|
||
│ СТАТИСТИКА │
|
||
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
|
||
│ │47км│ │820м│ │650м│ │120м│ │980м│ │
|
||
│ │длин│ │наб.│ │сбр.│ │мин │ │макс│ │
|
||
│ └────┘ └────┘ └────┘ └────┘ └────┘ │
|
||
├─────────────────────────────────────┤
|
||
│ ПРОФИЛЬ ВЫСОТ │
|
||
│ ┌───────────────────────────────┐ │
|
||
│ │ ╱╲ ╱╲╱╲ │ │
|
||
│ │╱ ╲╱╲╱ ╲╱╲ │ │
|
||
│ └───────────────────────────────┘ │
|
||
│ 0 km 23.5 km 47 km │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.4 Toast-уведомления
|
||
|
||
- Позиция: верх экрана, по центру.
|
||
- Автоскрытие: 4 секунды.
|
||
- Стиль: аналогично `#ruler-toast`.
|
||
|
||
## 4. Данные
|
||
|
||
### Входные данные (GPX 1.1)
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
|
||
<trk>
|
||
<name>Morning Ride</name>
|
||
<trkseg>
|
||
<trkpt lat="55.7558" lon="37.6173"><ele>150</ele><time>2026-01-01T08:00:00Z</time></trkpt>
|
||
...
|
||
</trkseg>
|
||
</trk>
|
||
<wpt lat="55.76" lon="37.62">
|
||
<name>Кафе</name>
|
||
<ele>155</ele>
|
||
</wpt>
|
||
</gpx>
|
||
```
|
||
|
||
### Внутренняя модель (JS)
|
||
|
||
```javascript
|
||
// Массив загруженных GPX-файлов
|
||
window.gpxTracks = [
|
||
{
|
||
id: 'gpx-1716336000000', // уникальный ID (timestamp)
|
||
filename: 'morning_ride', // имя файла без расширения
|
||
color: '#e6194b', // цвет из палитры
|
||
tracks: [ // массив треков из файла
|
||
{
|
||
name: 'Morning Ride',
|
||
points: [[lon, lat, ele, time], ...], // массив точек
|
||
stats: { distanceKm, elevGain, elevLoss, eleMin, eleMax }
|
||
}
|
||
],
|
||
waypoints: [
|
||
{ lon, lat, name, ele }
|
||
],
|
||
sourceId: 'gpx-source-1716336000000',
|
||
layerId: 'gpx-layer-1716336000000',
|
||
waypointLayerId: 'gpx-wpt-1716336000000'
|
||
}
|
||
];
|
||
```
|
||
|
||
## 5. Алгоритмы
|
||
|
||
### 5.1 Расчёт расстояния (Haversine)
|
||
|
||
Сумма расстояний между последовательными точками трека. Формула Haversine для каждой пары.
|
||
|
||
### 5.2 Расчёт набора/сброса высоты
|
||
|
||
```
|
||
elevGain = Σ max(0, ele[i+1] - ele[i]) для всех i
|
||
elevLoss = Σ max(0, ele[i] - ele[i+1]) для всех i
|
||
```
|
||
|
||
Фильтрация шума: игнорировать дельты < 2 м (GPS-шум).
|
||
|
||
### 5.3 Палитра цветов
|
||
|
||
Циклический массив из 8 цветов. Индекс = `gpxTracks.length % 8` на момент добавления.
|
||
|
||
## 6. Файловая структура изменений
|
||
|
||
```
|
||
src/web/
|
||
├── index.html # + кнопка в #map-controls-r, + sheet-gpx, + tb-btn
|
||
├── app.js # + gpx-модуль (парсинг, рендеринг, управление)
|
||
├── app.css # + стили sheet-gpx, профиля высот, toast
|
||
```
|
||
|
||
Альтернативно, GPX-логику можно вынести в отдельный файл `gpx.js` (аналогично `units.js`).
|
||
|
||
## 7. Взаимодействие с существующими режимами
|
||
|
||
- GPX-треки отображаются **параллельно** с любым активным режимом (роутинг, разведка, красивый маршрут).
|
||
- Z-order: GPX-треки ниже активного маршрута OSRM, но выше базовых слоёв (trails, terrain).
|
||
- Кнопка загрузки в `#map-controls-r` доступна всегда.
|
||
- Кнопка «GPX» в toolbar переключает sheet, но не деактивирует другие режимы.
|
||
- При смене стиля карты (`setStyle` — тёмная тема, слои рельефа) GPX-слои восстанавливаются через `rebuildMapOverlays()` — см. REQ-F-13.
|