feat(gps): last_seen по времени приёма, фильтр джиттера (настройки), линейка, трек по скорости; fix: карта не перекрывает диалоги (isolation)

This commit is contained in:
2026-08-25 17:01:44 +03:00
parent 670cecfe05
commit b635d1e16e
12 changed files with 453 additions and 34 deletions

View File

@@ -31,6 +31,18 @@ export function pointInPolygon(lat: number, lng: number, polygon: Array<[number,
return inside;
}
/** Расстояние между двумя точками (haversine), в метрах. Без зависимостей. */
export function haversineMeters(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371000; // радиус Земли, м
const toRad = (deg: number) => (deg * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
export interface ProcessPositionInput {
lat: number;
lng: number;
@@ -43,6 +55,16 @@ export interface ProcessPositionInput {
/**
* Основная точка входа: сохраняет позицию, обновляет last_* объекта,
* проверяет геозоны и рассылает SSE/уведомления.
*
* Семантика времени:
* - last_seen_at = ВСЕГДА время ПРИЁМА сервером (онлайн-индикатор на карте);
* - last_recorded_at = время точки по устройству (fixTime трекера);
* - gps_positions.recorded_at = время устройства (для треков).
*
* Фильтр джиттера: если в настройках jitterMeters > 0, у объекта есть
* предыдущая точка, расстояние до неё < jitterMeters и скорость мала
* (null или < 5 км/ч) — точка отбрасывается: не пишется в gps_positions,
* координаты и геозоны не трогаются, но last_seen_at/is_online обновляются.
*/
export async function processPosition(
assetId: number,
@@ -51,7 +73,26 @@ export async function processPosition(
): Promise<void> {
const recordedAt = input.recordedAt ?? new Date();
// 1. Сохраняем точку в историю
const asset = await gpsStorage.getAsset(assetId, organizationId);
if (!asset) return;
// Фильтр джиттера (округление мелких перемещений)
const settings = await gpsStorage.getSettings(organizationId);
if (
settings.jitterMeters > 0 &&
asset.lastLat !== null &&
asset.lastLng !== null
) {
const distance = haversineMeters(asset.lastLat, asset.lastLng, input.lat, input.lng);
const speed = input.speed ?? null;
if (distance < settings.jitterMeters && (speed === null || speed < 5)) {
// Устройство живо — фиксируем факт приёма, точку не сохраняем
await gpsStorage.touchAssetSeen(assetId);
return;
}
}
// 1. Сохраняем точку в историю (recorded_at — время устройства, для треков)
await gpsStorage.insertPosition({
assetId,
organizationId,
@@ -63,7 +104,7 @@ export async function processPosition(
recordedAt,
});
// 2. Обновляем последнюю позицию объекта (и переводим в online)
// 2. Обновляем последнюю позицию объекта (last_seen_at = время приёма, online)
await gpsStorage.updateAssetLastPosition(assetId, {
lat: input.lat,
lng: input.lng,
@@ -88,21 +129,19 @@ export async function processPosition(
});
// 4. Проверка геозон
await checkGeozones(assetId, organizationId, input, recordedAt);
await checkGeozones(asset, input, recordedAt);
}
async function checkGeozones(
assetId: number,
organizationId: number,
asset: GpsAsset,
input: ProcessPositionInput,
recordedAt: Date
): Promise<void> {
const { id: assetId, organizationId } = asset;
const geozones = await gpsStorage.listActiveGeozones(organizationId);
if (geozones.length === 0) return;
const states = await gpsStorage.getGeozoneStatesForAsset(assetId);
const asset = await gpsStorage.getAsset(assetId, organizationId);
if (!asset) return;
for (const geozone of geozones) {
try {