9.1 KiB
9.1 KiB
type, work_item_id, title, version, status, created_at, updated_at, authors
| type | work_item_id | title | version | status | created_at | updated_at | authors | |
|---|---|---|---|---|---|---|---|---|
| brd | ET-006 | BRD: Скачивание трека из popup на карте | 1 | approved | 2026-06-03 | 2026-06-03 |
|
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).