import { useState, useEffect } from 'react'; import { ComposedChart, Area, Line, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; import { Helmet } from 'react-helmet-async'; import { type Language, t } from '../translations'; import KpiCards from './KpiCards'; import { WeatherWidget } from './WeatherWidget'; import { WindChart } from './WindChart'; import { NAVIGATION_LIMITS } from '../utils/navigationLimits'; import { lakesConfig } from '../../scripts/lakesConfig'; import { FiAlertCircle, FiStar } from 'react-icons/fi'; import { TbSwimming, TbSailboat } from 'react-icons/tb'; import { useFavorites } from '../hooks/useFavorites'; import { Tooltip as IconTooltip } from './Tooltip'; interface LipnoData { timestamp: string; date: string; level: number; inflow: number; outflow: number; volume: number; fullness: number; temperature?: number | null; precipitation?: number | null; } interface Props { language: Language; lakeId: string | null; windUnit?: 'kmh' | 'ms'; } const CustomTooltip = ({ active, payload, label, language, isWeather }: any) => { if (active && payload && payload.length) { const dict = t[language as Language].chart; if (isWeather) { return (

{label}

{payload.map((entry: any, index: number) => { const isTemp = entry.name === 'temperature' || entry.dataKey === 'temperature'; return (

{isTemp ? (language === 'cs' ? 'Teplota' : 'Temperature') : (language === 'cs' ? 'Srážky' : 'Precipitation')}: {entry.value !== undefined && entry.value !== null ? entry.value.toFixed(1) : 'N/A'} {isTemp ? '°C' : 'mm'}

); })}
); } return (

{label}

{[...payload].sort((a: any, b: any) => { const order = ['level', 'inflow', 'outflow', 'temperature', 'precipitation']; const indexA = order.indexOf(a.dataKey); const indexB = order.indexOf(b.dataKey); return (indexA === -1 ? 99 : indexA) - (indexB === -1 ? 99 : indexB); }).map((entry: any, index: number) => { let labelStr = ''; let unit = ''; let color = ''; if (entry.dataKey === 'level') { labelStr = dict.level; unit = 'm n. m.'; color = 'var(--color-cyan)'; } else if (entry.dataKey === 'outflow') { labelStr = dict.outflow; unit = 'm³/s'; color = 'var(--color-red)'; } else if (entry.dataKey === 'inflow') { labelStr = dict.inflow; unit = 'm³/s'; color = 'var(--color-green)'; } else if (entry.dataKey === 'temperature') { labelStr = language === 'cs' ? 'Teplota' : 'Temperature'; unit = '°C'; color = 'var(--color-red)'; } else if (entry.dataKey === 'precipitation') { labelStr = language === 'cs' ? 'Srážky' : 'Precipitation'; unit = 'mm'; color = 'var(--color-cyan)'; } if (!labelStr || entry.value === null || entry.value === undefined) return null; return (
{labelStr}: {entry.value.toFixed(entry.dataKey === 'level' ? 2 : 1)} {unit}
); })}
); } return null; }; const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [lakeInfo, setLakeInfo] = useState(null); const [isSmoothed, setIsSmoothed] = useState(true); const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d' | '1y' | 'all'>('7d'); const dict = t[language].chart; const topbarDict = t[language].topbar; useEffect(() => { const loadData = () => { fetch(`/data/lakes_index.json?t=${Date.now()}`) .then(res => res.json()) .then(indexData => { const found = indexData.find((l: any) => l.id === lakeId); setLakeInfo(found || { name: 'Lipno 1', river: 'Vltava' }); }) .catch(err => console.error(err)); const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1'; fetch(`/data/${internalId}.json?t=${Date.now()}`) .then(res => res.json()) .then(json => { const formattedData = json.map((item: any) => { const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow; return { timestamp: item.timestamp, date: new Date(item.timestamp).toLocaleString(language === 'cs' ? 'cs-CZ' : 'en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }), level: item.level === null || isNaN(item.level) ? 0 : item.level, outflow: outflow, inflow: item.inflow || 0, volume: item.volume || 0, fullness: 0, temperature: item.temperature, precipitation: item.precipitation === null ? undefined : item.precipitation }; }); setData(formattedData); setLoading(false); }) .catch(err => { console.error('Failed to load data', err); setLoading(false); }); }; loadData(); const intervalId = setInterval(loadData, 60 * 1000); // Poll every 1 minute return () => clearInterval(intervalId); }, [language, lakeId]); if (loading) { return (
Loading HLADINATOR...
); } const latestData = data[data.length - 1] || { level: 0, inflow: 0, outflow: 0, volume: 0, fullness: 0 }; const curveType = isSmoothed ? 'monotone' : 'linear'; // Find last valid values for KPIs, including 0 const lastValidFlowData = [...data].reverse().find(d => d.outflow !== null && !isNaN(d.outflow) && d.outflow >= 0) || latestData; const now = new Date().getTime(); const getCutoff = () => { switch (timeRange) { case '24h': return now - 24 * 60 * 60 * 1000; case '7d': return now - 7 * 24 * 60 * 60 * 1000; case '30d': return now - 30 * 24 * 60 * 60 * 1000; case '1y': return now - 365 * 24 * 60 * 60 * 1000; default: return 0; } }; const cutoff = getCutoff(); const filteredData = data.filter(d => new Date(d.timestamp).getTime() >= cutoff); // Downsample data for large time ranges to prevent stuttering let chartData = filteredData; if (timeRange === '30d' && filteredData.length > 200) { chartData = filteredData.filter((_, i) => i % 4 === 0 || i === filteredData.length - 1); } else if ((timeRange === '1y' || timeRange === 'all') && filteredData.length > 200) { chartData = filteredData.filter((_, i) => i % 24 === 0 || i === filteredData.length - 1); } const animate = chartData.length < 150; // Find record from 24h, 7d, 30d ago const nowMs = new Date(latestData.timestamp).getTime(); const targetMs24h = nowMs - 24 * 60 * 60 * 1000; const targetMs7d = nowMs - 7 * 24 * 60 * 60 * 1000; const targetMs30d = nowMs - 30 * 24 * 60 * 60 * 1000; const { isFavorite, toggleFavorite } = useFavorites(); const isFav = lakeId ? isFavorite(lakeId) : false; let level24hAgo = latestData.level; let level7dAgo = latestData.level; let level30dAgo = latestData.level; let minDiff24h = Infinity; let minDiff7d = Infinity; let minDiff30d = Infinity; let inflowSum24h = 0; let outflowSum24h = 0; let flowCount24h = 0; for (const d of data) { const t = new Date(d.timestamp).getTime(); const diff24h = Math.abs(t - targetMs24h); if (diff24h < minDiff24h) { minDiff24h = diff24h; level24hAgo = d.level; } const diff7d = Math.abs(t - targetMs7d); if (diff7d < minDiff7d) { minDiff7d = diff7d; level7dAgo = d.level; } const diff30d = Math.abs(t - targetMs30d); if (diff30d < minDiff30d) { minDiff30d = diff30d; level30dAgo = d.level; } if (t >= targetMs24h && d.inflow !== undefined && d.outflow !== undefined) { inflowSum24h += d.inflow; outflowSum24h += d.outflow; flowCount24h++; } } const levelDiff24h = latestData.level - level24hAgo; const levelDiff7d = latestData.level - level7dAgo; const levelDiff30d = latestData.level - level30dAgo; const avgInflow24h = flowCount24h > 0 ? inflowSum24h / flowCount24h : undefined; const avgOutflow24h = flowCount24h > 0 ? outflowSum24h / flowCount24h : undefined; const limits = lakeInfo ? NAVIGATION_LIMITS[lakeInfo.id] : undefined; const staticConfig = lakeInfo ? lakesConfig.find(l => l.id === lakeInfo.id) : undefined; const kpiData = { level: latestData.level, levelDiff24h, levelDiff7d, levelDiff30d, inflow: lastValidFlowData.inflow, outflow: lastValidFlowData.outflow, volume: lakeInfo?.volume || 0, fullness: lakeInfo?.capacity || 0, storageDiff: lakeInfo?.storageDiff, minDiff: staticConfig?.minLevel ? latestData.level - staticConfig.minLevel : undefined, avgInflow24h, avgOutflow24h }; const leftYAxisDomain = [ (dataMin: number) => { let min = dataMin; if (limits) limits.forEach(l => { if (l.level < min) min = l.level; }); return min - 0.5; }, (dataMax: number) => { let max = dataMax; if (staticConfig?.maxLevel && staticConfig.maxLevel > max) max = staticConfig.maxLevel; if (staticConfig?.storageLevel && staticConfig.storageLevel > max) max = staticConfig.storageLevel; return max + 0.5; } ]; return (
{lakeInfo && ( <> {t[language].seo.lakeTitle.replace('{name}', lakeInfo.name)}

{lakeInfo.name}

{t[language].seo.lakeDesc.replace('{name}', lakeInfo.name)}

)}
{topbarDict.updated} {new Date().toLocaleDateString(language === 'cs' ? 'cs-CZ' : 'en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}, {new Date().toLocaleTimeString(language === 'cs' ? 'cs-CZ' : 'en-GB', { hour: '2-digit', minute: '2-digit' })} UTC
{lakeInfo && lakeInfo.lat && lakeInfo.lng && ( )}
{limits && limits.map((limit, idx) => { const diff = latestData.level - limit.level; if (diff < 0.3) { const isBelow = diff < 0; return (
{language === 'cs' ? limit.labelCs : limit.labelEn} ({limit.level.toFixed(2)} m n.m.):
{isBelow ? (language === 'cs' ? `Hladina je ${Math.abs(diff).toFixed(2)} m POD limitem! Přerušení provozu.` : `Level is ${Math.abs(diff).toFixed(2)} m BELOW limit! Operations suspended.`) : (language === 'cs' ? `Hladina se blíží k limitu (zbývá ${diff.toFixed(2)} m).` : `Level is approaching limit (${diff.toFixed(2)} m remaining).`) }
); } return null; })} {/* CHART SECTION */}

{dict.title} {lakeInfo ? `${lakeInfo.name} ${lakeInfo.river ? `- ${lakeInfo.river}` : ''}` : 'Lipno 1 - Vltava'}

v.toFixed(2)} /> Math.max(dataMax, 1)]} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} /> } /> {/* Data Series */} {limits && limits.map((limit, idx) => ( ))} {staticConfig?.maxLevel && ( )} {staticConfig?.storageLevel && ( )}
{/* Chart Legend */}
{dict.level}
{dict.outflow}
{dict.inflow}
{/* WEATHER CHART SECTION */}

{language === 'cs' ? 'Počasí (Teplota a Srážky)' : 'Weather (Temperature & Precipitation)'}

v.toFixed(1)} /> } />
{language === 'cs' ? 'Teplota vzduchu' : 'Temperature'} [°C]
{language === 'cs' ? 'Srážky' : 'Precipitation'} [mm]
{/* Wind Chart placed inside the main card below the weather graph */} {lakeInfo && lakeInfo.lat && lakeInfo.lng && ( )} {/* Smoothed Toggle Control */}
{dict.view}
{dict.raw}
setIsSmoothed(!isSmoothed)} >
{dict.smoothed}
); }; export default LakeDetail;