feat: implement sensor glitch detection for water levels and update data cleaning logic

This commit is contained in:
David Fencl
2026-06-08 19:45:37 +02:00
parent 62c861e610
commit f8a7be7fa3
28 changed files with 222 additions and 64 deletions
+16 -7
View File
@@ -32,7 +32,7 @@ npm run data:update
``` ```
Tento příkaz provede dvě věci: Tento příkaz provede dvě věci:
1. `npm run scrape`: Otevře stránky povodí pro všech 12 přehrad, přečte tabulky s historickými měřeními a najde "Aktuální hodnoty", odkud vytáhne exaktní **přítok, objem, srážky a teplotu**. Tato data inteligentně sloučí s tvojí lokální databází (`public/data/*.json`). Pokud Povodí aktuálně počasí neposkytuje, skript zrecykluje tvou dřívější uloženou hodnotu, aby se graf "nerozbil". 1. `npm run scrape`: Otevře stránky povodí pro všech **53 nádrží a říčních stanic**, přečte tabulky s historickými měřeními a najde "Aktuální hodnoty", odkud vytáhne exaktní **přítok, odtok, objem, srážky a teplotu**. Tato data inteligentně sloučí s tvojí lokální databází (`public/data/*.json`) a automaticky doplňuje chybějící hodnoty přítoku/objemu z minula, aby graf neměl výpadky k nule.
2. `npm run build-index`: Zaktualizuje hlavní indexový soubor `lakes_index.json`, který aplikace využívá pro vykreslení rychlých náhledů (např. v levém menu nebo na mapě). 2. `npm run build-index`: Zaktualizuje hlavní indexový soubor `lakes_index.json`, který aplikace využívá pro vykreslení rychlých náhledů (např. v levém menu nebo na mapě).
--- ---
@@ -56,17 +56,26 @@ Pokud ti běží počítač (nebo domácí server/Raspberry Pi) nepřetržitě,
### Možnost B: Pomocí GitHub Actions (Pro Produkci) ### Možnost B: Pomocí GitHub Actions (Pro Produkci)
Až projekt nahraješ na GitHub, můžeš si vytvořit workflow soubor (např. `.github/workflows/scrape.yml`), který bude skript spouštět na serverech GitHubu zdarma každou hodinu, a výsledné `.json` soubory automaticky commitne a publikuje na web. Až projekt nahraješ na GitHub, můžeš si vytvořit workflow soubor (např. `.github/workflows/scrape.yml`), který bude skript spouštět na serverech GitHubu zdarma každou hodinu, a výsledné `.json` soubory automaticky commitne a publikuje na web.
### Možnost C: Jednoduchý integrovaný spouštěč (Nejlehčí) ### Možnost C: Jednoduchý integrovaný spouštěč (Doporučeno pro vývoj)
Pokud nechceš řešit složitý systémový crontab, napsal jsem pro tebe přímo do Node.js malý spouštěč. Stačí si otevřít další okno terminálu a napsat: Pokud nechceš řešit složitý systémový crontab, je v projektu připraven inteligentní plánovač. Stačí si otevřít další okno terminálu a napsat:
```bash ```bash
npm run data:watch 10 npm run data:watch
``` ```
Tento příkaz ihned provede první stažení a následně bude aplikaci automaticky aktualizovat **každých 10 minut** (číslo na konci si můžeš libovolně přepsat podle toho, jak často chceš stahovat). Skript poběží, dokud okno terminálu nezavřeš. Tento příkaz provede okamžitou aktualizaci a poté automaticky spouští stahování vždy 7 minut po každém 10minutovém kroku (např. 18:07, 18:17, 18:27...). Tento posun zaručuje, že Povodí už stihlo na svůj web nahrát nová data a nestahuješ staré hodnoty.
--- ---
## 🛠️ Oprava chyb v historii (Zuby / Nuly v grafu)
Pokud ti aplikace delší dobu neběžela (např. při vypnutém počítači) a následně došlo k doplnění dat z historie, mohly se v grafech přítoku a objemu objevit falešné propady k nule (zuby). Pro vyčištění celé historie a dopočítání těchto bodů z posledních známých hodnot spusť:
```bash
npm run data:fix
```
Tento skript projde všechny datové JSON soubory, detekuje anomálie/nuly a opraví je.
## 📁 Struktura klíčových datových složek ## 📁 Struktura klíčových datových složek
* `/scripts/lakesConfig.ts` - Tady najdeš definici všech 12 sledovaných přehrad (včetně jejich ID pro Povodí Vltavy, GPS souřadnic, maximálních objemů a stavebních kót). Sem můžeš přidávat nové přehrady. * `/scripts/lakesConfig.ts` - Tady najdeš definici všech **53 sledovaných nádrží a řek** (včetně jejich ID pro Povodí Vltavy, GPS souřadnic, maximálních objemů a stavebních kót). Sem můžeš přidávat nové stanice.
* `/public/data/` - Zde se ukládají vygenerovaná JSON data. V produkci musí být tyto soubory přístupné jako statické assety. * `/public/data/` - Zde se ukládají vygenerovaná JSON data. V produkci musí být tyto soubory přístupné jako statické assety.
* `/src/components/` - Obsahuje samotné vizuální karty, Leaflet mapu a detailní `LakeDetail.tsx` (kde se vykresluje hydrologický a meteorologický graf přes Recharts). * `/src/components/` - Obsahuje samotné vizuální karty, Leaflet mapu a detailní `LakeDetail.tsx` (kde se vykresluje hydrologický a meteorologický graf přes Recharts s automatickým čištěním chyb a senzorických úletů).
+1
View File
@@ -13,6 +13,7 @@
"build-index": "tsx scripts/buildIndex.ts", "build-index": "tsx scripts/buildIndex.ts",
"data:update": "npm run scrape && npm run build-index", "data:update": "npm run scrape && npm run build-index",
"data:watch": "tsx scripts/watchData.ts", "data:watch": "tsx scripts/watchData.ts",
"data:fix": "tsx scripts/fix_lake_inflows.ts",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"coverage": "vitest run --coverage" "coverage": "vitest run --coverage"
+9
View File
@@ -2317,5 +2317,14 @@
"volume": 0, "volume": 0,
"temperature": 26, "temperature": 26,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:10:00.000Z",
"level": 96,
"flow": 8.667,
"inflow": 0,
"volume": 0,
"temperature": 26,
"precipitation": 0
} }
] ]
+9
View File
@@ -2207,5 +2207,14 @@
"volume": 0, "volume": 0,
"temperature": 25.5, "temperature": 25.5,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 90,
"flow": 4.745,
"inflow": 0,
"volume": 0,
"temperature": 25.5,
"precipitation": 0
} }
] ]
+1 -1
View File
@@ -8220,7 +8220,7 @@
"timestamp": "2026-06-08T17:30:00.000Z", "timestamp": "2026-06-08T17:30:00.000Z",
"level": 463.42, "level": 463.42,
"flow": 0, "flow": 0,
"inflow": 6.048207396473533, "inflow": 0,
"volume": 0, "volume": 0,
"temperature": 23.7, "temperature": 23.7,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8886,7 +8886,7 @@
"timestamp": "2026-06-08T17:30:00.000Z", "timestamp": "2026-06-08T17:30:00.000Z",
"level": 416.72, "level": 416.72,
"flow": 0, "flow": 0,
"inflow": 7.8121741402451015, "inflow": 0,
"volume": 0.09, "volume": 0.09,
"temperature": 23.8, "temperature": 23.8,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8859,7 +8859,7 @@
"timestamp": "2026-06-08T17:30:00.000Z", "timestamp": "2026-06-08T17:30:00.000Z",
"level": 448.81, "level": 448.81,
"flow": 0, "flow": 0,
"inflow": 4.662341665814382, "inflow": 0,
"volume": 0.67, "volume": 0.67,
"temperature": 23.8, "temperature": 23.8,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8076,7 +8076,7 @@
"timestamp": "2026-06-08T17:00:00.000Z", "timestamp": "2026-06-08T17:00:00.000Z",
"level": 580.99, "level": 580.99,
"flow": 0, "flow": 0,
"inflow": 9.58165824042722, "inflow": 0,
"volume": 0.03, "volume": 0.03,
"temperature": 23, "temperature": 23,
"precipitation": 0 "precipitation": 0
+10 -1
View File
@@ -8939,7 +8939,7 @@
{ {
"timestamp": "2026-06-08T17:10:00.000Z", "timestamp": "2026-06-08T17:10:00.000Z",
"level": 407.64, "level": 407.64,
"flow": 0, "flow": 0.2,
"inflow": 0.4, "inflow": 0.4,
"volume": 0.14, "volume": 0.14,
"temperature": 23, "temperature": 23,
@@ -8953,5 +8953,14 @@
"volume": 0.14, "volume": 0.14,
"temperature": 22.3, "temperature": 22.3,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 407.64,
"flow": 0,
"inflow": 0.4,
"volume": 0.14,
"temperature": 22.3,
"precipitation": 0
} }
] ]
+1 -1
View File
@@ -8202,7 +8202,7 @@
"timestamp": "2026-06-08T17:00:00.000Z", "timestamp": "2026-06-08T17:00:00.000Z",
"level": 632.76, "level": 632.76,
"flow": 0, "flow": 0,
"inflow": 10.515147444523139, "inflow": 0,
"volume": 0, "volume": 0,
"temperature": 21.7, "temperature": 21.7,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8202,7 +8202,7 @@
"timestamp": "2026-06-08T17:00:00.000Z", "timestamp": "2026-06-08T17:00:00.000Z",
"level": 635.7, "level": 635.7,
"flow": 0, "flow": 0,
"inflow": 5.418107522688602, "inflow": 0,
"volume": 0, "volume": 0,
"temperature": 22, "temperature": 22,
"precipitation": 0 "precipitation": 0
+9
View File
@@ -2245,5 +2245,14 @@
"volume": 0, "volume": 0,
"temperature": 24, "temperature": 24,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 82,
"flow": 2.666,
"inflow": 0,
"volume": 0,
"temperature": 24,
"precipitation": 0
} }
] ]
+1 -1
View File
@@ -8751,7 +8751,7 @@
"timestamp": "2026-06-08T17:20:00.000Z", "timestamp": "2026-06-08T17:20:00.000Z",
"level": 563.66, "level": 563.66,
"flow": 0.02, "flow": 0.02,
"inflow": 0.01, "inflow": 0,
"volume": 0.46, "volume": 0.46,
"temperature": 22.3, "temperature": 22.3,
"precipitation": 0 "precipitation": 0
+9
View File
@@ -2252,5 +2252,14 @@
"volume": 0, "volume": 0,
"temperature": 24.9, "temperature": 24.9,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 43,
"flow": 4.73,
"inflow": 0,
"volume": 0,
"temperature": 24.9,
"precipitation": 0
} }
] ]
+9
View File
@@ -2317,5 +2317,14 @@
"volume": 0, "volume": 0,
"temperature": 23, "temperature": 23,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:10:00.000Z",
"level": 25,
"flow": 2.95,
"inflow": 0,
"volume": 0,
"temperature": 23,
"precipitation": 0
} }
] ]
+1 -1
View File
@@ -8103,7 +8103,7 @@
"timestamp": "2026-06-08T16:20:00.000Z", "timestamp": "2026-06-08T16:20:00.000Z",
"level": 670.52, "level": 670.52,
"flow": 0, "flow": 0,
"inflow": 8.227779864526358, "inflow": 0,
"volume": 0, "volume": 0,
"temperature": 21.8, "temperature": 21.8,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8922,7 +8922,7 @@
"timestamp": "2026-06-08T17:30:00.000Z", "timestamp": "2026-06-08T17:30:00.000Z",
"level": 575.7, "level": 575.7,
"flow": 0.02, "flow": 0.02,
"inflow": 11.662041143485649, "inflow": 0,
"volume": 1.06, "volume": 1.06,
"temperature": 21.4, "temperature": 21.4,
"precipitation": 0 "precipitation": 0
+9
View File
@@ -2270,5 +2270,14 @@
"volume": 0, "volume": 0,
"temperature": 24, "temperature": 24,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:10:00.000Z",
"level": 68,
"flow": 1.452,
"inflow": 0,
"volume": 0,
"temperature": 24,
"precipitation": 0
} }
] ]
+9
View File
@@ -8926,5 +8926,14 @@
"volume": 0.1, "volume": 0.1,
"temperature": 23.6, "temperature": 23.6,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 0,
"flow": 0.06,
"inflow": 0.07,
"volume": 0.1,
"temperature": 23.6,
"precipitation": 0
} }
] ]
+1 -1
View File
@@ -8913,7 +8913,7 @@
"timestamp": "2026-06-08T17:20:00.000Z", "timestamp": "2026-06-08T17:20:00.000Z",
"level": 668.4, "level": 668.4,
"flow": 0, "flow": 0,
"inflow": 10.98258553088203, "inflow": 0,
"volume": 0.39, "volume": 0.39,
"temperature": 23.8, "temperature": 23.8,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8634,7 +8634,7 @@
"timestamp": "2026-06-08T17:20:00.000Z", "timestamp": "2026-06-08T17:20:00.000Z",
"level": 385.01, "level": 385.01,
"flow": 0.02, "flow": 0.02,
"inflow": 6.257864375579982, "inflow": 0,
"volume": 0.84, "volume": 0.84,
"temperature": 22.5, "temperature": 22.5,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8904,7 +8904,7 @@
"timestamp": "2026-06-08T17:20:00.000Z", "timestamp": "2026-06-08T17:20:00.000Z",
"level": 678.6, "level": 678.6,
"flow": 0, "flow": 0,
"inflow": 8.204576504122992, "inflow": 0,
"volume": 0.16, "volume": 0.16,
"temperature": 20.6, "temperature": 20.6,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8958,7 +8958,7 @@
"timestamp": "2026-06-08T17:20:00.000Z", "timestamp": "2026-06-08T17:20:00.000Z",
"level": 588.39, "level": 588.39,
"flow": 0.08, "flow": 0.08,
"inflow": 8.36186446216757, "inflow": 0,
"volume": 0.32, "volume": 0.32,
"temperature": 23, "temperature": 23,
"precipitation": 0 "precipitation": 0
+1 -1
View File
@@ -8913,7 +8913,7 @@
"timestamp": "2026-06-08T17:30:00.000Z", "timestamp": "2026-06-08T17:30:00.000Z",
"level": 0, "level": 0,
"flow": 0.05, "flow": 0.05,
"inflow": 8.055084527934373, "inflow": 0,
"volume": 0.41, "volume": 0.41,
"temperature": 21.4, "temperature": 21.4,
"precipitation": 0 "precipitation": 0
+9
View File
@@ -2243,5 +2243,14 @@
"volume": 0, "volume": 0,
"temperature": 26.1, "temperature": 26.1,
"precipitation": 0 "precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 46,
"flow": 53.88,
"inflow": 0,
"volume": 0,
"temperature": 26.1,
"precipitation": 0
} }
] ]
+1 -1
View File
@@ -9219,7 +9219,7 @@
"timestamp": "2026-06-08T17:20:00.000Z", "timestamp": "2026-06-08T17:20:00.000Z",
"level": 369.61, "level": 369.61,
"flow": 15.18, "flow": 15.18,
"inflow": 11.42, "inflow": 0,
"volume": 19.77, "volume": 19.77,
"temperature": 24.4, "temperature": 24.4,
"precipitation": 0 "precipitation": 0
+34 -34
View File
@@ -69,7 +69,7 @@
"level": "369.61", "level": "369.61",
"capacity": 93.7, "capacity": 93.7,
"storageDiff": -0.49, "storageDiff": -0.49,
"inflow": "11.4", "inflow": "0.0",
"outflow": "15.2", "outflow": "15.2",
"volume": 19.77, "volume": 19.77,
"maxVolume": 21.1, "maxVolume": 21.1,
@@ -658,7 +658,7 @@
"level": "670.52", "level": "670.52",
"capacity": 80.2, "capacity": 80.2,
"storageDiff": -0.88, "storageDiff": -0.88,
"inflow": "8.2", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 1.3, "volume": 1.3,
"maxVolume": 1.6, "maxVolume": 1.6,
@@ -751,7 +751,7 @@
"level": "588.39", "level": "588.39",
"capacity": 32, "capacity": 32,
"storageDiff": -0.21, "storageDiff": -0.21,
"inflow": "8.4", "inflow": "0.0",
"outflow": "0.1", "outflow": "0.1",
"volume": 0.32, "volume": 0.32,
"maxVolume": 1, "maxVolume": 1,
@@ -813,7 +813,7 @@
"level": "580.99", "level": "580.99",
"capacity": 2.1, "capacity": 2.1,
"storageDiff": -1.22, "storageDiff": -1.22,
"inflow": "9.6", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.03, "volume": 0.03,
"maxVolume": 1.4, "maxVolume": 1.4,
@@ -841,9 +841,9 @@
"name": "Humenice", "name": "Humenice",
"river": "", "river": "",
"priority": false, "priority": false,
"level": "534.70", "level": "0.00",
"capacity": 12.5, "capacity": 12.5,
"storageDiff": -1.3, "storageDiff": 0,
"inflow": "0.1", "inflow": "0.1",
"outflow": "0.1", "outflow": "0.1",
"volume": 0.1, "volume": 0.1,
@@ -852,7 +852,6 @@
"lat": 48.784, "lat": 48.784,
"lng": 14.735, "lng": 14.735,
"sparkline": [ "sparkline": [
534.69,
534.69, 534.69,
0, 0,
534.69, 534.69,
@@ -863,7 +862,8 @@
0, 0,
534.69, 534.69,
534.69, 534.69,
534.7 534.7,
0
], ],
"type": "lake" "type": "lake"
}, },
@@ -875,7 +875,7 @@
"level": "0.00", "level": "0.00",
"capacity": 34.2, "capacity": 34.2,
"storageDiff": 0, "storageDiff": 0,
"inflow": "8.1", "inflow": "0.0",
"outflow": "0.1", "outflow": "0.1",
"volume": 0.41, "volume": 0.41,
"maxVolume": 1.2, "maxVolume": 1.2,
@@ -906,7 +906,7 @@
"level": "575.70", "level": "575.70",
"capacity": 70.7, "capacity": 70.7,
"storageDiff": -0.9, "storageDiff": -0.9,
"inflow": "11.7", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 1.06, "volume": 1.06,
"maxVolume": 1.5, "maxVolume": 1.5,
@@ -968,7 +968,7 @@
"level": "448.81", "level": "448.81",
"capacity": 100, "capacity": 100,
"storageDiff": 0.02, "storageDiff": 0.02,
"inflow": "4.7", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.67, "volume": 0.67,
"maxVolume": 0.5, "maxVolume": 0.5,
@@ -999,7 +999,7 @@
"level": "463.42", "level": "463.42",
"capacity": 4.6, "capacity": 4.6,
"storageDiff": -0.61, "storageDiff": -0.61,
"inflow": "6.0", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0, "volume": 0,
"maxVolume": 0.3, "maxVolume": 0.3,
@@ -1030,7 +1030,7 @@
"level": "668.40", "level": "668.40",
"capacity": 100, "capacity": 100,
"storageDiff": 0, "storageDiff": 0,
"inflow": "11.0", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.39, "volume": 0.39,
"maxVolume": 0.3, "maxVolume": 0.3,
@@ -1061,7 +1061,7 @@
"level": "385.01", "level": "385.01",
"capacity": 100, "capacity": 100,
"storageDiff": 0.01, "storageDiff": 0.01,
"inflow": "6.3", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.84, "volume": 0.84,
"maxVolume": 0.4, "maxVolume": 0.4,
@@ -1092,7 +1092,7 @@
"level": "678.60", "level": "678.60",
"capacity": 80, "capacity": 80,
"storageDiff": 0, "storageDiff": 0,
"inflow": "8.2", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.16, "volume": 0.16,
"maxVolume": 0.2, "maxVolume": 0.2,
@@ -1123,7 +1123,7 @@
"level": "632.76", "level": "632.76",
"capacity": 4.4, "capacity": 4.4,
"storageDiff": -0.13, "storageDiff": -0.13,
"inflow": "10.5", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0, "volume": 0,
"maxVolume": 0.5, "maxVolume": 0.5,
@@ -1154,7 +1154,7 @@
"level": "635.70", "level": "635.70",
"capacity": 0, "capacity": 0,
"storageDiff": -0.66, "storageDiff": -0.66,
"inflow": "5.4", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0, "volume": 0,
"maxVolume": 0.7, "maxVolume": 0.7,
@@ -1185,7 +1185,7 @@
"level": "416.72", "level": "416.72",
"capacity": 90, "capacity": 90,
"storageDiff": 0.04, "storageDiff": 0.04,
"inflow": "7.8", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.09, "volume": 0.09,
"maxVolume": 0.1, "maxVolume": 0.1,
@@ -1213,18 +1213,17 @@
"name": "Praha - Malá Chuchle", "name": "Praha - Malá Chuchle",
"river": "Vltava", "river": "Vltava",
"priority": false, "priority": false,
"level": "49.00", "level": "46.00",
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "60.9", "outflow": "53.9",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 50.0294, "lat": 50.0294,
"lng": 14.3986, "lng": 14.3986,
"sparkline": [ "sparkline": [
45,
46, 46,
46, 46,
47, 47,
@@ -1235,7 +1234,8 @@
47, 47,
47, 47,
46, 46,
49 49,
46
], ],
"type": "river" "type": "river"
}, },
@@ -1279,14 +1279,13 @@
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "8.6", "outflow": "8.7",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.9642, "lat": 49.9642,
"lng": 14.0792, "lng": 14.0792,
"sparkline": [ "sparkline": [
100,
100, 100,
99, 99,
99, 99,
@@ -1297,6 +1296,7 @@
96, 96,
96, 96,
96, 96,
96,
96 96
], ],
"type": "river" "type": "river"
@@ -1341,14 +1341,13 @@
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "4.8", "outflow": "4.7",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.3083, "lat": 49.3083,
"lng": 14.1436, "lng": 14.1436,
"sparkline": [ "sparkline": [
41,
44, 44,
45, 45,
45, 45,
@@ -1359,6 +1358,7 @@
45, 45,
45, 45,
44, 44,
43,
43 43
], ],
"type": "river" "type": "river"
@@ -1383,9 +1383,9 @@
25, 25,
25, 25,
25, 25,
26,
26,
25, 25,
26,
26,
25, 25,
25, 25,
25, 25,
@@ -1399,18 +1399,17 @@
"name": "Bechyně", "name": "Bechyně",
"river": "Lužnice", "river": "Lužnice",
"priority": false, "priority": false,
"level": "81.00", "level": "82.00",
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "2.6", "outflow": "2.7",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.2931, "lat": 49.2931,
"lng": 14.4758, "lng": 14.4758,
"sparkline": [ "sparkline": [
82,
80, 80,
79, 79,
82, 82,
@@ -1421,7 +1420,8 @@
85, 85,
84, 84,
83, 83,
81 81,
82
], ],
"type": "river" "type": "river"
}, },
@@ -1589,7 +1589,7 @@
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "4.6", "outflow": "4.7",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
@@ -1603,10 +1603,10 @@
87, 87,
87, 87,
87, 87,
87,
88, 88,
89, 89,
89, 89,
90,
90 90
], ],
"type": "river" "type": "river"
+74 -7
View File
@@ -121,12 +121,34 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
.catch(err => console.error(err)); .catch(err => console.error(err));
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1'; const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
const staticConfig = lakesConfig.find(l => l.id.split('|')[0] === internalId);
fetch(`/data/${internalId}.json?t=${Date.now()}`) fetch(`/data/${internalId}.json?t=${Date.now()}`)
.then(res => res.json()) .then(res => res.json())
.then(json => { .then(json => {
let lastValidLevel: number | null = null;
const formattedData = json.map((item: any) => { const formattedData = json.map((item: any) => {
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow; const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
let level = item.level === null || isNaN(item.level) ? 0 : item.level;
// Outlier/sensor glitch detection
if (level > 0) {
if (staticConfig && staticConfig.minLevel && staticConfig.maxLevel) {
const minAllowed = staticConfig.minLevel - 5;
const maxAllowed = staticConfig.maxLevel + 5;
if (level < minAllowed || level > maxAllowed) {
// Glitch detected, fallback to last known valid level
level = lastValidLevel !== null ? lastValidLevel : (staticConfig.minLevel + staticConfig.maxLevel) / 2;
} else {
lastValidLevel = level;
}
} else {
lastValidLevel = level;
}
} else if (lastValidLevel !== null) {
// Level is 0 or less, fallback
level = lastValidLevel;
}
return { return {
timestamp: item.timestamp, timestamp: item.timestamp,
@@ -134,7 +156,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
day: '2-digit', month: '2-digit', year: 'numeric', day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit' hour: '2-digit', minute: '2-digit'
}), }),
level: item.level === null || isNaN(item.level) ? 0 : item.level, level: level,
outflow: outflow, outflow: outflow,
inflow: item.inflow || 0, inflow: item.inflow || 0,
volume: item.volume || 0, volume: item.volume || 0,
@@ -393,13 +415,58 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
{/* Data Series */} {/* Data Series */}
{limits && limits.map((limit, idx) => ( {limits && limits.map((limit, idx) => (
<ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `${limit.labelCs} (${limit.level.toFixed(2)} m n. m.)` : `${limit.labelEn} (${limit.level.toFixed(2)} m a.s.l.)`, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 12 }} /> <ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `${limit.labelCs} (${limit.level.toFixed(2)} m n. m.)` : `${limit.labelEn} (${limit.level.toFixed(2)} m a.s.l.)`, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 11 }} />
))} ))}
{!isRiver && staticConfig?.maxLevel && ( {!isRiver && staticConfig?.maxLevel && staticConfig?.storageLevel && Math.abs(staticConfig.maxLevel - staticConfig.storageLevel) < 0.05 ? (
<ReferenceLine yAxisId="left" y={staticConfig.maxLevel} stroke="var(--color-orange)" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Maximální retenční hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)` : `Max retention level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`, fill: 'var(--color-orange)', fontSize: 12 }} /> <ReferenceLine
)} yAxisId="left"
{!isRiver && staticConfig?.storageLevel && ( y={staticConfig.maxLevel}
<ReferenceLine yAxisId="left" y={staticConfig.storageLevel} stroke="#a855f7" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Hladina zásobního prostoru (${staticConfig.storageLevel.toFixed(2)} m n. m.)` : `Storage space level (${staticConfig.storageLevel.toFixed(2)} m a.s.l.)`, fill: '#a855f7', fontSize: 12 }} /> stroke="var(--color-orange)"
strokeDasharray="3 3"
label={{
position: 'insideBottomRight',
value: language === 'cs'
? `Max. retenční / zásobní hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)`
: `Max retention / storage level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`,
fill: 'var(--color-orange)',
fontSize: 11
}}
/>
) : (
<>
{!isRiver && staticConfig?.maxLevel && (
<ReferenceLine
yAxisId="left"
y={staticConfig.maxLevel}
stroke="var(--color-orange)"
strokeDasharray="3 3"
label={{
position: 'insideBottomRight',
value: language === 'cs'
? `Maximální retenční hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)`
: `Max retention level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`,
fill: 'var(--color-orange)',
fontSize: 11
}}
/>
)}
{!isRiver && staticConfig?.storageLevel && (
<ReferenceLine
yAxisId="left"
y={staticConfig.storageLevel}
stroke="#a855f7"
strokeDasharray="3 3"
label={{
position: 'insideBottomLeft',
value: language === 'cs'
? `Hladina zásobního prostoru (${staticConfig.storageLevel.toFixed(2)} m n. m.)`
: `Storage space level (${staticConfig.storageLevel.toFixed(2)} m a.s.l.)`,
fill: '#a855f7',
fontSize: 11
}}
/>
)}
</>
)} )}
<Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" isAnimationActive={animate} /> <Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" isAnimationActive={animate} />
<Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-red)" strokeWidth={2} dot={false} isAnimationActive={animate} /> <Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-red)" strokeWidth={2} dot={false} isAnimationActive={animate} />