import { useState, useEffect } from 'react'; import type { PlotMouseEvent, PlotDatum } from 'plotly.js'; import { ColorMap, FrameType, PixelData } from '../types'; import { VISUALIZATION_DEFAULTS } from '../constants'; import { useInterpolationStore } from '@/store/interpolation-store'; import { visualizationClient } from '@/lib/api/visualization-client'; export function useVisualization(fileId?: string | null) { const store = useInterpolationStore(); const { selectedVariable, selectedTimeIndex, currentFrame: data, visLoading, visError: error } = store; // Local UI state const [colorMap, setColorMap] = useState(VISUALIZATION_DEFAULTS.initialColorMap); const [frameType, setFrameType] = useState(VISUALIZATION_DEFAULTS.initialFrame); const [isFullscreen, setIsFullscreen] = useState(false); const [pixelData, setPixelData] = useState({ x: null, y: null, value: null, colormapValue: null }); const [layerUrl, setLayerUrl] = useState(null); const [bounds, setBounds] = useState<[number, number, number, number] | undefined>(undefined); // Derived state string for UI compatibility const state = visLoading ? 'loading' : error ? 'error' : 'ready'; useEffect(() => { if (store.status === 'completed') { console.log('Interpolation completed successfully'); } }, [store.status]); useEffect(() => { if (!fileId) return; const fetchVariables = async () => { try { const res = await visualizationClient.getAvailableVariables(fileId); if (res.success && res.data.length > 0) { store.setVisState({ availableVariables: res.data }); if (!selectedVariable) { store.setVisState({ selectedVariable: res.data[0] }); } } } catch (err) { console.error("Failed to fetch variables", err); } }; fetchVariables(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fileId]); useEffect(() => { if (!fileId || !selectedVariable) return; const fetchFrameAndImage = async () => { try { store.setVisState({ visLoading: true, visError: null }); // Generate layer url const url = visualizationClient.getLayerUrl(fileId, selectedVariable, selectedTimeIndex); setLayerUrl(url); // Fetch bounds try { const boundsRes = await visualizationClient.getBounds(fileId, selectedVariable); if (boundsRes.success && boundsRes.data && boundsRes.data.bounds) { const [[south, west], [north, east]] = boundsRes.data.bounds; setBounds([south, west, north, east]); } } catch(e) { console.warn("Could not load dynamic bounds", e); } // Backend only has images right now, getFrame might throw 404, we catch it silently. try { const res = await visualizationClient.getFrame(fileId, selectedVariable, selectedTimeIndex); if (res.success) { store.setVisState({ currentFrame: res.data, visLoading: false }); } } catch(err) { store.setVisState({ visLoading: false }); // image layer takes priority } } catch (err: unknown) { store.setVisState({ visError: err instanceof Error ? err.message : 'Failed to load frame', visLoading: false }); } }; fetchFrameAndImage(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fileId, selectedVariable, selectedTimeIndex]); const toggleFullscreen = () => setIsFullscreen(!isFullscreen); const handleHover = (event: Readonly) => { if (event.points && event.points[0]) { const pt = event.points[0] as PlotDatum & { z?: number }; setPixelData({ x: pt.x as number, y: pt.y as number, value: pt.z !== undefined ? Number(pt.z) : null, colormapValue: null }); } }; const handleUnhover = () => { setPixelData({ x: null, y: null, value: null, colormapValue: null }); }; return { state, data, colorMap, setColorMap, frameType, setFrameType, isFullscreen, toggleFullscreen, pixelData, handleHover, handleUnhover, // Add these for UI to change variable/time selectedVariable, setSelectedVariable: (v: string) => store.setVisState({ selectedVariable: v }), selectedTimeIndex, setSelectedTimeIndex: (t: number) => store.setVisState({ selectedTimeIndex: t }), availableVariables: store.availableVariables, layerUrl, bounds }; }