---
type: acceptance-criteria
work_item_id: ET-008
title: "AC: GPS-треки с публичных платформ на карте"
version: 2
status: draft
created_at: 2026-06-01
updated_at: 2026-06-01
changelog:
  - "v2 (2026-06-01): полная переработка под BRD/TRZ v2 — критерии серверной агрегации, дедупликации, MVT-тайлов, фильтров активности/источника, popup, halo-на-спутнике. Предыдущая v1 описывала URL-импорт + OSM live-поиск."
authors:
  - "agent:analyst"
---

# Acceptance Criteria — ET-008: GPS-треки с публичных платформ на карте

## AC-01: Конфигурация источников и регионов

```gherkin
Feature: Расширяемая конфигурация

  Scenario: Включение нового источника
    Given config/gps_sources.yaml содержит источник с enabled=false
    When оператор меняет на enabled=true и перезапускает pipeline
    Then источник участвует в следующем прогоне
    And в /api/gps-tracks/health он появляется в tracks_by_source

  Scenario: Добавление нового региона
    Given оператор добавляет в config/gps_regions.yaml новую запись с bbox
    And запись не превышает 30 строк YAML
    When оператор запускает pipeline без аргументов
    Then новый регион обрабатывается всеми указанными в нём источниками
    And никаких правок Python-кода не требуется

  Scenario: Отключение источника
    Given источник был enabled=true и собрал N треков
    When оператор меняет на enabled=false
    Then следующий прогон pipeline пропускает этот источник
    And ранее собранные треки остаются в БД и отдаются API
    And в фильтре по источнику соответствующий чекбокс не выбран по умолчанию
```

## AC-02: Pipeline сбора

```gherkin
Feature: Pipeline gps_collect.py

  Scenario: Полный прогон по умолчанию
    Given config содержит регион ЦФО+Чувашия и 3 source enabled
    When оператор запускает scripts/gps_collect.py
    Then pipeline проходит по всем регионам и всем enabled-источникам
    And для каждой пары (region, source) пишется запись в pipeline_runs
    And exit code == 0 если хотя бы один трек собран по каждому источнику

  Scenario: Прогон одного источника
    When оператор запускает scripts/gps_collect.py --source osm
    Then обрабатывается только OSM
    And остальные source пропускаются

  Scenario: Падение одного источника не валит остальные
    Given OSM возвращает 503 на весь прогон
    When pipeline запущен
    Then OSM-источник помечается status='error' в pipeline_runs
    And другие источники продолжают работу
    And exit code сигнализирует ошибку (1) если запрошен strict-mode, иначе 0

  Scenario: Dry-run
    When оператор запускает с --dry-run
    Then никаких INSERT в БД не делается
    And pipeline_runs тоже не пишется
    And в stdout выводится план: N треков было бы собрано

  Scenario: Уважение rate-limit
    Given у источника rate_limit_sec=5
    When pipeline делает 10 последовательных запросов к этому источнику
    Then суммарное время ≥ 9 * 5 = 45 сек (между запросами)
```

## AC-03: Дедупликация

```gherkin
Feature: Дедупликация треков

  Scenario: Один трек найден в двух источниках
    Given OSM и EnduroRussia отдали один и тот же трек
       (один автор выложил на обоих)
    And bbox и длина совпадают в пределах допуска
    And даты совпадают
    When pipeline обрабатывает обе записи
    Then в БД одна запись tracks
    And sources_json содержит обоих
    And external_urls_json содержит обе ссылки

  Scenario: Похожие треки разных дат — НЕ дубли
    Given два трека с одинаковым bbox и длиной
    And даты отличаются на > 1 день
    Then записи разные, дедуп НЕ срабатывает

  Scenario: Треки без даты от разных источников
    Given оба трека без created_at
    And bbox и длина совпадают
    Then дедуп срабатывает (по умолчанию консервативный merge)
    And это поведение задокументировано в ADR-002

  Scenario: Метрика < 5% дубликатов
    Given в БД собрано ≥ 5000 треков
    When QA-инженер выбирает 100 случайных треков и руками проверяет дубли
    Then не более 5 треков (5%) являются дублями
```

## AC-04: Endpoint /api/gps-tracks (GeoJSON)

```gherkin
Feature: GeoJSON endpoint

  Scenario: Запрос с малым bbox
    Given в БД 1000 треков, из них 50 в bbox=[37.5,55.6,37.7,55.8]
    When клиент шлёт GET /api/gps-tracks?bbox=37.5,55.6,37.7,55.8
    Then ответ 200, FeatureCollection с 50 features
    And total_in_bbox=50, returned=50, truncated=false
    And time ≤ 300 мс p95

  Scenario: Bbox с обрезкой по limit
    Given в bbox 1500 треков
    When клиент шлёт GET .../api/gps-tracks?bbox=...&limit=500
    Then returned=500, total_in_bbox=1500, truncated=true

  Scenario: Фильтр по активности
    Given в bbox 100 треков, 20 enduro, 30 moto, 50 hike
    When клиент шлёт ?activity=enduro,moto
    Then returned=50

  Scenario: Фильтр по источнику
    Given в bbox 100 треков: 60 OSM, 30 EnduroRussia, 10 ttrails
    When клиент шлёт ?source=osm
    Then returned=60

  Scenario: Невалидный bbox
    When клиент шлёт bbox=foo
    Then ответ 400

  Scenario: bbox вне диапазона координат
    When клиент шлёт bbox=200,100,250,150
    Then ответ 400

  Scenario: Поля feature.properties
    Then каждая feature содержит: name, activity_type, user, created_at,
         length_km, sources (array), external_urls (array)
```

## AC-05: Endpoint /api/gps-tracks/tiles MVT

```gherkin
Feature: MVT tiles

  Scenario: Отдача тайла на z=10
    Given в БД есть треки в видимой области
    When клиент шлёт GET /api/gps-tracks/tiles/10/623/325.mvt
    Then ответ 200, Content-Type: application/x-protobuf
    And тело содержит layer gps_tracks с LineString features

  Scenario: Тайл из кэша
    Given тайл уже запрашивали
    When повторный запрос того же z/x/y
    Then header X-Cache: HIT
    And время ≤ 20 мс p95

  Scenario: Упрощение геометрии на низких зумах
    Given исходный трек 1000 точек на z=7
    When MVT генерируется
    Then feature имеет упрощённую геометрию (≤ 100 точек после Douglas-Peucker)

  Scenario: Properties фичи в MVT
    Then feature.properties содержит: id, activity, source, sources,
         length_km, name, ext_url
```

## AC-06: Endpoint health

```gherkin
Feature: Health endpoint

  Scenario: Полный отчёт
    When клиент шлёт GET /api/gps-tracks/health
    Then ответ 200 JSON содержит:
      | db_path             |
      | db_size_mb          |
      | tracks_total        |
      | tracks_by_source    | (объект source_id → int)
      | tracks_by_activity  | (объект activity → int)
      | last_pipeline_run   | (объект с started/finished/sources_ok/sources_error)
      | tile_cache_size     |

  Scenario: Health без БД
    Given БД отсутствует на диске
    When клиент шлёт GET /api/gps-tracks/health
    Then ответ содержит tracks_total=0 и предупреждение о БД (или 503)
```

## AC-07: Чекбокс «Публичные треки» в попапе

```gherkin
Feature: Включение слоя из попапа

  Scenario: Чекбокс присутствует
    Given пользователь нажимает #terrain-toggle
    Then в попапе #terrain-popup видна строка «Публичные треки» с чекбоксом

  Scenario: Включение слоя
    When пользователь ставит галку «Публичные треки»
    Then на карте появляются линии треков
    And localStorage['gps-tracks-enabled'] = 'true'
    And рядом с чекбоксом появляется ссылка «Фильтры…»

  Scenario: Выключение слоя
    When пользователь снимает галку
    Then линии исчезают с карты
    And localStorage = 'false'
    And ссылка «Фильтры…» скрывается

  Scenario: Подсказка о минимальном zoom
    Given текущий zoom < 8
    And чекбокс включён
    Then рядом с чекбоксом видна подсказка «Зум 8+»
    And линии на карте не видны (без ошибок)
```

## AC-08: Фильтры по активности и источнику

```gherkin
Feature: Sheet фильтров

  Scenario: Открытие sheet
    Given слой включён
    When пользователь нажимает «Фильтры…»
    Then открывается #sheet-gps-filters
    And видны секции «Тип активности», «Источник», «Цвет линий»
    And по умолчанию выбраны все активности и все источники

  Scenario: Фильтрация по активности
    Given в видимой области карты 743 трека, 200 enduro, 50 moto, …
    When пользователь снимает все галки кроме «Эндуро» и «Мото»
    Then на карте отображаются только enduro и moto треки
    And gps-stat-shown отражает новое число
    And фильтрация мгновенная (≤ 200 мс), без сетевого запроса

  Scenario: Фильтрация по источнику
    Given включено 3 источника
    When пользователь снимает «OSM»
    Then OSM-треки скрываются на карте

  Scenario: Переключение режима цвета
    Given color-mode = 'source'
    When пользователь выбирает «По активности»
    Then цвета линий перерисовываются по палитре активности
    And localStorage сохраняет 'gps-tracks-color-mode' = 'activity'

  Scenario: Сохранение фильтров между сессиями
    Given пользователь настроил фильтры (только enduro, только OSM)
    When пользователь перезагружает страницу
    Then sheet-фильтров восстанавливает те же чекбоксы
    And слой отображает только enduro+OSM треки
```

## AC-09: Popup при клике на трек

```gherkin
Feature: Popup трека

  Scenario: Клик по линии трека
    Given на карте отображается слой публичных треков
    When пользователь кликает на линию трека
    Then открывается popup с полями: name, activity (иконка+текст),
         length_km, points_count, created_at, user, sources (со ссылками)

  Scenario: Трек из двух источников
    Given трек имеет sources=['osm', 'enduro_russia']
    Then popup показывает обе ссылки

  Scenario: Трек без user/name
    Then popup показывает «Без названия» и не показывает строку «Автор»

  Scenario: Клик по фону карты
    Given открыт popup
    When пользователь кликает на пустое место карты
    Then popup закрывается
```

## AC-10: Z-order и совместимость с другими слоями

```gherkin
Feature: Z-order

  Scenario: Слой выше trails, ниже маршрута OSRM
    Given на карте: OSM tiles + trails + публичные треки + маршрут OSRM
    Then визуально маршрут OSRM перекрывает публичные треки
    And публичные треки перекрывают trails из vector tiles
    And базовая карта (OSM) — самый нижний

  Scenario: Совместимость с ET-006 (личные GPX)
    Given пользователь загрузил свой GPX-файл (ET-006)
    And слой публичных треков включён
    Then оба видны параллельно
    And личный трек визуально выше публичных
```

## AC-11: Совместимость со спутниковой подложкой (ET-007)

```gherkin
Feature: Halo на спутнике

  Scenario: Включение спутника
    Given слой публичных треков включён
    When пользователь переключает подложку на «Спутник»
    Then линии треков видны на спутнике
    And появляется белая обводка (halo) для контраста

  Scenario: Возврат на схему
    When пользователь возвращается на «Схема»
    Then halo скрывается
    And линии отображаются обычными цветами

  Scenario: Halo учитывает чекбокс
    Given спутник активен
    When пользователь выключает чекбокс «Публичные треки»
    Then и линии, и halo скрываются
```

## AC-12: Сохранение при смене стиля карты

```gherkin
Feature: Переживание setStyle()

  Scenario: Переключение тёмной темы
    Given слой включён, фильтры настроены
    When пользователь переключает тёмную тему (вызывает map.setStyle())
    Then слой публичных треков восстанавливается
    And линии видны с теми же цветами по тому же color-mode
    And фильтры активности/источника сохранены

  Scenario: Переключение спутник→схема
    Given слой включён, активен спутник
    When пользователь переключается на схему
    Then слой остаётся видим, halo выключается

  Scenario: Включение hillshade
    Given слой включён
    When пользователь включает hillshade
    Then публичные треки остаются видны (поверх hillshade)
```

## AC-13: Производительность

```gherkin
Feature: SLA отклика

  Scenario: GeoJSON p95
    When 100 запросов GET /api/gps-tracks?bbox=… с ≤ 500 треков в bbox
    Then p95 ≤ 300 мс

  Scenario: MVT cold
    When запрос MVT-тайла без кэша
    Then p95 ≤ 200 мс

  Scenario: MVT hot
    When повторный запрос того же тайла
    Then ≤ 20 мс, X-Cache: HIT

  Scenario: Pan/zoom без фризов
    Given слой включён с 500 треками в видимой области
    When пользователь делает 10 быстрых pan-операций
    Then нет видимых фризов (FPS ≥ 30 на десктопе)
```

## AC-14: Защита от шторма запросов

```gherkin
Feature: Debounce и AbortController

  Scenario: Быстрый pan не плодит запросов
    Given слой включён на z ≥ 12
    When пользователь делает 5 быстрых pan-операций за 1 секунду
    Then выполняется не более 2 запросов /api/gps-tracks (debounce 500ms)
    And предыдущие запросы отменены AbortController

  Scenario: На z < 8 запросов нет
    Given пользователь на z=5
    When пользователь панит карту
    Then запросов /api/gps-tracks?bbox=… не выполняется
```

## AC-15: Атрибуция

```gherkin
Feature: Атрибуция источников

  Scenario: На карте видна атрибуция
    Given слой включён, включены OSM и EnduroRussia
    Then в правом нижнем углу карты отображается строка
         «© OpenStreetMap contributors (ODbL) | EnduroRussia.ru»

  Scenario: Popup содержит ссылку на оригинал
    Given пользователь открыл popup трека
    Then в нём видна ссылка «↗» на источник (или несколько)
    When пользователь кликает на ссылку
    Then открывается новая вкладка с оригиналом
```

## AC-16: Безопасность и юридические гарантии

```gherkin
Feature: Юридический минимум

  Scenario: Источник без ADR не активируется
    Given оператор пытается включить новый source в gps_sources.yaml
    But ADR licensing-review отсутствует
    Then pipeline-tests падают (CI блокирует merge)

  Scenario: Pipeline не сохраняет запрещённые поля
    Given source-ADR требует не сохранять `user`
    When pipeline получает трек с user='Vasya'
    Then в БД user=NULL для этой записи

  Scenario: Удаление по запросу автора
    Given автор оригинала пометил трек удалённым на источнике
    When следующий прогон pipeline обнаруживает это
    Then запись в нашей БД помечается как удалённая или удаляется
```

## AC-17: Расширяемость на новые регионы

```gherkin
Feature: Добавление региона ≤ 30 строк YAML

  Scenario: Добавление «Северный Кавказ»
    Given существующий config/gps_regions.yaml
    When разработчик добавляет YAML-блок региона: id, name, bbox, enabled,
         sources (≤ 30 строк суммарно)
    And запускает pipeline
    Then регион обрабатывается всеми указанными источниками
    And никаких правок Python-файлов не требуется
    And в /api/gps-tracks/health новый регион виден в last_pipeline_run.regions
```
