13 KiB
ТЗ: Скачивание трека из popup на карте
Work Item: ET-011 Стадия: Анализ → Architecture Автор: analyst Дата: 2026-06-03
1. Сводка
Добавить в существующий popup публичного GPS-трека (слой ET-008) кнопку
«Скачать», которая запрашивает с сервера GPX-файл и сохраняет его в
загрузки пользователя. Новый backend-эндпоинт собирает GPX 1.1 из
геометрии трека в БД gps_tracks.sqlite.
2. Функциональные требования
REQ-F-01 — Кнопка «Скачать» в popup трека
В popup публичного трека (создаётся в _renderTrackPopupHtml(props),
src/web/gps_tracks.js, l.463) должна появляться кнопка «Скачать».
- Иконка: download (SVG, как в
sheet-routedownloadGPX, l.135–137 вindex.html). - Tooltip / aria-label: «Скачать GPX».
- Размещение: в правом верхнем углу popup, рядом с названием трека, или отдельной строкой в конце popup перед источниками — на усмотрение архитектора, но всегда видна без скролла.
- Тапабельная зона: ≥ 32×32 CSS px (mobile-friendly, REQ-NF-04 ниже).
REQ-F-02 — Backend: эндпоинт скачивания
Реализовать в роутере src/api/gps_tracks/endpoint.py новый GET-эндпоинт:
GET /api/gps-tracks/{track_id}/download
GET /api/gps-tracks/{track_id}/download?format=gpx (синоним)
Параметры:
track_id(path, int, обязательный) —tracks.idиз БД.format(query, optional, default=gpx) — формат файла. Допустимые значения для текущей итерации:gpx. (При закрытии Q-2 = «делаем KML» — добавитсяkml.)
Поведение:
- 200 +
Content-Type: application/gpx+xml(для GPX) илиapplication/vnd.google-earth.kml+xml(для KML). Content-Disposition: attachment; filename="<safe-name>.gpx"; filename*=UTF-8''<urlencoded-name>.gpx(RFC 5987, REQ-NF-05 ниже).- 404, если
track_idне существует. - 400, если
formatне входит в whitelist. - 403, если источник трека запрещает реэкспорт (см. REQ-F-06 и Q-1 в BRD).
REQ-F-03 — Содержимое GPX
GPX-файл должен соответствовать схеме GPX 1.1 (http://www.topografix.com/GPX/1/1) и содержать:
- Корневой
<gpx>с атрибутами:version="1.1"creator="Enduro Trails"xmlns="http://www.topografix.com/GPX/1/1"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
- Блок
<metadata>с:<name>—tracks.nameили «Без названия».<desc>—tracks.description(если есть).<time>—tracks.created_atв ISO-8601 (если есть, иначе пропустить).<author><name>—tracks.user(если есть).<link href="<external_url>"><text>Источник: <source_id></text></link>— по одному<link>на каждый элементexternal_urls.<copyright author="Enduro Trails"><license>https://www.openstreetmap.org/copyright</license></copyright>— для OSM-источника. Для других — без<copyright>либо со ссылкой на исходный URL.
- Ровно один
<trk>с:<name>—tracks.name.<type>—activity_type(например,enduro).- Ровно один
<trkseg>с<trkpt lat="..." lon="...">для каждой координаты из WKB-геометрииtracks.geom. Без<ele>и<time>(см. BRD A2).
REQ-F-04 — Имя файла
Имя файла (для Content-Disposition и filename*) формируется так:
- Берём
tracks.name. Если пустое / NULL — используемtrack-<id>. - Заменяем все недопустимые для FAT/NTFS символы (
/ \ : * ? " < > |) на_. - Триммим до 80 символов.
- Транслитерация не нужна — современные браузеры понимают
filename*=UTF-8''…(RFC 5987). - Расширение:
.gpx(или.kml).
Например: tracks.name = "По грязи к Чёрному озеру" →
По грязи к Чёрному озеру.gpx (через filename*=UTF-8''%D0%9F%D0%BE…).
REQ-F-05 — Поведение на фронте
При клике на кнопку «Скачать»:
- Не закрывать popup (или закрывать — на усмотрение архитектора, главное консистентно с остальными кнопками в проекте). Рекомендация: не закрывать, чтобы пользователь видел индикатор/успех.
- Сделать GET-запрос на
/api/gps-tracks/{id}/downloadчерез<a href="..." download="...">.click()(стандартный паттерн, отлично работает в desktop и mobile-браузерах) или черезfetch+BlobURL.createObjectURL— выбор за архитектором, см. R-1 в BRD.
- На время запроса показать спиннер/индикатор на самой кнопке (опц.) — нужно если бэк > 200 ms. Hint: трек на 50 000 точек собирается ≈ 80–150 ms (см. NFR-01), так что индикатор большинству не нужен.
- При ошибке (HTTP ≠ 200) — показать
showToast(...)(функция уже есть в проекте) с человекочитаемым сообщением:- 403 → «Источник запрещает скачивание. Откройте трек на сайте источника.»
- 404 → «Трек не найден.»
- 5xx / network → «Не удалось скачать. Попробуйте ещё раз.»
REQ-F-06 — Защита по лицензии источника (зависит от Q-1)
Если Owner закрывает Q-1 как «только OSM»:
- Backend проверяет
tracks.sources_json. Если ни одного из источников не относится к разрешённому whitelist'у (по умолчанию["osm"]) — возвращает 403 c JSON{"detail":"source_forbidden", "external_urls":[...]}. - Frontend в обработчике 403 показывает toast и, если есть
external_urls, кнопку «Открыть на сайте источника».
Если Owner отвечает «всё разрешено» — этот REQ становится no-op (вырезать).
REQ-F-07 — Логирование
Каждое успешное скачивание логируется server-side:
uvicorn access-log + (опц.) отдельная строка в stdout формата
track_download id=<id> source=<sources> size_bytes=<n> ip=<remote>.
Это нужно для NFR-06 (наблюдаемость).
3. Нефункциональные требования
REQ-NF-01 — Производительность
Сборка GPX и отдача для трека до 50 000 точек — не дольше 300 ms от запроса до начала ответа (P95 на текущем железе test-среды). Размер ответа для типичного трека 100 км / 5 000 точек — до 800 КБ (чистый XML, без gzip; ответ может быть gzip'нут средствами uvicorn).
REQ-NF-02 — Потолок размера ответа
Если число точек в треке > 200 000 (защита от patho-кейсов) —
возвращать 413 Payload Too Large с сообщением «Трек слишком большой
для скачивания». Реализация: проверка tracks.points_count до сборки XML.
REQ-NF-03 — Соответствие схеме GPX 1.1
Полученный файл должен проходить валидацию по схеме
http://www.topografix.com/GPX/1/1/gpx.xsd без warnings/errors. Тест в
tests/api/test_gps_tracks_download.py (см. test plan).
REQ-NF-04 — UX mobile
- Кнопка «Скачать» должна быть удобно тапабельной на мобильных (≥ 32×32 CSS px).
- Popup не должен «прыгать» из-за появления кнопки — высота фиксирована или растёт плавно.
- При ширине viewport < 420 px кнопка остаётся видимой (popup имеет
max-width: 300px— см.gps_tracks.jsl.514).
REQ-NF-05 — Заголовок Content-Disposition
Заголовок должен поддерживать UTF-8 имена через RFC 5987:
Content-Disposition: attachment; filename="track.gpx"; filename*=UTF-8''%D0%9F%D0%BE…
Параметр filename (без *) — ASCII-fallback (транслит или track-<id>.gpx).
REQ-NF-06 — Наблюдаемость
- 200/4xx/5xx ответы видны в
uvicornaccess-log. - Стек-трейсы 5xx уходят в stderr (текущая практика FastAPI/uvicorn).
- Метрики (RPS / latency) — не требуются в этой итерации.
REQ-NF-07 — Безопасность
track_id— int, парсится FastAPI, защита от SQL-инjection встроенная.- Имя файла санитизуется (REQ-F-04) — защита от path-traversal в загрузках.
Access-Control-Allow-Origin: *уже стоит в CORS middleware — не трогаем; iframe-embed возможен.
4. Out of scope (явно)
- KML — в backlog (см. Q-2). Если Owner закрывает Q-2 как «делаем» —
REQ-F-02 расширяется (
format=kml), но это не предмет данной итерации. - Сохранение скачанного трека в IndexedDB / в
sheet-gpx(как пользовательский GPX по ET-006) — отдельная фича. - Bulk-download (несколько треков). Только один за запрос.
- Конвертация формата (waypoints, маркеры).
5. Артефакты, к которым прикасаемся
src/web/gps_tracks.js— функция_renderTrackPopupHtml(props)и (вероятно) обработчик клика на новую кнопку.src/web/app.css(илиgps_tracks.jsinline-стили) — стиль кнопки.src/api/gps_tracks/endpoint.py— добавляется новый route.src/api/gps_tracks/db.py(возможно) — функцияget_track_by_id().tests/api/test_gps_tracks_download.py— новые тесты (см. test plan).tests/web/test_gps_tracks_popup.spec.tsили аналог — UI-тесты (Playwright, см.04b-ui-test-cases.md).- ADR
docs/work-items/ET-011/06-adr/*.md(создаст architect): про механизм отдачи (link vs blob), про обработку лицензии источника.
6. Зависимости
- Слой ET-008 «Публичные треки» уже в проде (тестовая среда). Этот work item расширяет его popup.
- БД
gps_tracks.sqliteинициализируется через миграциюmigrations/gps_tracks_001_init.sql— её менять не нужно (все необходимые поля уже есть:id,name,description,activity_type,user,created_at,length_m,points_count,geom,sources_json,external_urls_json).
7. Глоссарий
- Public track — публичный GPS-трек из таблицы
tracksв БДgps_tracks.sqlite. Источник — OSM, EnduroRussia, Wikiloc, ttrails и т.п. - GPX — GPS Exchange Format 1.1, XML-формат для треков и точек.
- KML — Keyhole Markup Language 2.2, XML-формат Google Earth.
- Popup — MapLibre
maplibregl.Popup, всплывающее окно по клику на feature.