feat(gps): фильтр «далеко от дороги» в LBS-режиме (offRoadMeters, OSRM nearest); пробег «точно + примерно (по вышкам)» в Маршрутах

This commit is contained in:
2026-08-29 21:12:09 +03:00
parent 9eaff65500
commit a302f0c0ea
8 changed files with 135 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
import { gpsStorage } from "./storage";
import { OSRM_URL } from "./osrm";
import { storage } from "../storage";
import { webPushService } from "../services/web-push.service";
import { eventBus, publishNotificationSSE } from "../routes/shared";
@@ -309,8 +310,10 @@ export function filterTrackPoints<T extends { lat: number; lng: number; recorded
* Режим «потерян GPS»: идущие stale re-send'ы — сигнал, что трекер не видит
* спутники. Пока последний stale моложе 10 минут, свежие точки помечаются
* is_lbs (координаты сотовой вышки): сохраняются и отдаются клиенту
* (пунктирное отображение), но геозоны по ним не проверяются. Первый
* настоящий GPS-фикс после окна stale выключает режим (gps_lost = false).
* (пунктирное отображение), но геозоны по ним не проверяются. Дополнительно
* LBS-точки дальше off_road_meters от ближайшей дороги (OSRM nearest)
* отбрасываются совсем. Первый настоящий GPS-фикс после окна stale
* выключает режим (gps_lost = false).
*/
// In-memory фильтры по объектам (процесс один; при рестарте состояние
@@ -326,6 +329,49 @@ const assetLastStaleAt = new Map<number, number>();
// Окно после последнего stale re-send'а, в котором свежие точки считаются LBS
const STALE_LBS_WINDOW_MS = 10 * 60 * 1000;
// Таймаут запроса к OSRM Nearest API (фильтр «далеко от дороги»)
const OSRM_NEAREST_TIMEOUT_MS = 2000;
// Warn про недоступность OSRM для nearest пишем один раз до следующего успешного ответа
let osrmNearestWarned = false;
/**
* Проверка «далеко от дороги» через OSRM Nearest API (только для LBS-режима).
* Возвращает расстояние до ближайшей дороги (м), если оно БОЛЬШЕ порога
* (точку надо отбросить), иначе null.
* При недоступности OSRM/таймауте — null: приём точки не блокируем.
* code=NoSegment — ближайшая дорога вне радиуса поиска OSRM (порядка км),
* надёжный сигнал LBS: возвращаем Infinity (точно offroad).
*/
async function offRoadDistance(lat: number, lng: number, thresholdMeters: number): Promise<number | null> {
let data: { code?: string; waypoints?: Array<{ distance?: number }> };
try {
// ВНИМАНИЕ: OSRM ждёт координаты в порядке lng,lat
const res = await fetch(`${OSRM_URL}/nearest/v1/driving/${lng},${lat}`, {
signal: AbortSignal.timeout(OSRM_NEAREST_TIMEOUT_MS),
});
data = (await res.json()) as typeof data;
if (osrmNearestWarned) {
console.log("[GPS] OSRM снова доступен (nearest)");
osrmNearestWarned = false;
}
} catch (err) {
// OSRM недоступен — пропускаем проверку, точка идёт обычным путём
if (!osrmNearestWarned) {
console.warn(`[GPS] OSRM недоступен для nearest (${OSRM_URL}):`, err instanceof Error ? err.message : err);
osrmNearestWarned = true;
}
return null;
}
// Дорога вне радиуса поиска — точно далеко от дороги
if (data.code === "NoSegment") return Infinity;
if (data.code !== "Ok") return null;
const dist = data.waypoints?.[0]?.distance;
if (typeof dist !== "number") return null;
return dist > thresholdMeters ? dist : null;
}
function getAssetWalker(assetId: number, prev: PrevPoint | null, settings: GpsFilterSettings): JumpFilterWalker {
let walker = assetWalkers.get(assetId);
if (!walker) {
@@ -408,6 +454,22 @@ export async function processPosition(
const lastStaleAt = assetLastStaleAt.get(assetId);
const isLbs = lastStaleAt !== undefined && Date.now() - lastStaleAt < STALE_LBS_WINDOW_MS;
// Фильтр «далеко от дороги» — ТОЛЬКО в режиме потери GPS (LBS-точки):
// координаты сотовых вышек обычно >100 м от ближайшей дороги, настоящий
// GPS — почти никогда (p90 ≈ 7 м). Вне LBS-режима проверку не выполняем:
// при живом GPS заезды в поля/дворы легальны.
if (isLbs && settings.offRoadMeters > 0) {
const distToRoad = await offRoadDistance(input.lat, input.lng, settings.offRoadMeters);
if (distToRoad !== null) {
console.log(
`[GPS] skipped offroad: asset=${assetId} dist_to_road=${distToRoad === Infinity ? "> радиуса поиска OSRM" : `${Math.round(distToRoad)}m`} > порога ${settings.offRoadMeters}m`
);
// Точку не сохраняем, координаты и геозоны не трогаем — только «устройство живо»
await gpsStorage.touchAssetSeen(assetId);
return;
}
}
// Фильтр джиттера (округление мелких перемещений)
if (settings.jitterMeters > 0 && prev) {
const distance = haversineMeters(prev.lat, prev.lng, input.lat, input.lng);