Backend:
- Миграция gps_tracks_001_init.sql: таблицы tracks + pipeline_runs
- Пакет src/api/gps_tracks/: models, db (WAL+upsert с dedup), dedup
(bbox+length+date bucket-hash), mvt (LRU-кэш 1024 тайла), endpoint
(GET /api/gps-tracks, GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt,
GET /api/gps-tracks/health, POST /api/gps-tracks/cache/clear), config
- Парсеры: osm (split_bbox, haversine, defusedxml XXE-защита),
enduro_russia + ttrails — заглушки (ADR-010/011 proposed, блокированы)
- Licensing guard: pipeline проверяет status ADR-файла до запуска источника
- scripts/gps_collect.py: CLI с --region/--source/--dry-run/--gc
Frontend:
- src/web/gps_tracks.js: двухрежимный слой (MVT z≤11, GeoJSON z≥12),
debounced fetch + AbortController, фильтры активности/источника,
цветовая палитра by-source/by-activity, halo на спутнике, popup трека,
restorePublicTracksState(), localStorage persistence
- index.html: чекбокс «Публичные треки» в terrain-popup, #sheet-gps-filters
- app.css: .terrain-link-btn, .gps-filter-grid, .track-popup
- app.js: вызов restorePublicTracksState() в rebuildMapOverlays(),
applyGpsHaloVisibility() в applyBaseLayer()
Конфиги:
- config/gps_sources.yaml: osm (enabled), enduro_russia/ttrails (disabled)
- config/gps_regions.yaml: ЦФО+Чувашия (enabled), Кавказ (disabled)
Docker:
- gps-collector service с profiles: [batch]
Тесты: 48 новых тестов (unit + integration), 125/125 pass
Refs: ET-008
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
33 lines
1.1 KiB
Python
33 lines
1.1 KiB
Python
"""Функции дедупликации GPS-треков (ET-008)."""
|
||
|
||
|
||
def compute_dedup_key(geom_bounds: tuple, metadata: dict) -> str:
|
||
"""Вычисляет ключ дедупликации для трека.
|
||
|
||
Args:
|
||
geom_bounds: (min_lon, min_lat, max_lon, max_lat)
|
||
metadata: dict с полями length_m и created_at
|
||
|
||
Returns:
|
||
Строка вида "{bbox_round}|{length_bucket}|{date_bucket}"
|
||
"""
|
||
min_lon, min_lat, max_lon, max_lat = geom_bounds
|
||
|
||
# Округление bbox до 2 знаков после запятой
|
||
bbox_round = (
|
||
round(min_lon, 2),
|
||
round(min_lat, 2),
|
||
round(max_lon, 2),
|
||
round(max_lat, 2),
|
||
)
|
||
|
||
# Длина в бакетах по 1 км
|
||
length_m = metadata.get("length_m", 0) or 0
|
||
length_bucket = round(length_m / 1000) * 1000
|
||
|
||
# Дата: первые 10 символов (YYYY-MM-DD) или пустая строка
|
||
created_at = metadata.get("created_at") or ""
|
||
date_bucket = created_at[:10] if created_at else ""
|
||
|
||
return f"{bbox_round}|{length_bucket}|{date_bucket}"
|