fix(gps): zIndex точек трека над полилиниями + capture-клики; offroad-фильтр LBS при выдаче трека (чистит старый «мяч»)

This commit is contained in:
2026-08-29 21:36:55 +03:00
parent a302f0c0ea
commit 3ff74d0d17
6 changed files with 72 additions and 11 deletions

View File

@@ -342,7 +342,7 @@ let osrmNearestWarned = false;
* code=NoSegment — ближайшая дорога вне радиуса поиска OSRM (порядка км),
* надёжный сигнал LBS: возвращаем Infinity (точно offroad).
*/
async function offRoadDistance(lat: number, lng: number, thresholdMeters: number): Promise<number | null> {
export async function offRoadDistance(lat: number, lng: number, thresholdMeters: number): Promise<number | null> {
let data: { code?: string; waypoints?: Array<{ distance?: number }> };
try {
// ВНИМАНИЕ: OSRM ждёт координаты в порядке lng,lat

View File

@@ -2,7 +2,7 @@ import { Router } from "express";
import type { Express, Response } from "express";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { gpsStorage } from "./storage";
import { processPosition, filterTrackPoints } from "./geozone.service";
import { processPosition, filterTrackPoints, offRoadDistance } from "./geozone.service";
import { hasGpsAccess } from "./access";
import { ensureDevice, deleteDevice } from "./traccar.client";
import { OSRM_URL } from "./osrm";
@@ -149,6 +149,27 @@ function badRequest(res: Response, error: string) {
return res.status(400).json({ success: false, error });
}
// Максимум LBS-точек, проверяемых offroad-фильтром при выдаче трека
// (бюджет ~5 сек при ~1-2 мс на OSRM nearest и конкуренции 10)
const MAX_LBS_OFFROAD_CHECK = 2000;
// Конкуренция запросов к OSRM nearest при выдаче трека
const OFFROAD_CHECK_CONCURRENCY = 10;
/**
* Простой p-limit без зависимостей: выполняет fn над элементами
* с ограничением числа одновременных вызовов.
*/
async function runWithConcurrency<T>(items: T[], limit: number, fn: (item: T) => Promise<void>): Promise<void> {
let index = 0;
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
while (index < items.length) {
const item = items[index++];
await fn(item);
}
});
await Promise.all(workers);
}
// =====================
// Приём позиций от Traccar (публичный, по секрету)
// =====================
@@ -624,14 +645,46 @@ export function registerGpsRoutes(app: Express) {
const settings = await gpsStorage.getSettings(orgId);
const { points: filtered, filteredCount } = filterTrackPoints(all, settings);
// Offroad-фильтр при выдаче: СТАРЫЕ LBS-точки (помеченные до внедрения
// фильтра при приёме) далеко от дорог рисуют «мяч» зигзагов — выкидываем
// их из выдачи через OSRM nearest. Проверяются только isLbs-точки,
// настоящий GPS не трогаем (поля/дворы легальны). 0 в настройке = выкл.
let points = filtered;
let offroadFiltered = 0;
if (settings.offRoadMeters > 0) {
let lbsPoints = points.filter((p) => p.isLbs);
if (lbsPoints.length > MAX_LBS_OFFROAD_CHECK) {
// Бюджет времени ~5 сек: прореживаем LBS-точки равномерно до лимита;
// не попавшие в проверку остаются в выдаче как есть
const step = Math.ceil(lbsPoints.length / MAX_LBS_OFFROAD_CHECK);
const thinned = lbsPoints.filter((_, idx) => idx % step === 0);
console.log(
`[GPS] track offroad: asset=${id} LBS-точек ${lbsPoints.length} > ${MAX_LBS_OFFROAD_CHECK}, проверяем ${thinned.length} (прорежено)`
);
lbsPoints = thinned;
}
if (lbsPoints.length > 0) {
const dropped = new Set<(typeof lbsPoints)[number]>();
await runWithConcurrency(lbsPoints, OFFROAD_CHECK_CONCURRENCY, async (p) => {
const dist = await offRoadDistance(p.lat, p.lng, settings.offRoadMeters);
if (dist !== null) dropped.add(p);
});
if (dropped.size > 0) {
offroadFiltered = dropped.size;
points = points.filter((p) => !dropped.has(p));
}
}
}
let sampled = false;
if (points.length > MAX_TRACK_POINTS) {
// Точки ДО прореживания — для гарантии включения последней точки трека
const beforeSample = points;
const step = Math.ceil(points.length / MAX_TRACK_POINTS);
points = points.filter((_, idx) => idx % step === 0);
// Всегда включаем последнюю точку трека
if (points[points.length - 1] !== filtered[filtered.length - 1]) {
points.push(filtered[filtered.length - 1]);
if (points[points.length - 1] !== beforeSample[beforeSample.length - 1]) {
points.push(beforeSample[beforeSample.length - 1]);
}
sampled = true;
}
@@ -650,6 +703,7 @@ export function registerGpsRoutes(app: Express) {
points,
total,
filteredCount,
offroadFiltered,
sampled,
...(matched !== undefined ? { matched } : {}),
});