--- 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 | Имя файла: `.gpx` если есть имя, иначе `track-{id}.gpx`; передаётся в `Content-Disposition: attachment; filename=...` | | F-04 | Тело GPX: `` с `` (name, desc, time, link на первый external_url) и одним `` с одним ``, заполненным точками из `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 | Атрибуция источников сохранена в ``: `` и/или `` содержат 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 (трек не имеет ``). - Изменение схемы БД `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 символов (`/\:*?"<>|`) | | Атрибуция | В `` явно перечислены все 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: `` на оригинальную страницу для каждого external_url | | ODbL атрибуция в GPX | Высокая | Высокое | F-08: `` для треков с `source = osm` | | Опасный символ в `name` → XML-инъекция | Низкая | Высокое | Эскейп через стандартный XML-сериализатор (ElementTree / минимальный helper) | | Имя файла с кириллицей не сохраняется в Safari | Средняя | Низкое | RFC 5987 — `filename*=UTF-8''` + 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).