perf(web): reconcile fleet map incrementally
This commit is contained in:
@@ -39,6 +39,7 @@ export type AMapMassMarks = {
|
|||||||
export type AMapLabelsLayer = {
|
export type AMapLabelsLayer = {
|
||||||
setMap: (map: AMapMap | null) => void;
|
setMap: (map: AMapMap | null) => void;
|
||||||
add: (markers: AMapOverlay | AMapOverlay[]) => void;
|
add: (markers: AMapOverlay | AMapOverlay[]) => void;
|
||||||
|
remove: (markers: AMapOverlay | AMapOverlay[]) => void;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { FleetMap } from './FleetMap';
|
|||||||
const setData = vi.fn<(data: AMapMassPoint[]) => void>();
|
const setData = vi.fn<(data: AMapMassPoint[]) => void>();
|
||||||
const setStyle = vi.fn();
|
const setStyle = vi.fn();
|
||||||
const addLabels = vi.fn();
|
const addLabels = vi.fn();
|
||||||
|
const removeLabels = vi.fn();
|
||||||
const clearLabels = vi.fn();
|
const clearLabels = vi.fn();
|
||||||
const setLabelsMap = vi.fn();
|
const setLabelsMap = vi.fn();
|
||||||
const markerSetMap = vi.fn();
|
const markerSetMap = vi.fn();
|
||||||
@@ -55,6 +56,7 @@ class TestLabelsLayer {
|
|||||||
labelLayerOptions.push(options);
|
labelLayerOptions.push(options);
|
||||||
}
|
}
|
||||||
add = addLabels;
|
add = addLabels;
|
||||||
|
remove = removeLabels;
|
||||||
clear = clearLabels;
|
clear = clearLabels;
|
||||||
setMap = setLabelsMap;
|
setMap = setLabelsMap;
|
||||||
}
|
}
|
||||||
@@ -144,6 +146,7 @@ afterEach(() => {
|
|||||||
setData.mockReset();
|
setData.mockReset();
|
||||||
setStyle.mockReset();
|
setStyle.mockReset();
|
||||||
addLabels.mockReset();
|
addLabels.mockReset();
|
||||||
|
removeLabels.mockReset();
|
||||||
clearLabels.mockReset();
|
clearLabels.mockReset();
|
||||||
setLabelsMap.mockReset();
|
setLabelsMap.mockReset();
|
||||||
markerSetMap.mockReset();
|
markerSetMap.mockReset();
|
||||||
@@ -210,6 +213,41 @@ test('detaches AMap listeners and destroys the map on unmount', async () => {
|
|||||||
expect(mapDestroy).toHaveBeenCalledTimes(1);
|
expect(mapDestroy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('skips unchanged refresh redraws and replaces only a moved vehicle label', async () => {
|
||||||
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||||
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
||||||
|
const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
|
||||||
|
await waitFor(() => expect(addLabels).toHaveBeenCalled());
|
||||||
|
const dataCalls = setData.mock.calls.length;
|
||||||
|
const styleCalls = setStyle.mock.calls.length;
|
||||||
|
const addCalls = addLabels.mock.calls.length;
|
||||||
|
const removeCalls = removeLabels.mock.calls.length;
|
||||||
|
const clearCalls = clearLabels.mock.calls.length;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
view.rerender(<FleetMap vehicles={[]} monitorMap={{ ...pointMap, asOf: '2026-07-14T01:00:15Z' }} onSelect={() => undefined} />);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
expect(setData).toHaveBeenCalledTimes(dataCalls);
|
||||||
|
expect(setStyle).toHaveBeenCalledTimes(styleCalls);
|
||||||
|
expect(addLabels).toHaveBeenCalledTimes(addCalls);
|
||||||
|
expect(removeLabels).toHaveBeenCalledTimes(removeCalls);
|
||||||
|
expect(clearLabels).toHaveBeenCalledTimes(clearCalls);
|
||||||
|
|
||||||
|
view.rerender(<FleetMap vehicles={[]} monitorMap={{
|
||||||
|
...pointMap,
|
||||||
|
asOf: '2026-07-14T01:00:30Z',
|
||||||
|
points: [{ ...pointMap.points[0], longitude: 113.27 }, pointMap.points[1]]
|
||||||
|
}} onSelect={() => undefined} />);
|
||||||
|
await waitFor(() => expect(setData).toHaveBeenCalledTimes(dataCalls + 1));
|
||||||
|
expect(setStyle).toHaveBeenCalledTimes(styleCalls);
|
||||||
|
expect(removeLabels).toHaveBeenCalledTimes(removeCalls + 1);
|
||||||
|
expect(addLabels).toHaveBeenCalledTimes(addCalls + 1);
|
||||||
|
const replacementLabels = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
||||||
|
expect(replacementLabels).toHaveLength(1);
|
||||||
|
expect((replacementLabels[0].options.text as { content: string }).content).toBe('粤A12345');
|
||||||
|
});
|
||||||
|
|
||||||
test('converts AMap GCJ-02 bounds back to WGS-84 before requesting monitor data', async () => {
|
test('converts AMap GCJ-02 bounds back to WGS-84 before requesting monitor data', async () => {
|
||||||
const [west, south] = wgs84ToGcj02(113, 22);
|
const [west, south] = wgs84ToGcj02(113, 22);
|
||||||
const [east, north] = wgs84ToGcj02(114, 24);
|
const [east, north] = wgs84ToGcj02(114, 24);
|
||||||
@@ -334,10 +372,10 @@ test('renders one selected plate and smoothly follows it until the map is dragge
|
|||||||
expect(toggle).toHaveAttribute('aria-pressed', 'true');
|
expect(toggle).toHaveAttribute('aria-pressed', 'true');
|
||||||
fireEvent.click(toggle);
|
fireEvent.click(toggle);
|
||||||
await waitFor(() => expect(toggle).toHaveAttribute('aria-pressed', 'false'));
|
await waitFor(() => expect(toggle).toHaveAttribute('aria-pressed', 'false'));
|
||||||
expect(clearLabels).toHaveBeenCalled();
|
expect(removeLabels).toHaveBeenCalled();
|
||||||
const selectedOnlyLabels = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
const hiddenFloatingLabels = removeLabels.mock.calls[removeLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
||||||
expect(selectedOnlyLabels).toHaveLength(1);
|
expect(hiddenFloatingLabels).toHaveLength(1);
|
||||||
expect((selectedOnlyLabels[0].options.text as { content: string }).content).toBe('粤B67890');
|
expect((hiddenFloatingLabels[0].options.text as { content: string }).content).toBe('粤A12345');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders every nearby plate with staggered positions at maximum zoom', async () => {
|
test('renders every nearby plate with staggered positions at maximum zoom', async () => {
|
||||||
@@ -369,6 +407,7 @@ test('renders every nearby plate with staggered positions at maximum zoom', asyn
|
|||||||
view.rerender(<FleetMap vehicles={[]} monitorMap={crowdedMap} selectedVin="LTEST000000000002" onSelect={() => undefined} />);
|
view.rerender(<FleetMap vehicles={[]} monitorMap={crowdedMap} selectedVin="LTEST000000000002" onSelect={() => undefined} />);
|
||||||
await waitFor(() => expect(addLabels.mock.calls.length).toBeGreaterThan(labelRenderCount));
|
await waitFor(() => expect(addLabels.mock.calls.length).toBeGreaterThan(labelRenderCount));
|
||||||
const selectedDenseMarkers = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
const selectedDenseMarkers = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
||||||
|
expect(selectedDenseMarkers).toHaveLength(1);
|
||||||
expect(selectedDenseMarkers.every((marker) => marker.options.icon == null)).toBe(true);
|
expect(selectedDenseMarkers.every((marker) => marker.options.icon == null)).toBe(true);
|
||||||
expect(selectedDenseMarkers.find((marker) => marker.options.rank === 100)).toBeDefined();
|
expect(selectedDenseMarkers.find((marker) => marker.options.rank === 100)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,6 +85,24 @@ type PlateLabelPoint = {
|
|||||||
latitude: number;
|
latitude: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LabelMarkerEntry = {
|
||||||
|
marker: AMapOverlay;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function massPointSignature(monitorMap: MonitorMapResponse | undefined, points: VehicleRealtimeRow[]) {
|
||||||
|
if (monitorMap) return [
|
||||||
|
`mode:${monitorMap.mode}`,
|
||||||
|
...monitorMap.clusters.map((cluster) => `c:${cluster.id}:${cluster.longitude}:${cluster.latitude}:${cluster.count}`),
|
||||||
|
...monitorMap.points.map((point) => `p:${point.vin}:${point.longitude}:${point.latitude}:${point.status}:${point.plate || ''}`)
|
||||||
|
].join('|');
|
||||||
|
return ['vehicles', ...points.map((point) => `p:${point.vin}:${point.longitude}:${point.latitude}:${vehicleStatus(point)}:${point.plate || ''}`)].join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelMarkerSignature(point: PlateLabelPoint, selected: boolean, placement?: { direction: 'left' | 'right'; textOffset: [number, number] }) {
|
||||||
|
return `${point.longitude}:${point.latitude}:${point.plate || point.vin}:${selected ? 1 : 0}:${placement?.direction ?? 'right'}:${placement?.textOffset.join(',') ?? '8,0'}`;
|
||||||
|
}
|
||||||
|
|
||||||
function densePlatePlacements(points: PlateLabelPoint[]) {
|
function densePlatePlacements(points: PlateLabelPoint[]) {
|
||||||
const buckets = new Map<string, PlateLabelPoint[]>();
|
const buckets = new Map<string, PlateLabelPoint[]>();
|
||||||
for (const point of points) {
|
for (const point of points) {
|
||||||
@@ -128,6 +146,11 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
const labelsRef = useRef<AMapLabelsLayer | null>(null);
|
const labelsRef = useRef<AMapLabelsLayer | null>(null);
|
||||||
const denseLabelsRef = useRef<AMapLabelsLayer | null>(null);
|
const denseLabelsRef = useRef<AMapLabelsLayer | null>(null);
|
||||||
const selectionRef = useRef<AMapOverlay | null>(null);
|
const selectionRef = useRef<AMapOverlay | null>(null);
|
||||||
|
const labelMarkersRef = useRef(new Map<string, LabelMarkerEntry>());
|
||||||
|
const labelLayerModeRef = useRef<'normal' | 'dense' | ''>('');
|
||||||
|
const labelLayerStateRef = useRef('');
|
||||||
|
const massDataSignatureRef = useRef('__unset__');
|
||||||
|
const massStyleSignatureRef = useRef('__unset__');
|
||||||
const onSelectRef = useRef(onSelect);
|
const onSelectRef = useRef(onSelect);
|
||||||
const onSelectVinRef = useRef(onSelectVin);
|
const onSelectVinRef = useRef(onSelectVin);
|
||||||
const onViewportChangeRef = useRef(onViewportChange);
|
const onViewportChangeRef = useRef(onViewportChange);
|
||||||
@@ -262,6 +285,8 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
}
|
}
|
||||||
if (stopFollowing) mapInstance?.off?.('dragstart', stopFollowing);
|
if (stopFollowing) mapInstance?.off?.('dragstart', stopFollowing);
|
||||||
massRef.current?.setMap(null);
|
massRef.current?.setMap(null);
|
||||||
|
labelsRef.current?.clear();
|
||||||
|
denseLabelsRef.current?.clear();
|
||||||
labelsRef.current?.setMap(null);
|
labelsRef.current?.setMap(null);
|
||||||
denseLabelsRef.current?.setMap(null);
|
denseLabelsRef.current?.setMap(null);
|
||||||
selectionRef.current?.setMap?.(null);
|
selectionRef.current?.setMap?.(null);
|
||||||
@@ -270,6 +295,11 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
labelsRef.current = null;
|
labelsRef.current = null;
|
||||||
denseLabelsRef.current = null;
|
denseLabelsRef.current = null;
|
||||||
selectionRef.current = null;
|
selectionRef.current = null;
|
||||||
|
labelMarkersRef.current.clear();
|
||||||
|
labelLayerModeRef.current = '';
|
||||||
|
labelLayerStateRef.current = '';
|
||||||
|
massDataSignatureRef.current = '__unset__';
|
||||||
|
massStyleSignatureRef.current = '__unset__';
|
||||||
amapRef.current = null;
|
amapRef.current = null;
|
||||||
mapRef.current = null;
|
mapRef.current = null;
|
||||||
};
|
};
|
||||||
@@ -280,13 +310,19 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
const AMap = amapRef.current;
|
const AMap = amapRef.current;
|
||||||
if (!mass || !AMap) return;
|
if (!mass || !AMap) return;
|
||||||
clustersRef.current = new Map((monitorMap?.clusters ?? []).map((cluster) => [cluster.id, cluster]));
|
clustersRef.current = new Map((monitorMap?.clusters ?? []).map((cluster) => [cluster.id, cluster]));
|
||||||
const baseStyles = COLORS.map((color) => ({ url: dotDataUrl(color), anchor: new AMap.Pixel(9, 9), size: new AMap.Size(18, 18) }));
|
|
||||||
const clusterCounts = [...new Set((monitorMap?.clusters ?? []).map((cluster) => cluster.count))].sort((a, b) => a - b);
|
const clusterCounts = [...new Set((monitorMap?.clusters ?? []).map((cluster) => cluster.count))].sort((a, b) => a - b);
|
||||||
const clusterStyles = clusterCounts.map((count) => {
|
const styleSignature = clusterCounts.join(',');
|
||||||
const visual = clusterVisual(count);
|
if (massStyleSignatureRef.current !== styleSignature) {
|
||||||
return { url: visual.url, anchor: new AMap.Pixel(visual.diameter / 2, visual.diameter / 2), size: new AMap.Size(visual.diameter, visual.diameter) };
|
const baseStyles = COLORS.map((color) => ({ url: dotDataUrl(color), anchor: new AMap.Pixel(9, 9), size: new AMap.Size(18, 18) }));
|
||||||
});
|
const clusterStyles = clusterCounts.map((count) => {
|
||||||
mass.setStyle?.([...baseStyles, ...clusterStyles]);
|
const visual = clusterVisual(count);
|
||||||
|
return { url: visual.url, anchor: new AMap.Pixel(visual.diameter / 2, visual.diameter / 2), size: new AMap.Size(visual.diameter, visual.diameter) };
|
||||||
|
});
|
||||||
|
mass.setStyle?.([...baseStyles, ...clusterStyles]);
|
||||||
|
massStyleSignatureRef.current = styleSignature;
|
||||||
|
}
|
||||||
|
const dataSignature = massPointSignature(monitorMap, points);
|
||||||
|
if (massDataSignatureRef.current === dataSignature) return;
|
||||||
const clusterStyleIndexes = new Map(clusterCounts.map((count, index) => [count, COLORS.length + index]));
|
const clusterStyleIndexes = new Map(clusterCounts.map((count, index) => [count, COLORS.length + index]));
|
||||||
const data: AMapMassPoint[] = monitorMap ? [
|
const data: AMapMassPoint[] = monitorMap ? [
|
||||||
...monitorMap.clusters.map((cluster) => ({ lnglat: wgs84ToGcj02(cluster.longitude, cluster.latitude), style: clusterStyleIndexes.get(cluster.count) ?? COLORS.length, id: cluster.id, label: `${cluster.count} 辆` })),
|
...monitorMap.clusters.map((cluster) => ({ lnglat: wgs84ToGcj02(cluster.longitude, cluster.latitude), style: clusterStyleIndexes.get(cluster.count) ?? COLORS.length, id: cluster.id, label: `${cluster.count} 辆` })),
|
||||||
@@ -295,6 +331,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
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
|
||||||
}));
|
}));
|
||||||
mass.setData(data);
|
mass.setData(data);
|
||||||
|
massDataSignatureRef.current = dataSignature;
|
||||||
}, [monitorMap, points, state]);
|
}, [monitorMap, points, state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -303,13 +340,6 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
const AMap = amapRef.current;
|
const AMap = amapRef.current;
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!labels || !denseLabels || !AMap?.LabelMarker || !map) return;
|
if (!labels || !denseLabels || !AMap?.LabelMarker || !map) return;
|
||||||
labels.clear();
|
|
||||||
denseLabels.clear();
|
|
||||||
if (!showLabels && !selectedVin) {
|
|
||||||
labels.setMap(null);
|
|
||||||
denseLabels.setMap(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mapLabelPoints = (monitorMap
|
const mapLabelPoints = (monitorMap
|
||||||
? monitorMap.points
|
? monitorMap.points
|
||||||
: points.map((vehicle) => ({ ...vehicle, status: vehicleStatus(vehicle) })));
|
: points.map((vehicle) => ({ ...vehicle, status: vehicleStatus(vehicle) })));
|
||||||
@@ -318,13 +348,31 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
: mapLabelPoints;
|
: mapLabelPoints;
|
||||||
const labelPoints = showLabels ? allLabelPoints : allLabelPoints.filter((point) => point.vin === selectedVin);
|
const labelPoints = showLabels ? allLabelPoints : allLabelPoints.filter((point) => point.vin === selectedVin);
|
||||||
const showEveryPlate = mapZoom >= 19;
|
const showEveryPlate = mapZoom >= 19;
|
||||||
|
const layerMode = showEveryPlate ? 'dense' : 'normal';
|
||||||
const activeLabels = showEveryPlate ? denseLabels : labels;
|
const activeLabels = showEveryPlate ? denseLabels : labels;
|
||||||
labels.setMap(showEveryPlate ? null : map);
|
if (labelLayerModeRef.current && labelLayerModeRef.current !== layerMode) {
|
||||||
denseLabels.setMap(showEveryPlate ? map : null);
|
(labelLayerModeRef.current === 'dense' ? denseLabels : labels).clear();
|
||||||
|
labelMarkersRef.current.clear();
|
||||||
|
}
|
||||||
|
labelLayerModeRef.current = layerMode;
|
||||||
|
const layerState = `${layerMode}:${labelPoints.length > 0 ? 'visible' : 'hidden'}`;
|
||||||
|
if (labelLayerStateRef.current !== layerState) {
|
||||||
|
labels.setMap(layerMode === 'normal' && labelPoints.length ? map : null);
|
||||||
|
denseLabels.setMap(layerMode === 'dense' && labelPoints.length ? map : null);
|
||||||
|
labelLayerStateRef.current = layerState;
|
||||||
|
}
|
||||||
const densePlacements = showEveryPlate ? densePlatePlacements(labelPoints) : null;
|
const densePlacements = showEveryPlate ? densePlatePlacements(labelPoints) : null;
|
||||||
const markers = labelPoints.map((point) => {
|
const nextMarkers = new Map<string, LabelMarkerEntry>();
|
||||||
|
const markersToAdd: AMapOverlay[] = [];
|
||||||
|
for (const point of labelPoints) {
|
||||||
const placement = densePlacements?.get(point.vin);
|
const placement = densePlacements?.get(point.vin);
|
||||||
return new AMap.LabelMarker!({
|
const signature = labelMarkerSignature(point, point.vin === selectedVin, placement);
|
||||||
|
const existing = labelMarkersRef.current.get(point.vin);
|
||||||
|
if (existing?.signature === signature) {
|
||||||
|
nextMarkers.set(point.vin, existing);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const marker = new AMap.LabelMarker!({
|
||||||
name: point.plate || point.vin,
|
name: point.plate || point.vin,
|
||||||
position: wgs84ToGcj02(point.longitude, point.latitude),
|
position: wgs84ToGcj02(point.longitude, point.latitude),
|
||||||
rank: point.vin === selectedVin ? 100 : 1,
|
rank: point.vin === selectedVin ? 100 : 1,
|
||||||
@@ -350,8 +398,15 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
nextMarkers.set(point.vin, { marker, signature });
|
||||||
if (markers.length) activeLabels.add(markers);
|
markersToAdd.push(marker);
|
||||||
|
}
|
||||||
|
const markersToRemove = [...labelMarkersRef.current.entries()]
|
||||||
|
.filter(([vin, entry]) => nextMarkers.get(vin) !== entry)
|
||||||
|
.map(([, entry]) => entry.marker);
|
||||||
|
if (markersToRemove.length) activeLabels.remove(markersToRemove);
|
||||||
|
if (markersToAdd.length) activeLabels.add(markersToAdd);
|
||||||
|
labelMarkersRef.current = nextMarkers;
|
||||||
}, [mapZoom, monitorMap, points, selectedTarget, selectedVin, showLabels, state]);
|
}, [mapZoom, monitorMap, points, selectedTarget, selectedVin, showLabels, state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -381,9 +436,10 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
|||||||
} else if (selectionPositionRef.current && selectionPositionRef.current !== positionKey && followSelectedRef.current) {
|
} else if (selectionPositionRef.current && selectionPositionRef.current !== positionKey && followSelectedRef.current) {
|
||||||
mapRef.current.panTo?.(mapPosition, 650);
|
mapRef.current.panTo?.(mapPosition, 650);
|
||||||
}
|
}
|
||||||
|
const previousPositionKey = selectionPositionRef.current;
|
||||||
selectionPositionRef.current = positionKey;
|
selectionPositionRef.current = positionKey;
|
||||||
if (selectionKeyRef.current === selectionKey && selectionRef.current) {
|
if (selectionKeyRef.current === selectionKey && selectionRef.current) {
|
||||||
selectionRef.current.setPosition?.(mapPosition);
|
if (previousPositionKey !== positionKey) selectionRef.current.setPosition?.(mapPosition);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectionRef.current?.setMap?.(null);
|
selectionRef.current?.setMap?.(null);
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
This document records verified risks, the production controls that address them, and the next evidence to collect. It is intentionally operational: a passing build alone is not proof that the browser application is production-ready.
|
This document records verified risks, the production controls that address them, and the next evidence to collect. It is intentionally operational: a passing build alone is not proof that the browser application is production-ready.
|
||||||
|
|
||||||
|
## 2026-07-16: incremental fleet-map refresh
|
||||||
|
|
||||||
|
The 15-second monitor refresh previously treated every new response object as a complete visual change. Even when only `asOf` changed, it regenerated MassMarks styles/data, cleared both label layers and constructed every plate marker again. With hundreds of visible vehicles this caused avoidable canvas work and periodic main-thread pressure.
|
||||||
|
|
||||||
|
FleetMap now fingerprints only fields that affect map rendering. An unchanged refresh performs no AMap data/style/label mutation. A moving vehicle still updates MassMarks once, but the LabelsLayer removes and replaces only that vehicle's changed marker; full label-layer replacement is reserved for crossing the normal/dense zoom boundary. The selected ripple marker also skips redundant position writes. This uses the official AMap JS API 2.0 [`LabelsLayer.remove`](https://lbs.amap.com/api/javascript-api-v2/guide/amap-massmarker/label-marker) and `LabelMarker` update model instead of reconstructing all overlays. Unit tests assert zero map mutations for an `asOf`-only refresh and exactly one label replacement for one moved vehicle.
|
||||||
|
|
||||||
## 2026-07-16: high-cardinality address cache lifecycle
|
## 2026-07-16: high-cardinality address cache lifecycle
|
||||||
|
|
||||||
The monitor list previously retained every manually resolved coordinate for 24 hours, and track replay inherited the global five-minute cache for every paused point. Long-running dispatch sessions could therefore accumulate address payloads across pages and arbitrary playback coordinates even after their components disappeared.
|
The monitor list previously retained every manually resolved coordinate for 24 hours, and track replay inherited the global five-minute cache for every paused point. Long-running dispatch sessions could therefore accumulate address payloads across pages and arbitrary playback coordinates even after their components disappeared.
|
||||||
|
|||||||
Reference in New Issue
Block a user