Files
enduro-trails/docs/work-items/ET-012/02-trz.md
claude-bot eb9adbc930
All checks were successful
CI / lint (push) Successful in 5s
CI / test (push) Successful in 7s
CI / build (push) Successful in 2s
analyst(ET): auto-commit from analyst run_id=72
2026-06-04 06:00:55 +00:00

23 KiB
Raw Blame History

type, work_item_id, title, version, status, created_at, updated_at, authors, related
type work_item_id title version status created_at updated_at authors related
trz ET-012 ТЗ: Показывать пользовательские треки с зума z5 1 draft 2026-06-04 2026-06-04
agent:analyst
ET-008
ET-009

ТЗ — ET-012: Показывать пользовательские треки с зума z5

1. Терминология

  • MVT-слойgps-tracks-layer-mvt, отрисовка треков из vector-source gps-tracks-tiles (тайлы /api/gps-tracks/tiles/{z}/{x}/{y}.mvt). Активен при GPS_TRACKS_MIN_ZOOM ≤ zoom < GPS_TRACKS_ZOOM_CUTOFF.
  • GeoJSON-слойgps-tracks-layer-geo, отрисовка треков из GeoJSON-source (запрос /api/gps-tracks?bbox=…). Активен при zoom ≥ GPS_TRACKS_ZOOM_CUTOFF = 12. ET-012 не трогает этот слой.
  • Halo — белый ореол на спутниковой подложке (gps-tracks-halo-mvt-satellite, gps-tracks-halo-geo-satellite).
  • Zoom-tier — диапазон зумов (например, z ≤ 5, 6 ≤ z ≤ 7), для которого build_gps_mvt применяет общий набор лимитов (min_length_m, limit) и порог упрощения (tolerance).
  • Douglas-Peucker tolerance — параметр shapely.LineString.simplify, в градусах WGS84. На широте 55°: 1° долготы ≈ 64 км, 1° широты ≈ 111 км.
  • Zoom-hint — UI-надпись «Зум 8+» (#public-tracks-zoom-hint), подсказывающая, что нужно увеличить зум, чтобы увидеть слой.

2. Архитектурные опоры

ET-012 не строит новых модулей. Используем существующее:

  • src/web/gps_tracks.js — клиентский слой ET-008/ET-009/ET-011. Константы: GPS_TRACKS_ZOOM_CUTOFF = 12, GPS_TRACKS_MIN_ZOOM = 8.
  • src/api/gps_tracks/mvt.py:build_gps_mvt — серверная сборка MVT с tier-логикой min_length_m / limit и _simplify_coords.
  • src/api/gps_tracks/endpoint.py:get_gps_tile — обработчик /api/gps-tracks/tiles/{z}/{x}/{y}.mvt. Валидация 0 ≤ z ≤ 22 уже есть. LRU-кэш _gps_tile_cache размер 1024 — не меняем.
  • src/api/gps_tracks/db.py:get_tracks_in_bbox — bbox-запрос по индексам min_lon/max_lon/min_lat/max_lat. Не меняем.

ET-012 = значения констант + одна функция-tier + одна функция-simplify + три CSS/MapLibre-выражения + один hint.

3. Требования

REQ-F-01 — Клиентская константа GPS_TRACKS_MIN_ZOOM

Файл src/web/gps_tracks.js, строка

const GPS_TRACKS_MIN_ZOOM = 8;       // ниже — слой скрыт

заменить на

const GPS_TRACKS_MIN_ZOOM = 5;       // ниже — слой скрыт

Acceptance check.

grep -n "GPS_TRACKS_MIN_ZOOM" src/web/gps_tracks.js

Первое вхождение содержит = 5. Никаких других мест объявления этой константы в src/web/ нет (grep -R "GPS_TRACKS_MIN_ZOOM" src/web/).

REQ-F-02 — Vector-source minzoom использует ту же константу

В _ensureGpsSources (gps_tracks.js, около строки 178) запись

minzoom: GPS_TRACKS_MIN_ZOOM,

не меняется — она автоматически примет новое значение 5.

Acceptance check. Через DevTools на test-среде:

window._map.getSource('gps-tracks-tiles').minzoom === 5

REQ-F-03 — Backend: zoom-tier для z=5 и z=6 в build_gps_mvt

Файл src/api/gps_tracks/mvt.py, функция build_gps_mvt, блок «Min-length фильтр по зуму» (строки ~104-116) заменить на:

# Min-length фильтр и cap на число фич по зуму
if z <= 5:
    min_length_m = 10000   # 10 км — только «магистральные» треки
    limit = 1500
elif z == 6:
    min_length_m = 5000    # 5 км
    limit = 2000
elif z == 7:
    min_length_m = 2000    # как было для z<=7
    limit = 3000
elif z <= 9:
    min_length_m = 0
    limit = 8000
elif z <= 11:
    min_length_m = 0
    limit = 15000
else:
    min_length_m = 0
    limit = 25000

Цифры подобраны под цели:

  • z5: лимит 1500 фич × ~200 байт после генерализации ≈ 300 KB MVT до gzip — близко к гейту M-8 (200 KB). Если на реальных данных получится > 200 KB — снизить limit до 1000 в дев-итерации.
  • min_length 10 км отсекает короткие тестовые трассы — они визуально не различимы на z5.

REQ-F-04 — Backend: tier для tolerance в _simplify_coords

Файл src/api/gps_tracks/mvt.py, функция _simplify_coords, заменить блок выбора tolerance на:

if z >= 12:
    return coords
elif z >= 10:
    tolerance = 0.0005   # ~50 м
elif z >= 8:
    tolerance = 0.002    # ~200 м
elif z == 7:
    tolerance = 0.008    # ~800 м (как сейчас для z<=7)
elif z == 6:
    tolerance = 0.018    # ~2 км
else:
    tolerance = 0.04     # ~4 км (z5 и ниже)

Замечание. tolerance — в градусах долготы; на 55° с.ш. её эквивалент по расстоянию = tolerance * 64 км. Для z5 на пиксель карты приходится ≈ 5 км по долготе на 55° с.ш., так что 4 км tolerance даёт «1 точка на пиксель» — оптимум.

REQ-F-05 — Frontend: line-width для основного MVT-слоя на z5

Файл src/web/gps_tracks.js, функция _gpsLayerDef, выражение line-width:

'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1.0, 12, 2.0, 16, 3.0],

заменить на

'line-width': ['interpolate', ['linear'], ['zoom'],
  5, 0.8,
  8, 1.0,
  12, 2.0,
  16, 3.0],

Stop на z5 = 0.8 px подобран так, чтобы на 1× и 2×-DPR дисплеях линия гарантированно занимала ≥ 1 физический пиксель (с округлением GPU). На retina (3×) — 2.4 пикселя, видимо.

REQ-F-06 — Frontend: line-width для halo на z5

Файл src/web/gps_tracks.js, функция _gpsHaloDef, выражение line-width:

'line-width': ['interpolate', ['linear'], ['zoom'], 8, 2.5, 12, 4.0, 16, 6.0],

заменить на

'line-width': ['interpolate', ['linear'], ['zoom'],
  5, 1.8,
  8, 2.5,
  12, 4.0,
  16, 6.0],

Halo на z5 = 1.8 px — белый ореол не должен «съедать» линию толщиной 0.8 px. Соотношение ~2.25× оставляет халобакс по 0.5 px с каждой стороны.

REQ-F-07 — Frontend: zoom-hint «Зум 5+»

Файл src/web/index.html, строка

<span class="terrain-hint" id="public-tracks-zoom-hint" style="display:none">Зум 8+</span>

заменить на

<span class="terrain-hint" id="public-tracks-zoom-hint" style="display:none">Зум 5+</span>

В _syncGpsLayersVisibility (gps_tracks.js, строка ~358-362) логика

hint.style.display = (enabled && zoom < GPS_TRACKS_MIN_ZOOM) ? 'inline' : 'none';

не меняется — она автоматически подхватит новый порог.

Замечание. При z < 5 (фактически только z=0..4) hint всё ещё появится, что и желательно: у пользователя есть подсказка, в каких случаях линий нет «по дизайну».

REQ-F-08 — Endpoint без изменений

src/api/gps_tracks/endpoint.py:get_gps_tile остаётся прежним:

  • Валидация 0 ≤ z ≤ 22 уже корректно пропускает z=5..7.
  • Buffer 10 % bbox остаётся (для z≤6 это формально излишне, но не вредит — соседние тайлы кэшируются независимо).
  • LRU-кэш _gps_tile_cache размером 1024 остаётся.

Никаких новых query-параметров не вводится. Никаких изменений в /api/gps-tracks?bbox=… (GeoJSON endpoint) не делаем — z12+ не затрагивается.

REQ-F-09 — Unit-тесты zoom-tier в build_gps_mvt

Файл tests/unit/test_gps_mvt_zoom_tiers.py (новый или расширение существующего test_gps_mvt.py):

  • UT-Z5-01. При z=5 и 10 треках, из которых 3 короче 10 км, в итоговом MVT — ≤ 7 features.
  • UT-Z5-02. При z=5 и 2000 треках длиннее 10 км — в MVT не больше limit=1500 features.
  • UT-Z6-01. При z=6 и треках 3 км и 6 км — в MVT попадает только трек 6 км.
  • UT-Z6-02. При z=6 и 2500 треках длиной ≥ 5 км — в MVT не больше 2000 features.
  • UT-Z7-01. При z=7 поведение совпадает с прежним (min_length=2000, limit=3000). Регрессия.
  • UT-Z8-01. При z=8 поведение совпадает с прежним (min_length=0, limit=8000). Регрессия.
  • UT-Z12-01. При z=12 поведение совпадает с прежним (limit=25000). Регрессия.

REQ-F-10 — Unit-тесты _simplify_coords для новых тиров

Файл tests/unit/test_gps_mvt_simplify.py (новый или расширение):

  • UT-SIMP-Z5-01. Прямой трек 100 точек, диапазон ≈ 0.1° по широте/долготе: при z=5 — возвращает ≤ 5 точек (DP с большим tolerance схлопывает почти прямую).
  • UT-SIMP-Z5-02. Зигзаг 100 точек, амплитуда зигзагов 0.01° (≈ 1 км): при z=5 (tolerance ~4 км) — возвращает 2 точки (зигзаги меньше tolerance, остаются только концы).
  • UT-SIMP-Z6-01. Тот же зигзаг 100 точек, амплитуда 0.05° (~5 км): при z=6 (tolerance ~2 км) — возвращает > 5 точек (видны крупные зигзаги).
  • UT-SIMP-Z7-01. Регрессия: при z=7 tolerance = 0.008, поведение прежнее.
  • UT-SIMP-Z10-01. Регрессия: при z=10 tolerance = 0.0005, поведение прежнее.
  • UT-SIMP-Z12-01. Регрессия: при z=12 функция возвращает оригинальный coords без изменений.

REQ-F-11 — Integration-тесты endpoint z5-z7

Файл tests/integration/test_gps_tile_z5_z7.py (новый):

  • IT-Z5-01. На тестовой БД с 50 треками ≥ 10 км по ЦФО запрос GET /api/gps-tracks/tiles/5/19/9.mvt (тайл, накрывающий Москву): возвращает 200, Content-Type application/x-protobuf, тело длиной > 0 и < 200 KB (M-8).
  • IT-Z5-02. Размер MVT для того же тайла на БД из 200 треков ≥ 10 км — ≤ 200 KB.
  • IT-Z5-03. Тайл z=5 за пределами региона (например, центр Тихого океана tiles/5/4/12.mvt): тело пустое, ответ 200.
  • IT-Z6-01. Тайл z=6 над Москвой: размер < 200 KB, features > IT-Z5-01.
  • IT-Z7-01. Тайл z=7 над Москвой: features > IT-Z6-01 (более мелкие треки попадают в фильтр), но всё ещё < limit=3000.
  • IT-CACHE-01. Два подряд запроса одного тайла z=5: второй возвращает заголовок X-Cache: HIT.

REQ-F-12 — Регрессионный тест: контракт endpoint не сломался

  • IT-REGRESS-Z8-01. Endpoint /api/gps-tracks/tiles/8/x/y.mvt возвращает тот же набор треков, что и до ET-012 (sanity-check через сравнение mapbox_vector_tile.decode(body)['gps_tracks']['features'] до и после; допустимо различие только в порядке).
  • IT-REGRESS-Z10-01. Аналогично для z=10.

REQ-F-13 — Производительность: бенчмарк MVT z5

Файл tests/performance/test_gps_mvt_z5_perf.py (новый, помечается маркером @pytest.mark.perf):

  • PERF-Z5-01. При тестовой БД из 500 треков по ЦФО и 10 повторных вызовах build_gps_mvt(rows, 5, 19, 9):
    • среднее время выполнения ≤ 200 мс на CI-runner.
    • 95-й перцентиль ≤ 500 мс (метрика M-6).

Запуск отдельный (pytest -m perf), не в основной CI-gate. Цель — раз-в-релиз проверять, что мы не уплыли.

REQ-F-14 — UI-тесты (Playwright)

См. 04b-ui-test-cases.md. Ключевые проверки:

  • TC-UI-01-Z5: при zoom = 5 слой виден.
  • TC-UI-02-Z6: при zoom = 6 слой виден.
  • TC-UI-03-Z7: при zoom = 7 слой виден.
  • TC-UI-04-HINT-OFF: hint «Зум 5+» не показывается при zoom ≥ 5.
  • TC-UI-05-HINT-ON: hint показывается при zoom < 5.
  • TC-UI-06-FILTER-Z6: фильтр источников работает на z6 (регрессия).
  • TC-UI-07-POPUP-Z6: клик по треку на z6 открывает popup.
  • TC-UI-08-Z11-REGRESS: на z11 слой по-прежнему виден (регрессия).
  • TC-UI-09-Z12-CUTOFF: на z12 MVT-слой скрыт, GeoJSON-слой виден.
  • TC-UI-10-Z5-MOBILE: на мобильном при z5 слой виден.
  • TC-UI-11-Z5-SAT: на z5 со спутниковой подложкой halo не «глушит» подложку.
  • TC-UI-12-Z5-Q: качественная проверка читаемости на z5.

REQ-F-15 — Не менять контракт /api/gps-tracks*

Никаких новых query-параметров, заголовков, кодов ответа, полей в JSON. /health endpoint не меняется.

REQ-F-16 — Не менять конфиги

config/gps_sources.yaml, config/gps_regions.yaml, миграции БД — без изменений.

REQ-F-17 — Не менять стили карты

src/web/style.json и src/web/style-dark.json — без изменений. Color-by-source / color-by-activity match-expressions внутри _buildColorExpression в коде клиента — без изменений (треки на z5-z7 будут окрашены теми же цветами).

REQ-F-18 — localStorage без миграции

Текущий слой использует ключи gps-tracks-enabled, gps-tracks-activities, gps-tracks-sources, gps-tracks-color-mode. ET-012 не вводит новых ключей и не меняет существующие. Существующие пользователи увидят треки на z5-z7 при следующей загрузке без потери выбранных фильтров.

REQ-F-19 — Деплой и валидация

После merge в main и деплоя в test-среду:

  1. Открыть https://openclaw.mva154.duckdns.org/enduro/, включить «Публичные треки», установить zoom = 5 (через DevTools window._map.setZoom(5)), убедиться, что линии видны.
  2. Снять профайл DevTools Network: размер запроса /api/gps-tracks/tiles/5/19/9.mvt ≤ 200 KB.
  3. Проверить три тайла z=5 над разными регионами (Москва, Урал, Сибирь) — все ≤ 200 KB и тело > 0 для регионов с треками.
  4. Зафиксировать результаты в 14-deploy-log.md.

REQ-F-20 — Документация

В docs/work-items/ET-012/ после Анализа существуют:

  • 00-business-request.md (есть)
  • 01-brd.md
  • 02-trz.md (этот файл)
  • 03-acceptance-criteria.md
  • 04-test-plan.yaml
  • 04b-ui-test-cases.md

После реализации добавляются: 10-tech-risks.md (опционально), 12-review.md, 13-test-report.md, 14-deploy-log.md.

4. Не-функциональные требования

NFR-01 — Производительность сервера

  • p95 build_gps_mvt на z=5 при БД 500 треков ≤ 500 мс на CI-runner (метрика M-6).
  • p95 endpoint /api/gps-tracks/tiles/{5..7}/x/y.mvt cold ≤ 700 мс, hit ≤ 50 мс (M-7).
  • Не более 10 SQLite-запросов на тайл (в идеале — 2: COUNT + SELECT).

NFR-02 — Производительность клиента

  • На z5 рендер слоя не дольше +30 мс по сравнению с состоянием слой-выключен (замер через MapLibre map.on('render') интервал).
  • Не вызывает frame-drop ниже 30 FPS на средне-мобильном устройстве (iPhone 12 / Pixel 5 эквивалент).

NFR-03 — Сетевой трафик

  • Размер одного MVT-тайла z=5 ≤ 200 KB до gzip (метрика M-8).
  • gzip-compression на nginx даёт обычно ×3-4 по тайлам — финальный трафик 50-70 KB на тайл.

NFR-04 — Кэширование

  • LRU размер _GPS_TILE_CACHE_MAX = 1024 — не меняем. Опциональное увеличение до 2048 — на усмотрение разработчика, если в PERF-Z5-01 обнаружится частая инвалидация.

NFR-05 — Безопасность

Никаких изменений в auth / CSP / валидации входных данных ET-012 не вносит.

NFR-06 — Совместимость

  • API контракт /api/gps-tracks* не меняется → старые клиенты работают без обновления.
  • Существующие browser-tabs с открытой картой при следующей загрузке получат новые лимиты автоматически (никакой миграции localStorage не нужно).

NFR-07 — Логирование

Никаких новых лог-сообщений. Существующее логирование endpoint gps_tile (через uvicorn.access) показывает зум, x, y, размер ответа — это достаточно.

5. План работ (для разработчика)

  1. Backend: расширить build_gps_mvt tier-таблицу (REQ-F-03).
  2. Backend: расширить _simplify_coords tier-таблицу (REQ-F-04).
  3. Unit-тесты zoom-tier и simplify (REQ-F-09, F-10).
  4. Integration-тесты endpoint z5-z7 (REQ-F-11, F-12).
  5. Performance-тест PERF-Z5-01 (REQ-F-13). Если не проходит — ужесточить limit в REQ-F-03.
  6. Frontend: понизить GPS_TRACKS_MIN_ZOOM до 5 (REQ-F-01).
  7. Frontend: line-width stops для z5 в основном слое и halo (REQ-F-05, F-06).
  8. Frontend: текст hint (REQ-F-07).
  9. Прогон make lint, make test.
  10. Code review → merge → deploy в test.
  11. Ручная проверка REQ-F-19.
  12. Прогон UI-тестов по 04b-ui-test-cases.md.
  13. Запись результатов в 13-test-report.md и 14-deploy-log.md.

6. Открытые вопросы и решения по умолчанию

Вопрос Решение по умолчанию
Опускать ли порог ещё ниже (z3-z4)? Нет. На z3-z4 даже 10-км треки превращаются в точку — нужна heat-map. Это отдельный work item.
Увеличить ли _GPS_TILE_CACHE_MAX? Нет в MVP. Текущие 1024 покрывают z5..z11. Только если PERF-Z5-01 покажет деградацию.
Уменьшать ли buffer endpoint'а до 5 % для z≤6? Нет в MVP. 10 % буфер на z5-тайле в большинстве регионов не критичен (≈ 100 км запас в bbox-запросе вместо 1250). Можно вернуться, если PERF-Z5-01 не пройдёт.
Делать ли разные tier для color-by-source vs color-by-activity на z5? Нет. Геометрия одна, цвет — runtime-выражение MapLibre, не зависит от tier.
Что показывать пользователю на z3-z4? Hint «Зум 5+» (REQ-F-07) даёт явное объяснение. Heat-map — отдельный work item.
Сохранять ли поведение «слой пуст, но включён» через localStorage на z<5? Да — чекбокс остаётся checked, hint объясняет, что нужно зумить. Логика уже есть в _syncGpsLayersVisibility.
Сразу прогружать MVT z5 при включении слоя, если карта на z2? Нет. Source.minzoom=5 защищает: тайлы не запрашиваются до z≥5. Не меняем.
Менять ли LRU FIFO на настоящий LRU? Нет в MVP. При работе с 10-20 тайлами в кадре FIFO эквивалентен LRU; разница только при больших кэшах.