Files
enduro-trails/tests/api/test_gps_mvt_simplify.py
claude-bot bbed0e1082
All checks were successful
CI / lint (push) Successful in 5s
CI / test (push) Successful in 9s
CI / build (push) Successful in 2s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 10s
CI / build (pull_request) Successful in 3s
feat(gps-tracks): lower public-tracks minzoom to z5 (ET-012)
Калибровка существующих tier-таблиц `build_gps_mvt` /
`_simplify_coords` (ADR-016), чтобы при первом открытии карты
пользователь видел общее покрытие сети треков, а не пустую подложку.

Backend (src/api/gps_tracks/mvt.py):
- build_gps_mvt: добавлены тиры z<=5 (min_length=10 км, limit=1500)
  и z=6 (5 км / 2000); z=7+ — без изменений (регрессия).
- _simplify_coords: tolerance для z=6 = 0.018° (~2 км),
  для z<=5 = 0.04° (~4 км); z=7+ не меняется.

Frontend:
- GPS_TRACKS_MIN_ZOOM понижен с 8 до 5; vector-source.minzoom
  подхватывает константу автоматически.
- line-width / halo получили stop на z=5 (0.8 / 1.8 CSS-px),
  чтобы линия была читаема на любом DPR.
- Hint #public-tracks-zoom-hint: «Зум 8+» → «Зум 5+».

Тесты:
- 8 unit zoom-tier (UT-Z5/6/7/8/12) — REQ-F-09.
- 10 unit simplify (UT-SIMP-*) — REQ-F-10.
- 9 integration endpoint z5-z7 (IT-Z5/6/7, CACHE, REGRESS) — REQ-F-11/12.
- 2 perf (PERF-Z5-01/02; avg ~64 ms, p95 ~89 ms при 500 треках —
  ниже бюджета 200/500 ms по M-6) — REQ-F-13.
  Маркер @pytest.mark.perf, не в основном CI-gate.

Контракт API /api/gps-tracks* не меняется (REQ-F-15);
localStorage-ключи и конфиги тоже (REQ-F-16, F-18).

Refs: ET-012
ADR: docs/work-items/ET-012/06-adr/ADR-016-z5-tiling-policy.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 06:29:41 +00:00

187 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit-тесты ``_simplify_coords`` (ET-012, ADR-016).
Покрытие из 04-test-plan.yaml / 02-trz.md REQ-F-10:
UT-SIMP-Z5-01 — прямая 100 точек на z=5 → ≤ 5 точек.
UT-SIMP-Z5-02 — зигзаг 100 точек, амплитуда < tolerance → 2 точки.
UT-SIMP-Z6-01 — зигзаг с амплитудой ~5 км на z=6 → > 5 точек.
UT-SIMP-Z7-01 — регрессия: tolerance = 0.008.
UT-SIMP-Z10-01 — регрессия: tolerance = 0.0005.
UT-SIMP-Z12-01 — регрессия: без упрощения.
UT-SIMP-EDGE-01 — < 3 точек возвращаются без изменений.
UT-SIMP-EDGE-02 — DP схлопнул < 2 точек → возвращаем оригинал.
Замечание о масштабе: tolerance в градусах WGS84. На широте 55° с.ш.
1° долготы ≈ 64 км. Для зигзага амплитуда задаётся в градусах широты,
1° широты ≈ 111 км.
"""
from src.api.gps_tracks.mvt import _simplify_coords
# ─── UT-SIMP-Z5-01: прямая → ≤ 5 точек ──────────────────────────────────────
def test_ut_simp_z5_01_straight_line_collapses():
"""REQ-F-10 / UT-SIMP-Z5-01: 100 точек по прямой на z=5 → ≤ 5 точек.
DP с большим tolerance схлопывает прямую до начала и конца.
"""
# ~ 10 км по диагонали (шаг 0.001° × 100 = 0.1° ≈ 6.4 км по lon, 11 км по lat)
coords = [(37.0 + i * 0.001, 55.0 + i * 0.001) for i in range(100)]
result = _simplify_coords(coords, z=5)
assert len(result) <= 5
assert len(result) >= 2 # не схлопывается до 1 или 0
# Концы сохранены
assert result[0] == coords[0]
assert result[-1] == coords[-1]
# ─── UT-SIMP-Z5-02: зигзаг с амплитудой < tolerance → 2 точки ───────────────
def test_ut_simp_z5_02_zigzag_below_tolerance_collapses_to_endpoints():
"""REQ-F-10 / UT-SIMP-Z5-02: зигзаг амплитудой ~0.01° (~1 км) на z=5.
tolerance на z<=5 = 0.04° (~4 км по lon на 55° с.ш.), зигзаги
меньше tolerance — схлопываются до концов.
"""
coords = []
base_lon = 37.0
base_lat = 55.0
for i in range(100):
# Лонгитуда монотонно растёт, чтобы DP видел общее направление.
# Латитуда зигзагит с амплитудой 0.01° (~1.1 км по широте).
lon = base_lon + i * 0.002
lat = base_lat + (0.01 if i % 2 else -0.01)
coords.append((lon, lat))
result = _simplify_coords(coords, z=5)
# DP при таком tolerance оставит только начало и конец прямой.
assert len(result) == 2
assert result[0] == coords[0]
assert result[-1] == coords[-1]
# ─── UT-SIMP-Z6-01: зигзаг 5 км на z=6 → видны крупные пики ─────────────────
def test_ut_simp_z6_01_large_zigzag_keeps_peaks():
"""REQ-F-10 / UT-SIMP-Z6-01: на z=6 (tolerance ~2 км) зигзаг 5 км
оставляет крупные пики (> 5 точек).
"""
coords = []
base_lon = 37.0
base_lat = 55.0
for i in range(100):
lon = base_lon + i * 0.005
lat = base_lat + (0.05 if i % 2 else -0.05)
coords.append((lon, lat))
result = _simplify_coords(coords, z=6)
assert len(result) > 5
# ─── UT-SIMP-Z7-01: регрессия — tolerance = 0.008 ───────────────────────────
def test_ut_simp_z7_01_regression_tolerance_unchanged():
"""REQ-F-10 / UT-SIMP-Z7-01: tolerance на z=7 = 0.008 (как до ET-012).
Контроль: на синтетике зигзаг с амплитудой 0.01° (выше tolerance 0.008°)
— пики сохраняются (>5 точек), но число меньше, чем на z=6 (tolerance меньше).
"""
coords = []
base_lon = 37.0
base_lat = 55.0
for i in range(100):
lon = base_lon + i * 0.002
lat = base_lat + (0.012 if i % 2 else -0.012)
coords.append((lon, lat))
result_z7 = _simplify_coords(coords, z=7)
# Зигзаг чуть больше tolerance → точки сохраняются.
assert len(result_z7) > 2
# На z=10 с гораздо меньшим tolerance число сохранённых точек >= чем на z=7.
result_z10 = _simplify_coords(coords, z=10)
assert len(result_z10) >= len(result_z7)
# ─── UT-SIMP-Z10-01: регрессия — tolerance = 0.0005 ─────────────────────────
def test_ut_simp_z10_01_regression_fine_zigzag_kept():
"""REQ-F-10 / UT-SIMP-Z10-01: tolerance на z=10 = 0.0005 (как до ET-012).
Зигзаг с амплитудой 0.001° (~100 м) — выше tolerance, точки сохраняются.
"""
coords = []
base_lon = 37.0
base_lat = 55.0
for i in range(100):
lon = base_lon + i * 0.0005
lat = base_lat + (0.001 if i % 2 else -0.001)
coords.append((lon, lat))
result = _simplify_coords(coords, z=10)
# На z=10 с tolerance 0.0005° зигзаг 0.001° сохраняет почти все точки.
assert len(result) >= 50
# ─── UT-SIMP-Z12-01: регрессия — без упрощения ──────────────────────────────
def test_ut_simp_z12_01_no_simplification():
"""REQ-F-10 / UT-SIMP-Z12-01: на z=12 функция возвращает coords без изменений."""
coords = [(37.0 + i * 0.0001, 55.0 + i * 0.0001) for i in range(100)]
result = _simplify_coords(coords, z=12)
# Object identity preserved (return coords, не копия)
assert result is coords
def test_ut_simp_z12_01_high_zoom_no_simplification():
"""REQ-F-10: на z>12 (например, z=15, z=22) — также без упрощения."""
coords = [(37.0 + i * 0.0001, 55.0 + i * 0.0001) for i in range(50)]
assert _simplify_coords(coords, z=15) is coords
assert _simplify_coords(coords, z=22) is coords
# ─── UT-SIMP-EDGE-01: < 3 точек ─────────────────────────────────────────────
def test_ut_simp_edge_01_two_points_returned_as_is():
"""REQ-F-10 / UT-SIMP-EDGE-01: trace из 2 точек возвращается без изменений на любом z."""
coords = [(37.0, 55.0), (37.001, 55.001)]
for z in (5, 6, 7, 8, 10, 12):
result = _simplify_coords(coords, z)
assert result == coords
# ─── UT-SIMP-EDGE-02: вырожденный трек ──────────────────────────────────────
def test_ut_simp_edge_02_degenerate_track_falls_back_to_original():
"""REQ-F-10 / UT-SIMP-EDGE-02: 100 одинаковых точек.
Shapely.simplify на дегенеративной геометрии может вернуть < 2 точек —
функция должна fallback'нуть на оригинал, а не отдавать пустой список.
"""
coords = [(37.0, 55.0)] * 100
for z in (5, 6, 7, 8, 10):
result = _simplify_coords(coords, z)
# Минимум — оригинал, не пустой/одноточечный
assert len(result) >= 2
# ─── Кросс-проверка: z=5 упрощает сильнее, чем z=6, чем z=7, чем z=10 ───────
def test_simp_tier_monotonic_for_complex_trace():
"""Дополнительная проверка монотонности tolerance по зумам.
На сложном треке (100 точек со случайной вариативностью) ожидается:
len(simp(z=5)) <= len(simp(z=6)) <= len(simp(z=7))
<= len(simp(z=10)) <= len(simp(z=12)) == 100
"""
# Детерминированный pseudo-noise через index (без random — стабильно в CI)
coords = []
for i in range(100):
lon = 37.0 + i * 0.003 + ((i * 7) % 13) * 0.0003
lat = 55.0 + i * 0.002 + ((i * 11) % 17) * 0.0004
coords.append((lon, lat))
n5 = len(_simplify_coords(coords, z=5))
n6 = len(_simplify_coords(coords, z=6))
n7 = len(_simplify_coords(coords, z=7))
n10 = len(_simplify_coords(coords, z=10))
n12 = len(_simplify_coords(coords, z=12))
assert n5 <= n6 <= n7 <= n10 <= n12
assert n12 == 100 # без упрощения