Files
enduro-trails/docs/work-items/ET-006/02-trz.md
claude-bot 6edf97fe79
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Successful in 6s
CI / build (push) Has been skipped
analyst(ET): auto-commit from analyst run_id=59
2026-06-03 17:33:09 +00:00

15 KiB
Raw Blame History

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
agent:analyst

ТЗ — 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-8
  • Content-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

  1. Пользователь включает слой «Публичные треки».
  2. Зумит на z=14, кликает по линии трека.
  3. Открывается popup с метаданными.
  4. Видит кнопку «⬇ Скачать GPX».
  5. Кликает — браузер начинает загрузку файла <name>.gpx.
  6. Popup остаётся открытым.

6.2 Happy path — mobile, z = 9 (MVT)

  1. Пользователь на мобильном устройстве включил слой и зумит до z=9.
  2. Тапает по треку — открывается popup.
  3. Тапает кнопку «Скачать GPX» — то же поведение что и на desktop.

6.3 Edge — нет геометрии в БД

  1. Клик по треку → popup открывается (запись есть).
  2. Тап на «Скачать GPX» → сервер вернул 404.
  3. Показать toast «Не удалось скачать трек».
  4. Popup открыт, остальные кнопки работают.

6.4 Edge — невалидное имя

  1. Трек с именем OSM/Trail*?<2024>.
  2. Имя файла очищается до OSM-Trail-2024.gpx (по REQ-F-05).
  3. Файл скачивается; внутри 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. Принятые ограничения

  1. Высоты в GPX отсутствуют — потому что их нет в БД. Возможное дополнение (DEM-обогащение) — отдельный work item.
  2. Нет batch-скачивания — отдельный work item при необходимости.
  3. Нет аналитики кликов — телеметрия в проекте не введена.
  4. Нет авторизации — все данные публичные.