Files
enduro-trails/docs/work-items/ET-006/01-brd.md
claude-bot 6edf97fe79
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Successful in 6s
CI / build (push) Has been skipped
analyst(ET): auto-commit from analyst run_id=59
2026-06-03 17:33:09 +00:00

107 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: brd
work_item_id: ET-006
title: "BRD: Скачивание трека из popup на карте"
version: 1
status: approved
created_at: 2026-06-03
updated_at: 2026-06-03
authors:
- "agent:analyst"
---
# BRD — ET-006: Скачивание трека из popup на карте
## 1. Цель
Дать пользователю возможность сохранить публичный GPS-трек (источники
OSM, EnduroRussia, Wikiloc, ttrails — слой ET-008) к себе на устройство
в виде GPX-файла прямо из popup, который открывается по клику на трек
на карте. Это закрывает базовый use case «увидел чужой интересный
трек → забрал к себе → запланировал поездку».
## 2. Контекст
- Слой публичных GPS-треков реализован в ET-008 (`src/web/gps_tracks.js`,
`src/api/gps_tracks/`). При клике на трек открывается MapLibre popup
с метаданными (имя, активность, длина, дата, пользователь, источники).
- Геометрия треков хранится на сервере в SQLite: 2D WKB LineString
(`tracks.geom`, см. `src/api/gps_tracks/db.py` и `mvt.py`). Высот
по точкам в БД нет.
- На клиенте геометрия доступна только при zoom ≥ 12 (режим GeoJSON
через `GET /api/gps-tracks?bbox=...`). При zoom 811 popup
открывается над MVT-фичей, у которой геометрия по тайлу
упрощена/обрезана и непригодна для повторного экспорта.
- Существующий GPX в проекте умеет только парсить локальный файл и
отдаёт OSRM-маршрут (кнопка «Скачать GPX» в `sheet-route`). Экспорт
публичного трека из БД в GPX отсутствует.
- Backend — FastAPI; новый эндпоинт добавляется в существующий
router `/api/gps-tracks` (`src/api/gps_tracks/endpoint.py`).
## 3. Scope
### In scope
| # | Функция |
|------|---------|
| F-01 | Кнопка/ссылка «Скачать GPX» в popup'е публичного трека (`_renderTrackPopupHtml` в `gps_tracks.js`) |
| F-02 | Backend-эндпоинт `GET /api/gps-tracks/{id}.gpx` — возвращает корректный GPX 1.1 для трека по его БД-id |
| F-03 | Имя файла: `<sanitized-name>.gpx` если есть имя, иначе `track-{id}.gpx`; передаётся в `Content-Disposition: attachment; filename=...` |
| F-04 | Тело GPX: `<gpx version="1.1">` с `<metadata>` (name, desc, time, link на первый external_url) и одним `<trk>` с одним `<trkseg>`, заполненным точками из `tracks.geom` |
| F-05 | MIME-тип ответа: `application/gpx+xml; charset=utf-8` |
| F-06 | Работа единообразна на обоих zoom-режимах: при MVT (z 811) и при GeoJSON (z ≥ 12) popup использует один и тот же id трека и один и тот же URL — клиент НЕ собирает GPX из MVT-геометрии |
| F-07 | Ошибки: 404 для несуществующего id, 500 — через стандартный HTTPException; на клиенте: ошибка качания → toast «Не удалось скачать трек», popup не закрывается |
| F-08 | Атрибуция источников сохранена в `<metadata>`: `<copyright>` и/или `<desc>` содержат source_id'ы и внешние ссылки (ODbL/Wikiloc TOS — обязательно) |
| F-09 | Мобильный UX: кнопка тапабельна (≥ 36 px высота), не ломает раскладку popup'а на узких экранах |
| F-10 | Аналитика клика: событие в консоль через существующий механизм `console.log` / `showToast` (телеметрия не вводится) |
### Out of scope
- Скачивание сразу нескольких треков (batch).
- Конвертация в KML / TCX / FIT / KMZ.
- Шаринг ссылки на скачивание (короткая ссылка / OG-карточка).
- Сохранение в личную «библиотеку» пользователя (нет аккаунтов).
- Загрузка скачанного файла обратно на карту — уже покрыто текущим
`gpx.js` (UI «Загрузить GPX»), и пересечения функциональностей нет.
- Авторизация / rate-limit на загрузку — публичные данные, тот же
cors-разрешённый эндпоинт что и GeoJSON.
- Восстановление высот по DEM для GPX (трек не имеет `<ele>`).
- Изменение схемы БД `gps_tracks.sqlite`.
## 4. Метрики успеха
| Метрика | Критерий |
|---------|----------|
| Доступность кнопки | Кнопка «Скачать GPX» видна в каждом popup'е публичного трека на desktop и mobile, на обоих zoom-режимах (z = 9 и z = 14) |
| Корректность файла | Скачанный файл валидируется парсером `gpx.js` (drag-and-drop в приложение) и рисуется на карте без ошибок |
| Корректность GPX 1.1 | Файл проходит XSD-валидацию `http://www.topografix.com/GPX/1/1/gpx.xsd` (online valdiator или `xmllint --schema`) |
| Совместимость | Файл открывается в OsmAnd / Locus / `gpx.studio` без ошибок |
| Имя файла | Имя файла читаемое, не содержит запрещённых на Windows/Android символов (`/\:*?"<>|`) |
| Атрибуция | В `<metadata>` явно перечислены все source_id и external_urls трека |
| Производительность | Эндпоинт отвечает ≤ 500 ms p95 для трека до 50 000 точек (среднестатистический эндуро-трек < 5000 точек) |
| Размер ответа | Для трека 5000 точек тело GPX ≤ 600 КБ (без gzip), ≤ 150 КБ (с gzip) |
| Регрессии | Существующий popup, MVT-/GeoJSON-режимы слоя, фильтры активностей/источников, halo на спутнике — не сломаны |
## 5. Риски
| Риск | Вероятность | Влияние | Митигация |
|------|-------------|---------|-----------|
| Геометрия трека отсутствует (`row.geom is None`) | Низкая | Низкое | 404 + toast «Геометрия трека недоступна»; такие записи редки (защита уже в `_wkb_to_coords`) |
| Очень длинный трек (десятки тысяч точек) → ответ > 5 МБ | Низкая | Среднее | Без упрощения геометрии. Документировать лимит. При необходимости — gzip через nginx (уже включён) |
| Wikiloc TOS требует ссылки на оригинал | Высокая | Высокое | F-08: `<metadata><link>` на оригинальную страницу для каждого external_url |
| ODbL атрибуция в GPX | Высокая | Высокое | F-08: `<copyright author="OpenStreetMap contributors" license="https://opendatacommons.org/licenses/odbl/">` для треков с `source = osm` |
| Опасный символ в `name` → XML-инъекция | Низкая | Высокое | Эскейп через стандартный XML-сериализатор (ElementTree / минимальный helper) |
| Имя файла с кириллицей не сохраняется в Safari | Средняя | Низкое | RFC 5987 — `filename*=UTF-8''<percent-encoded>` + ASCII-fallback `filename=...` |
| Кнопка обрезается на узком экране (≤ 360 px) | Средняя | Низкое | Кнопка на отдельной строке, `flex-wrap`, тестируется в UI-тестах TC-UI-02 |
## 6. Зависимости
- Бэкенд: существующий router `/api/gps-tracks`
(`src/api/gps_tracks/endpoint.py`), функции `_wkb_to_coords` и БД-слой.
Изменения чисто аддитивные.
- Фронтенд: модуль `src/web/gps_tracks.js`, функция
`_renderTrackPopupHtml`. Точечная правка HTML-шаблона.
- Никаких внешних сервисов / новых данных / миграций БД.
- Никаких новых npm/pip-зависимостей: GPX генерируется штатным
`xml.etree.ElementTree` (есть в stdlib Python 3.12).