import { PathLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers'; import type { Layer, PickingInfo } from '@deck.gl/core'; import { DEPTH_DISABLED_PARAMS } from '../../../shared/lib/map/mapConstants'; import { getShipKindColor } from '../lib/adapters'; import type { CurrentVesselPosition, ProcessedTrack } from '../model/track.types'; export const TRACK_REPLAY_LAYER_IDS = { PATH: 'track-replay-path', POINTS: 'track-replay-points', VIRTUAL_SHIP: 'track-replay-virtual-ship', VIRTUAL_LABEL: 'track-replay-virtual-label', TRAIL: 'track-replay-trail', } as const; interface PathData { vesselId: string; path: [number, number][]; color: [number, number, number, number]; } interface PointData { vesselId: string; position: [number, number]; color: [number, number, number, number]; timestamp: number; speed: number; index: number; } const MAX_POINTS_PER_TRACK = 800; export function createStaticTrackLayers(options: { tracks: ProcessedTrack[]; showPoints: boolean; highlightedVesselId?: string | null; onPathHover?: (vesselId: string | null) => void; }): Layer[] { const { tracks, showPoints, highlightedVesselId, onPathHover } = options; const layers: Layer[] = []; if (!tracks || tracks.length === 0) return layers; const pathData: PathData[] = tracks.map((track) => ({ vesselId: track.vesselId, path: track.geometry, color: getShipKindColor(track.shipKindCode), })); layers.push( new PathLayer({ id: TRACK_REPLAY_LAYER_IDS.PATH, data: pathData, getPath: (d) => d.path, getColor: (d) => highlightedVesselId && highlightedVesselId === d.vesselId ? [255, 255, 0, 255] : [d.color[0], d.color[1], d.color[2], 235], getWidth: (d) => (highlightedVesselId && highlightedVesselId === d.vesselId ? 5 : 3), widthUnits: 'pixels', widthMinPixels: 1, widthMaxPixels: 6, parameters: DEPTH_DISABLED_PARAMS, jointRounded: true, capRounded: true, pickable: true, onHover: (info: PickingInfo) => { onPathHover?.(info.object?.vesselId ?? null); }, updateTriggers: { getColor: [highlightedVesselId], getWidth: [highlightedVesselId], }, }), ); if (showPoints) { const pointData: PointData[] = []; for (const track of tracks) { const color = getShipKindColor(track.shipKindCode); const len = track.geometry.length; if (len <= MAX_POINTS_PER_TRACK) { for (let i = 0; i < len; i++) { pointData.push({ vesselId: track.vesselId, position: track.geometry[i], color, timestamp: track.timestampsMs[i] || 0, speed: track.speeds[i] || 0, index: i, }); } } else { const step = len / MAX_POINTS_PER_TRACK; for (let i = 0; i < MAX_POINTS_PER_TRACK; i++) { const idx = Math.min(Math.floor(i * step), len - 1); pointData.push({ vesselId: track.vesselId, position: track.geometry[idx], color, timestamp: track.timestampsMs[idx] || 0, speed: track.speeds[idx] || 0, index: idx, }); } } } layers.push( new ScatterplotLayer({ id: TRACK_REPLAY_LAYER_IDS.POINTS, data: pointData, getPosition: (d) => d.position, getFillColor: (d) => d.color, getRadius: 3, radiusUnits: 'pixels', radiusMinPixels: 2, radiusMaxPixels: 5, parameters: DEPTH_DISABLED_PARAMS, pickable: false, }), ); } return layers; } export function createDynamicTrackLayers(options: { currentPositions: CurrentVesselPosition[]; showVirtualShip: boolean; showLabels: boolean; onIconHover?: (position: CurrentVesselPosition | null, x: number, y: number) => void; onPathHover?: (vesselId: string | null) => void; }): Layer[] { const { currentPositions, showVirtualShip, showLabels, onIconHover, onPathHover } = options; const layers: Layer[] = []; if (!currentPositions || currentPositions.length === 0) return layers; if (showVirtualShip) { layers.push( new ScatterplotLayer({ id: TRACK_REPLAY_LAYER_IDS.VIRTUAL_SHIP, data: currentPositions, getPosition: (d) => d.position, getFillColor: (d) => { const base = getShipKindColor(d.shipKindCode); return [base[0], base[1], base[2], 230] as [number, number, number, number]; }, getLineColor: [255, 255, 255, 200], getRadius: 5, radiusUnits: 'pixels', radiusMinPixels: 4, radiusMaxPixels: 8, stroked: true, lineWidthMinPixels: 1, parameters: DEPTH_DISABLED_PARAMS, pickable: true, onHover: (info: PickingInfo) => { if (info.object) { onPathHover?.(info.object.vesselId); onIconHover?.(info.object, info.x, info.y); } else { onPathHover?.(null); onIconHover?.(null, 0, 0); } }, }), ); } if (showLabels) { const labelData = currentPositions.filter((position) => (position.shipName || '').trim().length > 0); if (labelData.length > 0) { layers.push( new TextLayer({ id: TRACK_REPLAY_LAYER_IDS.VIRTUAL_LABEL, data: labelData, getPosition: (d) => d.position, getText: (d) => d.shipName, getColor: [226, 232, 240, 240], getSize: 11, getTextAnchor: 'start', getAlignmentBaseline: 'center', getPixelOffset: [14, 0], fontFamily: 'Malgun Gothic, Arial, sans-serif', fontSettings: { sdf: true }, outlineColor: [2, 6, 23, 220], outlineWidth: 2, parameters: DEPTH_DISABLED_PARAMS, pickable: false, }), ); } } return layers; } export function isTrackReplayLayerId(id: unknown): boolean { return typeof id === 'string' && id.startsWith('track-replay-'); }