fix(monitor): arbitrate conflicting location sources

This commit is contained in:
lingniu
2026-07-16 11:05:28 +08:00
parent 9384d6acf5
commit b9fcf476ec
11 changed files with 360 additions and 44 deletions

View File

@@ -2,7 +2,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-libra
import { afterEach, expect, test, vi } from 'vitest';
import type { MonitorMapResponse } from '../../api/types';
import { wgs84ToGcj02, type AMapLike, type AMapMap, type AMapMassPoint } from '../../integrations/amap';
import { advancePointMotions, FleetMap, interpolateMassPoints, massPointFingerprint, pointMoveDurationMs, pointMoveFrameMs } from './FleetMap';
import { advancePointMotions, FleetMap, interpolateMassPoints, massPointFingerprint, movingPointIDs, pointMoveDurationMs, pointMoveFrameMs } from './FleetMap';
const setData = vi.fn<(data: AMapMassPoint[]) => void>();
const setStyle = vi.fn();
@@ -170,6 +170,15 @@ test('interpolates existing mass points at constant speed while keeping new poin
expect(interpolateMassPoints(previous, target, 1)).toEqual(target);
});
test('does not interpolate across an authoritative location source switch', () => {
const previous: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.2, 23.1], sourceToken: 'JT808:terminal-a' }];
const sameSource: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.3, 23.2], sourceToken: 'JT808:terminal-a' }];
const switchedSource: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.4, 23.3], sourceToken: 'GB32960:canonical' }];
expect(movingPointIDs(previous, sameSource).has('vehicle')).toBe(true);
expect(movingPointIDs(previous, switchedSource).has('vehicle')).toBe(false);
});
test('uses each protocol report interval with bounded fallbacks for point motion', () => {
expect(pointMoveDurationMs(10_000)).toBe(10_000);
expect(pointMoveDurationMs(30_000)).toBe(30_000);

View File

@@ -160,6 +160,7 @@ export function massPointFingerprint(
fingerprint.add(point.status);
fingerprint.add(point.plate || '');
fingerprint.add(point.reportIntervalMs ?? '');
fingerprint.add(point.locationSource || point.protocol);
}
} else {
fingerprint.add(points.length);
@@ -170,6 +171,7 @@ export function massPointFingerprint(
fingerprint.add(vehicleStatus(point));
fingerprint.add(point.plate || '');
fingerprint.add(point.reportIntervalMs ?? '');
fingerprint.add(point.locationSource || point.primaryProtocol);
}
}
return fingerprint.value();
@@ -225,12 +227,12 @@ export function pointMoveFrameMs(totalPoints: number, movingPoints: number) {
return Math.round(Math.min(MAX_POINT_MOVE_FRAME_MS, MIN_POINT_MOVE_FRAME_MS + pressure * 120));
}
function movingPointIDs(previous: AMapMassPoint[], target: AMapMassPoint[]) {
const previousByID = new Map(previous.map((point) => [point.id, point.lnglat]));
const moving = new Set<string>();
for (const point of target) {
const start = previousByID.get(point.id);
if (start && (Math.abs(start[0] - point.lnglat[0]) >= POINT_MOVE_EPSILON || Math.abs(start[1] - point.lnglat[1]) >= POINT_MOVE_EPSILON)) moving.add(point.id);
export function movingPointIDs(previous: AMapMassPoint[], target: AMapMassPoint[]) {
const previousByID = new Map(previous.map((point) => [point.id, point]));
const moving = new Set<string>();
for (const point of target) {
const start = previousByID.get(point.id);
if (start && start.sourceToken === point.sourceToken && (Math.abs(start.lnglat[0] - point.lnglat[0]) >= POINT_MOVE_EPSILON || Math.abs(start.lnglat[1] - point.lnglat[1]) >= POINT_MOVE_EPSILON)) moving.add(point.id);
}
return moving;
}
@@ -486,9 +488,9 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
const clusterStyleIndexes = new Map(clusterCounts.map((count, index) => [count, COLORS.length + index]));
const data: AMapMassPoint[] = monitorMode ? [
...clusters.map((cluster) => ({ lnglat: wgs84ToGcj02(cluster.longitude, cluster.latitude), style: clusterStyleIndexes.get(cluster.count) ?? COLORS.length, id: cluster.id, label: `${cluster.count}` })),
...mapPoints.map((point) => ({ lnglat: wgs84ToGcj02(point.longitude, point.latitude), style: point.status === 'driving' ? 2 : point.status === 'idle' ? 0 : point.status === 'offline' ? 1 : 3, id: point.vin, label: point.plate || point.vin }))
...mapPoints.map((point) => ({ lnglat: wgs84ToGcj02(point.longitude, point.latitude), style: point.status === 'driving' ? 2 : point.status === 'idle' ? 0 : point.status === 'offline' ? 1 : 3, id: point.vin, label: point.plate || point.vin, sourceToken: point.locationSource || point.protocol }))
] : fallbackPoints.map((vehicle) => ({
lnglat: wgs84ToGcj02(vehicle.longitude, vehicle.latitude), style: styleIndex(vehicle), id: vehicle.vin, label: vehicle.plate || vehicle.vin
lnglat: wgs84ToGcj02(vehicle.longitude, vehicle.latitude), style: styleIndex(vehicle), id: vehicle.vin, label: vehicle.plate || vehicle.vin, sourceToken: vehicle.locationSource || vehicle.primaryProtocol
}));
const moveDurationByID = new Map((monitorMode ? mapPoints : fallbackPoints)
.map((point) => [point.vin, pointMoveDurationMs(point.reportIntervalMs)]));