107 lines
9.1 KiB
Markdown
107 lines
9.1 KiB
Markdown
---
|
||
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 8–11 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 8–11) и при 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).
|