feat(gps): фильтр «далеко от дороги» в LBS-режиме (offRoadMeters, OSRM nearest); пробег «точно + примерно (по вышкам)» в Маршрутах
This commit is contained in:
@@ -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);
|
||||
|
||||
5
server/gps/osrm.ts
Normal file
5
server/gps/osrm.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Общая конфигурация OSRM-сервиса (map matching / nearest).
|
||||
* Контейнер во внутренней Docker-сети; адрес переопределяется env OSRM_URL.
|
||||
*/
|
||||
export const OSRM_URL = process.env.OSRM_URL || "http://osrm:5000";
|
||||
@@ -5,6 +5,7 @@ import { gpsStorage } from "./storage";
|
||||
import { processPosition, filterTrackPoints } from "./geozone.service";
|
||||
import { hasGpsAccess } from "./access";
|
||||
import { ensureDevice, deleteDevice } from "./traccar.client";
|
||||
import { OSRM_URL } from "./osrm";
|
||||
import { storage } from "../storage";
|
||||
|
||||
/**
|
||||
@@ -19,8 +20,7 @@ const MAX_TRACK_POINTS = 2000;
|
||||
// OSRM map matching (привязка трека к дорогам)
|
||||
// =====================
|
||||
|
||||
// URL OSRM-сервиса (контейнер во внутренней Docker-сети)
|
||||
const OSRM_URL = process.env.OSRM_URL || "http://osrm:5000";
|
||||
// URL OSRM-сервиса — см. ./osrm (общий модуль, используется и в geozone.service)
|
||||
// Лимит OSRM Match API — 100 координат на запрос; берём с запасом
|
||||
const OSRM_CHUNK_SIZE = 95;
|
||||
// Таймаут запроса к OSRM
|
||||
@@ -432,6 +432,11 @@ export function registerGpsRoutes(app: Express) {
|
||||
if (!Number.isInteger(gpsLostHours) || gpsLostHours < 1 || gpsLostHours > 72) {
|
||||
return badRequest(res, "gpsLostHours: целое число от 1 до 72");
|
||||
}
|
||||
const offRoadMeters =
|
||||
req.body?.offRoadMeters !== undefined ? Number(req.body.offRoadMeters) : current.offRoadMeters;
|
||||
if (!Number.isInteger(offRoadMeters) || offRoadMeters < 0 || offRoadMeters > 2000) {
|
||||
return badRequest(res, "offRoadMeters: целое число от 0 до 2000 (0 = фильтр выключен)");
|
||||
}
|
||||
|
||||
// Центр карты по умолчанию — только для admin (у остальных молча игнорируется)
|
||||
let defaultLat = current.defaultLat;
|
||||
@@ -471,6 +476,7 @@ export function registerGpsRoutes(app: Express) {
|
||||
maxJumpSpeedKmh,
|
||||
tripStopMinutes,
|
||||
gpsLostHours,
|
||||
offRoadMeters,
|
||||
defaultLat,
|
||||
defaultLng,
|
||||
defaultZoom,
|
||||
@@ -481,6 +487,7 @@ export function registerGpsRoutes(app: Express) {
|
||||
maxJumpSpeedKmh: row.maxJumpSpeedKmh,
|
||||
tripStopMinutes: row.tripStopMinutes,
|
||||
gpsLostHours: row.gpsLostHours,
|
||||
offRoadMeters: row.offRoadMeters,
|
||||
defaultLat: row.defaultLat,
|
||||
defaultLng: row.defaultLng,
|
||||
defaultZoom: row.defaultZoom,
|
||||
|
||||
@@ -504,6 +504,7 @@ export class GpsStorage {
|
||||
maxJumpSpeedKmh: number;
|
||||
tripStopMinutes: number;
|
||||
gpsLostHours: number;
|
||||
offRoadMeters: number;
|
||||
defaultLat: number | null;
|
||||
defaultLng: number | null;
|
||||
defaultZoom: number | null;
|
||||
@@ -517,6 +518,7 @@ export class GpsStorage {
|
||||
maxJumpSpeedKmh: row?.maxJumpSpeedKmh ?? 250,
|
||||
tripStopMinutes: row?.tripStopMinutes ?? 10,
|
||||
gpsLostHours: row?.gpsLostHours ?? 5,
|
||||
offRoadMeters: row?.offRoadMeters ?? 200,
|
||||
defaultLat: row?.defaultLat ?? null,
|
||||
defaultLng: row?.defaultLng ?? null,
|
||||
defaultZoom: row?.defaultZoom ?? null,
|
||||
@@ -530,6 +532,7 @@ export class GpsStorage {
|
||||
maxJumpSpeedKmh: number;
|
||||
tripStopMinutes: number;
|
||||
gpsLostHours: number;
|
||||
offRoadMeters: number;
|
||||
defaultLat: number | null;
|
||||
defaultLng: number | null;
|
||||
defaultZoom: number | null;
|
||||
|
||||
Reference in New Issue
Block a user