diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13b9287..2537081 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,21 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
## [Unreleased]
+### Changed
+- ET-012: Слой публичных GPS-треков теперь виден с зума z=5 (раньше — с z=8).
+ Калибровка существующей tier-структуры `build_gps_mvt`/`_simplify_coords`
+ (ADR-016): для z≤5 фильтр `min_length=10 км`, `limit=1500`; для z=6 —
+ `5 км`/`2000`; для z=7 — без изменений (`2 км`/`3000`). DP-tolerance
+ расширен парой стопов: z=6 → 0.018° (~2 км), z≤5 → 0.04° (~4 км).
+ На клиенте константа `GPS_TRACKS_MIN_ZOOM` понижена до 5;
+ `line-width`/halo-stops в MapLibre получили stop на z=5 (0.8/1.8 px),
+ hint обновлён с «Зум 8+» на «Зум 5+». Контракт API
+ `/api/gps-tracks/tiles/{z}/{x}/{y}.mvt` не изменился (REQ-F-15);
+ z≥8 не затронут (регрессия). Тесты: 18 unit zoom-tier+simplify,
+ 9 integration endpoint z5-z7, 2 perf (PERF-Z5-01/02; avg ~64 мс,
+ p95 ~89 мс при 500 треках — ниже бюджета 200 мс/500 мс по M-6).
+ Refs: ET-012.
+
## [v0.0.3] — 2026-06-03 (tagged, NOT deployed)
> ⚠️ Тег создан и запушен, PR смерджен в `main`, но docker-образ на test
diff --git a/pyproject.toml b/pyproject.toml
index fe87d40..0abb1bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,5 +40,6 @@ asyncio_mode = "auto"
testpaths = ["tests"]
markers = [
"network: contract smoke tests that hit live HTTP endpoints (deselect with '-m \"not network\"')",
+ "perf: performance tests; run on-demand with '-m perf' (ET-012 REQ-F-13)",
]
-addopts = "-m 'not network'"
+addopts = "-m 'not network and not perf'"
diff --git a/src/api/gps_tracks/mvt.py b/src/api/gps_tracks/mvt.py
index f2b7c00..a59edda 100644
--- a/src/api/gps_tracks/mvt.py
+++ b/src/api/gps_tracks/mvt.py
@@ -31,15 +31,28 @@ def clear_gps_tile_cache() -> None:
# ─── Geometry helpers ────────────────────────────────────────────────────────
def _simplify_coords(coords: list, z: int) -> list:
- """Упрощает геометрию трека по зуму через Douglas-Peucker."""
+ """Упрощает геометрию трека по зуму через Douglas-Peucker.
+
+ Tolerance задаётся в градусах WGS84. На широте 55° с.ш. 1° долготы
+ ≈ 64 км, поэтому tolerance=0.04 ≈ 2.6 км. На z5 один пиксель карты
+ ≈ 5 км по долготе на 55° с.ш., так что 2.6 км даёт «одна точка на
+ пиксель» — оптимум обзорного зума.
+
+ ET-012 (ADR-016): добавлены тиры z==6 и z<=5; для z>=7 поведение
+ не меняется (регрессия).
+ """
if z >= 12:
return coords
elif z >= 10:
- tolerance = 0.0005 # ~50м
+ tolerance = 0.0005 # ~50 м
elif z >= 8:
- tolerance = 0.002 # ~200м
+ tolerance = 0.002 # ~200 м
+ elif z == 7:
+ tolerance = 0.008 # ~800 м (как было до ET-012)
+ elif z == 6:
+ tolerance = 0.018 # ~2 км
else:
- tolerance = 0.008 # ~800м на z7 и ниже
+ tolerance = 0.04 # ~4 км (z5 и ниже)
if len(coords) < 3:
return coords
@@ -101,9 +114,18 @@ def build_gps_mvt(rows: list, z: int, x: int, y: int) -> bytes:
west, south, east, north = _tile_to_bbox(z, x, y)
- # Min-length фильтр по зуму
- if z <= 7:
- min_length_m = 2000
+ # Min-length фильтр и cap на число фич по зуму.
+ # ET-012 (ADR-016): добавлены тиры z<=5 и z==6, чтобы при понижении
+ # GPS_TRACKS_MIN_ZOOM до 5 размер тайла оставался <= 200 KB (M-8)
+ # и в кадре оставались только «магистральные» треки (M-9).
+ 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 до ET-012
limit = 3000
elif z <= 9:
min_length_m = 0
diff --git a/src/web/gps_tracks.js b/src/web/gps_tracks.js
index 9440bc8..a62e3e4 100644
--- a/src/web/gps_tracks.js
+++ b/src/web/gps_tracks.js
@@ -5,7 +5,10 @@
// ─── Константы ────────────────────────────────────────────────────
const GPS_TRACKS_ZOOM_CUTOFF = 12; // ниже — MVT, выше — GeoJSON
-const GPS_TRACKS_MIN_ZOOM = 8; // ниже — слой скрыт
+// ET-012 (ADR-016): порог понижен с 8 до 5, чтобы при обзорном зуме
+// пользователь видел общее покрытие сети треков. Серверная сторона
+// (build_gps_mvt z<=5 / z==6) даёт корректный размер MVT и читаемость.
+const GPS_TRACKS_MIN_ZOOM = 5; // ниже — слой скрыт
const GPS_SOURCE_COLORS = {
osm: '#3cb44b',
@@ -129,7 +132,14 @@ function _gpsLayerDef(id, source, sourceLayer) {
'source-layer': sourceLayer || undefined,
paint: {
'line-color': colorExpr,
- 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1.0, 12, 2.0, 16, 3.0],
+ // ET-012 (REQ-F-05): stop на z=5 = 0.8 CSS-px. На 1×-дисплеях это
+ // даёт 1 физ.px (с округлением GPU), на 2× — 1.6, на 3× — 2.4.
+ // Линия гарантированно видна на любом DPR.
+ 'line-width': ['interpolate', ['linear'], ['zoom'],
+ 5, 0.8,
+ 8, 1.0,
+ 12, 2.0,
+ 16, 3.0],
'line-opacity': 0.75,
},
layout: { 'line-cap': 'round', 'line-join': 'round', visibility: 'none' }
@@ -144,7 +154,14 @@ function _gpsHaloDef(id, source, sourceLayer) {
'source-layer': sourceLayer || undefined,
paint: {
'line-color': '#ffffff',
- 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 2.5, 12, 4.0, 16, 6.0],
+ // ET-012 (REQ-F-06): halo на z=5 = 1.8 CSS-px при основной линии 0.8 px
+ // (соотношение ~2.25×). Ореол не «съедает» линию: по 0.5 px с каждой
+ // стороны, остаётся видна цветная сердцевина.
+ 'line-width': ['interpolate', ['linear'], ['zoom'],
+ 5, 1.8,
+ 8, 2.5,
+ 12, 4.0,
+ 16, 6.0],
'line-opacity': 0.6,
},
layout: { visibility: 'none' }
@@ -355,7 +372,7 @@ function _syncGpsLayersVisibility(map) {
setVis(window.gpsTracksLayer.layerId, mvtVisible);
setVis(window.gpsTracksLayer.layerGeoId, geoVisible);
- // Hint «Зум 8+»
+ // Hint «Зум 5+» (ET-012: порог переехал автоматически через GPS_TRACKS_MIN_ZOOM)
const hint = document.getElementById('public-tracks-zoom-hint');
if (hint) {
hint.style.display = (enabled && zoom < GPS_TRACKS_MIN_ZOOM) ? 'inline' : 'none';
diff --git a/src/web/index.html b/src/web/index.html
index 4f983c0..6710435 100644
--- a/src/web/index.html
+++ b/src/web/index.html
@@ -77,7 +77,7 @@
Публичные треки
- Зум 8+
+ Зум 5+