feat: extend map targeting and overlay controls
Support batch feature targeting and clear map highlights and overlays from the More panel. Remove misleading action chevrons and unused configuration export entries. Validated the isolated patch with lint, typecheck, 36 focused tests and 5 browser cases using mocked data.
This commit is contained in:
@@ -47,6 +47,24 @@ function setup(overrides: Partial<ConstructorParameters<typeof InvestigationPres
|
||||
}
|
||||
|
||||
describe("live investigation presentation following", () => {
|
||||
it("discards queued map commands while retaining chart commands and later new map events", async () => {
|
||||
const painting = deferred<void>();
|
||||
const execute = vi.fn((command: InvestigationPresentationCommand) =>
|
||||
command.action === "open_chart" ? painting.promise : Promise.resolve());
|
||||
const test = setup({ execute });
|
||||
test.follower.start();
|
||||
test.channel.publish({ type: "open" });
|
||||
try {
|
||||
await vi.waitFor(() => expect(test.onStatus).toHaveBeenCalledWith("live"));
|
||||
test.channel.publish({ type: "events", events: [event(11, openChart), event(12), event(13, closeChart)] });
|
||||
test.follower.discardPendingMapCommands();
|
||||
painting.resolve();
|
||||
await vi.waitFor(() => expect(execute.mock.calls.map(([command]) => command.action)).toEqual(["open_chart", "close_chart"]));
|
||||
test.channel.publish({ type: "events", events: [event(14)] });
|
||||
await vi.waitFor(() => expect(execute.mock.calls.map(([command]) => command.action)).toEqual(["open_chart", "close_chart", "highlight_features"]));
|
||||
} finally { painting.resolve(); test.follower.stop(); }
|
||||
});
|
||||
|
||||
it("listens before fetching the baseline and preserves events arriving during cursor lookup", async () => {
|
||||
const baseline = deferred<InvestigationPresentationCursor>();
|
||||
const order: string[] = [];
|
||||
|
||||
@@ -43,6 +43,12 @@ export class InvestigationPresentationFollower {
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
discardPendingMapCommands() {
|
||||
this.pending = this.pending.filter(({ event }) =>
|
||||
event.command.action === "open_chart" || event.command.action === "close_chart"
|
||||
);
|
||||
}
|
||||
|
||||
updateIdentity(identity: InvestigationIdentity) {
|
||||
if (identity.task_id !== this.options.taskId || this.identity && identity.execution_generation < this.identity.execution_generation) return;
|
||||
const changed = this.identity && (identity.execution_generation !== this.identity.execution_generation
|
||||
@@ -138,6 +144,7 @@ export class InvestigationPresentationFollower {
|
||||
if (next.event.execution_generation !== this.identity!.execution_generation || next.event.run_id !== this.identity!.active_run?.run_id) {
|
||||
await this.resolveIdentity(next.event);
|
||||
if (epoch !== this.epoch) break;
|
||||
if (this.pending[0] !== next) continue;
|
||||
if (next.event.execution_generation !== this.identity!.execution_generation || next.event.run_id !== this.identity!.active_run?.run_id) break;
|
||||
}
|
||||
this.pending.shift();
|
||||
|
||||
@@ -55,15 +55,25 @@ export function useInvestigationPresentation(options: Options) {
|
||||
const [highlightLayers, setHighlightLayers] = useState<string[]>([]);
|
||||
const followerRef = useRef<InvestigationPresentationFollower | null>(null);
|
||||
const manualRequestRef = useRef<AbortController | null>(null);
|
||||
const mapCommandsRef = useRef(new AbortController());
|
||||
const scene = useMemo(() => new InvestigationMapPresentation(() => options.mapRef.current, () => valuesRef.current.getPadding()), [options.mapRef]);
|
||||
const taskId = options.task?.task_id ?? null;
|
||||
const subscribe = options.sync.subscribePresentationEvents;
|
||||
|
||||
const clear = useCallback(() => {
|
||||
scene.clear(); setCharts([]); setOverlay(null); setHighlightLayers([]);
|
||||
valuesRef.current.onClearLayers();
|
||||
const clearMapOverlays = useCallback((discardPending = true) => {
|
||||
mapCommandsRef.current.abort();
|
||||
mapCommandsRef.current = new AbortController();
|
||||
manualRequestRef.current?.abort();
|
||||
if (discardPending) followerRef.current?.discardPendingMapCommands();
|
||||
scene.clear(); setOverlay(null); setHighlightLayers([]);
|
||||
}, [scene]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
// New task generations retain their first queued map command.
|
||||
clearMapOverlays(false); setCharts([]);
|
||||
valuesRef.current.onClearLayers();
|
||||
}, [clearMapOverlays]);
|
||||
|
||||
useEffect(() => {
|
||||
manualRequestRef.current?.abort();
|
||||
cameraPausedRef.current = false;
|
||||
@@ -77,26 +87,35 @@ export function useInvestigationPresentation(options: Options) {
|
||||
stopPulse: () => scene.stopPulse(),
|
||||
onStatus: (next, message) => { setStatus(next); setError(message ?? null); },
|
||||
execute: async (command, signal) => {
|
||||
const sourceSignal = signal;
|
||||
if (command.action !== "open_chart" && command.action !== "close_chart") {
|
||||
signal = AbortSignal.any([signal, mapCommandsRef.current.signal]);
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
if (command.action === "highlight_features") {
|
||||
const collection = await valuesRef.current.controller.queryPresentationFeatures(command.layer, command.feature_ids, signal);
|
||||
signal.throwIfAborted();
|
||||
scene.highlight(collection, command.effect === "pulse", !cameraPausedRef.current);
|
||||
setHighlightLayers([command.layer]);
|
||||
} else if (command.action === "clear_highlights") {
|
||||
scene.clearHighlights(); setHighlightLayers([]);
|
||||
} else if (command.action === "open_chart") {
|
||||
setCharts((current) => [...current.filter((item) => item.chart_id !== command.chart_id).slice(-5), command]);
|
||||
await waitForChart(command.chart_id, signal);
|
||||
} else if (command.action === "close_chart") {
|
||||
setCharts((current) => current.filter((item) => item.chart_id !== command.chart_id));
|
||||
await frame(signal);
|
||||
} else if (command.action === "set_overlay") {
|
||||
const next = { ...command, visible: true };
|
||||
scene.setOverlay(next, !cameraPausedRef.current); setOverlay(next);
|
||||
} else {
|
||||
scene.clearOverlay(command.overlay_id);
|
||||
setOverlay((current) => current?.overlay_id === command.overlay_id ? null : current);
|
||||
try {
|
||||
if (command.action === "highlight_features") {
|
||||
const collection = await valuesRef.current.controller.queryPresentationFeatures(command.layer, command.feature_ids, signal);
|
||||
signal.throwIfAborted();
|
||||
scene.highlight(collection, command.effect === "pulse", !cameraPausedRef.current);
|
||||
setHighlightLayers([command.layer]);
|
||||
} else if (command.action === "clear_highlights") {
|
||||
scene.clearHighlights(); setHighlightLayers([]);
|
||||
} else if (command.action === "open_chart") {
|
||||
setCharts((current) => [...current.filter((item) => item.chart_id !== command.chart_id).slice(-5), command]);
|
||||
await waitForChart(command.chart_id, signal);
|
||||
} else if (command.action === "close_chart") {
|
||||
setCharts((current) => current.filter((item) => item.chart_id !== command.chart_id));
|
||||
await frame(signal);
|
||||
} else if (command.action === "set_overlay") {
|
||||
const next = { ...command, visible: true };
|
||||
scene.setOverlay(next, !cameraPausedRef.current); setOverlay(next);
|
||||
} else {
|
||||
scene.clearOverlay(command.overlay_id);
|
||||
setOverlay((current) => current?.overlay_id === command.overlay_id ? null : current);
|
||||
}
|
||||
} catch (cause) {
|
||||
if (signal.aborted && !sourceSignal.aborted) return;
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -138,7 +157,7 @@ export function useInvestigationPresentation(options: Options) {
|
||||
}, [pauseCamera, scene]);
|
||||
|
||||
return {
|
||||
charts, overlay, cameraPaused, status, error, highlightLayers,
|
||||
charts, overlay, cameraPaused, status, error, highlightLayers, clearMapOverlays,
|
||||
pauseCamera,
|
||||
resumeCamera: () => { cameraPausedRef.current = false; setCameraPaused(false); },
|
||||
locateTargets,
|
||||
|
||||
Reference in New Issue
Block a user