15 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 | |
|---|---|---|---|---|---|---|---|---|
| trz | ET-006 | ТЗ: Скачивание трека из popup на карте | 1 | approved | 2026-06-03 | 2026-06-03 |
|
ТЗ — ET-006: Скачивание трека из popup на карте
Документ описывает требования к функциональности. Архитектурные решения (выбор XML-библиотеки, формат потоковой генерации и т. п.) остаются на этап Architect → ADR.
1. Сводка
Добавить кнопку «Скачать GPX» в popup публичного GPS-трека (слой
ET-008, gps_tracks.js). По нажатию браузер инициирует загрузку
файла GPX 1.1 c геометрией и метаданными трека, который сервер
формирует на лету по новому HTTP-эндпоинту
GET /api/gps-tracks/{id}.gpx.
2. Затрагиваемые компоненты
| Слой | Файл / модуль | Тип правки |
|---|---|---|
| Backend | src/api/gps_tracks/endpoint.py |
новый handler get_track_gpx |
| Backend | src/api/gps_tracks/db.py (или новый модуль) |
новый helper get_track_by_id(conn, id) |
| Backend | src/api/gps_tracks/gpx_builder.py (новый) |
сериализация WKB → GPX 1.1 XML |
| Frontend | src/web/gps_tracks.js |
дополнение _renderTrackPopupHtml (кнопка) |
| Frontend | src/web/app.css |
стили для .track-popup-download |
| Tests | tests/unit/test_gpx_builder.py (новый) |
unit GPX-сериализатора |
| Tests | tests/integration/test_gps_tracks_endpoint.py (расширить) |
новый case на .gpx endpoint |
| Tests | tests/e2e/test_track_popup_download.spec.js (новый) |
Playwright e2e |
3. Функциональные требования
3.1 Backend: GET /api/gps-tracks/{id}.gpx
REQ-F-01. Эндпоинт GET /api/gps-tracks/{track_id}.gpx
регистрируется в существующем router'е (prefix="/api/gps-tracks"),
без auth.
REQ-F-02. Параметр track_id: int валидируется FastAPI как int.
Не-int → 422 (стандартное поведение FastAPI).
REQ-F-03. Если трек с таким id не найден или tracks.geom пустой /
WKB не парсится — ответ 404 Not Found, тело
{"detail":"Track not found"}.
REQ-F-04. Успешный ответ:
Content-Type: application/gpx+xml; charset=utf-8Content-Disposition: attachment; filename="<ascii-fallback>"; filename*=UTF-8''<percent-encoded-utf8>Access-Control-Allow-Origin: *(как у соседних эндпоинтов).- Тело — валидный GPX 1.1 XML.
REQ-F-05. Имя файла:
- Базовое имя =
track.name, если есть и не пустое; иначеtrack-{id}. - Удалить запрещённые символы Windows/Android:
/\:*?"<>|и управляющие < 0x20. - Свернуть пробелы, обрезать до 64 символов.
- Расширение
.gpx. - ASCII-fallback: транслит/удаление не-ASCII, минимум
track-{id}.gpx.
3.2 Структура GPX 1.1
REQ-F-06. Корень:
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1"
creator="enduro-trails (https://openclaw.mva154.duckdns.org/enduro/)"
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">
...
</gpx>
REQ-F-07. <metadata> обязательно содержит:
<name>= имя трека (илиtrack-{id}).<desc>= склейка изtrack.description(если есть), активности (русскоеGPS_ACTIVITY_LABELS[activity]), длины (км), источников (список source_id через запятую). Многострочно через\n.<time>=track.created_atесли есть и парсится в ISO 8601; иначе — текущее UTC в форматеYYYY-MM-DDTHH:MM:SSZ.<link href="...">для каждого external_url, с<text>{source_id}</text>. Если ссылок нет — секция опущена.<author><name>{track.user}</name></author>если естьuser, иначе секция опущена.<copyright>(см. REQ-F-08).
REQ-F-08. Атрибуция в <copyright>:
- Если
sourcesсодержит"osm"→<copyright author="OpenStreetMap contributors"><license>https://opendatacommons.org/licenses/odbl/</license></copyright>. - Иначе, если есть
wikiloc→<copyright author="Wikiloc contributors"><license>https://www.wikiloc.com/wikiloc/legalNotice.do</license></copyright>. - Иначе, если есть
enduro_russia→<copyright author="EnduroRussia.ru"/>. - Иначе — секция опущена.
REQ-F-09. Тело трека:
<trk>
<name>{track.name or "track-{id}"}</name>
<type>{activity_type}</type>
<trkseg>
<trkpt lat="..." lon="...">
<!-- ele опускается: высот в БД нет -->
</trkpt>
...
</trkseg>
</trk>
- Один
<trk>с одним<trkseg>(без разбиения на сегменты). - Координаты выводятся с точностью 6 знаков после запятой (≈ 0.1 м на экваторе).
- Порядок точек — как в БД (WKB порядок == порядок исходного трека).
- Тег
<ele>не выводится (БД 2D).
REQ-F-10. Все текстовые поля экранируются по правилам XML:
&, <, >, ", ' → entity'и. Управляющие символы < 0x20
вырезаются. Никакого ручного склеивания строк — использовать
стандартный XML-сериализатор (xml.etree.ElementTree или
аналогичный, без потери порядка детей).
REQ-F-11. Сериализация выполняется в память (bytes). Потоковый
ответ не требуется: средний трек < 500 КБ, лимит верхней границы —
50 000 точек (~6 МБ) приемлем.
3.3 Frontend: кнопка в popup'е
REQ-F-12. В _renderTrackPopupHtml (src/web/gps_tracks.js)
после блока sourcesHtml добавляется блок:
<div class="track-popup-actions">
<a class="track-popup-download" href="{basePath}/api/gps-tracks/{id}.gpx"
download="{filename}">
⬇ Скачать GPX
</a>
</div>
basePathсобирается так же, как в_ensureGpsSources(window.location.pathname.replace(/\/[^/]*$/, '') || '').idберётся изfeature.properties.id(это БД-id, оно есть и в GeoJSON-ответе REQ-F-09 ET-008, и в MVT-атрибутах).download="{filename}"— клиентский hint для имени файла; сервер дублирует черезContent-Disposition(надёжнее в Safari).
REQ-F-13. Если feature.properties.id отсутствует (например, в
edge case'е) — кнопка не рендерится.
REQ-F-14. Клик по кнопке НЕ должен закрывать popup. Стандартный
<a download> срабатывает без preventDefault и popup остаётся
открытым (MapLibre popup закрывается только при клике вне popup'а или
по крестику; closeOnClick: true относится к карте, не к содержимому).
REQ-F-15. Кнопка стилизуется как акцентный action (не серый текст).
Цвет — var(--accent) если объявлен в app.css, иначе синий
#2271b1. Тёмная и светлая тема: использовать css-переменные,
которые уже есть в app.css.
REQ-F-16. Минимальная высота тач-таргета — 36 px; padding
8px 12px; курсор pointer.
3.4 Обработка ошибок на клиенте
REQ-F-17. Если сервер вернул не-2xx (определяется по событию
error через fetch-обёртку), показать showToast('Не удалось скачать трек'). Реализация на клиенте — пассивный <a download>,
поэтому достаточно ловить ошибку через fetch + blob + временный
<a> (см. ADR-03 / Architect определит окончательный вариант).
REQ-F-18. Существующее поведение popup'а не меняется: клик по треку открывает popup, клик вне трека — закрывает. Кнопка в popup'е не интерферирует с MapLibre.
4. Нефункциональные требования
| Код | Требование |
|---|---|
| NF-01 | Эндпоинт отвечает ≤ 500 ms p95 на треке до 50 000 точек (i7-12700, SSD, БД ~1 ГБ) |
| NF-02 | Память: пик ≤ 10 МБ на запрос (50 000 точек × ~120 байт). Без стриминга |
| NF-03 | Кэширование: ответ помечается Cache-Control: public, max-age=86400 — данные публичные, обновляются раз в неделю pipeline'ом |
| NF-04 | gzip — на уровне nginx, без ручного сжатия в Python |
| NF-05 | Безопасность: только GET, никаких user-input в SQL кроме track_id: int (FastAPI приведёт тип). Никакого XML-external-entity (использовать стандартный сериализатор, не парсер) |
| NF-06 | i18n: UI-надпись «Скачать GPX» только на русском, без переключения языка (на проекте RU-only) |
| NF-07 | Совместимость браузеров: Chrome ≥ 120, Safari iOS ≥ 16, Firefox ≥ 120 — все поддерживают <a download> для same-origin |
| NF-08 | Регрессия: все существующие тесты в tests/unit/test_gps_tracks_* и tests/integration/test_gps_tracks_endpoint.py — зелёные |
5. Контракт API
5.1 GET /api/gps-tracks/{track_id}.gpx
| Поле | Значение |
|---|---|
| Method | GET |
| Path | /api/gps-tracks/{track_id}.gpx |
| Path params | track_id: int ≥ 1 |
| Query | — |
| Body | — |
| 200 Content-Type | application/gpx+xml; charset=utf-8 |
| 200 Headers | Content-Disposition: attachment; filename="..."; filename*=UTF-8''..., Cache-Control: public, max-age=86400, Access-Control-Allow-Origin: * |
| 200 Body | GPX 1.1 XML |
| 404 | {"detail": "Track not found"} если трека нет или геометрия пустая |
| 422 | track_id не int (стандарт FastAPI) |
| 500 | {"detail": "DB error: ..."} (HTTPException) |
5.2 Пример успешного ответа
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="enduro-trails (https://openclaw.mva154.duckdns.org/enduro/)"
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>Кольцо вокруг Малинок</name>
<desc>Эндуро · 42.5 км · Источники: osm, wikiloc</desc>
<author><name>vasya42</name></author>
<copyright author="OpenStreetMap contributors">
<license>https://opendatacommons.org/licenses/odbl/</license>
</copyright>
<link href="https://www.openstreetmap.org/user/vasya42/traces/123"><text>osm</text></link>
<link href="https://www.wikiloc.com/trail/456"><text>wikiloc</text></link>
<time>2025-08-14T09:30:00Z</time>
</metadata>
<trk>
<name>Кольцо вокруг Малинок</name>
<type>enduro</type>
<trkseg>
<trkpt lat="55.789012" lon="37.123456"/>
<trkpt lat="55.789543" lon="37.124012"/>
...
</trkseg>
</trk>
</gpx>
6. Сценарии работы
6.1 Happy path — desktop, z = 14
- Пользователь включает слой «Публичные треки».
- Зумит на z=14, кликает по линии трека.
- Открывается popup с метаданными.
- Видит кнопку «⬇ Скачать GPX».
- Кликает — браузер начинает загрузку файла
<name>.gpx. - Popup остаётся открытым.
6.2 Happy path — mobile, z = 9 (MVT)
- Пользователь на мобильном устройстве включил слой и зумит до z=9.
- Тапает по треку — открывается popup.
- Тапает кнопку «Скачать GPX» — то же поведение что и на desktop.
6.3 Edge — нет геометрии в БД
- Клик по треку → popup открывается (запись есть).
- Тап на «Скачать GPX» → сервер вернул 404.
- Показать toast «Не удалось скачать трек».
- Popup открыт, остальные кнопки работают.
6.4 Edge — невалидное имя
- Трек с именем
OSM/Trail*?<2024>. - Имя файла очищается до
OSM-Trail-2024.gpx(по REQ-F-05). - Файл скачивается; внутри GPX
<name>сохранён в исходном виде (экранированный XML).
7. Невырезаемые ссылки на источники
- ET-008 /
02-trz.md(этот же work_item folder в прошлой жизни ID ET-008): эндпоинтGET /api/gps-tracksи popup-логика. - ET-009 /
01-brd.md: правила атрибуции источников. docs/architecture/README.md§«GPS Tracks Pipeline» — общая архитектура слоя публичных треков.- ADR-007 §6 «Licensing guard» — обязательство сохранять атрибуцию во всех клиентских артефактах.
8. Принятые ограничения
- Высоты в GPX отсутствуют — потому что их нет в БД. Возможное дополнение (DEM-обогащение) — отдельный work item.
- Нет batch-скачивания — отдельный work item при необходимости.
- Нет аналитики кликов — телеметрия в проекте не введена.
- Нет авторизации — все данные публичные.