fix(gps): фильтр несогласованности скоростей против LBS-телепортов; fix: плейхед не закрывает balloon точек трека
This commit is contained in:
@@ -57,6 +57,7 @@ export function Yandex21Map({
|
||||
const onReadyRef = useRef(onReady);
|
||||
const onErrorRef = useRef(onError);
|
||||
const onMapClickRef = useRef(onMapClick);
|
||||
const placemarksRef = useRef(new Map<string, any>());
|
||||
const markersByIdRef = useRef(new Map<string, GpsMapMarker>());
|
||||
useEffect(() => {
|
||||
onReadyRef.current = onReady;
|
||||
@@ -189,7 +190,11 @@ export function Yandex21Map({
|
||||
// (объектов немного — removeAll + добавить заново дёшево);
|
||||
// колбэки кликов не входят в ключ — они читаются из ref в момент клика
|
||||
const dataKey = JSON.stringify([
|
||||
markers.map((m) => [m.id, m.lat, m.lng, m.color, m.kind, m.label, m.offline, !!m.onClick, m.zIndex, m.popup, m.noCluster, m.course]),
|
||||
// Плейхед без координат: его перемещение не должно пересобирать коллекцию
|
||||
// (иначе открытый balloon точки трека уничтожается ребилдом при seek)
|
||||
markers.map((m) => m.id === 'playhead'
|
||||
? [m.id, m.kind, m.color, m.zIndex]
|
||||
: [m.id, m.lat, m.lng, m.color, m.kind, m.label, m.offline, !!m.onClick, m.zIndex, m.popup, m.noCluster, m.course]),
|
||||
polygons.map((p) => [p.id, p.points, p.color, p.fillOpacity, p.strokeWidth]),
|
||||
polylines.map((l) => [l.id, l.points, l.color, l.dashed]),
|
||||
]);
|
||||
@@ -199,6 +204,7 @@ export function Yandex21Map({
|
||||
if (!mapReady || !collection || !clusterer) return;
|
||||
collection.removeAll();
|
||||
clusterer.removeAll();
|
||||
placemarksRef.current.clear();
|
||||
|
||||
for (const p of polygons) {
|
||||
if (p.points.length < 3) continue;
|
||||
@@ -255,6 +261,7 @@ export function Yandex21Map({
|
||||
// в обычную коллекцию (точки трека, старт/финиш, вершины геозон)
|
||||
const clusterable = (m.kind === 'vehicle' || m.kind === 'person') && !m.noCluster;
|
||||
const placemark = createPlacemark(m);
|
||||
placemarksRef.current.set(String(m.id), placemark);
|
||||
if (clusterable) {
|
||||
clusterer.add(placemark);
|
||||
} else {
|
||||
@@ -264,6 +271,15 @@ export function Yandex21Map({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataKey, mapReady]);
|
||||
|
||||
// Плейхед двигаем напрямую (без removeAll — иначе закрывается balloon)
|
||||
const playhead = markers.find((m) => m.id === 'playhead');
|
||||
useEffect(() => {
|
||||
if (!mapReady || !playhead) return;
|
||||
const pm = placemarksRef.current.get('playhead');
|
||||
if (pm) pm.geometry?.setCoordinates([playhead.lat, playhead.lng]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [playhead?.lat, playhead?.lng, mapReady]);
|
||||
|
||||
return (
|
||||
<div className={className} style={{ position: 'relative' }}>
|
||||
<div ref={containerRef} className="h-full w-full" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user