fix(gps): фильтр несогласованности скоростей против LBS-телепортов; fix: плейхед не закрывает balloon точек трека

This commit is contained in:
2026-08-28 10:33:45 +03:00
parent ac4f89d42e
commit 96e088c890
2 changed files with 54 additions and 9 deletions

View File

@@ -168,6 +168,12 @@ export class JumpFilterWalker {
return "accept";
}
// Жёсткое противоречие скоростей (LBS-телепорт): отбрасываем сразу,
// такая точка никогда не принимается и не может стать jump-кандидатом
if (this.isContradiction(point)) {
return "jump";
}
// jump: логика кандидата на смену локации
const now = point.recordedAt.getTime();
if (this.candidate && now - this.candidate.firstAt > CANDIDATE_TTL_MS) {
@@ -199,17 +205,40 @@ export class JumpFilterWalker {
return "jump";
}
/** Метрики перемещения относительно последней принятой точки */
private jumpMetrics(point: PrevPoint): { dtSec: number; dist: number; impliedKmh: number } | null {
if (!this.prev) return null;
const dtSec = (point.recordedAt.getTime() - this.prev.recordedAt.getTime()) / 1000;
const dist = haversineMeters(this.prev.lat, this.prev.lng, point.lat, point.lng);
const impliedKmh =
dtSec > 0 ? dist / 1000 / (dtSec / 3600) : dist > this.settings.jitterMeters ? Infinity : 0;
return { dtSec, dist, impliedKmh };
}
/**
* Жёсткое противоречие скоростей (LBS-телепорт к вышке): точка «стоит»
* (speed < 5), но перемещение от предыдущей принятой требует скорости
* выше предельной — физически невозможно. Отбрасывается всегда и не
* может стать jump-кандидатом (иначе стабильный LBS-кластер легализуется).
*/
private isContradiction(point: PrevPoint): boolean {
const m = this.jumpMetrics(point);
if (!m) return false;
const reported = point.speed ?? 0;
const maxJump = this.settings.maxJumpSpeedKmh > 0 ? this.settings.maxJumpSpeedKmh : 250;
return reported < 5 && m.dtSec < 300 && m.impliedKmh > maxJump;
}
/** Нереалистичный скачок скорости относительно prev (LBS-позиции) */
private isJump(point: PrevPoint): boolean {
if (!this.prev || this.settings.maxJumpSpeedKmh <= 0) return false;
const dtSec = (point.recordedAt.getTime() - this.prev.recordedAt.getTime()) / 1000;
const dist = haversineMeters(this.prev.lat, this.prev.lng, point.lat, point.lng);
if (dtSec <= 0) {
// Защита (после stale-проверки недостижимо, но на всякий случай)
return dist > this.settings.jitterMeters;
}
const impliedKmh = dist / 1000 / (dtSec / 3600);
return impliedKmh > this.settings.maxJumpSpeedKmh;
const m = this.jumpMetrics(point);
if (!m || m.impliedKmh <= this.settings.maxJumpSpeedKmh) return false;
// Реальная быстрая езда: трекер разгонялся между редкими точками,
// implied согласован с заявленной скоростью (допуск x3.5, минимум 90 км/ч)
const reported = point.speed ?? 0;
if (m.impliedKmh <= Math.max(reported * 3.5, 90)) return false;
return true;
}
}