import { useMemo } from 'react'; import { getCurrentPositions } from '../lib/interpolate'; import { createReplayTrailLayer } from '../layers/replayLayers'; import { createDynamicTrackLayers, createStaticTrackLayers } from '../layers/trackLayers'; import type { CurrentVesselPosition, ProcessedTrack } from '../model/track.types'; import { useTrackPlaybackStore } from '../stores/trackPlaybackStore'; import { useTrackQueryStore } from '../stores/trackQueryStore'; export interface TrackReplayDeckRenderState { trackReplayDeckLayers: unknown[]; enabledTracks: ProcessedTrack[]; currentPositions: CurrentVesselPosition[]; showPoints: boolean; showVirtualShip: boolean; showLabels: boolean; renderEpoch: number; } export function useTrackReplayDeckLayers(): TrackReplayDeckRenderState { const tracks = useTrackQueryStore((state) => state.tracks); const disabledVesselIds = useTrackQueryStore((state) => state.disabledVesselIds); const highlightedVesselId = useTrackQueryStore((state) => state.highlightedVesselId); const setHighlightedVesselId = useTrackQueryStore((state) => state.setHighlightedVesselId); const showPoints = useTrackQueryStore((state) => state.showPoints); const showVirtualShip = useTrackQueryStore((state) => state.showVirtualShip); const showLabels = useTrackQueryStore((state) => state.showLabels); const showTrail = useTrackQueryStore((state) => state.showTrail); const renderEpoch = useTrackQueryStore((state) => state.renderEpoch); const isPlaying = useTrackPlaybackStore((state) => state.isPlaying); const currentTime = useTrackPlaybackStore((state) => state.currentTime); const playbackRenderTime = useMemo(() => { if (!isPlaying) return currentTime; // Throttle to ~10fps while playing to reduce relayout pressure. return Math.floor(currentTime / 100) * 100; }, [isPlaying, currentTime]); const enabledTracks = useMemo(() => { if (!tracks.length) return []; if (disabledVesselIds.size === 0) return tracks; return tracks.filter((track) => !disabledVesselIds.has(track.vesselId)); }, [tracks, disabledVesselIds]); const currentPositions = useMemo(() => { void renderEpoch; if (enabledTracks.length === 0) return []; const sampled = getCurrentPositions(enabledTracks, playbackRenderTime); if (sampled.length > 0 || isPlaying) return sampled; // Ensure an immediate first-frame marker when query data arrives but // playback has not started yet (globe static-render case). return enabledTracks.flatMap((track) => { if (track.geometry.length === 0) return []; const firstTs = track.timestampsMs[0] ?? playbackRenderTime; return [ { vesselId: track.vesselId, targetId: track.targetId, sigSrcCd: track.sigSrcCd, shipName: track.shipName, shipKindCode: track.shipKindCode, nationalCode: track.nationalCode, position: track.geometry[0], heading: 0, speed: track.speeds[0] ?? 0, timestamp: firstTs, } as CurrentVesselPosition, ]; }); }, [enabledTracks, playbackRenderTime, isPlaying, renderEpoch]); const staticLayers = useMemo( () => { void renderEpoch; return createStaticTrackLayers({ tracks: enabledTracks, showPoints, highlightedVesselId, onPathHover: setHighlightedVesselId, }); }, [enabledTracks, showPoints, highlightedVesselId, setHighlightedVesselId, renderEpoch], ); const dynamicLayers = useMemo( () => { void renderEpoch; return createDynamicTrackLayers({ currentPositions, showVirtualShip, showLabels, onPathHover: setHighlightedVesselId, }); }, [currentPositions, showVirtualShip, showLabels, setHighlightedVesselId, renderEpoch], ); const trailLayer = useMemo( () => { void renderEpoch; return createReplayTrailLayer({ tracks: enabledTracks, currentTime: playbackRenderTime, showTrail, }); }, [enabledTracks, playbackRenderTime, showTrail, renderEpoch], ); const trackReplayDeckLayers = useMemo( () => [...staticLayers, ...(trailLayer ? [trailLayer] : []), ...dynamicLayers], [staticLayers, dynamicLayers, trailLayer], ); return { trackReplayDeckLayers, enabledTracks, currentPositions, showPoints, showVirtualShip, showLabels, renderEpoch, }; }