Compare commits
10 Commits
57e9bf12ca
...
a67a2247c3
| Author | SHA1 | Date | |
|---|---|---|---|
| a67a2247c3 | |||
| cf05e844d8 | |||
| 6395df1992 | |||
| 66021e001e | |||
| db1aadcc8d | |||
| dbb22e7972 | |||
| 6d77c20c84 | |||
| a3b3d40769 | |||
| 27551f9183 | |||
| b660f0f6c3 |
@@ -0,0 +1,63 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import fs from 'fs';
|
||||
import https from 'https';
|
||||
|
||||
async function compare() {
|
||||
const URL = 'https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?oid=1&id=VLL1';
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const response = await axios.get(URL, { httpsAgent: agent });
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
let tblFound = null;
|
||||
$('table').each((i, tbl) => {
|
||||
if ($(tbl).text().includes('Datum') && $(tbl).text().includes('Odtok')) {
|
||||
tblFound = $(tbl);
|
||||
}
|
||||
});
|
||||
|
||||
const pvlRows = [];
|
||||
if (tblFound) {
|
||||
tblFound.find('tr').each((i, row) => {
|
||||
if (i === 0) return;
|
||||
const cols = $(row).find('td');
|
||||
if (cols.length >= 3) {
|
||||
const rawDate = $(cols[0]).text().trim();
|
||||
const levelStr = $(cols[1]).text().trim().replace(',', '.');
|
||||
let flowStr = $(cols[2]).text().trim().replace(',', '.');
|
||||
if (flowStr === '' && cols.length >= 4) {
|
||||
flowStr = $(cols[3]).text().trim().replace(',', '.');
|
||||
}
|
||||
pvlRows.push({
|
||||
date: rawDate,
|
||||
level: parseFloat(levelStr),
|
||||
flow: parseFloat(flowStr)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const localData = JSON.parse(fs.readFileSync('public/data/VLL1.json', 'utf-8'));
|
||||
// Sort local data descending (newest first) to match PVL which is newest first
|
||||
const sortedLocal = localData.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
console.log('--- POROVNÁNÍ DAT: LIPNO 1 ---');
|
||||
console.log(String('PVL.CZ').padEnd(40) + ' | ' + 'NAŠE LOKÁLNÍ DATABÁZE');
|
||||
console.log('-'.repeat(85));
|
||||
|
||||
for (let i = 0; i < Math.min(10, pvlRows.length); i++) {
|
||||
const p = pvlRows[i];
|
||||
const l = sortedLocal[i];
|
||||
|
||||
// Format our local UTC timestamp back to something readable
|
||||
const d = new Date(l.timestamp);
|
||||
const localDateStr = `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth()+1).toString().padStart(2, '0')}.${d.getFullYear()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
||||
|
||||
const pvlStr = `[${p.date}] H: ${p.level} m, O: ${p.flow} m3/s`.padEnd(40);
|
||||
const locStr = `[${localDateStr}] H: ${l.level} m, O: ${l.flow} m3/s, P: ${l.inflow} m3/s`;
|
||||
|
||||
console.log(`${pvlStr} | ${locStr}`);
|
||||
}
|
||||
}
|
||||
|
||||
compare().catch(console.error);
|
||||
@@ -1,224 +0,0 @@
|
||||
body, html {
|
||||
margin:0; padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, Arial;
|
||||
font-size: 14px;
|
||||
color:#333;
|
||||
}
|
||||
.small { font-size: 12px; }
|
||||
*, *:after, *:before {
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing:border-box;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
h1 { font-size: 20px; margin: 0;}
|
||||
h2 { font-size: 14px; }
|
||||
pre {
|
||||
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
}
|
||||
a { color:#0074D9; text-decoration:none; }
|
||||
a:hover { text-decoration:underline; }
|
||||
.strong { font-weight: bold; }
|
||||
.space-top1 { padding: 10px 0 0 0; }
|
||||
.pad2y { padding: 20px 0; }
|
||||
.pad1y { padding: 10px 0; }
|
||||
.pad2x { padding: 0 20px; }
|
||||
.pad2 { padding: 20px; }
|
||||
.pad1 { padding: 10px; }
|
||||
.space-left2 { padding-left:55px; }
|
||||
.space-right2 { padding-right:20px; }
|
||||
.center { text-align:center; }
|
||||
.clearfix { display:block; }
|
||||
.clearfix:after {
|
||||
content:'';
|
||||
display:block;
|
||||
height:0;
|
||||
clear:both;
|
||||
visibility:hidden;
|
||||
}
|
||||
.fl { float: left; }
|
||||
@media only screen and (max-width:640px) {
|
||||
.col3 { width:100%; max-width:100%; }
|
||||
.hide-mobile { display:none!important; }
|
||||
}
|
||||
|
||||
.quiet {
|
||||
color: #7f7f7f;
|
||||
color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.quiet a { opacity: 0.7; }
|
||||
|
||||
.fraction {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
background: #E8E8E8;
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.path a:link, div.path a:visited { color: #333; }
|
||||
table.coverage {
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.coverage td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.coverage td.line-count {
|
||||
text-align: right;
|
||||
padding: 0 5px 0 20px;
|
||||
}
|
||||
table.coverage td.line-coverage {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
min-width:20px;
|
||||
}
|
||||
|
||||
table.coverage td span.cline-any {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
}
|
||||
.missing-if-branch {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #333;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.skip-if-branch {
|
||||
display: none;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #ccc;
|
||||
color: white;
|
||||
}
|
||||
.missing-if-branch .typ, .skip-if-branch .typ {
|
||||
color: inherit !important;
|
||||
}
|
||||
.coverage-summary {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
||||
.keyline-all { border: 1px solid #ddd; }
|
||||
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
||||
.coverage-summary tbody { border: 1px solid #bbb; }
|
||||
.coverage-summary td { border-right: 1px solid #bbb; }
|
||||
.coverage-summary td:last-child { border-right: none; }
|
||||
.coverage-summary th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.coverage-summary th.file { border-right: none !important; }
|
||||
.coverage-summary th.pct { }
|
||||
.coverage-summary th.pic,
|
||||
.coverage-summary th.abs,
|
||||
.coverage-summary td.pct,
|
||||
.coverage-summary td.abs { text-align: right; }
|
||||
.coverage-summary td.file { white-space: nowrap; }
|
||||
.coverage-summary td.pic { min-width: 120px !important; }
|
||||
.coverage-summary tfoot td { }
|
||||
|
||||
.coverage-summary .sorter {
|
||||
height: 10px;
|
||||
width: 7px;
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
||||
}
|
||||
.coverage-summary .sorted .sorter {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
.coverage-summary .sorted-desc .sorter {
|
||||
background-position: 0 -10px;
|
||||
}
|
||||
.status-line { height: 10px; }
|
||||
/* yellow */
|
||||
.cbranch-no { background: yellow !important; color: #111; }
|
||||
/* dark red */
|
||||
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
||||
.low .chart { border:1px solid #C21F39 }
|
||||
.highlighted,
|
||||
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
||||
background: #C21F39 !important;
|
||||
}
|
||||
/* medium red */
|
||||
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
||||
/* light red */
|
||||
.low, .cline-no { background:#FCE1E5 }
|
||||
/* light green */
|
||||
.high, .cline-yes { background:rgb(230,245,208) }
|
||||
/* medium green */
|
||||
.cstat-yes { background:rgb(161,215,106) }
|
||||
/* dark green */
|
||||
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
||||
.high .chart { border:1px solid rgb(77,146,33) }
|
||||
/* dark yellow (gold) */
|
||||
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
||||
.medium .chart { border:1px solid #f9cd0b; }
|
||||
/* light yellow */
|
||||
.medium { background: #fff4c2; }
|
||||
|
||||
.cstat-skip { background: #ddd; color: #111; }
|
||||
.fstat-skip { background: #ddd; color: #111 !important; }
|
||||
.cbranch-skip { background: #ddd !important; color: #111; }
|
||||
|
||||
span.cline-neutral { background: #eaeaea; }
|
||||
|
||||
.coverage-summary td.empty {
|
||||
opacity: .5;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
line-height: 1;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cover-fill, .cover-empty {
|
||||
display:inline-block;
|
||||
height: 12px;
|
||||
}
|
||||
.chart {
|
||||
line-height: 0;
|
||||
}
|
||||
.cover-empty {
|
||||
background: white;
|
||||
}
|
||||
.cover-full {
|
||||
border-right: none !important;
|
||||
}
|
||||
pre.prettyprint {
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.com { color: #999 !important; }
|
||||
.ignore-none { color: #999; font-weight: normal; }
|
||||
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -48px;
|
||||
}
|
||||
.footer, .push {
|
||||
height: 48px;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/* eslint-disable */
|
||||
var jumpToCode = (function init() {
|
||||
// Classes of code we would like to highlight in the file view
|
||||
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
||||
|
||||
// Elements to highlight in the file listing view
|
||||
var fileListingElements = ['td.pct.low'];
|
||||
|
||||
// We don't want to select elements that are direct descendants of another match
|
||||
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
||||
|
||||
// Selector that finds elements on the page to which we can jump
|
||||
var selector =
|
||||
fileListingElements.join(', ') +
|
||||
', ' +
|
||||
notSelector +
|
||||
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
||||
|
||||
// The NodeList of matching elements
|
||||
var missingCoverageElements = document.querySelectorAll(selector);
|
||||
|
||||
var currentIndex;
|
||||
|
||||
function toggleClass(index) {
|
||||
missingCoverageElements
|
||||
.item(currentIndex)
|
||||
.classList.remove('highlighted');
|
||||
missingCoverageElements.item(index).classList.add('highlighted');
|
||||
}
|
||||
|
||||
function makeCurrent(index) {
|
||||
toggleClass(index);
|
||||
currentIndex = index;
|
||||
missingCoverageElements.item(index).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
|
||||
function goToPrevious() {
|
||||
var nextIndex = 0;
|
||||
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
||||
nextIndex = missingCoverageElements.length - 1;
|
||||
} else if (missingCoverageElements.length > 1) {
|
||||
nextIndex = currentIndex - 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
function goToNext() {
|
||||
var nextIndex = 0;
|
||||
|
||||
if (
|
||||
typeof currentIndex === 'number' &&
|
||||
currentIndex < missingCoverageElements.length - 1
|
||||
) {
|
||||
nextIndex = currentIndex + 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
return function jump(event) {
|
||||
if (
|
||||
document.getElementById('fileSearch') === document.activeElement &&
|
||||
document.activeElement != null
|
||||
) {
|
||||
// if we're currently focused on the search input, we don't want to navigate
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.which) {
|
||||
case 78: // n
|
||||
case 74: // j
|
||||
goToNext();
|
||||
break;
|
||||
case 66: // b
|
||||
case 75: // k
|
||||
case 80: // p
|
||||
goToPrevious();
|
||||
break;
|
||||
}
|
||||
};
|
||||
})();
|
||||
window.addEventListener('keydown', jumpToCode);
|
||||
@@ -1,133 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<coverage generated="1780693430332" clover="3.2.0">
|
||||
<project timestamp="1780693430332" name="All files">
|
||||
<metrics statements="106" coveredstatements="32" conditionals="98" coveredconditionals="29" methods="18" coveredmethods="5" elements="222" coveredelements="66" complexity="0" loc="106" ncloc="106" packages="3" files="4" classes="4"/>
|
||||
<package name="scripts">
|
||||
<metrics statements="93" coveredstatements="24" conditionals="75" coveredconditionals="14" methods="12" coveredmethods="3"/>
|
||||
<file name="lakesConfig.ts" path="/Users/davis/WebstormProjects/davisfe.cz/scripts/lakesConfig.ts">
|
||||
<metrics statements="1" coveredstatements="1" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
|
||||
<line num="12" count="1" type="stmt"/>
|
||||
</file>
|
||||
<file name="scrapeLakes.ts" path="/Users/davis/WebstormProjects/davisfe.cz/scripts/scrapeLakes.ts">
|
||||
<metrics statements="92" coveredstatements="23" conditionals="75" coveredconditionals="14" methods="12" coveredmethods="3"/>
|
||||
<line num="20" count="7" type="stmt"/>
|
||||
<line num="21" count="7" type="cond" truecount="4" falsecount="0"/>
|
||||
<line num="22" count="4" type="stmt"/>
|
||||
<line num="23" count="4" type="stmt"/>
|
||||
<line num="24" count="4" type="stmt"/>
|
||||
<line num="26" count="4" type="cond" truecount="4" falsecount="0"/>
|
||||
<line num="28" count="2" type="stmt"/>
|
||||
<line num="29" count="2" type="stmt"/>
|
||||
<line num="30" count="2" type="stmt"/>
|
||||
<line num="31" count="2" type="stmt"/>
|
||||
<line num="32" count="2" type="cond" truecount="1" falsecount="1"/>
|
||||
<line num="33" count="2" type="cond" truecount="5" falsecount="0"/>
|
||||
<line num="34" count="1" type="stmt"/>
|
||||
<line num="36" count="0" type="stmt"/>
|
||||
<line num="41" count="1" type="stmt"/>
|
||||
<line num="42" count="1" type="stmt"/>
|
||||
<line num="44" count="1" type="stmt"/>
|
||||
<line num="45" count="1" type="stmt"/>
|
||||
<line num="46" count="1" type="stmt"/>
|
||||
<line num="53" count="0" type="stmt"/>
|
||||
<line num="55" count="0" type="stmt"/>
|
||||
<line num="56" count="0" type="stmt"/>
|
||||
<line num="57" count="0" type="stmt"/>
|
||||
<line num="58" count="0" type="stmt"/>
|
||||
<line num="60" count="0" type="stmt"/>
|
||||
<line num="61" count="0" type="stmt"/>
|
||||
<line num="62" count="0" type="cond" truecount="0" falsecount="4"/>
|
||||
<line num="63" count="0" type="stmt"/>
|
||||
<line num="64" count="0" type="stmt"/>
|
||||
<line num="65" count="0" type="stmt"/>
|
||||
<line num="66" count="0" type="cond" truecount="0" falsecount="4"/>
|
||||
<line num="67" count="0" type="cond" truecount="0" falsecount="4"/>
|
||||
<line num="68" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="69" count="0" type="stmt"/>
|
||||
<line num="70" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="72" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="73" count="0" type="stmt"/>
|
||||
<line num="74" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="80" count="0" type="stmt"/>
|
||||
<line num="81" count="0" type="stmt"/>
|
||||
<line num="82" count="0" type="stmt"/>
|
||||
<line num="83" count="0" type="cond" truecount="0" falsecount="4"/>
|
||||
<line num="84" count="0" type="stmt"/>
|
||||
<line num="88" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="89" count="0" type="stmt"/>
|
||||
<line num="90" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="91" count="0" type="stmt"/>
|
||||
<line num="92" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="93" count="0" type="stmt"/>
|
||||
<line num="94" count="0" type="stmt"/>
|
||||
<line num="95" count="0" type="stmt"/>
|
||||
<line num="96" count="0" type="cond" truecount="0" falsecount="4"/>
|
||||
<line num="97" count="0" type="stmt"/>
|
||||
<line num="100" count="0" type="stmt"/>
|
||||
<line num="101" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="102" count="0" type="stmt"/>
|
||||
<line num="114" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="116" count="0" type="stmt"/>
|
||||
<line num="117" count="0" type="stmt"/>
|
||||
<line num="118" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="119" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="122" count="0" type="stmt"/>
|
||||
<line num="123" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="124" count="0" type="stmt"/>
|
||||
<line num="125" count="0" type="stmt"/>
|
||||
<line num="128" count="0" type="stmt"/>
|
||||
<line num="129" count="0" type="stmt"/>
|
||||
<line num="130" count="0" type="stmt"/>
|
||||
<line num="132" count="0" type="stmt"/>
|
||||
<line num="133" count="0" type="stmt"/>
|
||||
<line num="137" count="0" type="stmt"/>
|
||||
<line num="138" count="0" type="stmt"/>
|
||||
<line num="139" count="0" type="stmt"/>
|
||||
<line num="140" count="0" type="cond" truecount="0" falsecount="4"/>
|
||||
<line num="141" count="0" type="stmt"/>
|
||||
<line num="142" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="143" count="0" type="stmt"/>
|
||||
<line num="146" count="0" type="cond" truecount="0" falsecount="4"/>
|
||||
<line num="147" count="0" type="stmt"/>
|
||||
<line num="148" count="0" type="cond" truecount="0" falsecount="2"/>
|
||||
<line num="149" count="0" type="stmt"/>
|
||||
<line num="153" count="0" type="stmt"/>
|
||||
<line num="154" count="0" type="stmt"/>
|
||||
<line num="156" count="0" type="stmt"/>
|
||||
<line num="159" count="0" type="stmt"/>
|
||||
<line num="164" count="1" type="stmt"/>
|
||||
<line num="166" count="1" type="stmt"/>
|
||||
<line num="168" count="1" type="stmt"/>
|
||||
<line num="169" count="1" type="stmt"/>
|
||||
<line num="171" count="0" type="stmt"/>
|
||||
<line num="174" count="0" type="stmt"/>
|
||||
<line num="177" count="1" type="stmt"/>
|
||||
</file>
|
||||
</package>
|
||||
<package name="src">
|
||||
<metrics statements="1" coveredstatements="1" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
|
||||
<file name="translations.ts" path="/Users/davis/WebstormProjects/davisfe.cz/src/translations.ts">
|
||||
<metrics statements="1" coveredstatements="1" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
|
||||
<line num="3" count="2" type="stmt"/>
|
||||
</file>
|
||||
</package>
|
||||
<package name="src.components">
|
||||
<metrics statements="12" coveredstatements="7" conditionals="23" coveredconditionals="15" methods="6" coveredmethods="2"/>
|
||||
<file name="KpiCards.tsx" path="/Users/davis/WebstormProjects/davisfe.cz/src/components/KpiCards.tsx">
|
||||
<metrics statements="12" coveredstatements="7" conditionals="23" coveredconditionals="15" methods="6" coveredmethods="2"/>
|
||||
<line num="21" count="1" type="cond" truecount="1" falsecount="0"/>
|
||||
<line num="22" count="3" type="stmt"/>
|
||||
<line num="23" count="3" type="stmt"/>
|
||||
<line num="24" count="3" type="stmt"/>
|
||||
<line num="26" count="3" type="stmt"/>
|
||||
<line num="27" count="3" type="cond" truecount="1" falsecount="1"/>
|
||||
<line num="28" count="0" type="stmt"/>
|
||||
<line num="29" count="0" type="stmt"/>
|
||||
<line num="31" count="0" type="stmt"/>
|
||||
<line num="35" count="3" type="stmt"/>
|
||||
<line num="86" count="0" type="stmt"/>
|
||||
<line num="93" count="0" type="stmt"/>
|
||||
</file>
|
||||
</package>
|
||||
</project>
|
||||
</coverage>
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 445 B |
@@ -1,146 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for All files</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="prettify.css" />
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1>All files</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">28.92% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>35/121</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">29.59% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>29/98</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">27.77% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>5/18</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">30.18% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>32/106</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line low'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td class="file low" data-value="scripts"><a href="scripts/index.html">scripts</a></td>
|
||||
<td data-value="25.23" class="pic low">
|
||||
<div class="chart"><div class="cover-fill" style="width: 25%"></div><div class="cover-empty" style="width: 75%"></div></div>
|
||||
</td>
|
||||
<td data-value="25.23" class="pct low">25.23%</td>
|
||||
<td data-value="107" class="abs low">27/107</td>
|
||||
<td data-value="18.66" class="pct low">18.66%</td>
|
||||
<td data-value="75" class="abs low">14/75</td>
|
||||
<td data-value="25" class="pct low">25%</td>
|
||||
<td data-value="12" class="abs low">3/12</td>
|
||||
<td data-value="25.8" class="pct low">25.8%</td>
|
||||
<td data-value="93" class="abs low">24/93</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="file high" data-value="src"><a href="src/index.html">src</a></td>
|
||||
<td data-value="100" class="pic high">
|
||||
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||
</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="1" class="abs high">1/1</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="0" class="abs high">0/0</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="0" class="abs high">0/0</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="1" class="abs high">1/1</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="file medium" data-value="src/components"><a href="src/components/index.html">src/components</a></td>
|
||||
<td data-value="53.84" class="pic medium">
|
||||
<div class="chart"><div class="cover-fill" style="width: 53%"></div><div class="cover-empty" style="width: 47%"></div></div>
|
||||
</td>
|
||||
<td data-value="53.84" class="pct medium">53.84%</td>
|
||||
<td data-value="13" class="abs medium">7/13</td>
|
||||
<td data-value="65.21" class="pct medium">65.21%</td>
|
||||
<td data-value="23" class="abs medium">15/23</td>
|
||||
<td data-value="33.33" class="pct low">33.33%</td>
|
||||
<td data-value="6" class="abs low">2/6</td>
|
||||
<td data-value="58.33" class="pct medium">58.33%</td>
|
||||
<td data-value="12" class="abs medium">7/12</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="sorter.js"></script>
|
||||
<script src="block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,131 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for scripts</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../prettify.css" />
|
||||
<link rel="stylesheet" href="../base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(../sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="../index.html">All files</a> scripts</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">25.23% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>27/107</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">18.66% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>14/75</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">25% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>3/12</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">25.8% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>24/93</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line low'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td class="file high" data-value="lakesConfig.ts"><a href="lakesConfig.ts.html">lakesConfig.ts</a></td>
|
||||
<td data-value="100" class="pic high">
|
||||
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||
</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="1" class="abs high">1/1</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="0" class="abs high">0/0</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="0" class="abs high">0/0</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="1" class="abs high">1/1</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="file low" data-value="scrapeLakes.ts"><a href="scrapeLakes.ts.html">scrapeLakes.ts</a></td>
|
||||
<td data-value="24.52" class="pic low">
|
||||
<div class="chart"><div class="cover-fill" style="width: 24%"></div><div class="cover-empty" style="width: 76%"></div></div>
|
||||
</td>
|
||||
<td data-value="24.52" class="pct low">24.52%</td>
|
||||
<td data-value="106" class="abs low">26/106</td>
|
||||
<td data-value="18.66" class="pct low">18.66%</td>
|
||||
<td data-value="75" class="abs low">14/75</td>
|
||||
<td data-value="25" class="pct low">25%</td>
|
||||
<td data-value="12" class="abs low">3/12</td>
|
||||
<td data-value="25" class="pct low">25%</td>
|
||||
<td data-value="92" class="abs low">23/92</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="../prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="../sorter.js"></script>
|
||||
<script src="../block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for scripts/lakesConfig.ts</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../prettify.css" />
|
||||
<link rel="stylesheet" href="../base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(../sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="../index.html">All files</a> / <a href="index.html">scripts</a> lakesConfig.ts</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>1/1</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>1/1</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line high'></div>
|
||||
<pre><table class="coverage">
|
||||
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||
<a name='L2'></a><a href='#L2'>2</a>
|
||||
<a name='L3'></a><a href='#L3'>3</a>
|
||||
<a name='L4'></a><a href='#L4'>4</a>
|
||||
<a name='L5'></a><a href='#L5'>5</a>
|
||||
<a name='L6'></a><a href='#L6'>6</a>
|
||||
<a name='L7'></a><a href='#L7'>7</a>
|
||||
<a name='L8'></a><a href='#L8'>8</a>
|
||||
<a name='L9'></a><a href='#L9'>9</a>
|
||||
<a name='L10'></a><a href='#L10'>10</a>
|
||||
<a name='L11'></a><a href='#L11'>11</a>
|
||||
<a name='L12'></a><a href='#L12'>12</a>
|
||||
<a name='L13'></a><a href='#L13'>13</a>
|
||||
<a name='L14'></a><a href='#L14'>14</a>
|
||||
<a name='L15'></a><a href='#L15'>15</a>
|
||||
<a name='L16'></a><a href='#L16'>16</a>
|
||||
<a name='L17'></a><a href='#L17'>17</a>
|
||||
<a name='L18'></a><a href='#L18'>18</a>
|
||||
<a name='L19'></a><a href='#L19'>19</a>
|
||||
<a name='L20'></a><a href='#L20'>20</a>
|
||||
<a name='L21'></a><a href='#L21'>21</a>
|
||||
<a name='L22'></a><a href='#L22'>22</a>
|
||||
<a name='L23'></a><a href='#L23'>23</a>
|
||||
<a name='L24'></a><a href='#L24'>24</a>
|
||||
<a name='L25'></a><a href='#L25'>25</a>
|
||||
<a name='L26'></a><a href='#L26'>26</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">export interface LakeConfig {
|
||||
id: string;
|
||||
text: string;
|
||||
priority?: boolean;
|
||||
coords: [number, number];
|
||||
maxVolume?: number;
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
storageLevel?: number;
|
||||
}
|
||||
|
||||
export const lakesConfig: LakeConfig[] = [
|
||||
{ id: "VLL1|1", text: "VD Lipno 1 - Vltava", priority: true, coords: [48.6322, 14.2215], maxVolume: 306.0, minLevel: 715.00, maxLevel: 725.60, storageLevel: 724.9 },
|
||||
{ id: "VLL2|1", text: "VD Lipno II - Vltava", coords: [48.6250, 14.3180], maxVolume: 1.5, minLevel: 510.0, maxLevel: 511.5, storageLevel: 511.5 },
|
||||
{ id: "VLHN|1", text: "VD Hněvkovice - Vltava", priority: true, coords: [49.1830, 14.4440], maxVolume: 21.1, minLevel: 365.0, maxLevel: 370.5, storageLevel: 370.1 },
|
||||
{ id: "VLKO|1", text: "VD Kořensko - Vltava", coords: [49.2550, 14.3980], maxVolume: 2.8, minLevel: 352.0, maxLevel: 353.5, storageLevel: 352.6 },
|
||||
{ id: "VLOR|2", text: "VD Orlík - Vltava", priority: true, coords: [49.6060, 14.1700], maxVolume: 716.5, minLevel: 330.00, maxLevel: 354.00, storageLevel: 349.9 },
|
||||
{ id: "UHKA|2", text: "VD Kamýk - Vltava", coords: [49.6360, 14.2540], maxVolume: 12.8, minLevel: 283.0, maxLevel: 285.6, storageLevel: 285.6 },
|
||||
{ id: "VLSL|2", text: "VD Slapy - Vltava", priority: true, coords: [49.8220, 14.4360], maxVolume: 269.3, minLevel: 265.50, maxLevel: 271.10, storageLevel: 270.6 },
|
||||
{ id: "VLST|2", text: "VD Štěchovice - Vltava", coords: [49.8450, 14.4120], maxVolume: 11.2, minLevel: 217.0, maxLevel: 219.5, storageLevel: 219.4 },
|
||||
{ id: "VRSN|2", text: "VD Vrané - Vltava", coords: [49.9340, 14.3850], maxVolume: 11.1, minLevel: 198.5, maxLevel: 200.5, storageLevel: 200.5 },
|
||||
{ id: "SVKR|2", text: "VD Švihov - Želivka", priority: true, coords: [49.7180, 15.1060], maxVolume: 266.6, minLevel: 343.0, maxLevel: 377.0, storageLevel: 377.0 },
|
||||
{ id: "MARI|1", text: "VD Římov - Malše", priority: true, coords: [48.8470, 14.4870], maxVolume: 33.8, minLevel: 458.0, maxLevel: 471.0, storageLevel: 470.65 },
|
||||
{ id: "MZHR|3", text: "VD Hracholusky - Mže", priority: true, coords: [49.7890, 13.1550], maxVolume: 56.7, minLevel: 360.0, maxLevel: 373.0, storageLevel: 354.1 }
|
||||
];
|
||||
</pre></td></tr></table></pre>
|
||||
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="../prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="../sorter.js"></script>
|
||||
<script src="../block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,616 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for scripts/scrapeLakes.ts</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../prettify.css" />
|
||||
<link rel="stylesheet" href="../base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(../sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="../index.html">All files</a> / <a href="index.html">scripts</a> scrapeLakes.ts</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">24.52% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>26/106</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">18.66% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>14/75</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">25% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>3/12</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">25% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>23/92</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line low'></div>
|
||||
<pre><table class="coverage">
|
||||
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||
<a name='L2'></a><a href='#L2'>2</a>
|
||||
<a name='L3'></a><a href='#L3'>3</a>
|
||||
<a name='L4'></a><a href='#L4'>4</a>
|
||||
<a name='L5'></a><a href='#L5'>5</a>
|
||||
<a name='L6'></a><a href='#L6'>6</a>
|
||||
<a name='L7'></a><a href='#L7'>7</a>
|
||||
<a name='L8'></a><a href='#L8'>8</a>
|
||||
<a name='L9'></a><a href='#L9'>9</a>
|
||||
<a name='L10'></a><a href='#L10'>10</a>
|
||||
<a name='L11'></a><a href='#L11'>11</a>
|
||||
<a name='L12'></a><a href='#L12'>12</a>
|
||||
<a name='L13'></a><a href='#L13'>13</a>
|
||||
<a name='L14'></a><a href='#L14'>14</a>
|
||||
<a name='L15'></a><a href='#L15'>15</a>
|
||||
<a name='L16'></a><a href='#L16'>16</a>
|
||||
<a name='L17'></a><a href='#L17'>17</a>
|
||||
<a name='L18'></a><a href='#L18'>18</a>
|
||||
<a name='L19'></a><a href='#L19'>19</a>
|
||||
<a name='L20'></a><a href='#L20'>20</a>
|
||||
<a name='L21'></a><a href='#L21'>21</a>
|
||||
<a name='L22'></a><a href='#L22'>22</a>
|
||||
<a name='L23'></a><a href='#L23'>23</a>
|
||||
<a name='L24'></a><a href='#L24'>24</a>
|
||||
<a name='L25'></a><a href='#L25'>25</a>
|
||||
<a name='L26'></a><a href='#L26'>26</a>
|
||||
<a name='L27'></a><a href='#L27'>27</a>
|
||||
<a name='L28'></a><a href='#L28'>28</a>
|
||||
<a name='L29'></a><a href='#L29'>29</a>
|
||||
<a name='L30'></a><a href='#L30'>30</a>
|
||||
<a name='L31'></a><a href='#L31'>31</a>
|
||||
<a name='L32'></a><a href='#L32'>32</a>
|
||||
<a name='L33'></a><a href='#L33'>33</a>
|
||||
<a name='L34'></a><a href='#L34'>34</a>
|
||||
<a name='L35'></a><a href='#L35'>35</a>
|
||||
<a name='L36'></a><a href='#L36'>36</a>
|
||||
<a name='L37'></a><a href='#L37'>37</a>
|
||||
<a name='L38'></a><a href='#L38'>38</a>
|
||||
<a name='L39'></a><a href='#L39'>39</a>
|
||||
<a name='L40'></a><a href='#L40'>40</a>
|
||||
<a name='L41'></a><a href='#L41'>41</a>
|
||||
<a name='L42'></a><a href='#L42'>42</a>
|
||||
<a name='L43'></a><a href='#L43'>43</a>
|
||||
<a name='L44'></a><a href='#L44'>44</a>
|
||||
<a name='L45'></a><a href='#L45'>45</a>
|
||||
<a name='L46'></a><a href='#L46'>46</a>
|
||||
<a name='L47'></a><a href='#L47'>47</a>
|
||||
<a name='L48'></a><a href='#L48'>48</a>
|
||||
<a name='L49'></a><a href='#L49'>49</a>
|
||||
<a name='L50'></a><a href='#L50'>50</a>
|
||||
<a name='L51'></a><a href='#L51'>51</a>
|
||||
<a name='L52'></a><a href='#L52'>52</a>
|
||||
<a name='L53'></a><a href='#L53'>53</a>
|
||||
<a name='L54'></a><a href='#L54'>54</a>
|
||||
<a name='L55'></a><a href='#L55'>55</a>
|
||||
<a name='L56'></a><a href='#L56'>56</a>
|
||||
<a name='L57'></a><a href='#L57'>57</a>
|
||||
<a name='L58'></a><a href='#L58'>58</a>
|
||||
<a name='L59'></a><a href='#L59'>59</a>
|
||||
<a name='L60'></a><a href='#L60'>60</a>
|
||||
<a name='L61'></a><a href='#L61'>61</a>
|
||||
<a name='L62'></a><a href='#L62'>62</a>
|
||||
<a name='L63'></a><a href='#L63'>63</a>
|
||||
<a name='L64'></a><a href='#L64'>64</a>
|
||||
<a name='L65'></a><a href='#L65'>65</a>
|
||||
<a name='L66'></a><a href='#L66'>66</a>
|
||||
<a name='L67'></a><a href='#L67'>67</a>
|
||||
<a name='L68'></a><a href='#L68'>68</a>
|
||||
<a name='L69'></a><a href='#L69'>69</a>
|
||||
<a name='L70'></a><a href='#L70'>70</a>
|
||||
<a name='L71'></a><a href='#L71'>71</a>
|
||||
<a name='L72'></a><a href='#L72'>72</a>
|
||||
<a name='L73'></a><a href='#L73'>73</a>
|
||||
<a name='L74'></a><a href='#L74'>74</a>
|
||||
<a name='L75'></a><a href='#L75'>75</a>
|
||||
<a name='L76'></a><a href='#L76'>76</a>
|
||||
<a name='L77'></a><a href='#L77'>77</a>
|
||||
<a name='L78'></a><a href='#L78'>78</a>
|
||||
<a name='L79'></a><a href='#L79'>79</a>
|
||||
<a name='L80'></a><a href='#L80'>80</a>
|
||||
<a name='L81'></a><a href='#L81'>81</a>
|
||||
<a name='L82'></a><a href='#L82'>82</a>
|
||||
<a name='L83'></a><a href='#L83'>83</a>
|
||||
<a name='L84'></a><a href='#L84'>84</a>
|
||||
<a name='L85'></a><a href='#L85'>85</a>
|
||||
<a name='L86'></a><a href='#L86'>86</a>
|
||||
<a name='L87'></a><a href='#L87'>87</a>
|
||||
<a name='L88'></a><a href='#L88'>88</a>
|
||||
<a name='L89'></a><a href='#L89'>89</a>
|
||||
<a name='L90'></a><a href='#L90'>90</a>
|
||||
<a name='L91'></a><a href='#L91'>91</a>
|
||||
<a name='L92'></a><a href='#L92'>92</a>
|
||||
<a name='L93'></a><a href='#L93'>93</a>
|
||||
<a name='L94'></a><a href='#L94'>94</a>
|
||||
<a name='L95'></a><a href='#L95'>95</a>
|
||||
<a name='L96'></a><a href='#L96'>96</a>
|
||||
<a name='L97'></a><a href='#L97'>97</a>
|
||||
<a name='L98'></a><a href='#L98'>98</a>
|
||||
<a name='L99'></a><a href='#L99'>99</a>
|
||||
<a name='L100'></a><a href='#L100'>100</a>
|
||||
<a name='L101'></a><a href='#L101'>101</a>
|
||||
<a name='L102'></a><a href='#L102'>102</a>
|
||||
<a name='L103'></a><a href='#L103'>103</a>
|
||||
<a name='L104'></a><a href='#L104'>104</a>
|
||||
<a name='L105'></a><a href='#L105'>105</a>
|
||||
<a name='L106'></a><a href='#L106'>106</a>
|
||||
<a name='L107'></a><a href='#L107'>107</a>
|
||||
<a name='L108'></a><a href='#L108'>108</a>
|
||||
<a name='L109'></a><a href='#L109'>109</a>
|
||||
<a name='L110'></a><a href='#L110'>110</a>
|
||||
<a name='L111'></a><a href='#L111'>111</a>
|
||||
<a name='L112'></a><a href='#L112'>112</a>
|
||||
<a name='L113'></a><a href='#L113'>113</a>
|
||||
<a name='L114'></a><a href='#L114'>114</a>
|
||||
<a name='L115'></a><a href='#L115'>115</a>
|
||||
<a name='L116'></a><a href='#L116'>116</a>
|
||||
<a name='L117'></a><a href='#L117'>117</a>
|
||||
<a name='L118'></a><a href='#L118'>118</a>
|
||||
<a name='L119'></a><a href='#L119'>119</a>
|
||||
<a name='L120'></a><a href='#L120'>120</a>
|
||||
<a name='L121'></a><a href='#L121'>121</a>
|
||||
<a name='L122'></a><a href='#L122'>122</a>
|
||||
<a name='L123'></a><a href='#L123'>123</a>
|
||||
<a name='L124'></a><a href='#L124'>124</a>
|
||||
<a name='L125'></a><a href='#L125'>125</a>
|
||||
<a name='L126'></a><a href='#L126'>126</a>
|
||||
<a name='L127'></a><a href='#L127'>127</a>
|
||||
<a name='L128'></a><a href='#L128'>128</a>
|
||||
<a name='L129'></a><a href='#L129'>129</a>
|
||||
<a name='L130'></a><a href='#L130'>130</a>
|
||||
<a name='L131'></a><a href='#L131'>131</a>
|
||||
<a name='L132'></a><a href='#L132'>132</a>
|
||||
<a name='L133'></a><a href='#L133'>133</a>
|
||||
<a name='L134'></a><a href='#L134'>134</a>
|
||||
<a name='L135'></a><a href='#L135'>135</a>
|
||||
<a name='L136'></a><a href='#L136'>136</a>
|
||||
<a name='L137'></a><a href='#L137'>137</a>
|
||||
<a name='L138'></a><a href='#L138'>138</a>
|
||||
<a name='L139'></a><a href='#L139'>139</a>
|
||||
<a name='L140'></a><a href='#L140'>140</a>
|
||||
<a name='L141'></a><a href='#L141'>141</a>
|
||||
<a name='L142'></a><a href='#L142'>142</a>
|
||||
<a name='L143'></a><a href='#L143'>143</a>
|
||||
<a name='L144'></a><a href='#L144'>144</a>
|
||||
<a name='L145'></a><a href='#L145'>145</a>
|
||||
<a name='L146'></a><a href='#L146'>146</a>
|
||||
<a name='L147'></a><a href='#L147'>147</a>
|
||||
<a name='L148'></a><a href='#L148'>148</a>
|
||||
<a name='L149'></a><a href='#L149'>149</a>
|
||||
<a name='L150'></a><a href='#L150'>150</a>
|
||||
<a name='L151'></a><a href='#L151'>151</a>
|
||||
<a name='L152'></a><a href='#L152'>152</a>
|
||||
<a name='L153'></a><a href='#L153'>153</a>
|
||||
<a name='L154'></a><a href='#L154'>154</a>
|
||||
<a name='L155'></a><a href='#L155'>155</a>
|
||||
<a name='L156'></a><a href='#L156'>156</a>
|
||||
<a name='L157'></a><a href='#L157'>157</a>
|
||||
<a name='L158'></a><a href='#L158'>158</a>
|
||||
<a name='L159'></a><a href='#L159'>159</a>
|
||||
<a name='L160'></a><a href='#L160'>160</a>
|
||||
<a name='L161'></a><a href='#L161'>161</a>
|
||||
<a name='L162'></a><a href='#L162'>162</a>
|
||||
<a name='L163'></a><a href='#L163'>163</a>
|
||||
<a name='L164'></a><a href='#L164'>164</a>
|
||||
<a name='L165'></a><a href='#L165'>165</a>
|
||||
<a name='L166'></a><a href='#L166'>166</a>
|
||||
<a name='L167'></a><a href='#L167'>167</a>
|
||||
<a name='L168'></a><a href='#L168'>168</a>
|
||||
<a name='L169'></a><a href='#L169'>169</a>
|
||||
<a name='L170'></a><a href='#L170'>170</a>
|
||||
<a name='L171'></a><a href='#L171'>171</a>
|
||||
<a name='L172'></a><a href='#L172'>172</a>
|
||||
<a name='L173'></a><a href='#L173'>173</a>
|
||||
<a name='L174'></a><a href='#L174'>174</a>
|
||||
<a name='L175'></a><a href='#L175'>175</a>
|
||||
<a name='L176'></a><a href='#L176'>176</a>
|
||||
<a name='L177'></a><a href='#L177'>177</a>
|
||||
<a name='L178'></a><a href='#L178'>178</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as cheerio from 'cheerio';
|
||||
import axios from 'axios';
|
||||
import https from 'https';
|
||||
import { lakesConfig } from './lakesConfig';
|
||||
|
||||
interface DataRecord {
|
||||
timestamp: string;
|
||||
level: number;
|
||||
flow: number;
|
||||
inflow?: number;
|
||||
volume?: number;
|
||||
temperature?: number | null;
|
||||
precipitation?: number | null;
|
||||
}
|
||||
|
||||
// Parse date from DD.MM.YYYY HH:MM to ISO
|
||||
export function parseDateString(dateStr: string): string | null {
|
||||
try {
|
||||
if (!dateStr || !dateStr.includes(' ')) return null;
|
||||
const [datePart, timePart] = dateStr.trim().split(' ');
|
||||
const [day, month, year] = datePart.split('.');
|
||||
const [hours, minutes] = timePart.split(':');
|
||||
|
||||
if (!year || !hours) return null;
|
||||
|
||||
const y = parseInt(year);
|
||||
const m = parseInt(month) - 1;
|
||||
const dDay = parseInt(day);
|
||||
const d = new Date(y, m, dDay, parseInt(hours), parseInt(minutes));
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (isNaN(d.getTime())) <span class="cstat-no" title="statement not covered" >return null;</span>
|
||||
if (d.getFullYear() !== y || d.getMonth() !== m || d.getDate() !== dDay) return null;
|
||||
return d.toISOString();
|
||||
} catch (e) {
|
||||
<span class="cstat-no" title="statement not covered" > return null;</span>
|
||||
}
|
||||
}
|
||||
|
||||
async function scrapeLake(lakeId: string, oid: string, internalId: string) {
|
||||
const URL = `https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?oid=${oid}&id=${internalId}`;
|
||||
const DATA_FILE = path.resolve(`public/data/${internalId}.json`);
|
||||
|
||||
try {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const response = await axios.get(URL, {
|
||||
httpsAgent: agent,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
const $ = <span class="cstat-no" title="statement not covered" >cheerio.load(response.data);</span>
|
||||
|
||||
let currentInflow = <span class="cstat-no" title="statement not covered" >0;</span>
|
||||
let currentVolume = <span class="cstat-no" title="statement not covered" >0;</span>
|
||||
let currentTemp: number | null = <span class="cstat-no" title="statement not covered" >null;</span>
|
||||
let currentPrecip: number | null = <span class="cstat-no" title="statement not covered" >null;</span>
|
||||
|
||||
<span class="cstat-no" title="statement not covered" > $('table').each(<span class="fstat-no" title="function not covered" >(i</span>, tbl) => {</span>
|
||||
const text = <span class="cstat-no" title="statement not covered" >$(tbl).text();</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (text.includes('Aktuální hodnoty') && text.includes('Přítok')) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > $(tbl).find('tr').each(<span class="fstat-no" title="function not covered" >(j</span>, r) => {</span>
|
||||
const label = <span class="cstat-no" title="statement not covered" >$(r).find('td').eq(0).text().trim();</span>
|
||||
const valStr = <span class="cstat-no" title="statement not covered" >$(r).find('td').eq(1).text().trim().replace(/\s/g, '').replace(',', '.');</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (label.includes('Přítok')) <span class="cstat-no" title="statement not covered" >currentInflow = parseFloat(valStr) || 0;</span></span>
|
||||
<span class="cstat-no" title="statement not covered" > if (label.includes('Objem')) <span class="cstat-no" title="statement not covered" >currentVolume = parseFloat(valStr) || 0;</span></span>
|
||||
<span class="cstat-no" title="statement not covered" > if (label.includes('Teplota')) {</span>
|
||||
const v = <span class="cstat-no" title="statement not covered" >parseFloat(valStr);</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (!isNaN(v)) <span class="cstat-no" title="statement not covered" >currentTemp = v;</span></span>
|
||||
}
|
||||
<span class="cstat-no" title="statement not covered" > if (label.includes('Srážky')) {</span>
|
||||
const v = <span class="cstat-no" title="statement not covered" >parseFloat(valStr);</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (!isNaN(v)) <span class="cstat-no" title="statement not covered" >currentPrecip = v;</span></span>
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const records: DataRecord[] = <span class="cstat-no" title="statement not covered" >[];</span>
|
||||
let dataTable = <span class="cstat-no" title="statement not covered" >null;</span>
|
||||
<span class="cstat-no" title="statement not covered" > $('table').each(<span class="fstat-no" title="function not covered" >(i</span>, tbl) => {</span>
|
||||
<span class="cstat-no" title="statement not covered" > if ($(tbl).text().includes('Datum') && $(tbl).text().includes('Odtok')) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > dataTable = $(tbl);</span>
|
||||
}
|
||||
});
|
||||
|
||||
<span class="cstat-no" title="statement not covered" > if (dataTable) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > dataTable.find('tr').each(<span class="fstat-no" title="function not covered" >(i</span>, row) => {</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (i === 0) <span class="cstat-no" title="statement not covered" >return; // skip header</span></span>
|
||||
const cols = <span class="cstat-no" title="statement not covered" >$(row).find('td');</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (cols.length >= 3) {</span>
|
||||
const rawDate = <span class="cstat-no" title="statement not covered" >$(cols[0]).text().trim(); </span>
|
||||
const levelStr = <span class="cstat-no" title="statement not covered" >$(cols[1]).text().trim().replace(',', '.');</span>
|
||||
let flowStr = <span class="cstat-no" title="statement not covered" >$(cols[2]).text().trim().replace(',', '.');</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (flowStr === '' && cols.length >= 4) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > flowStr = $(cols[3]).text().trim().replace(',', '.');</span>
|
||||
}
|
||||
|
||||
const parsedDateStr = <span class="cstat-no" title="statement not covered" >parseDateString(rawDate);</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (parsedDateStr) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > records.push({</span>
|
||||
timestamp: parsedDateStr,
|
||||
level: parseFloat(levelStr) || 0,
|
||||
flow: parseFloat(flowStr) || 0,
|
||||
inflow: 0,
|
||||
volume: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
<span class="cstat-no" title="statement not covered" > if (records.length > 0) {</span>
|
||||
// Apply current values to the latest record
|
||||
<span class="cstat-no" title="statement not covered" > records[0].inflow = currentInflow;</span>
|
||||
<span class="cstat-no" title="statement not covered" > records[0].volume = currentVolume;</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (currentTemp !== null) <span class="cstat-no" title="statement not covered" >records[0].temperature = currentTemp;</span></span>
|
||||
<span class="cstat-no" title="statement not covered" > if (currentPrecip !== null) <span class="cstat-no" title="statement not covered" >records[0].precipitation = currentPrecip;</span></span>
|
||||
}
|
||||
|
||||
let existingData: DataRecord[] = <span class="cstat-no" title="statement not covered" >[];</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (fs.existsSync(DATA_FILE)) {</span>
|
||||
const fileContent = <span class="cstat-no" title="statement not covered" >fs.readFileSync(DATA_FILE, 'utf-8');</span>
|
||||
<span class="cstat-no" title="statement not covered" > existingData = JSON.parse(fileContent);</span>
|
||||
}
|
||||
|
||||
const dataMap = <span class="cstat-no" title="statement not covered" >new Map<string, DataRecord>();</span>
|
||||
<span class="cstat-no" title="statement not covered" > existingData.forEach(<span class="fstat-no" title="function not covered" >item => <span class="cstat-no" title="statement not covered" >d</span>ataMap.set(item.timestamp, item))</span>;</span>
|
||||
<span class="cstat-no" title="statement not covered" > records.forEach(<span class="fstat-no" title="function not covered" >item => <span class="cstat-no" title="statement not covered" >d</span>ataMap.set(item.timestamp, item))</span>;</span>
|
||||
|
||||
const mergedData = <span class="cstat-no" title="statement not covered" >Array.from(dataMap.values()).sort(<span class="fstat-no" title="function not covered" >(a</span>, b) => {</span>
|
||||
<span class="cstat-no" title="statement not covered" > return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();</span>
|
||||
});
|
||||
|
||||
// Propagate previous values if missing (user requested)
|
||||
let lastKnownTemp: number | null = <span class="cstat-no" title="statement not covered" >null;</span>
|
||||
let lastKnownPrecip: number | null = <span class="cstat-no" title="statement not covered" >null;</span>
|
||||
<span class="cstat-no" title="statement not covered" > mergedData.forEach(<span class="fstat-no" title="function not covered" >item => {</span></span>
|
||||
<span class="cstat-no" title="statement not covered" > if (item.temperature !== undefined && item.temperature !== null) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > lastKnownTemp = item.temperature;</span>
|
||||
<span class="cstat-no" title="statement not covered" > } else if (lastKnownTemp !== null) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > item.temperature = lastKnownTemp;</span>
|
||||
}
|
||||
|
||||
<span class="cstat-no" title="statement not covered" > if (item.precipitation !== undefined && item.precipitation !== null) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > lastKnownPrecip = item.precipitation;</span>
|
||||
<span class="cstat-no" title="statement not covered" > } else if (lastKnownPrecip !== null) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > item.precipitation = lastKnownPrecip;</span>
|
||||
}
|
||||
});
|
||||
|
||||
<span class="cstat-no" title="statement not covered" > fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });</span>
|
||||
<span class="cstat-no" title="statement not covered" > fs.writeFileSync(DATA_FILE, JSON.stringify(mergedData, null, 2), 'utf-8');</span>
|
||||
|
||||
<span class="cstat-no" title="statement not covered" > console.log(`[${internalId}] Scraped ${records.length} records. DB total: ${mergedData.length}`);</span>
|
||||
|
||||
} catch (error: any) {
|
||||
<span class="cstat-no" title="statement not covered" > console.error(`[${internalId}] Error scraping data:`, error.message);</span>
|
||||
}
|
||||
}
|
||||
|
||||
async function runScraper() {
|
||||
console.log(`Starting bulk scraper for ${lakesConfig.length} lakes...`);
|
||||
|
||||
for (const lake of lakesConfig) {
|
||||
// ID format: VLL1|1 -> internalId=VLL1, oid=1
|
||||
const [internalId, oid] = lake.id.split('|');
|
||||
await scrapeLake(lake.id, oid, internalId);
|
||||
// Add small delay to not hammer the server
|
||||
<span class="cstat-no" title="statement not covered" > await new Promise(<span class="fstat-no" title="function not covered" >resolve => <span class="cstat-no" title="statement not covered" >s</span>etTimeout(resolve, 500))</span>;</span>
|
||||
}
|
||||
|
||||
<span class="cstat-no" title="statement not covered" > console.log('Bulk scraping finished.');</span>
|
||||
}
|
||||
|
||||
runScraper();
|
||||
</pre></td></tr></table></pre>
|
||||
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="../prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="../sorter.js"></script>
|
||||
<script src="../block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 138 B |
@@ -1,210 +0,0 @@
|
||||
/* eslint-disable */
|
||||
var addSorting = (function() {
|
||||
'use strict';
|
||||
var cols,
|
||||
currentSort = {
|
||||
index: 0,
|
||||
desc: false
|
||||
};
|
||||
|
||||
// returns the summary table element
|
||||
function getTable() {
|
||||
return document.querySelector('.coverage-summary');
|
||||
}
|
||||
// returns the thead element of the summary table
|
||||
function getTableHeader() {
|
||||
return getTable().querySelector('thead tr');
|
||||
}
|
||||
// returns the tbody element of the summary table
|
||||
function getTableBody() {
|
||||
return getTable().querySelector('tbody');
|
||||
}
|
||||
// returns the th element for nth column
|
||||
function getNthColumn(n) {
|
||||
return getTableHeader().querySelectorAll('th')[n];
|
||||
}
|
||||
|
||||
function onFilterInput() {
|
||||
const searchValue = document.getElementById('fileSearch').value;
|
||||
const rows = document.getElementsByTagName('tbody')[0].children;
|
||||
|
||||
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
|
||||
// it will be treated as a plain text search
|
||||
let searchRegex;
|
||||
try {
|
||||
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
|
||||
} catch (error) {
|
||||
searchRegex = null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
let isMatch = false;
|
||||
|
||||
if (searchRegex) {
|
||||
// If a valid regex was created, use it for matching
|
||||
isMatch = searchRegex.test(row.textContent);
|
||||
} else {
|
||||
// Otherwise, fall back to the original plain text search
|
||||
isMatch = row.textContent
|
||||
.toLowerCase()
|
||||
.includes(searchValue.toLowerCase());
|
||||
}
|
||||
|
||||
row.style.display = isMatch ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// loads the search box
|
||||
function addSearchBox() {
|
||||
var template = document.getElementById('filterTemplate');
|
||||
var templateClone = template.content.cloneNode(true);
|
||||
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
||||
template.parentElement.appendChild(templateClone);
|
||||
}
|
||||
|
||||
// loads all columns
|
||||
function loadColumns() {
|
||||
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||
colNode,
|
||||
cols = [],
|
||||
col,
|
||||
i;
|
||||
|
||||
for (i = 0; i < colNodes.length; i += 1) {
|
||||
colNode = colNodes[i];
|
||||
col = {
|
||||
key: colNode.getAttribute('data-col'),
|
||||
sortable: !colNode.getAttribute('data-nosort'),
|
||||
type: colNode.getAttribute('data-type') || 'string'
|
||||
};
|
||||
cols.push(col);
|
||||
if (col.sortable) {
|
||||
col.defaultDescSort = col.type === 'number';
|
||||
colNode.innerHTML =
|
||||
colNode.innerHTML + '<span class="sorter"></span>';
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
// attaches a data attribute to every tr element with an object
|
||||
// of data values keyed by column name
|
||||
function loadRowData(tableRow) {
|
||||
var tableCols = tableRow.querySelectorAll('td'),
|
||||
colNode,
|
||||
col,
|
||||
data = {},
|
||||
i,
|
||||
val;
|
||||
for (i = 0; i < tableCols.length; i += 1) {
|
||||
colNode = tableCols[i];
|
||||
col = cols[i];
|
||||
val = colNode.getAttribute('data-value');
|
||||
if (col.type === 'number') {
|
||||
val = Number(val);
|
||||
}
|
||||
data[col.key] = val;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// loads all row data
|
||||
function loadData() {
|
||||
var rows = getTableBody().querySelectorAll('tr'),
|
||||
i;
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
rows[i].data = loadRowData(rows[i]);
|
||||
}
|
||||
}
|
||||
// sorts the table using the data for the ith column
|
||||
function sortByIndex(index, desc) {
|
||||
var key = cols[index].key,
|
||||
sorter = function(a, b) {
|
||||
a = a.data[key];
|
||||
b = b.data[key];
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
},
|
||||
finalSorter = sorter,
|
||||
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||
rowNodes = tableBody.querySelectorAll('tr'),
|
||||
rows = [],
|
||||
i;
|
||||
|
||||
if (desc) {
|
||||
finalSorter = function(a, b) {
|
||||
return -1 * sorter(a, b);
|
||||
};
|
||||
}
|
||||
|
||||
for (i = 0; i < rowNodes.length; i += 1) {
|
||||
rows.push(rowNodes[i]);
|
||||
tableBody.removeChild(rowNodes[i]);
|
||||
}
|
||||
|
||||
rows.sort(finalSorter);
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
tableBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
// removes sort indicators for current column being sorted
|
||||
function removeSortIndicators() {
|
||||
var col = getNthColumn(currentSort.index),
|
||||
cls = col.className;
|
||||
|
||||
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||
col.className = cls;
|
||||
}
|
||||
// adds sort indicators for current column being sorted
|
||||
function addSortIndicators() {
|
||||
getNthColumn(currentSort.index).className += currentSort.desc
|
||||
? ' sorted-desc'
|
||||
: ' sorted';
|
||||
}
|
||||
// adds event listeners for all sorter widgets
|
||||
function enableUI() {
|
||||
var i,
|
||||
el,
|
||||
ithSorter = function ithSorter(i) {
|
||||
var col = cols[i];
|
||||
|
||||
return function() {
|
||||
var desc = col.defaultDescSort;
|
||||
|
||||
if (currentSort.index === i) {
|
||||
desc = !currentSort.desc;
|
||||
}
|
||||
sortByIndex(i, desc);
|
||||
removeSortIndicators();
|
||||
currentSort.index = i;
|
||||
currentSort.desc = desc;
|
||||
addSortIndicators();
|
||||
};
|
||||
};
|
||||
for (i = 0; i < cols.length; i += 1) {
|
||||
if (cols[i].sortable) {
|
||||
// add the click event handler on the th so users
|
||||
// dont have to click on those tiny arrows
|
||||
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener('click', ithSorter(i));
|
||||
} else {
|
||||
el.attachEvent('onclick', ithSorter(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// adds sorting functionality to the UI
|
||||
return function() {
|
||||
if (!getTable()) {
|
||||
return;
|
||||
}
|
||||
cols = loadColumns();
|
||||
loadData();
|
||||
addSearchBox();
|
||||
addSortIndicators();
|
||||
enableUI();
|
||||
};
|
||||
})();
|
||||
|
||||
window.addEventListener('load', addSorting);
|
||||
@@ -1,466 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for src/components/KpiCards.tsx</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../../prettify.css" />
|
||||
<link rel="stylesheet" href="../../base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(../../sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> KpiCards.tsx</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">53.84% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>7/13</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">65.21% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>15/23</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">33.33% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>2/6</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">58.33% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>7/12</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line medium'></div>
|
||||
<pre><table class="coverage">
|
||||
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||
<a name='L2'></a><a href='#L2'>2</a>
|
||||
<a name='L3'></a><a href='#L3'>3</a>
|
||||
<a name='L4'></a><a href='#L4'>4</a>
|
||||
<a name='L5'></a><a href='#L5'>5</a>
|
||||
<a name='L6'></a><a href='#L6'>6</a>
|
||||
<a name='L7'></a><a href='#L7'>7</a>
|
||||
<a name='L8'></a><a href='#L8'>8</a>
|
||||
<a name='L9'></a><a href='#L9'>9</a>
|
||||
<a name='L10'></a><a href='#L10'>10</a>
|
||||
<a name='L11'></a><a href='#L11'>11</a>
|
||||
<a name='L12'></a><a href='#L12'>12</a>
|
||||
<a name='L13'></a><a href='#L13'>13</a>
|
||||
<a name='L14'></a><a href='#L14'>14</a>
|
||||
<a name='L15'></a><a href='#L15'>15</a>
|
||||
<a name='L16'></a><a href='#L16'>16</a>
|
||||
<a name='L17'></a><a href='#L17'>17</a>
|
||||
<a name='L18'></a><a href='#L18'>18</a>
|
||||
<a name='L19'></a><a href='#L19'>19</a>
|
||||
<a name='L20'></a><a href='#L20'>20</a>
|
||||
<a name='L21'></a><a href='#L21'>21</a>
|
||||
<a name='L22'></a><a href='#L22'>22</a>
|
||||
<a name='L23'></a><a href='#L23'>23</a>
|
||||
<a name='L24'></a><a href='#L24'>24</a>
|
||||
<a name='L25'></a><a href='#L25'>25</a>
|
||||
<a name='L26'></a><a href='#L26'>26</a>
|
||||
<a name='L27'></a><a href='#L27'>27</a>
|
||||
<a name='L28'></a><a href='#L28'>28</a>
|
||||
<a name='L29'></a><a href='#L29'>29</a>
|
||||
<a name='L30'></a><a href='#L30'>30</a>
|
||||
<a name='L31'></a><a href='#L31'>31</a>
|
||||
<a name='L32'></a><a href='#L32'>32</a>
|
||||
<a name='L33'></a><a href='#L33'>33</a>
|
||||
<a name='L34'></a><a href='#L34'>34</a>
|
||||
<a name='L35'></a><a href='#L35'>35</a>
|
||||
<a name='L36'></a><a href='#L36'>36</a>
|
||||
<a name='L37'></a><a href='#L37'>37</a>
|
||||
<a name='L38'></a><a href='#L38'>38</a>
|
||||
<a name='L39'></a><a href='#L39'>39</a>
|
||||
<a name='L40'></a><a href='#L40'>40</a>
|
||||
<a name='L41'></a><a href='#L41'>41</a>
|
||||
<a name='L42'></a><a href='#L42'>42</a>
|
||||
<a name='L43'></a><a href='#L43'>43</a>
|
||||
<a name='L44'></a><a href='#L44'>44</a>
|
||||
<a name='L45'></a><a href='#L45'>45</a>
|
||||
<a name='L46'></a><a href='#L46'>46</a>
|
||||
<a name='L47'></a><a href='#L47'>47</a>
|
||||
<a name='L48'></a><a href='#L48'>48</a>
|
||||
<a name='L49'></a><a href='#L49'>49</a>
|
||||
<a name='L50'></a><a href='#L50'>50</a>
|
||||
<a name='L51'></a><a href='#L51'>51</a>
|
||||
<a name='L52'></a><a href='#L52'>52</a>
|
||||
<a name='L53'></a><a href='#L53'>53</a>
|
||||
<a name='L54'></a><a href='#L54'>54</a>
|
||||
<a name='L55'></a><a href='#L55'>55</a>
|
||||
<a name='L56'></a><a href='#L56'>56</a>
|
||||
<a name='L57'></a><a href='#L57'>57</a>
|
||||
<a name='L58'></a><a href='#L58'>58</a>
|
||||
<a name='L59'></a><a href='#L59'>59</a>
|
||||
<a name='L60'></a><a href='#L60'>60</a>
|
||||
<a name='L61'></a><a href='#L61'>61</a>
|
||||
<a name='L62'></a><a href='#L62'>62</a>
|
||||
<a name='L63'></a><a href='#L63'>63</a>
|
||||
<a name='L64'></a><a href='#L64'>64</a>
|
||||
<a name='L65'></a><a href='#L65'>65</a>
|
||||
<a name='L66'></a><a href='#L66'>66</a>
|
||||
<a name='L67'></a><a href='#L67'>67</a>
|
||||
<a name='L68'></a><a href='#L68'>68</a>
|
||||
<a name='L69'></a><a href='#L69'>69</a>
|
||||
<a name='L70'></a><a href='#L70'>70</a>
|
||||
<a name='L71'></a><a href='#L71'>71</a>
|
||||
<a name='L72'></a><a href='#L72'>72</a>
|
||||
<a name='L73'></a><a href='#L73'>73</a>
|
||||
<a name='L74'></a><a href='#L74'>74</a>
|
||||
<a name='L75'></a><a href='#L75'>75</a>
|
||||
<a name='L76'></a><a href='#L76'>76</a>
|
||||
<a name='L77'></a><a href='#L77'>77</a>
|
||||
<a name='L78'></a><a href='#L78'>78</a>
|
||||
<a name='L79'></a><a href='#L79'>79</a>
|
||||
<a name='L80'></a><a href='#L80'>80</a>
|
||||
<a name='L81'></a><a href='#L81'>81</a>
|
||||
<a name='L82'></a><a href='#L82'>82</a>
|
||||
<a name='L83'></a><a href='#L83'>83</a>
|
||||
<a name='L84'></a><a href='#L84'>84</a>
|
||||
<a name='L85'></a><a href='#L85'>85</a>
|
||||
<a name='L86'></a><a href='#L86'>86</a>
|
||||
<a name='L87'></a><a href='#L87'>87</a>
|
||||
<a name='L88'></a><a href='#L88'>88</a>
|
||||
<a name='L89'></a><a href='#L89'>89</a>
|
||||
<a name='L90'></a><a href='#L90'>90</a>
|
||||
<a name='L91'></a><a href='#L91'>91</a>
|
||||
<a name='L92'></a><a href='#L92'>92</a>
|
||||
<a name='L93'></a><a href='#L93'>93</a>
|
||||
<a name='L94'></a><a href='#L94'>94</a>
|
||||
<a name='L95'></a><a href='#L95'>95</a>
|
||||
<a name='L96'></a><a href='#L96'>96</a>
|
||||
<a name='L97'></a><a href='#L97'>97</a>
|
||||
<a name='L98'></a><a href='#L98'>98</a>
|
||||
<a name='L99'></a><a href='#L99'>99</a>
|
||||
<a name='L100'></a><a href='#L100'>100</a>
|
||||
<a name='L101'></a><a href='#L101'>101</a>
|
||||
<a name='L102'></a><a href='#L102'>102</a>
|
||||
<a name='L103'></a><a href='#L103'>103</a>
|
||||
<a name='L104'></a><a href='#L104'>104</a>
|
||||
<a name='L105'></a><a href='#L105'>105</a>
|
||||
<a name='L106'></a><a href='#L106'>106</a>
|
||||
<a name='L107'></a><a href='#L107'>107</a>
|
||||
<a name='L108'></a><a href='#L108'>108</a>
|
||||
<a name='L109'></a><a href='#L109'>109</a>
|
||||
<a name='L110'></a><a href='#L110'>110</a>
|
||||
<a name='L111'></a><a href='#L111'>111</a>
|
||||
<a name='L112'></a><a href='#L112'>112</a>
|
||||
<a name='L113'></a><a href='#L113'>113</a>
|
||||
<a name='L114'></a><a href='#L114'>114</a>
|
||||
<a name='L115'></a><a href='#L115'>115</a>
|
||||
<a name='L116'></a><a href='#L116'>116</a>
|
||||
<a name='L117'></a><a href='#L117'>117</a>
|
||||
<a name='L118'></a><a href='#L118'>118</a>
|
||||
<a name='L119'></a><a href='#L119'>119</a>
|
||||
<a name='L120'></a><a href='#L120'>120</a>
|
||||
<a name='L121'></a><a href='#L121'>121</a>
|
||||
<a name='L122'></a><a href='#L122'>122</a>
|
||||
<a name='L123'></a><a href='#L123'>123</a>
|
||||
<a name='L124'></a><a href='#L124'>124</a>
|
||||
<a name='L125'></a><a href='#L125'>125</a>
|
||||
<a name='L126'></a><a href='#L126'>126</a>
|
||||
<a name='L127'></a><a href='#L127'>127</a>
|
||||
<a name='L128'></a><a href='#L128'>128</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { FiArrowUp, FiArrowDown } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface KpiData {
|
||||
level: number;
|
||||
inflow: number;
|
||||
outflow: number;
|
||||
outflow: number;
|
||||
volume: number;
|
||||
fullness: number;
|
||||
storageDiff?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: KpiData;
|
||||
language: Language;
|
||||
lakeName?: string;
|
||||
}
|
||||
|
||||
const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const dict = t[language].kpi;
|
||||
const flowDiff = data.inflow - data.outflow;
|
||||
|
||||
useEffect(() => {
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (showTooltip) {
|
||||
const timer = <span class="cstat-no" title="statement not covered" >setTimeout(<span class="fstat-no" title="function not covered" >() => {</span></span>
|
||||
<span class="cstat-no" title="statement not covered" > setShowTooltip(false);</span>
|
||||
}, 3500);
|
||||
<span class="cstat-no" title="statement not covered" > return <span class="fstat-no" title="function not covered" >() => <span class="cstat-no" title="statement not covered" >c</span>learTimeout(timer);</span></span>
|
||||
}
|
||||
}, [showTooltip]);
|
||||
|
||||
return (
|
||||
<div className="kpi-grid-container">
|
||||
{/* CARD 1: HLADINA */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.level} {lakeName}
|
||||
</div>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, marginBottom: '0.5rem' }}>
|
||||
{data.level.toFixed(2)} <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>m n. m.</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--color-green)' }}>
|
||||
(+0.02 m / 24h)
|
||||
</div>
|
||||
|
||||
{/* Decorative Circle for Level */}
|
||||
<div style={{ position: 'absolute', right: '1.5rem', top: '1.5rem', width: '40px', height: '40px', borderRadius: '50%', border: '4px solid rgba(0, 195, 255, 0.2)', borderTopColor: 'var(--color-cyan)', transform: 'rotate(45deg)' }}></div>
|
||||
</div>
|
||||
|
||||
{/* CARD 2: PRŮTOK */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.flow}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem' }}>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#8b5cf6', marginRight: '6px' }}></span>
|
||||
{dict.inflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)', marginLeft: '4px' }}>{data.inflow.toFixed(1)} m³/s</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-orange)', marginRight: '2px' }}></span>
|
||||
{dict.outflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.outflow.toFixed(1)} m³/s</span>
|
||||
{flowDiff > 0 ? <FiArrowUp color="var(--color-green)" /> : <span class="branch-1 cbranch-no" title="branch not covered" >flowDiff < 0 ? <FiArrowDown color="var(--color-red)" /> : null}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flow Circle */}
|
||||
<div style={{ width: '40px', height: '40px', borderRadius: '50%', border: '4px solid rgba(0, 195, 255, 0.2)', borderTopColor: 'var(--color-cyan)', borderRightColor: 'var(--color-cyan)', display: 'flex', alignItems: 'center', justifyContent: 'center', transform: 'rotate(-45deg)' }}>
|
||||
<span style={{ fontSize: '0.65rem', transform: 'rotate(45deg)', color: 'var(--text-main)', fontWeight: 'bold' }}>
|
||||
<div style={{ lineHeight: 1 }}>{data.outflow.toFixed(1)}</div>
|
||||
<div style={{ fontSize: '0.45rem', opacity: 0.7 }}>m³/s</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CARD 3: NAPLNĚNOST */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
|
||||
{dict.fullness}
|
||||
<span
|
||||
onClick={<span class="fstat-no" title="function not covered" >() => <span class="cstat-no" title="statement not covered" >s</span>etShowTooltip(!showTooltip)}</span>
|
||||
style={{ cursor: 'pointer', fontSize: '0.85rem', opacity: 0.6, padding: '0 4px' }}
|
||||
>
|
||||
ⓘ
|
||||
</span>
|
||||
{showTooltip && (
|
||||
<span class="branch-1 cbranch-no" title="branch not covered" > <div </span>
|
||||
onClick={<span class="fstat-no" title="function not covered" >() => <span class="cstat-no" title="statement not covered" >s</span>etShowTooltip(false)}</span>
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: 'var(--bg-card)',
|
||||
border: '1px solid var(--border-color)',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '8px',
|
||||
width: '250px',
|
||||
zIndex: 100,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
color: 'var(--text-main)',
|
||||
fontSize: '0.85rem',
|
||||
lineHeight: 1.4,
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
{language === 'cs' ? "Rozdíl mezi aktuální hladinou a hladinou zásobního prostoru (důležité pro jachtaře a rekreaci)." : "Difference between current water level and storage space level (important for sailing and recreation)."}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)' }}>
|
||||
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)} m` : `${data.storageDiff.toFixed(2)} m`) : (data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : <span class="branch-1 cbranch-no" title="branch not covered" >'N/A')}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
|
||||
{dict.volume}: {data.volume.toFixed(1)} mil. m³
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KpiCards;
|
||||
</pre></td></tr></table></pre>
|
||||
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="../../prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="../../sorter.js"></script>
|
||||
<script src="../../block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for src/components</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../../prettify.css" />
|
||||
<link rel="stylesheet" href="../../base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(../../sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="../../index.html">All files</a> src/components</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">53.84% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>7/13</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">65.21% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>15/23</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">33.33% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>2/6</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">58.33% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>7/12</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line medium'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td class="file medium" data-value="KpiCards.tsx"><a href="KpiCards.tsx.html">KpiCards.tsx</a></td>
|
||||
<td data-value="53.84" class="pic medium">
|
||||
<div class="chart"><div class="cover-fill" style="width: 53%"></div><div class="cover-empty" style="width: 47%"></div></div>
|
||||
</td>
|
||||
<td data-value="53.84" class="pct medium">53.84%</td>
|
||||
<td data-value="13" class="abs medium">7/13</td>
|
||||
<td data-value="65.21" class="pct medium">65.21%</td>
|
||||
<td data-value="23" class="abs medium">15/23</td>
|
||||
<td data-value="33.33" class="pct low">33.33%</td>
|
||||
<td data-value="6" class="abs low">2/6</td>
|
||||
<td data-value="58.33" class="pct medium">58.33%</td>
|
||||
<td data-value="12" class="abs medium">7/12</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="../../prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="../../sorter.js"></script>
|
||||
<script src="../../block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for src</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../prettify.css" />
|
||||
<link rel="stylesheet" href="../base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(../sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="../index.html">All files</a> src</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>1/1</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>1/1</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line high'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td class="file high" data-value="translations.ts"><a href="translations.ts.html">translations.ts</a></td>
|
||||
<td data-value="100" class="pic high">
|
||||
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||
</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="1" class="abs high">1/1</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="0" class="abs high">0/0</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="0" class="abs high">0/0</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="1" class="abs high">1/1</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="../prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="../sorter.js"></script>
|
||||
<script src="../block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for src/translations.ts</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../prettify.css" />
|
||||
<link rel="stylesheet" href="../base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(../sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="../index.html">All files</a> / <a href="index.html">src</a> translations.ts</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>1/1</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>1/1</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line high'></div>
|
||||
<pre><table class="coverage">
|
||||
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||
<a name='L2'></a><a href='#L2'>2</a>
|
||||
<a name='L3'></a><a href='#L3'>3</a>
|
||||
<a name='L4'></a><a href='#L4'>4</a>
|
||||
<a name='L5'></a><a href='#L5'>5</a>
|
||||
<a name='L6'></a><a href='#L6'>6</a>
|
||||
<a name='L7'></a><a href='#L7'>7</a>
|
||||
<a name='L8'></a><a href='#L8'>8</a>
|
||||
<a name='L9'></a><a href='#L9'>9</a>
|
||||
<a name='L10'></a><a href='#L10'>10</a>
|
||||
<a name='L11'></a><a href='#L11'>11</a>
|
||||
<a name='L12'></a><a href='#L12'>12</a>
|
||||
<a name='L13'></a><a href='#L13'>13</a>
|
||||
<a name='L14'></a><a href='#L14'>14</a>
|
||||
<a name='L15'></a><a href='#L15'>15</a>
|
||||
<a name='L16'></a><a href='#L16'>16</a>
|
||||
<a name='L17'></a><a href='#L17'>17</a>
|
||||
<a name='L18'></a><a href='#L18'>18</a>
|
||||
<a name='L19'></a><a href='#L19'>19</a>
|
||||
<a name='L20'></a><a href='#L20'>20</a>
|
||||
<a name='L21'></a><a href='#L21'>21</a>
|
||||
<a name='L22'></a><a href='#L22'>22</a>
|
||||
<a name='L23'></a><a href='#L23'>23</a>
|
||||
<a name='L24'></a><a href='#L24'>24</a>
|
||||
<a name='L25'></a><a href='#L25'>25</a>
|
||||
<a name='L26'></a><a href='#L26'>26</a>
|
||||
<a name='L27'></a><a href='#L27'>27</a>
|
||||
<a name='L28'></a><a href='#L28'>28</a>
|
||||
<a name='L29'></a><a href='#L29'>29</a>
|
||||
<a name='L30'></a><a href='#L30'>30</a>
|
||||
<a name='L31'></a><a href='#L31'>31</a>
|
||||
<a name='L32'></a><a href='#L32'>32</a>
|
||||
<a name='L33'></a><a href='#L33'>33</a>
|
||||
<a name='L34'></a><a href='#L34'>34</a>
|
||||
<a name='L35'></a><a href='#L35'>35</a>
|
||||
<a name='L36'></a><a href='#L36'>36</a>
|
||||
<a name='L37'></a><a href='#L37'>37</a>
|
||||
<a name='L38'></a><a href='#L38'>38</a>
|
||||
<a name='L39'></a><a href='#L39'>39</a>
|
||||
<a name='L40'></a><a href='#L40'>40</a>
|
||||
<a name='L41'></a><a href='#L41'>41</a>
|
||||
<a name='L42'></a><a href='#L42'>42</a>
|
||||
<a name='L43'></a><a href='#L43'>43</a>
|
||||
<a name='L44'></a><a href='#L44'>44</a>
|
||||
<a name='L45'></a><a href='#L45'>45</a>
|
||||
<a name='L46'></a><a href='#L46'>46</a>
|
||||
<a name='L47'></a><a href='#L47'>47</a>
|
||||
<a name='L48'></a><a href='#L48'>48</a>
|
||||
<a name='L49'></a><a href='#L49'>49</a>
|
||||
<a name='L50'></a><a href='#L50'>50</a>
|
||||
<a name='L51'></a><a href='#L51'>51</a>
|
||||
<a name='L52'></a><a href='#L52'>52</a>
|
||||
<a name='L53'></a><a href='#L53'>53</a>
|
||||
<a name='L54'></a><a href='#L54'>54</a>
|
||||
<a name='L55'></a><a href='#L55'>55</a>
|
||||
<a name='L56'></a><a href='#L56'>56</a>
|
||||
<a name='L57'></a><a href='#L57'>57</a>
|
||||
<a name='L58'></a><a href='#L58'>58</a>
|
||||
<a name='L59'></a><a href='#L59'>59</a>
|
||||
<a name='L60'></a><a href='#L60'>60</a>
|
||||
<a name='L61'></a><a href='#L61'>61</a>
|
||||
<a name='L62'></a><a href='#L62'>62</a>
|
||||
<a name='L63'></a><a href='#L63'>63</a>
|
||||
<a name='L64'></a><a href='#L64'>64</a>
|
||||
<a name='L65'></a><a href='#L65'>65</a>
|
||||
<a name='L66'></a><a href='#L66'>66</a>
|
||||
<a name='L67'></a><a href='#L67'>67</a>
|
||||
<a name='L68'></a><a href='#L68'>68</a>
|
||||
<a name='L69'></a><a href='#L69'>69</a>
|
||||
<a name='L70'></a><a href='#L70'>70</a>
|
||||
<a name='L71'></a><a href='#L71'>71</a>
|
||||
<a name='L72'></a><a href='#L72'>72</a>
|
||||
<a name='L73'></a><a href='#L73'>73</a>
|
||||
<a name='L74'></a><a href='#L74'>74</a>
|
||||
<a name='L75'></a><a href='#L75'>75</a>
|
||||
<a name='L76'></a><a href='#L76'>76</a>
|
||||
<a name='L77'></a><a href='#L77'>77</a>
|
||||
<a name='L78'></a><a href='#L78'>78</a>
|
||||
<a name='L79'></a><a href='#L79'>79</a>
|
||||
<a name='L80'></a><a href='#L80'>80</a>
|
||||
<a name='L81'></a><a href='#L81'>81</a>
|
||||
<a name='L82'></a><a href='#L82'>82</a>
|
||||
<a name='L83'></a><a href='#L83'>83</a>
|
||||
<a name='L84'></a><a href='#L84'>84</a>
|
||||
<a name='L85'></a><a href='#L85'>85</a>
|
||||
<a name='L86'></a><a href='#L86'>86</a>
|
||||
<a name='L87'></a><a href='#L87'>87</a>
|
||||
<a name='L88'></a><a href='#L88'>88</a>
|
||||
<a name='L89'></a><a href='#L89'>89</a>
|
||||
<a name='L90'></a><a href='#L90'>90</a>
|
||||
<a name='L91'></a><a href='#L91'>91</a>
|
||||
<a name='L92'></a><a href='#L92'>92</a>
|
||||
<a name='L93'></a><a href='#L93'>93</a>
|
||||
<a name='L94'></a><a href='#L94'>94</a>
|
||||
<a name='L95'></a><a href='#L95'>95</a>
|
||||
<a name='L96'></a><a href='#L96'>96</a>
|
||||
<a name='L97'></a><a href='#L97'>97</a>
|
||||
<a name='L98'></a><a href='#L98'>98</a>
|
||||
<a name='L99'></a><a href='#L99'>99</a>
|
||||
<a name='L100'></a><a href='#L100'>100</a>
|
||||
<a name='L101'></a><a href='#L101'>101</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">export type Language = 'en' | 'cs';
|
||||
|
||||
export const t = {
|
||||
en: {
|
||||
sidebar: {
|
||||
favorites: 'Favorites',
|
||||
lakes: 'Lakes',
|
||||
map: 'Map',
|
||||
settings: 'Settings'
|
||||
},
|
||||
topbar: {
|
||||
search: 'Search river or reservoir (e.g. Lipno)...',
|
||||
updated: 'Last updated:'
|
||||
},
|
||||
kpi: {
|
||||
level: 'WATER LEVEL',
|
||||
flow: 'FLOW RATE',
|
||||
inflow: 'Inflow',
|
||||
outflow: 'Outflow',
|
||||
fullness: 'STORAGE LEVEL',
|
||||
volume: 'Volume'
|
||||
},
|
||||
chart: {
|
||||
title: 'Long-term development',
|
||||
timeframe: 'Timeframe',
|
||||
timeframeMobile: 'Time',
|
||||
view: 'View',
|
||||
raw: 'Raw data',
|
||||
smoothed: 'Smoothed',
|
||||
calendar: 'Calendar',
|
||||
all: 'All',
|
||||
year: 'Year',
|
||||
level: 'Water level',
|
||||
inflow: 'Inflow',
|
||||
outflow: 'Outflow',
|
||||
maxLevel: 'Max retention level',
|
||||
storageLevel: 'Storage space level',
|
||||
dataSources: 'Data sources:',
|
||||
createdIn: 'Created with ♥ in the Czech Republic'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
theme: 'Theme',
|
||||
dark: 'Dark',
|
||||
light: 'Light',
|
||||
language: 'Language',
|
||||
english: 'English',
|
||||
czech: 'Čeština',
|
||||
buyCoffee: 'Buy Me a Coffee'
|
||||
}
|
||||
},
|
||||
cs: {
|
||||
sidebar: {
|
||||
favorites: 'Oblíbené',
|
||||
lakes: 'Jezera',
|
||||
map: 'Mapa',
|
||||
settings: 'Nastavení'
|
||||
},
|
||||
topbar: {
|
||||
search: 'Hledat tok nebo nádrž (např. Lipno)...',
|
||||
updated: 'Aktualizováno:'
|
||||
},
|
||||
kpi: {
|
||||
level: 'HLADINA',
|
||||
flow: 'PRŮTOK',
|
||||
inflow: 'Přítok',
|
||||
outflow: 'Odtok',
|
||||
fullness: 'ZÁSOBNÍ PROSTOR',
|
||||
volume: 'Objem'
|
||||
},
|
||||
chart: {
|
||||
title: 'Dlouhodobý vývoj',
|
||||
timeframe: 'Časové období',
|
||||
timeframeMobile: 'Časové',
|
||||
view: 'Zobrazení',
|
||||
raw: 'Syrová data',
|
||||
smoothed: 'Vyhlazená',
|
||||
calendar: 'Kalendář',
|
||||
all: 'Vše',
|
||||
year: 'Rok',
|
||||
level: 'Hladina',
|
||||
inflow: 'Přítok',
|
||||
outflow: 'Odtok',
|
||||
maxLevel: 'Max. retenční hladina',
|
||||
storageLevel: 'Hladina zásobního prostoru',
|
||||
dataSources: 'Zdroje dat:',
|
||||
createdIn: 'Vytvořeno s ♥ v České republice'
|
||||
},
|
||||
settings: {
|
||||
title: 'Nastavení',
|
||||
theme: 'Vzhled',
|
||||
dark: 'Tmavý',
|
||||
light: 'Světlý',
|
||||
language: 'Jazyk',
|
||||
english: 'English',
|
||||
czech: 'Čeština',
|
||||
buyCoffee: 'Kup mi kávu'
|
||||
}
|
||||
}
|
||||
};
|
||||
</pre></td></tr></table></pre>
|
||||
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-06-05T21:03:50.324Z
|
||||
</div>
|
||||
<script src="../prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="../sorter.js"></script>
|
||||
<script src="../block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+6
-1
@@ -4,7 +4,12 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/jpeg" href="/favicon.jpg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Davis Fencl</title>
|
||||
<title>Hladinátor - Aktuální stav přehrad a nádrží</title>
|
||||
<meta name="description" content="Sledujte aktuální vodní stav, průtok a vývoj počasí na českých přehradách a nádržích v reálném čase." />
|
||||
<meta property="og:title" content="Hladinátor - Aktuální stav přehrad a nádrží" />
|
||||
<meta property="og:description" content="Sledujte aktuální vodní stav, průtok a vývoj počasí na českých přehradách a nádržích v reálném čase." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://hladinator.cz" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+48
-1
@@ -14,6 +14,7 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-helmet-async": "^3.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-router-dom": "^7.9.6",
|
||||
@@ -3936,6 +3937,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -4016,7 +4026,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -4219,6 +4228,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -4688,6 +4709,26 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-helmet-async": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-3.0.0.tgz",
|
||||
"integrity": "sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"shallowequal": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
@@ -4957,6 +4998,12 @@
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-helmet-async": "^3.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-router-dom": "^7.9.6",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3545
-3248
File diff suppressed because it is too large
Load Diff
+3577
-3280
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
[]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
[]
|
||||
File diff suppressed because it is too large
Load Diff
+3538
-3241
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3554
-3257
File diff suppressed because it is too large
Load Diff
+3549
-3252
File diff suppressed because it is too large
Load Diff
+3558
-3261
File diff suppressed because it is too large
Load Diff
+3559
-3262
File diff suppressed because it is too large
Load Diff
+3590
-3284
File diff suppressed because it is too large
Load Diff
+3575
-3269
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
[]
|
||||
File diff suppressed because it is too large
Load Diff
+1026
-243
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 582 KiB |
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://hladinator.cz/sitemap.xml
|
||||
+1449
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import https from 'https';
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
const URL = 'https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?oid=1&id=VLL1';
|
||||
|
||||
async function testPostback() {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const res = await axios.get(URL, { httpsAgent: agent, timeout: 10000 });
|
||||
const $ = cheerio.load(res.data);
|
||||
|
||||
const viewstate = $('#__VIEWSTATE').val();
|
||||
const viewstategenerator = $('#__VIEWSTATEGENERATOR').val();
|
||||
const eventvalidation = $('#__EVENTVALIDATION').val();
|
||||
|
||||
// Try to POST for monthly data
|
||||
const postData = new URLSearchParams();
|
||||
postData.append('__EVENTTARGET', 'ctl00$ObsahCPH$PrechodNaBilancniData');
|
||||
postData.append('__EVENTARGUMENT', '');
|
||||
postData.append('__VIEWSTATE', viewstate as string);
|
||||
postData.append('__VIEWSTATEGENERATOR', viewstategenerator as string);
|
||||
postData.append('__EVENTVALIDATION', eventvalidation as string);
|
||||
|
||||
const postRes = await axios.post(URL, postData.toString(), {
|
||||
httpsAgent: agent,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync('pvl_raw_month.html', postRes.data);
|
||||
console.log('Saved monthly data to pvl_raw_month.html');
|
||||
|
||||
const $post = cheerio.load(postRes.data);
|
||||
const rows = $post('table.tabulka-seznam tr:not(:first-child)');
|
||||
console.log(`Found ${rows.length} rows in the table.`);
|
||||
if (rows.length > 0) {
|
||||
const firstRow = rows.first().find('td').first().text().trim();
|
||||
const lastRow = rows.last().find('td').first().text().trim();
|
||||
console.log(`Date range: ${firstRow} to ${lastRow}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
testPostback().catch(console.error);
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateLakeMetrics, LakeCalculationConfig } from '../utils/calculations';
|
||||
|
||||
describe('calculateLakeMetrics', () => {
|
||||
const config: LakeCalculationConfig = {
|
||||
minLevel: 100,
|
||||
maxLevel: 110,
|
||||
storageLevel: 108,
|
||||
maxVolume: 50,
|
||||
};
|
||||
|
||||
it('should calculate capacity based on reported volume when available', () => {
|
||||
// 25 / 50 = 50%
|
||||
const result = calculateLakeMetrics(105, 25, config);
|
||||
expect(result.capacity).toBe(50);
|
||||
expect(result.volume).toBe(25);
|
||||
});
|
||||
|
||||
it('should cap capacity at 100% when volume exceeds maxVolume', () => {
|
||||
const result = calculateLakeMetrics(111, 55, config);
|
||||
expect(result.capacity).toBe(100);
|
||||
expect(result.volume).toBe(55);
|
||||
});
|
||||
|
||||
it('should floor capacity at 0% when volume is negative', () => {
|
||||
const result = calculateLakeMetrics(99, -5, config);
|
||||
expect(result.capacity).toBe(0);
|
||||
expect(result.volume).toBe(-5);
|
||||
});
|
||||
|
||||
it('should estimate capacity and volume from level when reported volume is 0', () => {
|
||||
// Level 105 is exactly halfway between 100 and 110 -> 50%
|
||||
// 50% of 50 maxVolume = 25
|
||||
const result = calculateLakeMetrics(105, 0, config);
|
||||
expect(result.capacity).toBe(50);
|
||||
expect(result.volume).toBe(25);
|
||||
});
|
||||
|
||||
it('should cap estimated capacity at 100% when level exceeds maxLevel', () => {
|
||||
const result = calculateLakeMetrics(115, 0, config);
|
||||
expect(result.capacity).toBe(100);
|
||||
expect(result.volume).toBe(50); // 100% of 50
|
||||
});
|
||||
|
||||
it('should floor estimated capacity at 0% when level is below minLevel', () => {
|
||||
const result = calculateLakeMetrics(90, 0, config);
|
||||
expect(result.capacity).toBe(0);
|
||||
expect(result.volume).toBe(0); // 0% of 50
|
||||
});
|
||||
|
||||
it('should correctly calculate storageDiff', () => {
|
||||
const result = calculateLakeMetrics(106, 25, config);
|
||||
// 106 - 108 = -2.00
|
||||
expect(result.storageDiff).toBe(-2);
|
||||
});
|
||||
|
||||
it('should calculate positive storageDiff when above storageLevel', () => {
|
||||
const result = calculateLakeMetrics(109, 25, config);
|
||||
// 109 - 108 = 1.00
|
||||
expect(result.storageDiff).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle missing config gracefully', () => {
|
||||
const emptyConfig: LakeCalculationConfig = {};
|
||||
const result = calculateLakeMetrics(105, 0, emptyConfig);
|
||||
expect(result.capacity).toBe(0);
|
||||
expect(result.volume).toBe(0);
|
||||
expect(result.storageDiff).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface LakeConfig {
|
||||
id: string;
|
||||
text: string;
|
||||
priority?: boolean;
|
||||
coords: [number, number];
|
||||
maxVolume?: number;
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
storageLevel?: number;
|
||||
navigationForbidden?: boolean;
|
||||
}
|
||||
|
||||
// Preserve existing minLevel, maxLevel, storageLevel that were scraped from PVL.
|
||||
// Only update maxVolume, coords, and navigationForbidden.
|
||||
import { lakesConfig as oldConfig } from './lakesConfig';
|
||||
|
||||
const exactData: Record<string, Partial<LakeConfig>> = {
|
||||
"VLL1|1": { maxVolume: 306.0, coords: [48.6322, 14.2215], navigationForbidden: false },
|
||||
"VLL2|1": { maxVolume: 1.6, coords: [48.6250, 14.3180], navigationForbidden: false },
|
||||
"VLHN|1": { maxVolume: 21.1, coords: [49.1830, 14.4440], navigationForbidden: false },
|
||||
"VLKO|1": { maxVolume: 2.8, coords: [49.2550, 14.3980], navigationForbidden: false },
|
||||
"VLOR|2": { maxVolume: 716.5, coords: [49.6060, 14.1700], navigationForbidden: false },
|
||||
"VLSL|2": { maxVolume: 269.3, coords: [49.8220, 14.4360], navigationForbidden: false },
|
||||
"VLST|2": { maxVolume: 11.2, coords: [49.8450, 14.4120], navigationForbidden: false },
|
||||
"MARI|1": { maxVolume: 33.8, coords: [48.8470, 14.4870], navigationForbidden: true },
|
||||
"MZHR|3": { maxVolume: 56.7, coords: [49.7890, 13.1550], navigationForbidden: false },
|
||||
"ZESV|2": { maxVolume: 266.6, coords: [49.7040, 15.1150], navigationForbidden: true },
|
||||
"VLKA|2": { maxVolume: 12.8, coords: [49.6380, 14.2580], navigationForbidden: false },
|
||||
"VLVE|2": { maxVolume: 11.1, coords: [49.9390, 14.3910], navigationForbidden: false },
|
||||
"BLHU|1": { maxVolume: 5.7, coords: [49.0270, 13.9870], navigationForbidden: true },
|
||||
"UHNY|3": { maxVolume: 16.0, coords: [49.2610, 13.1230], navigationForbidden: true },
|
||||
"KCKC|3": { maxVolume: 9.3, coords: [50.0630, 13.9310], navigationForbidden: true },
|
||||
"KLKL|3": { maxVolume: 1.5, coords: [49.7540, 13.5640], navigationForbidden: false },
|
||||
"RACU|3": { maxVolume: 5.5, coords: [49.7150, 13.3640], navigationForbidden: false },
|
||||
"TRTR|2": { maxVolume: 4.1, coords: [49.5260, 15.1950], navigationForbidden: false },
|
||||
"HESE|2": { maxVolume: 1.9, coords: [49.5070, 15.2630], navigationForbidden: false },
|
||||
"MZLU|3": { maxVolume: 2.3, coords: [49.8050, 12.6390], navigationForbidden: true },
|
||||
"STZL|3": { maxVolume: 14.5, coords: [50.0930, 13.1360], navigationForbidden: true },
|
||||
"PPPI|3": { maxVolume: 1.6, coords: [49.6910, 13.9570], navigationForbidden: true },
|
||||
"LILA|3": { maxVolume: 0.8, coords: [49.6640, 13.8820], navigationForbidden: true },
|
||||
"OPOB|3": { maxVolume: 0.6, coords: [49.7110, 13.9370], navigationForbidden: true },
|
||||
"STST|2": { maxVolume: 1.0, coords: [49.7910, 14.0040], navigationForbidden: false },
|
||||
"HEVR|2": { maxVolume: 0.5, coords: [49.5070, 15.2440], navigationForbidden: false },
|
||||
"CRSO|1": { maxVolume: 1.4, coords: [48.7750, 14.5360], navigationForbidden: false },
|
||||
"SCHU|1": { maxVolume: 0.8, coords: [48.7840, 14.7350], navigationForbidden: false },
|
||||
"SVSV|2": { maxVolume: 1.2, coords: [49.5750, 15.9520], navigationForbidden: true },
|
||||
"SAPI|2": { maxVolume: 1.5, coords: [49.5930, 15.9320], navigationForbidden: false },
|
||||
"SMSM|3": { maxVolume: 0.7, coords: [49.8970, 14.0580], navigationForbidden: false },
|
||||
"CPZA|3": { maxVolume: 0.5, coords: [49.8050, 13.8510], navigationForbidden: false },
|
||||
"BIBI|1": { maxVolume: 0.3, coords: [49.1670, 14.0410], navigationForbidden: false },
|
||||
"SPKA|1": { maxVolume: 0.3, coords: [48.9740, 14.5450], navigationForbidden: false },
|
||||
"SPNE|2": { maxVolume: 0.4, coords: [49.7710, 15.1760], navigationForbidden: false },
|
||||
"SPZH|1": { maxVolume: 0.2, coords: [49.2310, 15.3120], navigationForbidden: true },
|
||||
"KLDP|3": { maxVolume: 0.5, coords: [49.6640, 13.7530], navigationForbidden: true },
|
||||
"KLHP|3": { maxVolume: 0.7, coords: [49.6550, 13.7610], navigationForbidden: true },
|
||||
"CPDR|3": { maxVolume: 0.1, coords: [49.8050, 13.8550], navigationForbidden: false },
|
||||
};
|
||||
|
||||
function main() {
|
||||
const updated = oldConfig.map(lake => {
|
||||
const fresh = exactData[lake.id];
|
||||
if (fresh) {
|
||||
return {
|
||||
...lake,
|
||||
maxVolume: fresh.maxVolume,
|
||||
coords: fresh.coords,
|
||||
navigationForbidden: fresh.navigationForbidden
|
||||
};
|
||||
}
|
||||
return lake;
|
||||
});
|
||||
|
||||
let newContent = `export interface LakeConfig {
|
||||
id: string;
|
||||
text: string;
|
||||
priority?: boolean;
|
||||
coords: [number, number];
|
||||
maxVolume?: number;
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
storageLevel?: number;
|
||||
navigationForbidden?: boolean;
|
||||
}
|
||||
|
||||
export const lakesConfig: LakeConfig[] = [
|
||||
`;
|
||||
updated.forEach((l, idx) => {
|
||||
newContent += ` { id: "${l.id}", text: "${l.text}", ${l.priority ? 'priority: true, ' : ''}coords: [${l.coords[0].toFixed(4)}, ${l.coords[1].toFixed(4)}], maxVolume: ${l.maxVolume}, minLevel: ${l.minLevel}, maxLevel: ${l.maxLevel}, storageLevel: ${l.storageLevel}, navigationForbidden: ${l.navigationForbidden} }${idx === updated.length - 1 ? '' : ','}\n`;
|
||||
});
|
||||
newContent += `];\n`;
|
||||
|
||||
fs.writeFileSync(path.resolve('./scripts/lakesConfig.ts'), newContent);
|
||||
console.log("lakesConfig.ts updated with precise static data and navigation limits!");
|
||||
}
|
||||
|
||||
main();
|
||||
+6
-18
@@ -1,6 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { lakesConfig } from './lakesConfig';
|
||||
import { calculateLakeMetrics } from './utils/calculations';
|
||||
|
||||
interface DataRecord {
|
||||
timestamp: string;
|
||||
@@ -33,7 +34,7 @@ const lakes = lakesConfig.map(lake => {
|
||||
|
||||
// Take up to 12 last records for sparkline
|
||||
const recentData = data.slice(-12);
|
||||
sparkline = recentData.map(d => (d.flow === null || isNaN(d.flow) ? 0 : d.flow));
|
||||
sparkline = recentData.map(d => (d.level === null || isNaN(d.level) ? 0 : d.level));
|
||||
|
||||
// Pad with zeros if less than 12
|
||||
while (sparkline.length < 12) {
|
||||
@@ -53,20 +54,7 @@ const lakes = lakesConfig.map(lake => {
|
||||
}
|
||||
}
|
||||
|
||||
if (lake.minLevel && lake.maxLevel && currentLevel > 0) {
|
||||
const percentage = ((currentLevel - lake.minLevel) / (lake.maxLevel - lake.minLevel)) * 100;
|
||||
capacity = Math.max(0, Math.min(100, Math.round(percentage * 10) / 10)); // Round to 1 decimal place
|
||||
if (volume === 0) {
|
||||
volume = Number(((capacity / 100) * (lake.maxVolume || 0)).toFixed(1));
|
||||
}
|
||||
} else {
|
||||
if (volume === 0) volume = lake.maxVolume || 0;
|
||||
}
|
||||
|
||||
let storageDiff = 0;
|
||||
if (lake.storageLevel && currentLevel > 0) {
|
||||
storageDiff = Number((currentLevel - lake.storageLevel).toFixed(2));
|
||||
}
|
||||
const metrics = calculateLakeMetrics(currentLevel, volume, lake);
|
||||
|
||||
return {
|
||||
id: lake.id,
|
||||
@@ -74,11 +62,11 @@ const lakes = lakesConfig.map(lake => {
|
||||
river: lake.text.includes('-') ? lake.text.split('-')[1].trim() : '',
|
||||
priority: lake.priority || false,
|
||||
level: currentLevel.toFixed(2),
|
||||
capacity: capacity,
|
||||
storageDiff: storageDiff,
|
||||
capacity: metrics.capacity,
|
||||
storageDiff: metrics.storageDiff,
|
||||
inflow: inflow.toFixed(1),
|
||||
outflow: currentFlow.toFixed(1),
|
||||
volume: volume,
|
||||
volume: metrics.volume,
|
||||
maxVolume: lake.maxVolume || 0,
|
||||
lat: lake.coords[0],
|
||||
lng: lake.coords[1],
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import https from 'https';
|
||||
|
||||
const ALL_LAKES = [
|
||||
{"href": "Mereni.aspx?id=BIBI&oid=1", "text": "VD Bílsko"},
|
||||
{"href": "Mereni.aspx?id=RACU&oid=3", "text": "VD České Údolí"},
|
||||
{"href": "Mereni.aspx?id=KLDP&oid=3", "text": "VD Dolejší Padrťský rybník"},
|
||||
{"href": "Mereni.aspx?id=CPDR&oid=3", "text": "VD Dráteník"},
|
||||
{"href": "Mereni.aspx?id=KLHP&oid=3", "text": "VD Hořejší Padrťský rybník"},
|
||||
{"href": "Mereni.aspx?id=SCHU&oid=1", "text": "VD Humenice"},
|
||||
{"href": "Mereni.aspx?id=BLHU&oid=1", "text": "VD Husinec"},
|
||||
{"href": "Mereni.aspx?id=VLKA&oid=2", "text": "VD Kamýk"},
|
||||
{"href": "Mereni.aspx?id=SPKA&oid=1", "text": "VD Karhof"},
|
||||
{"href": "Mereni.aspx?id=KLKL&oid=3", "text": "VD Klabava"},
|
||||
{"href": "Mereni.aspx?id=KCKC&oid=3", "text": "VD Klíčava"},
|
||||
{"href": "Mereni.aspx?id=LILA&oid=3", "text": "VD Láz"},
|
||||
{"href": "Mereni.aspx?id=MZLU&oid=3", "text": "VD Lučina"},
|
||||
{"href": "Mereni.aspx?id=SPNE&oid=2", "text": "VD Němčice"},
|
||||
{"href": "Mereni.aspx?id=UHNY&oid=3", "text": "VD Nýrsko"},
|
||||
{"href": "Mereni.aspx?id=OPOB&oid=3", "text": "VD Obecnice"},
|
||||
{"href": "Mereni.aspx?id=PPPI&oid=3", "text": "VD Pilská (u Příbramě)"},
|
||||
{"href": "Mereni.aspx?id=SAPI&oid=2", "text": "VD Pilská u Žďáru"},
|
||||
{"href": "Mereni.aspx?id=HESE&oid=2", "text": "VD Sedlice"},
|
||||
{"href": "Mereni.aspx?id=CRSO&oid=1", "text": "VD Soběnov"},
|
||||
{"href": "Mereni.aspx?id=SVSV&oid=2", "text": "VD Staviště"},
|
||||
{"href": "Mereni.aspx?id=STST&oid=2", "text": "VD Strž"},
|
||||
{"href": "Mereni.aspx?id=SMSM&oid=3", "text": "VD Suchomasty"},
|
||||
{"href": "Mereni.aspx?id=ZESV&oid=2", "text": "VD Švihov (Želivka)"},
|
||||
{"href": "Mereni.aspx?id=TRTR&oid=2", "text": "VD Trnávka"},
|
||||
{"href": "Mereni.aspx?id=VLVE&oid=2", "text": "VD Vrané"},
|
||||
{"href": "Mereni.aspx?id=HEVR&oid=2", "text": "VD Vřesník"},
|
||||
{"href": "Mereni.aspx?id=CPZA&oid=3", "text": "VD Záskalská"},
|
||||
{"href": "Mereni.aspx?id=SPZH&oid=1", "text": "VD Zhejral"},
|
||||
{"href": "Mereni.aspx?id=STZL&oid=3", "text": "VD Žlutice"}
|
||||
];
|
||||
|
||||
async function checkLakes() {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const validLakes: any[] = [];
|
||||
|
||||
for (const lake of ALL_LAKES) {
|
||||
const url = `https://www.pvl.cz/portal/nadrze/cz/pc/${lake.href}`;
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
httpsAgent: agent,
|
||||
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||
});
|
||||
const $ = cheerio.load(response.data);
|
||||
let hasHistory = false;
|
||||
let hasInflow = false;
|
||||
|
||||
$('table').each((i, tbl) => {
|
||||
const text = $(tbl).text();
|
||||
if (text.includes('Aktuální hodnoty') && text.includes('Přítok')) {
|
||||
hasInflow = true;
|
||||
}
|
||||
if (text.includes('Datum') && text.includes('Odtok')) {
|
||||
const rows = $(tbl).find('tr').length;
|
||||
if (rows > 2) hasHistory = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasHistory && hasInflow) {
|
||||
validLakes.push(lake);
|
||||
console.log(`[VALID] ${lake.text}`);
|
||||
} else {
|
||||
console.log(`[INVALID] ${lake.text} (Hist:${hasHistory}, In:${hasInflow})`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[ERROR] ${lake.text}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\\n--- SUMMARY OF VALID LAKES ---');
|
||||
console.log(JSON.stringify(validLakes, null, 2));
|
||||
}
|
||||
|
||||
checkLakes();
|
||||
@@ -0,0 +1,29 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import https from 'https';
|
||||
|
||||
async function checkMap() {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
try {
|
||||
const response = await axios.get('https://www.pvl.cz/portal/nadrze/cz/pc/Prehled.aspx', {
|
||||
httpsAgent: agent,
|
||||
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||
});
|
||||
const html = response.data;
|
||||
|
||||
// Look for variables or inline JSON with coordinates
|
||||
const scriptMatches = html.match(/<script\\b[^>]*>([\\s\\S]*?)<\\/script>/gi);
|
||||
if (scriptMatches) {
|
||||
scriptMatches.forEach((m: string, i: number) => {
|
||||
if (m.includes('lat') || m.includes('Lng') || m.includes('Points') || m.includes('Markers')) {
|
||||
console.log("Found something in script " + i);
|
||||
console.log(m.substring(0, 500)); // preview
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
checkMap();
|
||||
@@ -0,0 +1,33 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import https from 'https';
|
||||
|
||||
async function fetchLakes() {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
try {
|
||||
const response = await axios.get('https://www.pvl.cz/portal/nadrze/cz/pc/Prehled.aspx', {
|
||||
httpsAgent: agent,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0'
|
||||
}
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data);
|
||||
const lakes: any[] = [];
|
||||
|
||||
// Links to lakes usually look like Mereni.aspx?oid=xxx&id=yyy
|
||||
$('a[href^="Mereni.aspx"]').each((i, el) => {
|
||||
const href = $(el).attr('href');
|
||||
const text = $(el).text().trim();
|
||||
if (href && text) {
|
||||
lakes.push({ href, text });
|
||||
}
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(lakes, null, 2));
|
||||
} catch (err: any) {
|
||||
console.error('Error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
fetchLakes();
|
||||
@@ -0,0 +1,103 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { lakesConfig } from './lakesConfig';
|
||||
|
||||
async function fixLevels() {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const updatedConfig = [...lakesConfig];
|
||||
|
||||
for (let i = 0; i < updatedConfig.length; i++) {
|
||||
const lake = updatedConfig[i];
|
||||
// id is like SPKA|1 -> internalId is SPKA, oid is 1
|
||||
const parts = lake.id.split('|');
|
||||
if (parts.length !== 2) continue;
|
||||
|
||||
const internalId = parts[0];
|
||||
const oid = parts[1];
|
||||
|
||||
const url = `https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?id=${internalId}&oid=${oid}`;
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
httpsAgent: agent,
|
||||
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||
});
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
let maxRet: number | null = null;
|
||||
let minStale: number | null = null;
|
||||
let maxVol: number | null = null;
|
||||
|
||||
$('table').each((_, tbl) => {
|
||||
const text = $(tbl).text();
|
||||
|
||||
// Parse levels
|
||||
if (text.includes('Maximální retenční hladina:')) {
|
||||
$(tbl).find('tr').each((_, row) => {
|
||||
const rowText = $(row).text().replace(/\\s+/g, ' ');
|
||||
if (rowText.includes('Maximální retenční hladina:')) {
|
||||
const val = parseFloat(rowText.replace('Maximální retenční hladina:', '').replace('[m n.m.]', '').replace(',', '.').trim());
|
||||
if (!isNaN(val)) maxRet = val;
|
||||
}
|
||||
if (rowText.includes('Hladina stálého nadržení:')) {
|
||||
const val = parseFloat(rowText.replace('Hladina stálého nadržení:', '').replace('[m n.m.]', '').replace(',', '.').trim());
|
||||
if (!isNaN(val)) minStale = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Parse volume (this is current volume, wait, does PVL show max volume? Usually no, but current volume might be bigger than our guessed maxVolume)
|
||||
if (text.includes('Aktuální hodnoty') && text.includes('Objem')) {
|
||||
$(tbl).find('tr').each((_, row) => {
|
||||
const rowText = $(row).text().replace(/\\s+/g, ' ');
|
||||
if (rowText.includes('Objem [mil. m3]')) {
|
||||
const valStr = $(row).find('td').eq(1).text().trim().replace(',', '.');
|
||||
const val = parseFloat(valStr);
|
||||
if (!isNaN(val)) maxVol = val; // We will just use the current volume as a baseline if it's bigger than our maxVolume
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (maxRet) updatedConfig[i].maxLevel = maxRet;
|
||||
if (minStale) updatedConfig[i].minLevel = minStale;
|
||||
|
||||
// For volume, if the current volume is larger than the configured maxVolume, increase maxVolume
|
||||
if (maxVol && updatedConfig[i].maxVolume && maxVol > updatedConfig[i].maxVolume!) {
|
||||
updatedConfig[i].maxVolume = Math.ceil(maxVol * 1.2 * 10) / 10; // add 20% buffer
|
||||
} else if (maxVol && !updatedConfig[i].maxVolume) {
|
||||
updatedConfig[i].maxVolume = Math.ceil(maxVol * 1.5 * 10) / 10;
|
||||
}
|
||||
|
||||
console.log(`Updated ${lake.text}: min=${minStale}, max=${maxRet}, vol=${maxVol} -> newMaxVol=${updatedConfig[i].maxVolume}`);
|
||||
} catch (err: any) {
|
||||
console.error(`Failed for ${lake.text}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new file content
|
||||
let newContent = `export interface LakeConfig {
|
||||
id: string;
|
||||
text: string;
|
||||
priority?: boolean;
|
||||
coords: [number, number];
|
||||
maxVolume?: number;
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
storageLevel?: number;
|
||||
}
|
||||
|
||||
export const lakesConfig: LakeConfig[] = [
|
||||
`;
|
||||
updatedConfig.forEach((l, idx) => {
|
||||
newContent += ` { id: "${l.id}", text: "${l.text}", ${l.priority ? 'priority: true, ' : ''}coords: [${l.coords[0].toFixed(4)}, ${l.coords[1].toFixed(4)}], maxVolume: ${l.maxVolume}, minLevel: ${l.minLevel}, maxLevel: ${l.maxLevel}, storageLevel: ${l.storageLevel} }${idx === updatedConfig.length - 1 ? '' : ','}\\n`;
|
||||
});
|
||||
newContent += `];\\n`;
|
||||
|
||||
fs.writeFileSync(path.resolve('./scripts/lakesConfig.ts'), newContent);
|
||||
console.log("lakesConfig.ts updated!");
|
||||
}
|
||||
|
||||
fixLevels();
|
||||
@@ -0,0 +1,101 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { lakesConfig } from './lakesConfig';
|
||||
|
||||
async function fixStorageLevels() {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const updatedConfig = [...lakesConfig];
|
||||
|
||||
for (let i = 0; i < updatedConfig.length; i++) {
|
||||
const lake = updatedConfig[i];
|
||||
const parts = lake.id.split('|');
|
||||
if (parts.length !== 2) continue;
|
||||
|
||||
const internalId = parts[0];
|
||||
const oid = parts[1];
|
||||
|
||||
const url = `https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?id=${internalId}&oid=${oid}`;
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
httpsAgent: agent,
|
||||
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||
});
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
let storageLevelFound: number | null = null;
|
||||
let maxVol: number | null = null;
|
||||
|
||||
$('table').each((_, tbl) => {
|
||||
const text = $(tbl).text();
|
||||
|
||||
// Parse storage level
|
||||
if (text.includes('Hladina zásobního prostoru:')) {
|
||||
$(tbl).find('tr').each((_, row) => {
|
||||
const rowText = $(row).text().replace(/\\s+/g, ' ');
|
||||
if (rowText.includes('Hladina zásobního prostoru:')) {
|
||||
const val = parseFloat(rowText.replace('Hladina zásobního prostoru:', '').replace('[m n.m.]', '').replace(',', '.').trim());
|
||||
if (!isNaN(val)) storageLevelFound = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Parse current volume
|
||||
if (text.includes('Aktuální hodnoty') && text.includes('Objem')) {
|
||||
$(tbl).find('tr').each((_, row) => {
|
||||
const rowText = $(row).text().replace(/\\s+/g, ' ');
|
||||
if (rowText.includes('Objem [mil. m3]')) {
|
||||
const valStr = $(row).find('td').eq(1).text().trim().replace(',', '.');
|
||||
const val = parseFloat(valStr);
|
||||
if (!isNaN(val)) maxVol = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (storageLevelFound !== null) {
|
||||
updatedConfig[i].storageLevel = storageLevelFound;
|
||||
} else {
|
||||
// if PVL doesn't have it, remove our fake guess so we fallback to maxLevel
|
||||
delete updatedConfig[i].storageLevel;
|
||||
}
|
||||
|
||||
// Fix maxVolume if current volume exceeds it
|
||||
if (maxVol && updatedConfig[i].maxVolume && maxVol > updatedConfig[i].maxVolume!) {
|
||||
updatedConfig[i].maxVolume = Math.ceil(maxVol * 1.05 * 10) / 10;
|
||||
} else if (maxVol && !updatedConfig[i].maxVolume) {
|
||||
updatedConfig[i].maxVolume = Math.ceil(maxVol * 1.05 * 10) / 10;
|
||||
}
|
||||
|
||||
console.log(`Updated ${lake.text}: storageLevel=${storageLevelFound}, currVol=${maxVol} -> newMaxVol=${updatedConfig[i].maxVolume}`);
|
||||
} catch (err: any) {
|
||||
console.error(`Failed for ${lake.text}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
let newContent = `export interface LakeConfig {
|
||||
id: string;
|
||||
text: string;
|
||||
priority?: boolean;
|
||||
coords: [number, number];
|
||||
maxVolume?: number;
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
storageLevel?: number;
|
||||
navigationForbidden?: boolean;
|
||||
}
|
||||
|
||||
export const lakesConfig: LakeConfig[] = [
|
||||
`;
|
||||
updatedConfig.forEach((l, idx) => {
|
||||
newContent += ` { id: "${l.id}", text: "${l.text}", ${l.priority ? 'priority: true, ' : ''}coords: [${l.coords[0].toFixed(4)}, ${l.coords[1].toFixed(4)}], maxVolume: ${l.maxVolume}, minLevel: ${l.minLevel}, maxLevel: ${l.maxLevel}, ${l.storageLevel ? 'storageLevel: ' + l.storageLevel + ', ' : ''}navigationForbidden: ${l.navigationForbidden} }${idx === updatedConfig.length - 1 ? '' : ','}\n`;
|
||||
});
|
||||
newContent += `];\n`;
|
||||
|
||||
fs.writeFileSync(path.resolve('./scripts/lakesConfig.ts'), newContent);
|
||||
console.log("lakesConfig.ts updated with precise storage levels!");
|
||||
}
|
||||
|
||||
fixStorageLevels();
|
||||
+40
-12
@@ -7,19 +7,47 @@ export interface LakeConfig {
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
storageLevel?: number;
|
||||
navigationForbidden?: boolean;
|
||||
}
|
||||
|
||||
export const lakesConfig: LakeConfig[] = [
|
||||
{ id: "VLL1|1", text: "VD Lipno 1 - Vltava", priority: true, coords: [48.6322, 14.2215], maxVolume: 306.0, minLevel: 715.00, maxLevel: 725.60, storageLevel: 724.9 },
|
||||
{ id: "VLL2|1", text: "VD Lipno II - Vltava", coords: [48.6250, 14.3180], maxVolume: 1.5, minLevel: 510.0, maxLevel: 511.5, storageLevel: 511.5 },
|
||||
{ id: "VLHN|1", text: "VD Hněvkovice - Vltava", priority: true, coords: [49.1830, 14.4440], maxVolume: 21.1, minLevel: 365.0, maxLevel: 370.5, storageLevel: 370.1 },
|
||||
{ id: "VLKO|1", text: "VD Kořensko - Vltava", coords: [49.2550, 14.3980], maxVolume: 2.8, minLevel: 352.0, maxLevel: 353.5, storageLevel: 352.6 },
|
||||
{ id: "VLOR|2", text: "VD Orlík - Vltava", priority: true, coords: [49.6060, 14.1700], maxVolume: 716.5, minLevel: 330.00, maxLevel: 354.00, storageLevel: 349.9 },
|
||||
{ id: "UHKA|2", text: "VD Kamýk - Vltava", coords: [49.6360, 14.2540], maxVolume: 12.8, minLevel: 283.0, maxLevel: 285.6, storageLevel: 285.6 },
|
||||
{ id: "VLSL|2", text: "VD Slapy - Vltava", priority: true, coords: [49.8220, 14.4360], maxVolume: 269.3, minLevel: 265.50, maxLevel: 271.10, storageLevel: 270.6 },
|
||||
{ id: "VLST|2", text: "VD Štěchovice - Vltava", coords: [49.8450, 14.4120], maxVolume: 11.2, minLevel: 217.0, maxLevel: 219.5, storageLevel: 219.4 },
|
||||
{ id: "VRSN|2", text: "VD Vrané - Vltava", coords: [49.9340, 14.3850], maxVolume: 11.1, minLevel: 198.5, maxLevel: 200.5, storageLevel: 200.5 },
|
||||
{ id: "SVKR|2", text: "VD Švihov - Želivka", priority: true, coords: [49.7180, 15.1060], maxVolume: 266.6, minLevel: 343.0, maxLevel: 377.0, storageLevel: 377.0 },
|
||||
{ id: "MARI|1", text: "VD Římov - Malše", priority: true, coords: [48.8470, 14.4870], maxVolume: 33.8, minLevel: 458.0, maxLevel: 471.0, storageLevel: 470.65 },
|
||||
{ id: "MZHR|3", text: "VD Hracholusky - Mže", priority: true, coords: [49.7890, 13.1550], maxVolume: 56.7, minLevel: 360.0, maxLevel: 373.0, storageLevel: 354.1 }
|
||||
{ id: "VLL1|1", text: "VD Lipno 1 - Vltava", priority: true, coords: [48.6322, 14.2215], maxVolume: 306, minLevel: 716.1, maxLevel: 725.6, storageLevel: 724.9, navigationForbidden: false },
|
||||
{ id: "VLL2|1", text: "VD Lipno II - Vltava", priority: true, coords: [48.6250, 14.3180], maxVolume: 1.6, minLevel: 557.6, maxLevel: 563.35, storageLevel: 562.7, navigationForbidden: false },
|
||||
{ id: "VLHN|1", text: "VD Hněvkovice - Vltava", priority: true, coords: [49.1830, 14.4440], maxVolume: 21.1, minLevel: 364.6, maxLevel: 370.1, storageLevel: 370.1, navigationForbidden: false },
|
||||
{ id: "VLKO|1", text: "VD Kořensko - Vltava", priority: true, coords: [49.2550, 14.3980], maxVolume: 2.8, minLevel: 347.8, maxLevel: 353.6, storageLevel: 352.6, navigationForbidden: false },
|
||||
{ id: "VLOR|2", text: "VD Orlík - Vltava", priority: true, coords: [49.6060, 14.1700], maxVolume: 716.5, minLevel: 329.6, maxLevel: 353.6, storageLevel: 349.9, navigationForbidden: false },
|
||||
{ id: "VLSL|2", text: "VD Slapy - Vltava", priority: true, coords: [49.8220, 14.4360], maxVolume: 269.3, minLevel: 246.6, maxLevel: 270.6, storageLevel: 270.6, navigationForbidden: false },
|
||||
{ id: "VLST|2", text: "VD Štěchovice - Vltava", priority: true, coords: [49.8450, 14.4120], maxVolume: 11.2, minLevel: 215.8, maxLevel: 219.4, storageLevel: 219.4, navigationForbidden: false },
|
||||
{ id: "MARI|1", text: "VD Římov - Malše", priority: true, coords: [48.8470, 14.4870], maxVolume: 33.8, minLevel: 442.5, maxLevel: 471.48, storageLevel: 470.65, navigationForbidden: true },
|
||||
{ id: "MZHR|3", text: "VD Hracholusky - Mže", priority: true, coords: [49.7890, 13.1550], maxVolume: 56.7, minLevel: 339.6, maxLevel: 357.97, storageLevel: 354.1, navigationForbidden: false },
|
||||
{ id: "ZESV|2", text: "VD Švihov (Želivka)", priority: true, coords: [49.7040, 15.1150], maxVolume: 266.6, minLevel: 343.1, maxLevel: 379.8, storageLevel: 377, navigationForbidden: true },
|
||||
{ id: "VLKA|2", text: "VD Kamýk", coords: [49.6380, 14.2580], maxVolume: 12.8, minLevel: 282.1, maxLevel: 284.6, storageLevel: 284.6, navigationForbidden: false },
|
||||
{ id: "VLVE|2", text: "VD Vrané", coords: [49.9390, 14.3910], maxVolume: 11.1, minLevel: 199.1, maxLevel: 200.1, storageLevel: 200.1, navigationForbidden: false },
|
||||
{ id: "BLHU|1", text: "VD Husinec", coords: [49.0270, 13.9870], maxVolume: 5.7, minLevel: 515.33, maxLevel: 529.88, storageLevel: 522.33, navigationForbidden: true },
|
||||
{ id: "UHNY|3", text: "VD Nýrsko", coords: [49.2610, 13.1230], maxVolume: 16, minLevel: 501.2, maxLevel: 524.25, storageLevel: 521.55, navigationForbidden: true },
|
||||
{ id: "KCKC|3", text: "VD Klíčava", coords: [50.0630, 13.9310], maxVolume: 9.3, minLevel: 267.6, maxLevel: 296.91, storageLevel: 293.7, navigationForbidden: true },
|
||||
{ id: "KLKL|3", text: "VD Klabava", coords: [49.7540, 13.5640], maxVolume: 1.5, minLevel: 344.4, maxLevel: 351.1, storageLevel: 345.7, navigationForbidden: false },
|
||||
{ id: "RACU|3", text: "VD České Údolí", coords: [49.7150, 13.3640], maxVolume: 5.5, minLevel: 310.6, maxLevel: 315.2, storageLevel: 313.6, navigationForbidden: false },
|
||||
{ id: "TRTR|2", text: "VD Trnávka", coords: [49.5260, 15.1950], maxVolume: 5.6, minLevel: 412, maxLevel: 414.5, storageLevel: 413.2, navigationForbidden: false },
|
||||
{ id: "HESE|2", text: "VD Sedlice", coords: [49.5070, 15.2630], maxVolume: 1.9, minLevel: 443.9, maxLevel: 448.64, storageLevel: 447.4, navigationForbidden: false },
|
||||
{ id: "MZLU|3", text: "VD Lučina", coords: [49.8050, 12.6390], maxVolume: 2.3, minLevel: 523, maxLevel: 534.68, storageLevel: 532.1, navigationForbidden: true },
|
||||
{ id: "STZL|3", text: "VD Žlutice", coords: [50.0930, 13.1360], maxVolume: 14.5, minLevel: 493.6, maxLevel: 509.72, storageLevel: 507.05, navigationForbidden: true },
|
||||
{ id: "PPPI|3", text: "VD Pilská (u Příbramě)", coords: [49.6910, 13.9570], maxVolume: 1.6, minLevel: 661.7, maxLevel: 672.7, storageLevel: 671.4, navigationForbidden: true },
|
||||
{ id: "LILA|3", text: "VD Láz", coords: [49.6640, 13.8820], maxVolume: 0.8, minLevel: 630, maxLevel: 642.15, storageLevel: 641.35, navigationForbidden: true },
|
||||
{ id: "OPOB|3", text: "VD Obecnice", coords: [49.7110, 13.9370], maxVolume: 0.6, minLevel: 555.65, maxLevel: 565.87, storageLevel: 564.55, navigationForbidden: true },
|
||||
{ id: "STST|2", text: "VD Strž", coords: [49.7910, 14.0040], maxVolume: 1, minLevel: 586.6, maxLevel: 589.2, storageLevel: 588.6, navigationForbidden: false },
|
||||
{ id: "HEVR|2", text: "VD Vřesník", coords: [49.5070, 15.2440], maxVolume: 0.5, minLevel: 406.85, maxLevel: 409.08, storageLevel: 407.6, navigationForbidden: false },
|
||||
{ id: "CRSO|1", text: "VD Soběnov", coords: [48.7750, 14.5360], maxVolume: 1.4, minLevel: 579.81, maxLevel: 583.26, storageLevel: 582.21, navigationForbidden: false },
|
||||
{ id: "SCHU|1", text: "VD Humenice", coords: [48.7840, 14.7350], maxVolume: 0.8, minLevel: 531, maxLevel: 544, storageLevel: 536, navigationForbidden: false },
|
||||
{ id: "SVSV|2", text: "VD Staviště", coords: [49.5750, 15.9520], maxVolume: 1.2, minLevel: 574.6, maxLevel: 581.6, storageLevel: 580.6, navigationForbidden: true },
|
||||
{ id: "SAPI|2", text: "VD Pilská u Žďáru", coords: [49.5930, 15.9320], maxVolume: 1.5, minLevel: 571.8, maxLevel: 577.3, storageLevel: 576.6, navigationForbidden: false },
|
||||
{ id: "SMSM|3", text: "VD Suchomasty", coords: [49.8970, 14.0580], maxVolume: 0.7, minLevel: 249.8, maxLevel: 260.9, storageLevel: 260.1, navigationForbidden: false },
|
||||
{ id: "CPZA|3", text: "VD Záskalská", coords: [49.8050, 13.8510], maxVolume: 0.5, minLevel: 440.54, maxLevel: 449.39, storageLevel: 448.79, navigationForbidden: false },
|
||||
{ id: "BIBI|1", text: "VD Bílsko", coords: [49.1670, 14.0410], maxVolume: 0.3, minLevel: 463.03, maxLevel: 471.6, storageLevel: 464.03, navigationForbidden: false },
|
||||
{ id: "SPKA|1", text: "VD Karhof", coords: [48.9740, 14.5450], maxVolume: 0.3, minLevel: 666.8, maxLevel: 669.1, storageLevel: 668.4, navigationForbidden: false },
|
||||
{ id: "SPNE|2", text: "VD Němčice", coords: [49.7710, 15.1760], maxVolume: 0.4, minLevel: 384.5, maxLevel: 386.4, storageLevel: 385, navigationForbidden: false },
|
||||
{ id: "SPZH|1", text: "VD Zhejral", coords: [49.2310, 15.3120], maxVolume: 0.2, minLevel: 675.2, maxLevel: 679.7, storageLevel: 678.6, navigationForbidden: true },
|
||||
{ id: "KLDP|3", text: "VD Dolejší Padrťský rybník", coords: [49.6640, 13.7530], maxVolume: 0.5, minLevel: 632.69, maxLevel: 634.29, storageLevel: 632.89, navigationForbidden: true },
|
||||
{ id: "KLHP|3", text: "VD Hořejší Padrťský rybník", coords: [49.6550, 13.7610], maxVolume: 0.7, minLevel: 635.76, maxLevel: 637.56, storageLevel: 636.36, navigationForbidden: true },
|
||||
{ id: "CPDR|3", text: "VD Dráteník", coords: [49.8050, 13.8550], maxVolume: 0.1, minLevel: 413.75, maxLevel: 417.91, storageLevel: 416.68, navigationForbidden: false }
|
||||
];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import https from 'https';
|
||||
|
||||
async function checkLake() {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
// Check Lipno 1
|
||||
const url = `https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?id=VLL1&oid=1`;
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
httpsAgent: agent,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
});
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
let hasData = false;
|
||||
$('table').each((i, tbl) => {
|
||||
const firstRowText = $(tbl).find('tr').first().text();
|
||||
console.log(`Table ${i} first row:`, firstRowText.trim().replace(/\\s+/g, ' '));
|
||||
if (firstRowText.includes('Datum a čas') || firstRowText.includes('Hladina')) {
|
||||
hasData = true;
|
||||
}
|
||||
});
|
||||
console.log(`hasData=${hasData}`);
|
||||
} catch (err: any) {
|
||||
console.error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
checkLake();
|
||||
@@ -0,0 +1,52 @@
|
||||
export interface LakeCalculationConfig {
|
||||
maxVolume?: number;
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
storageLevel?: number;
|
||||
}
|
||||
|
||||
export interface LakeMetrics {
|
||||
capacity: number; // 0-100 percentage
|
||||
volume: number; // in mil. m3
|
||||
storageDiff: number; // in meters
|
||||
}
|
||||
|
||||
export function calculateLakeMetrics(
|
||||
currentLevel: number,
|
||||
reportedVolume: number,
|
||||
config: LakeCalculationConfig
|
||||
): LakeMetrics {
|
||||
let capacity = 0;
|
||||
let volume = reportedVolume;
|
||||
let storageDiff = 0;
|
||||
|
||||
// 1. Calculate capacity and volume
|
||||
if (volume > 0 && config.maxVolume && config.maxVolume > 0) {
|
||||
// If real volume is available, calculate capacity from volume
|
||||
capacity = Math.max(0, Math.min(100, Math.round((volume / config.maxVolume) * 1000) / 10));
|
||||
} else if (config.minLevel && config.maxLevel && currentLevel > 0) {
|
||||
// Fallback: estimate capacity and volume from level difference
|
||||
const percentage = ((currentLevel - config.minLevel) / (config.maxLevel - config.minLevel)) * 100;
|
||||
capacity = Math.max(0, Math.min(100, Math.round(percentage * 10) / 10)); // Round to 1 decimal place
|
||||
|
||||
if (volume === 0) {
|
||||
volume = Number(((capacity / 100) * (config.maxVolume || 0)).toFixed(1));
|
||||
}
|
||||
} else {
|
||||
// Missing required config data or bad level
|
||||
if (volume === 0) {
|
||||
volume = config.maxVolume || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Calculate storage difference
|
||||
if (config.storageLevel && currentLevel > 0) {
|
||||
storageDiff = Number((currentLevel - config.storageLevel).toFixed(2));
|
||||
}
|
||||
|
||||
return {
|
||||
capacity,
|
||||
volume,
|
||||
storageDiff
|
||||
};
|
||||
}
|
||||
+36
-9
@@ -1,25 +1,52 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const minutes = parseInt(args[0], 10) || 10;
|
||||
const intervalMs = minutes * 60 * 1000;
|
||||
// How many minutes after the 10-minute mark should we run the scraper?
|
||||
// The basin authority (PVL) generates data at HH:00, HH:10, HH:20... but it takes time to publish.
|
||||
// 5 minutes (HH:05, HH:15...) is a safe buffer to avoid fetching outdated data.
|
||||
const offsetMinutes = 5;
|
||||
|
||||
console.log(`\n⏱️ HLADINATOR Watcher spuštěn!`);
|
||||
console.log(`Budu automaticky stahovat nová data každých ${minutes} minut.\n`);
|
||||
console.log(`Budu automaticky stahovat nová data vždy v časech končících na ${offsetMinutes} (např. 10:05, 10:15, 10:25...).\nTo zajistí, že má Povodí dostatek času data vygenerovat a nahrát.\n`);
|
||||
|
||||
function runUpdate() {
|
||||
const now = new Date().toLocaleTimeString('cs-CZ');
|
||||
console.log(`[${now}] 🔄 Spouštím npm run data:update...`);
|
||||
try {
|
||||
execSync('npm run data:update', { stdio: 'inherit' });
|
||||
console.log(`[${new Date().toLocaleTimeString('cs-CZ')}] ✅ Úspěšně hotovo. Další kontrola za ${minutes} minut...\n`);
|
||||
console.log(`[${new Date().toLocaleTimeString('cs-CZ')}] ✅ Úspěšně hotovo.\n`);
|
||||
} catch (error: any) {
|
||||
console.error(`[${new Date().toLocaleTimeString('cs-CZ')}] ❌ Chyba při aktualizaci:`, error.message);
|
||||
}
|
||||
scheduleNextRun();
|
||||
}
|
||||
|
||||
// Spustit ihned po zapnutí
|
||||
runUpdate();
|
||||
function scheduleNextRun() {
|
||||
const now = new Date();
|
||||
const currentMinute = now.getMinutes();
|
||||
|
||||
// A pak periodicky v zadaném intervalu
|
||||
setInterval(runUpdate, intervalMs);
|
||||
// Find the next target minute (ending in 5)
|
||||
// E.g. if it's 12, next will be 15. If it's 26, next will be 35.
|
||||
let nextMinute = Math.floor(currentMinute / 10) * 10 + offsetMinutes;
|
||||
if (nextMinute <= currentMinute) {
|
||||
nextMinute += 10;
|
||||
}
|
||||
|
||||
const targetTime = new Date(now);
|
||||
if (nextMinute >= 60) {
|
||||
targetTime.setHours(targetTime.getHours() + 1);
|
||||
targetTime.setMinutes(nextMinute % 60);
|
||||
} else {
|
||||
targetTime.setMinutes(nextMinute);
|
||||
}
|
||||
targetTime.setSeconds(0);
|
||||
targetTime.setMilliseconds(0);
|
||||
|
||||
const waitMs = targetTime.getTime() - now.getTime();
|
||||
|
||||
console.log(`[${new Date().toLocaleTimeString('cs-CZ')}] ⏳ Další stahování naplánováno na: ${targetTime.toLocaleTimeString('cs-CZ')} (za ${(waitMs / 60000).toFixed(1)} minut)\n`);
|
||||
|
||||
setTimeout(runUpdate, waitMs);
|
||||
}
|
||||
|
||||
// Run update immediately on first launch and then set the timer
|
||||
runUpdate();
|
||||
|
||||
+9
-4
@@ -7,12 +7,12 @@
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
width: 190px;
|
||||
background-color: var(--bg-card);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 1rem;
|
||||
padding: 1.5rem 0.75rem;
|
||||
transition: width 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -75,8 +75,8 @@
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
@@ -86,6 +86,11 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
flex-shrink: 0;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text-main);
|
||||
|
||||
+46
-12
@@ -1,28 +1,36 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Routes, Route, useParams, useLocation, useNavigate, Navigate } from 'react-router-dom';
|
||||
import { Routes, Route, useParams, Navigate } from 'react-router-dom';
|
||||
import LakeDetail from './components/LakeDetail';
|
||||
import LakesOverview from './components/LakesOverview';
|
||||
import LakeMap from './components/LakeMap';
|
||||
import FavoritesOverview from './components/FavoritesOverview';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Topbar from './components/Topbar';
|
||||
import SettingsModal from './components/SettingsModal';
|
||||
import { type Language } from './translations';
|
||||
import { type Language, t } from './translations';
|
||||
import { lakesConfig } from '../scripts/lakesConfig';
|
||||
import { slugify } from './utils/slugify';
|
||||
import './App.css';
|
||||
|
||||
const LakeDetailWrapper = ({ language }: { language: Language }) => {
|
||||
const LakeDetailWrapper = ({ language, windUnit }: { language: Language, windUnit: 'kmh' | 'ms' }) => {
|
||||
const { slug } = useParams();
|
||||
const lake = lakesConfig.find(l => slugify(l.text) === slug);
|
||||
|
||||
if (!lake) return <Navigate to="/" replace />;
|
||||
|
||||
return <LakeDetail language={language} lakeId={lake.id} />;
|
||||
return <LakeDetail language={language} lakeId={lake.id} windUnit={windUnit} />;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [language, setLanguage] = useState<Language>('en');
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
|
||||
const [language, setLanguage] = useState<Language>(() => {
|
||||
return (localStorage.getItem('hladinator_lang') as Language) || 'en';
|
||||
});
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
|
||||
return (localStorage.getItem('hladinator_theme') as 'dark' | 'light') || 'dark';
|
||||
});
|
||||
const [windUnit, setWindUnit] = useState<'kmh' | 'ms'>(() => {
|
||||
return (localStorage.getItem('hladinator_windUnit') as 'kmh' | 'ms') || 'kmh';
|
||||
});
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
@@ -32,6 +40,7 @@ function App() {
|
||||
} else {
|
||||
document.body.classList.remove('light-mode');
|
||||
}
|
||||
localStorage.setItem('hladinator_theme', theme);
|
||||
|
||||
// Clean up empty hash from URL (e.g. if the user clicked an empty anchor)
|
||||
if (window.location.href.endsWith('#')) {
|
||||
@@ -39,6 +48,14 @@ function App() {
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('hladinator_lang', language);
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('hladinator_windUnit', windUnit);
|
||||
}, [windUnit]);
|
||||
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
{/* Mobile overlay */}
|
||||
@@ -56,13 +73,28 @@ function App() {
|
||||
onCloseMobileMenu={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
|
||||
<div className="main-content">
|
||||
<div className="main-content" style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<Topbar language={language} onToggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)} />
|
||||
<Routes>
|
||||
<Route path="/" element={<LakesOverview language={language} />} />
|
||||
<Route path="/map" element={<LakeMap language={language} />} />
|
||||
<Route path="/:slug" element={<LakeDetailWrapper language={language} />} />
|
||||
</Routes>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<LakesOverview language={language} />} />
|
||||
<Route path="/favorites" element={<FavoritesOverview language={language} />} />
|
||||
<Route path="/map" element={<LakeMap language={language} />} />
|
||||
<Route path="/:slug" element={<LakeDetailWrapper language={language} windUnit={windUnit} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
<footer style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '1.5rem',
|
||||
fontSize: '0.8rem',
|
||||
color: 'var(--text-muted)',
|
||||
marginTop: 'auto'
|
||||
}}>
|
||||
<span>{t[language].chart.dataSources} pvl.cz, open-meteo.com</span>
|
||||
<span>{t[language].chart.createdIn}</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{isSettingsOpen && (
|
||||
@@ -71,6 +103,8 @@ function App() {
|
||||
setLanguage={setLanguage}
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
windUnit={windUnit}
|
||||
setWindUnit={setWindUnit}
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
size?: number;
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
export const CircularProgress: React.FC<Props> = ({ value, size = 60, strokeWidth = 6 }) => {
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = radius * 2 * Math.PI;
|
||||
const offset = circumference - (value / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: size, height: size }}>
|
||||
<svg width={size} height={size}>
|
||||
<circle
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
fill="transparent"
|
||||
strokeWidth={strokeWidth}
|
||||
r={radius}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
/>
|
||||
<circle
|
||||
stroke="var(--color-cyan)"
|
||||
fill="transparent"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
style={{ strokeDasharray: circumference, strokeDashoffset: offset, transition: 'stroke-dashoffset 0.5s ease-in-out' }}
|
||||
r={radius}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
/>
|
||||
</svg>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: size * 0.25, fontWeight: 'bold' }}>
|
||||
{value > 0 ? `${value.toFixed(1)}%` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FiStar } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { useFavorites } from '../hooks/useFavorites';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { CircularProgress } from './CircularProgress';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { slugify } from '../utils/slugify';
|
||||
import { AreaChart, Area, ResponsiveContainer, YAxis } from 'recharts';
|
||||
import { FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
|
||||
|
||||
interface Lake {
|
||||
id: string;
|
||||
name: string;
|
||||
river: string;
|
||||
priority: boolean;
|
||||
level: number;
|
||||
capacity: number;
|
||||
storageDiff?: number;
|
||||
inflow: number;
|
||||
outflow: number;
|
||||
volume: number;
|
||||
maxVolume: number;
|
||||
sparkline: number[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
language: Language;
|
||||
}
|
||||
|
||||
const FavoritesOverview = ({ language }: Props) => {
|
||||
const [lakes, setLakes] = useState<Lake[]>([]);
|
||||
const { isFavorite, toggleFavorite } = useFavorites();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/data/lakes_index.json?t=${Date.now()}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setLakes(data))
|
||||
.catch(err => console.error(err));
|
||||
}, []);
|
||||
|
||||
const favoriteLakes = lakes.filter(l => isFavorite(l.id));
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<Helmet>
|
||||
<title>{t[language].seo.favoritesTitle}</title>
|
||||
<meta name="description" content={t[language].seo.favoritesDesc} />
|
||||
<meta property="og:title" content={t[language].seo.favoritesTitle} />
|
||||
<meta property="og:description" content={t[language].seo.favoritesDesc} />
|
||||
</Helmet>
|
||||
<div>
|
||||
<h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0 0 0.5rem 0', display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||
<FiStar size={24} fill="#f59e0b" color="#f59e0b" />
|
||||
{language === 'cs' ? 'Oblíbená' : 'Favourites'}
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.9rem' }}>
|
||||
{language === 'cs'
|
||||
? 'Jezera připnutá v přehledu. Připnout nebo odepnout lze ikonou hvězdičky.'
|
||||
: 'Lakes you pinned in the overview. Use the star icon to pin or unpin.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{favoriteLakes.length === 0 ? (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
gap: '1rem', padding: '4rem 2rem', color: 'var(--text-muted)', textAlign: 'center'
|
||||
}}>
|
||||
<FiStar size={48} strokeWidth={1.2} color="var(--text-muted)" />
|
||||
<p style={{ margin: 0, fontSize: '1.1rem' }}>
|
||||
{language === 'cs' ? 'Zatím žádná oblíbená jezera.' : 'No favourites yet.'}
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: '0.85rem' }}>
|
||||
{language === 'cs'
|
||||
? 'Přejdi na Jezera a nádrže a klikni na ⭐ u jezera, které tě zajímá.'
|
||||
: 'Go to Lakes & Reservoirs and click the ⭐ on any lake to pin it here.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: '1.5rem' }}>
|
||||
{favoriteLakes.map(lake => {
|
||||
const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val }));
|
||||
|
||||
const minVal = Math.min(...lake.sparkline);
|
||||
const maxVal = Math.max(...lake.sparkline);
|
||||
const diff = maxVal - minVal;
|
||||
const padding = diff === 0 ? 0.1 : diff * 0.1;
|
||||
const yDomain = [minVal - padding, maxVal + padding];
|
||||
|
||||
const firstVal = lake.sparkline[0] || 0;
|
||||
const lastVal = lake.sparkline[lake.sparkline.length - 1] || 0;
|
||||
const trendDiff = lastVal - firstVal;
|
||||
|
||||
let trendColor = 'var(--color-cyan)';
|
||||
if (trendDiff > 0.01) trendColor = 'var(--color-green)';
|
||||
else if (trendDiff < -0.01) trendColor = 'var(--color-red)';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lake.id}
|
||||
className="kpi-card priority-lake-card"
|
||||
onClick={() => navigate(`/${slugify(lake.name)}`)}
|
||||
style={{ cursor: 'pointer', padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative' }}
|
||||
>
|
||||
{/* Unpin button */}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); toggleFavorite(lake.id); }}
|
||||
title={language === 'cs' ? 'Odepnout' : 'Unpin'}
|
||||
style={{
|
||||
position: 'absolute', top: '1rem', right: '1rem',
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: '#f59e0b', transition: 'transform 0.15s',
|
||||
padding: '4px', display: 'flex', alignItems: 'center', zIndex: 2,
|
||||
}}
|
||||
onMouseOver={(e) => { e.currentTarget.style.transform = 'scale(1.2)'; }}
|
||||
onMouseOut={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
|
||||
>
|
||||
<FiStar size={18} fill="#f59e0b" />
|
||||
</button>
|
||||
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0, paddingRight: '2rem' }}>
|
||||
{lake.name} {lake.river ? `- ${lake.river}` : ''}
|
||||
</h3>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}>
|
||||
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
|
||||
<div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{t[language].kpi.level}</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', display: 'flex', alignItems: 'baseline', gap: '0.25rem', lineHeight: '1.1' }}>
|
||||
{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>m n.m.</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginTop: '0.25rem' }}>
|
||||
{lake.storageDiff !== undefined && (
|
||||
<div style={{ fontSize: '1rem', color: lake.storageDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', whiteSpace: 'nowrap' }}>
|
||||
{lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m
|
||||
</div>
|
||||
)}
|
||||
{lake.maxVolume > 0 && (
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
|
||||
{lake.volume.toFixed(1)} / {lake.maxVolume.toFixed(1)} mil. m³
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1, height: '40px', marginRight: '2rem' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id={`colorSparkFav-${lake.id}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={trendColor} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={trendColor} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<YAxis domain={yDomain} hide />
|
||||
<Area type="monotone" dataKey="value" stroke={trendColor} fillOpacity={1} fill={`url(#colorSparkFav-${lake.id})`} baseValue={yDomain[0]} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem' }}>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<FiTrendingUp color="var(--color-green)" />
|
||||
<span style={{ color: 'var(--text-muted)' }}>{t[language].kpi.inflow} <span style={{ color: 'var(--color-green)' }}>{lake.inflow} m³/s</span></span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<FiTrendingDown color="var(--color-red)" />
|
||||
<span style={{ color: 'var(--text-muted)' }}>{t[language].kpi.outflow} <span style={{ color: 'var(--color-red)' }}>{lake.outflow} m³/s</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavoritesOverview;
|
||||
+94
-49
@@ -1,6 +1,7 @@
|
||||
import { FiArrowUp, FiArrowDown } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CircularProgress } from './CircularProgress';
|
||||
|
||||
interface KpiData {
|
||||
level: number;
|
||||
@@ -12,6 +13,9 @@ interface KpiData {
|
||||
volume: number;
|
||||
fullness: number;
|
||||
storageDiff?: number;
|
||||
minDiff?: number;
|
||||
avgInflow24h?: number;
|
||||
avgOutflow24h?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -35,62 +39,90 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
|
||||
}, [showTooltip]);
|
||||
|
||||
return (
|
||||
<div className="kpi-grid-container">
|
||||
{/* CARD 1: HLADINA */}
|
||||
<>
|
||||
{/* CARD 1: WATER LEVEL */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
||||
{dict.level} {lakeName}
|
||||
</div>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, marginBottom: '0.5rem' }}>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, whiteSpace: 'nowrap' }}>
|
||||
{data.level.toFixed(2)} <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>m n. m.</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
<div style={{ fontSize: '0.85rem', color: (data.levelDiff24h ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
||||
({(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{((data.levelDiff24h ?? 0) * 100).toFixed(1)} cm / 24h)
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', alignContent: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>1D</span>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff24h ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
||||
{(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{((data.levelDiff24h ?? 0) * 100).toFixed(1)} cm
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: (data.levelDiff7d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
||||
({(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{((data.levelDiff7d ?? 0) * 100).toFixed(1)} cm / 7d)
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>7D</span>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff7d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
||||
{(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{((data.levelDiff7d ?? 0) * 100).toFixed(1)} cm
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: (data.levelDiff30d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
||||
({(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{((data.levelDiff30d ?? 0) * 100).toFixed(1)} cm / 30d)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Circle for Level */}
|
||||
<div style={{ position: 'absolute', right: '1.5rem', top: '1.5rem', width: '40px', height: '40px', borderRadius: '50%', border: '4px solid rgba(0, 195, 255, 0.2)', borderTopColor: 'var(--color-cyan)', transform: 'rotate(45deg)' }}></div>
|
||||
</div>
|
||||
|
||||
{/* CARD 2: PRŮTOK */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.flow}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem' }}>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#8b5cf6', marginRight: '6px' }}></span>
|
||||
{dict.inflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)', marginLeft: '4px' }}>{data.inflow.toFixed(1)} m³/s</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-orange)', marginRight: '2px' }}></span>
|
||||
{dict.outflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.outflow.toFixed(1)} m³/s</span>
|
||||
{flowDiff > 0 ? <FiArrowUp color="var(--color-green)" /> : flowDiff < 0 ? <FiArrowDown color="var(--color-red)" /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flow Circle */}
|
||||
<div style={{ width: '40px', height: '40px', borderRadius: '50%', border: '4px solid rgba(0, 195, 255, 0.2)', borderTopColor: 'var(--color-cyan)', borderRightColor: 'var(--color-cyan)', display: 'flex', alignItems: 'center', justifyContent: 'center', transform: 'rotate(-45deg)' }}>
|
||||
<span style={{ fontSize: '0.65rem', transform: 'rotate(45deg)', color: 'var(--text-main)', fontWeight: 'bold' }}>
|
||||
<div style={{ lineHeight: 1 }}>{data.outflow.toFixed(1)}</div>
|
||||
<div style={{ fontSize: '0.45rem', opacity: 0.7 }}>m³/s</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>30D</span>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff30d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
||||
{(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{((data.levelDiff30d ?? 0) * 100).toFixed(1)} cm
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CARD 3: NAPLNĚNOST */}
|
||||
{/* CARD 2: FLOW */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
||||
{dict.flow}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem' }}>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-green)', marginRight: '6px', flexShrink: 0 }}></span>
|
||||
{dict.inflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)', marginLeft: '4px' }}>{data.inflow.toFixed(1)} m³/s</span>
|
||||
</div>
|
||||
{data.avgInflow24h !== undefined && (
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', paddingLeft: '14px', marginTop: '-2px' }}>
|
||||
Ø 24h: {data.avgInflow24h.toFixed(1)} m³/s
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.25rem', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-red)', marginRight: '2px', flexShrink: 0 }}></span>
|
||||
{dict.outflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.outflow.toFixed(1)} m³/s</span>
|
||||
{flowDiff > 0 ? <FiArrowUp color="var(--color-green)" /> : flowDiff < 0 ? <FiArrowDown color="var(--color-red)" /> : null}
|
||||
</div>
|
||||
{data.avgOutflow24h !== undefined && (
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', paddingLeft: '14px', marginTop: '-2px' }}>
|
||||
Ø 24h: {data.avgOutflow24h.toFixed(1)} m³/s
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Flow Circle */}
|
||||
<div style={{
|
||||
width: '70px',
|
||||
height: '70px',
|
||||
borderRadius: '50%',
|
||||
border: `4px solid ${flowDiff >= 0 ? 'rgba(74, 222, 128, 0.2)' : 'rgba(248, 113, 113, 0.2)'}`,
|
||||
borderTopColor: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)',
|
||||
borderRightColor: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transform: 'rotate(-45deg)',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<span style={{ transform: 'rotate(45deg)', color: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', textAlign: 'center', lineHeight: 1.2 }}>
|
||||
<div style={{ fontSize: '0.8rem' }}>{flowDiff > 0 ? '+' : ''}{flowDiff.toFixed(1)}</div>
|
||||
<div style={{ fontSize: '0.6rem', opacity: 0.8 }}>m³/s</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CARD 3: CAPACITY */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
|
||||
{dict.fullness}
|
||||
<span
|
||||
onClick={() => setShowTooltip(!showTooltip)}
|
||||
@@ -123,14 +155,27 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)' }}>
|
||||
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)} m` : `${data.storageDiff.toFixed(2)} m`) : (data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A')}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
|
||||
{dict.volume}: {data.volume.toFixed(1)} mil. m³
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', flex: 1, minWidth: 0, paddingRight: '0.5rem' }}>
|
||||
<div style={{ fontSize: '1.7rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem', whiteSpace: 'nowrap', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)' }}>
|
||||
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)} m` : `${data.storageDiff.toFixed(2)} m`) : (data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A')}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
|
||||
{dict.volume}: {data.volume.toFixed(1)} <span style={{ fontSize: '0.7rem' }}>mil. m³</span>
|
||||
</div>
|
||||
{data.minDiff !== undefined && (
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '4px', whiteSpace: 'nowrap' }}>
|
||||
{language === 'cs' ? 'K minimu:' : 'To min:'} <span style={{ color: data.minDiff < 0.5 ? 'var(--color-red)' : 'var(--color-green)' }}>{data.minDiff.toFixed(2)} m</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<CircularProgress value={data.fullness} size={80} strokeWidth={8} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+166
-52
@@ -1,7 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, ComposedChart, ReferenceLine, Bar } from 'recharts';
|
||||
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 { useFavorites } from '../hooks/useFavorites';
|
||||
|
||||
interface LipnoData {
|
||||
timestamp: string;
|
||||
@@ -18,11 +25,12 @@ interface LipnoData {
|
||||
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].chart;
|
||||
const dict = t[language as Language].chart;
|
||||
if (isWeather) {
|
||||
return (
|
||||
<div style={{ backgroundColor: 'var(--bg-card)', padding: '1rem', border: '1px solid var(--border-color)', borderRadius: '0.5rem', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)' }}>
|
||||
@@ -31,7 +39,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
|
||||
const isTemp = entry.name === 'temperature' || entry.dataKey === 'temperature';
|
||||
return (
|
||||
<p key={index} style={{ margin: 0, color: 'var(--text-main)' }}>
|
||||
{isTemp ? 'Teplota' : 'Srážky'}: <span style={{ fontWeight: 'bold' }}>{entry.value !== undefined && entry.value !== null ? entry.value.toFixed(1) : 'N/A'} {isTemp ? '°C' : 'mm'}</span>
|
||||
{isTemp ? (language === 'cs' ? 'Teplota' : 'Temperature') : (language === 'cs' ? 'Srážky' : 'Precipitation')}: <span style={{ fontWeight: 'bold' }}>{entry.value !== undefined && entry.value !== null ? entry.value.toFixed(1) : 'N/A'} {isTemp ? '°C' : 'mm'}</span>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
@@ -51,8 +59,8 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
|
||||
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-orange)'; }
|
||||
else if (entry.dataKey === 'inflow') { labelStr = dict.inflow; unit = 'm³/s'; color = '#8b5cf6'; }
|
||||
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)'; }
|
||||
|
||||
@@ -71,7 +79,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
|
||||
return null;
|
||||
};
|
||||
|
||||
const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
const [data, setData] = useState<LipnoData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lakeInfo, setLakeInfo] = useState<any>(null);
|
||||
@@ -81,44 +89,50 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
const topbarDict = t[language].topbar;
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/lakes_index.json')
|
||||
.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 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';
|
||||
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
|
||||
|
||||
fetch(`/data/${internalId}.json`)
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
const formattedData = json.map((item: any) => {
|
||||
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
|
||||
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
|
||||
};
|
||||
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);
|
||||
});
|
||||
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) {
|
||||
@@ -165,6 +179,9 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
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;
|
||||
@@ -173,6 +190,10 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
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();
|
||||
|
||||
@@ -193,12 +214,24 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
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,
|
||||
@@ -208,17 +241,89 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
outflow: lastValidFlowData.outflow,
|
||||
volume: lakeInfo?.volume || 0,
|
||||
fullness: lakeInfo?.capacity || 0,
|
||||
storageDiff: lakeInfo?.storageDiff
|
||||
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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', width: '100%' }}>
|
||||
{lakeInfo && (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t[language].seo.lakeTitle.replace('{name}', lakeInfo.name)}</title>
|
||||
<meta name="description" content={t[language].seo.lakeDesc.replace('{name}', lakeInfo.name)} />
|
||||
<meta property="og:title" content={t[language].seo.lakeTitle.replace('{name}', lakeInfo.name)} />
|
||||
<meta property="og:description" content={t[language].seo.lakeDesc.replace('{name}', lakeInfo.name)} />
|
||||
</Helmet>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', margin: '0 0 0.5rem 0' }}>
|
||||
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', margin: 0, color: 'var(--text-main)' }}>
|
||||
{lakeInfo.name}
|
||||
</h1>
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); if (lakeId) toggleFavorite(lakeId); }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', padding: '0.25rem' }}
|
||||
title={isFav ? (language === 'cs' ? "Odebrat z oblíbených" : "Remove from favorites") : (language === 'cs' ? "Přidat do oblíbených" : "Add to favorites")}
|
||||
>
|
||||
<FiStar size={24} fill={isFav ? '#f59e0b' : 'none'} color={isFav ? '#f59e0b' : 'var(--text-muted)'} />
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.95rem', lineHeight: '1.5' }}>
|
||||
{t[language].seo.lakeDesc.replace('{name}', lakeInfo.name)}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginTop: '-0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span>{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</span>
|
||||
<div className="status-dot"></div>
|
||||
</div>
|
||||
|
||||
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} />
|
||||
<div className="kpi-grid-container">
|
||||
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} />
|
||||
|
||||
{lakeInfo && lakeInfo.lat && lakeInfo.lng && (
|
||||
<WeatherWidget lat={lakeInfo.lat} lng={lakeInfo.lng} language={language} windUnit={windUnit} sensorTemp={latestData.temperature ?? undefined} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{limits && limits.map((limit, idx) => {
|
||||
const diff = latestData.level - limit.level;
|
||||
if (diff < 0.3) {
|
||||
const isBelow = diff < 0;
|
||||
return (
|
||||
<div key={idx} style={{ padding: '1rem', borderRadius: '8px', backgroundColor: isBelow ? 'rgba(248, 113, 113, 0.1)' : 'rgba(245, 158, 11, 0.1)', border: `1px solid ${isBelow ? 'var(--color-red)' : '#f59e0b'}`, color: isBelow ? 'var(--color-red)' : '#f59e0b', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<FiAlertCircle style={{ flexShrink: 0, fontSize: '1.5rem' }} />
|
||||
<div>
|
||||
<strong>{language === 'cs' ? limit.labelCs : limit.labelEn} ({limit.level.toFixed(2)} m n.m.):</strong>
|
||||
<br/>
|
||||
{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).`)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
{/* CHART SECTION */}
|
||||
<div className="chart-card">
|
||||
@@ -243,16 +348,25 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="date" stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} minTickGap={50} />
|
||||
<YAxis yAxisId="left" domain={['dataMin - 0.5', 'dataMax + 0.5']} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} tickFormatter={(v) => v.toFixed(2)} />
|
||||
<YAxis yAxisId="left" domain={leftYAxisDomain as any} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} tickFormatter={(v) => v.toFixed(2)} />
|
||||
<YAxis yAxisId="right" orientation="right" domain={[0, (dataMax: number) => Math.max(dataMax, 1)]} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} />
|
||||
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
|
||||
<Tooltip content={<CustomTooltip language={language} />} />
|
||||
|
||||
{/* Data Series */}
|
||||
{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 }} />
|
||||
))}
|
||||
{staticConfig?.maxLevel && (
|
||||
<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 }} />
|
||||
)}
|
||||
{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: 12 }} />
|
||||
)}
|
||||
<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-orange)" strokeWidth={2} dot={false} isAnimationActive={animate} />
|
||||
<Line yAxisId="right" type={curveType} dataKey="inflow" stroke="#8b5cf6" strokeWidth={2} dot={false} 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="inflow" stroke="var(--color-green)" strokeWidth={2} dot={false} isAnimationActive={animate} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -260,8 +374,8 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
{/* Chart Legend */}
|
||||
<div className="chart-legend-container" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '1rem', marginTop: '1rem', fontSize: '0.85rem', color: 'var(--text-main)' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-cyan)' }}></div> {dict.level}</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-orange)' }}></div> {dict.outflow}</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: '#8b5cf6' }}></div> {dict.inflow}</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-red)' }}></div> {dict.outflow}</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-green)' }}></div> {dict.inflow}</span>
|
||||
</div>
|
||||
|
||||
{/* WEATHER CHART SECTION */}
|
||||
@@ -290,6 +404,11 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '12px', backgroundColor: 'var(--color-cyan)', opacity: 0.6 }}></div> {language === 'cs' ? 'Srážky' : 'Precipitation'} [mm]</span>
|
||||
</div>
|
||||
|
||||
{/* Wind Chart placed inside the main card below the weather graph */}
|
||||
{lakeInfo && lakeInfo.lat && lakeInfo.lng && (
|
||||
<WindChart lat={lakeInfo.lat} lng={lakeInfo.lng} language={language} timeRange={timeRange} windUnit={windUnit} />
|
||||
)}
|
||||
|
||||
{/* Smoothed Toggle Control */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '3rem', marginBottom: '1rem' }}>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{dict.view}</span>
|
||||
@@ -303,11 +422,6 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-footer" style={{ marginTop: '0' }}>
|
||||
<span>{dict.dataSources} <a href="https://www.chmi.cz/" target="_blank" rel="noreferrer">ČHMÚ</a>, <a href="https://www.pvl.cz/" target="_blank" rel="noreferrer">pvl.cz</a></span>
|
||||
<span>{dict.createdIn}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,11 @@ import { useState, useEffect } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { FiX, FiSearch, FiDroplet } from 'react-icons/fi';
|
||||
import { FiX, FiSearch } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { slugify } from '../utils/slugify';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
|
||||
interface LakeData {
|
||||
id: string;
|
||||
@@ -61,6 +62,13 @@ const LakeMap = ({ language }: Props) => {
|
||||
|
||||
return (
|
||||
<div className="map-view-container">
|
||||
<Helmet>
|
||||
<title>{t[language].seo.mapTitle}</title>
|
||||
<meta name="description" content={t[language].seo.mapDesc} />
|
||||
<meta property="og:title" content={t[language].seo.mapTitle} />
|
||||
<meta property="og:description" content={t[language].seo.mapDesc} />
|
||||
</Helmet>
|
||||
|
||||
{/* Leaflet Map */}
|
||||
<MapContainer
|
||||
center={[49.8, 15.5]}
|
||||
@@ -95,18 +103,18 @@ const LakeMap = ({ language }: Props) => {
|
||||
<div className="map-overlay-panel">
|
||||
<div className="map-overlay-header">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '1.1rem' }}>Seznam Jezer (Lakes List)</h3>
|
||||
<h3 style={{ margin: 0, fontSize: '1.1rem' }}>{language === 'cs' ? 'Seznam jezer a nádrží' : 'Lakes and Reservoirs List'}</h3>
|
||||
<FiX style={{ cursor: 'pointer', color: 'var(--text-muted)' }} onClick={() => setIsPanelVisible(false)} />
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
||||
Nalezeno: {filteredLakes.length} Jezer
|
||||
{language === 'cs' ? `Nalezeno: ${filteredLakes.length} záznamů` : `Found: ${filteredLakes.length} records`}
|
||||
</div>
|
||||
|
||||
<div className="search-bar" style={{ width: '100%', padding: '0.5rem', backgroundColor: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
<FiSearch />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Find a lake..."
|
||||
placeholder={language === 'cs' ? 'Najít jezero...' : 'Find a lake...'}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{ width: '100%', background: 'transparent', border: 'none', color: 'white', outline: 'none' }}
|
||||
@@ -117,14 +125,14 @@ const LakeMap = ({ language }: Props) => {
|
||||
<div className="map-overlay-list">
|
||||
{filteredLakes.map((lake, index) => (
|
||||
<div key={lake.id} className="map-lake-card" onClick={() => navigate(`/${slugify(lake.name)}`)}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '0.5rem' }}>{index + 1}. Jezero {lake.name}</div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '0.5rem' }}>{index + 1}. {lake.name}</div>
|
||||
<div className="map-lake-stats">
|
||||
<div>
|
||||
<span style={{ color: 'var(--text-muted)', display: 'block' }}>Area</span>
|
||||
<span style={{ color: 'var(--text-muted)', display: 'block' }}>{language === 'cs' ? 'Rozloha' : 'Area'}</span>
|
||||
<span style={{ color: 'var(--text-main)' }}>{(Math.random() * 50 + 10).toFixed(1)} km²</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ color: 'var(--text-muted)', display: 'block' }}>Depth</span>
|
||||
<span style={{ color: 'var(--text-muted)', display: 'block' }}>{language === 'cs' ? 'Hloubka' : 'Depth'}</span>
|
||||
<span style={{ color: 'var(--text-main)' }}>{(Math.random() * 30 + 5).toFixed(1)}m</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +147,7 @@ const LakeMap = ({ language }: Props) => {
|
||||
style={{ position: 'absolute', top: 10, right: 10, zIndex: 1000, background: 'var(--bg-card)', border: '1px solid var(--border-color)', color: 'white', padding: '0.5rem 1rem', borderRadius: '8px', cursor: 'pointer' }}
|
||||
onClick={() => setIsPanelVisible(true)}
|
||||
>
|
||||
Show List
|
||||
{language === 'cs' ? 'Zobrazit seznam' : 'Show List'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+126
-104
@@ -1,9 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { FiTrendingUp, FiTrendingDown, FiStar } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
import { AreaChart, Area, ResponsiveContainer, YAxis } from 'recharts';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { slugify } from '../utils/slugify';
|
||||
import { useFavorites } from '../hooks/useFavorites';
|
||||
import { CircularProgress } from './CircularProgress';
|
||||
|
||||
interface Lake {
|
||||
id: string;
|
||||
@@ -12,9 +15,11 @@ interface Lake {
|
||||
priority: boolean;
|
||||
level: number;
|
||||
capacity: number;
|
||||
storageDiff?: number;
|
||||
inflow: number;
|
||||
outflow: number;
|
||||
volume: number;
|
||||
maxVolume: number;
|
||||
sparkline: number[];
|
||||
}
|
||||
|
||||
@@ -22,70 +27,71 @@ interface Props {
|
||||
language: Language;
|
||||
}
|
||||
|
||||
const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number, size?: number, strokeWidth?: number }) => {
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = radius * 2 * Math.PI;
|
||||
const offset = circumference - (value / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: size, height: size }}>
|
||||
<svg width={size} height={size}>
|
||||
<circle
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
fill="transparent"
|
||||
strokeWidth={strokeWidth}
|
||||
r={radius}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
/>
|
||||
<circle
|
||||
stroke="var(--color-cyan)"
|
||||
fill="transparent"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
style={{ strokeDasharray: circumference, strokeDashoffset: offset, transition: 'stroke-dashoffset 0.5s ease-in-out' }}
|
||||
r={radius}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
/>
|
||||
</svg>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: size * 0.25, fontWeight: 'bold' }}>
|
||||
{value > 0 ? `${value}%` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => {
|
||||
const LakeCard = ({ lake, language, isFav, onToggleFav }: { lake: Lake, language: Language, isFav: boolean, onToggleFav: (id: string) => void }) => {
|
||||
const navigate = useNavigate();
|
||||
const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val }));
|
||||
|
||||
const minVal = Math.min(...lake.sparkline);
|
||||
const maxVal = Math.max(...lake.sparkline);
|
||||
const diff = maxVal - minVal;
|
||||
const padding = diff === 0 ? 0.1 : diff * 0.1; // dynamic 10% padding
|
||||
const yDomain = [minVal - padding, maxVal + padding];
|
||||
|
||||
const firstVal = lake.sparkline[0] || 0;
|
||||
const lastVal = lake.sparkline[lake.sparkline.length - 1] || 0;
|
||||
const trendDiff = lastVal - firstVal;
|
||||
|
||||
// Dynamic color based on trend direction: stable=cyan, rising=green, falling=red
|
||||
let trendColor = 'var(--color-cyan)';
|
||||
if (trendDiff > 0.01) trendColor = 'var(--color-green)';
|
||||
else if (trendDiff < -0.01) trendColor = 'var(--color-red)';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="kpi-card priority-lake-card"
|
||||
onClick={() => navigate(`/${slugify(lake.name)}`)}
|
||||
style={{ cursor: 'pointer', flex: 1, padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative' }}
|
||||
>
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0 }}>{lake.name} {lake.river ? `- ${lake.river}` : ''}</h3>
|
||||
{/* Star / Favorite button */}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onToggleFav(lake.id); }}
|
||||
title={isFav ? (language === 'cs' ? 'Odepnout' : 'Unpin') : (language === 'cs' ? 'Připnout jako oblíbené' : 'Pin to favorites')}
|
||||
style={{
|
||||
position: 'absolute', top: '1rem', right: '1rem',
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: isFav ? '#f59e0b' : 'var(--text-muted)',
|
||||
opacity: isFav ? 1 : 0.4,
|
||||
transition: 'color 0.2s, opacity 0.2s, transform 0.15s',
|
||||
padding: '4px',
|
||||
display: 'flex', alignItems: 'center',
|
||||
zIndex: 2,
|
||||
}}
|
||||
onMouseOver={(e) => { e.currentTarget.style.opacity = '1'; e.currentTarget.style.transform = 'scale(1.2)'; }}
|
||||
onMouseOut={(e) => { e.currentTarget.style.opacity = isFav ? '1' : '0.4'; e.currentTarget.style.transform = 'scale(1)'; }}
|
||||
>
|
||||
<FiStar size={18} fill={isFav ? '#f59e0b' : 'none'} />
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<div style={{ width: '40px', height: '60px', backgroundColor: 'rgba(255,255,255,0.05)', position: 'relative', borderRadius: '4px', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, width: '100%', height: `${lake.capacity}%`, backgroundColor: 'var(--color-cyan)', opacity: 0.3 }}></div>
|
||||
<div style={{ position: 'absolute', bottom: `${lake.capacity}%`, left: 0, width: '100%', height: '2px', backgroundColor: 'var(--color-cyan)' }}></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Water level</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold' }}>{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)' }}>m n.m.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0, paddingRight: '2rem' }}>{lake.name} {lake.river ? `- ${lake.river}` : ''}</h3>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
|
||||
<div>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 'bold' }}>{lake.capacity > 0 ? `${lake.capacity}%` : 'N/A'} / <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>{lake.volume} mil. m³</span></div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Volume</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}>
|
||||
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
|
||||
<div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Water level</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', display: 'flex', alignItems: 'baseline', gap: '0.25rem', lineHeight: '1.1' }}>
|
||||
{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>m n.m.</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginTop: '0.25rem' }}>
|
||||
{lake.storageDiff !== undefined && (
|
||||
<div style={{ fontSize: '1rem', color: lake.storageDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', whiteSpace: 'nowrap' }}>
|
||||
{lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m
|
||||
</div>
|
||||
)}
|
||||
{lake.maxVolume > 0 && (
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
|
||||
{lake.volume.toFixed(1)} / {lake.maxVolume.toFixed(1)} mil. m³
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,12 +101,13 @@ const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => {
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="colorSpark" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-cyan)" stopOpacity={0.8}/>
|
||||
<stop offset="95%" stopColor="var(--color-cyan)" stopOpacity={0}/>
|
||||
<linearGradient id={`colorSpark-${lake.id}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={trendColor} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={trendColor} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey="value" stroke="var(--color-cyan)" fillOpacity={1} fill="url(#colorSpark)" />
|
||||
<YAxis domain={yDomain} hide />
|
||||
<Area type="monotone" dataKey="value" stroke={trendColor} fillOpacity={1} fill={`url(#colorSpark-${lake.id})`} baseValue={yDomain[0]} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -122,71 +129,86 @@ const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => {
|
||||
|
||||
const LakesOverview = ({ language }: Props) => {
|
||||
const [lakes, setLakes] = useState<Lake[]>([]);
|
||||
const [sortBy, setSortBy] = useState<'name' | 'level' | 'capacity' | 'inflow'>('name');
|
||||
const { isFavorite, toggleFavorite } = useFavorites();
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/lakes_index.json')
|
||||
.then(res => res.json())
|
||||
.then(data => setLakes(data))
|
||||
.catch(err => console.error(err));
|
||||
const loadData = () => {
|
||||
fetch(`/data/lakes_index.json?t=${Date.now()}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setLakes(data))
|
||||
.catch(err => console.error(err));
|
||||
};
|
||||
|
||||
loadData();
|
||||
const intervalId = setInterval(loadData, 60 * 1000); // Poll every 1 minute
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const priorityLakes = lakes.filter(l => l.priority);
|
||||
const otherLakes = lakes.filter(l => !l.priority);
|
||||
const favoriteLakes = lakes.filter(l => isFavorite(l.id));
|
||||
const priorityLakes = lakes.filter(l => l.priority && !isFavorite(l.id));
|
||||
const otherLakes = lakes.filter(l => !l.priority && !isFavorite(l.id));
|
||||
|
||||
otherLakes.sort((a, b) => {
|
||||
if (sortBy === 'name') return a.name.localeCompare(b.name);
|
||||
if (sortBy === 'level') return b.level - a.level;
|
||||
if (sortBy === 'capacity') return b.capacity - a.capacity;
|
||||
if (sortBy === 'inflow') return b.inflow - a.inflow;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const sortButtonStyle = (type: string) => ({
|
||||
background: 'none', border: 'none',
|
||||
color: sortBy === type ? 'var(--text-main)' : 'var(--text-muted)',
|
||||
cursor: 'pointer', fontSize: '0.85rem'
|
||||
});
|
||||
otherLakes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0 0 0.5rem 0' }}>Overview: Lakes ({lakes.length})</h1>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.9rem' }}>Monitoring {lakes.length} reservoirs across the Czech Republic</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', color: 'var(--text-muted)', fontSize: '0.85rem' }}>
|
||||
<span>Sort by:</span>
|
||||
<button style={sortButtonStyle('name')} onClick={() => setSortBy('name')}>Name (A-Z)</button> |
|
||||
<button style={sortButtonStyle('level')} onClick={() => setSortBy('level')}>Level</button> |
|
||||
<button style={sortButtonStyle('capacity')} onClick={() => setSortBy('capacity')}>Capacity</button> |
|
||||
<button style={sortButtonStyle('inflow')} onClick={() => setSortBy('inflow')}>Flow In</button>
|
||||
</div>
|
||||
<Helmet>
|
||||
<title>{t[language].seo.homeTitle}</title>
|
||||
<meta name="description" content={t[language].seo.homeDesc} />
|
||||
<meta property="og:title" content={t[language].seo.homeTitle} />
|
||||
<meta property="og:description" content={t[language].seo.homeDesc} />
|
||||
</Helmet>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0', color: 'var(--text-main)' }}>{t[language].sidebar.lakes} ({lakes.length})</h1>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.95rem', lineHeight: '1.5' }}>
|
||||
{t[language].seo.homeDesc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{priorityLakes.length > 0 && (
|
||||
{/* Favorites section */}
|
||||
{favoriteLakes.length > 0 && (
|
||||
<section>
|
||||
<h2 style={{ fontSize: '1.1rem', fontWeight: 'bold', marginBottom: '1rem' }}>Priority Reservoirs</h2>
|
||||
<h2 style={{ fontSize: '1.1rem', fontWeight: 'bold', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<FiStar size={16} fill="#f59e0b" color="#f59e0b" /> {language === 'cs' ? 'Oblíbená' : 'Favorites'} ({favoriteLakes.length})
|
||||
</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
{priorityLakes.map(lake => <LakeCard key={lake.id} lake={lake} language={language} />)}
|
||||
{favoriteLakes.map(lake => (
|
||||
<LakeCard key={lake.id} lake={lake} language={language} isFav={true} onToggleFav={toggleFavorite} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 style={{ fontSize: '1.1rem', fontWeight: 'bold', marginBottom: '1rem' }}>Other Reservoirs ({otherLakes.length})</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
{otherLakes.map(lake => <LakeCard key={lake.id} lake={lake} language={language} />)}
|
||||
</div>
|
||||
</section>
|
||||
{priorityLakes.length > 0 && (
|
||||
<section>
|
||||
<h2 style={{ fontSize: '1.1rem', fontWeight: 'bold', marginBottom: '1rem' }}>{language === 'cs' ? 'Jezera a nádrže' : 'Lakes and Reservoirs'}</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
{priorityLakes.map(lake => <LakeCard key={lake.id} lake={lake} language={language} isFav={false} onToggleFav={toggleFavorite} />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{otherLakes.length > 0 && (
|
||||
<section>
|
||||
<h2 style={{ fontSize: '1.1rem', fontWeight: 'bold', marginBottom: '1rem', marginTop: '1rem' }}>{language === 'cs' ? 'Ostatní' : 'Other'}</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
{otherLakes.map(lake => <LakeCard key={lake.id} lake={lake} language={language} isFav={false} onToggleFav={toggleFavorite} />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FiX, FiMoon, FiSun, FiGlobe, FiCoffee } from 'react-icons/fi';
|
||||
import { FiX, FiMoon, FiSun, FiCoffee, FiWind } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
|
||||
interface Props {
|
||||
@@ -6,10 +6,12 @@ interface Props {
|
||||
setLanguage: (lang: Language) => void;
|
||||
theme: 'dark' | 'light';
|
||||
setTheme: (theme: 'dark' | 'light') => void;
|
||||
windUnit: 'kmh' | 'ms';
|
||||
setWindUnit: (unit: 'kmh' | 'ms') => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SettingsModal = ({ language, setLanguage, theme, setTheme, onClose }: Props) => {
|
||||
const SettingsModal = ({ language, setLanguage, theme, setTheme, windUnit, setWindUnit, onClose }: Props) => {
|
||||
const dict = t[language].settings;
|
||||
|
||||
return (
|
||||
@@ -96,7 +98,7 @@ const SettingsModal = ({ language, setLanguage, theme, setTheme, onClose }: Prop
|
||||
cursor: 'pointer', transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
<FiGlobe /> {dict.english}
|
||||
<span style={{ fontSize: '1.2rem', lineHeight: 1 }}>🇬🇧</span> {dict.english}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLanguage('cs')}
|
||||
@@ -109,11 +111,71 @@ const SettingsModal = ({ language, setLanguage, theme, setTheme, onClose }: Prop
|
||||
cursor: 'pointer', transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
<FiGlobe /> {dict.czech}
|
||||
<span style={{ fontSize: '1.2rem', lineHeight: 1 }}>🇨🇿</span> {dict.czech}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wind Units Setting */}
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.75rem', color: 'var(--text-muted)', fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '1px' }}>
|
||||
{dict.windUnits}
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<button
|
||||
onClick={() => setWindUnit('kmh')}
|
||||
style={{
|
||||
flex: 1, padding: '0.75rem', borderRadius: '0.5rem',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem',
|
||||
border: windUnit === 'kmh' ? '1px solid var(--color-cyan)' : '1px solid var(--border-color)',
|
||||
backgroundColor: windUnit === 'kmh' ? 'rgba(6, 182, 212, 0.1)' : 'transparent',
|
||||
color: windUnit === 'kmh' ? 'var(--color-cyan)' : 'var(--text-main)',
|
||||
cursor: 'pointer', transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
<FiWind /> {dict.windUnitKmh}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWindUnit('ms')}
|
||||
style={{
|
||||
flex: 1, padding: '0.75rem', borderRadius: '0.5rem',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem',
|
||||
border: windUnit === 'ms' ? '1px solid var(--color-cyan)' : '1px solid var(--border-color)',
|
||||
backgroundColor: windUnit === 'ms' ? 'rgba(6, 182, 212, 0.1)' : 'transparent',
|
||||
color: windUnit === 'ms' ? 'var(--color-cyan)' : 'var(--text-main)',
|
||||
cursor: 'pointer', transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
<FiWind /> {dict.windUnitMs}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.75rem', color: 'var(--text-muted)', fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '1px' }}>
|
||||
{dict.contact}
|
||||
</label>
|
||||
<a
|
||||
href="mailto:info@hladinator.cz"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
color: 'var(--color-cyan)',
|
||||
fontSize: '0.95rem',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
opacity: 0.9,
|
||||
transition: 'opacity 0.2s'
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.opacity = '1'}
|
||||
onMouseOut={(e) => e.currentTarget.style.opacity = '0.9'}
|
||||
>
|
||||
✉ info@hladinator.cz
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Buy me a coffee */}
|
||||
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '1.5rem', textAlign: 'center' }}>
|
||||
<a
|
||||
|
||||
+41
-10
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { FiDroplet, FiStar, FiMap, FiSettings, FiMenu, FiChevronLeft, FiChevronRight } from 'react-icons/fi';
|
||||
import { FiDroplet, FiStar, FiMap, FiSettings, FiChevronLeft, FiChevronRight, FiDatabase } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { useFavorites } from '../hooks/useFavorites';
|
||||
|
||||
interface Props {
|
||||
language: Language;
|
||||
@@ -15,10 +16,11 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dict = t[language].sidebar;
|
||||
const { favorites } = useFavorites();
|
||||
|
||||
const isOverview = location.pathname === '/';
|
||||
const isFavoritesPage = location.pathname === '/favorites';
|
||||
const isMap = location.pathname === '/map';
|
||||
const isDetail = !isOverview && !isMap;
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
navigate(path);
|
||||
@@ -27,18 +29,19 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
|
||||
|
||||
return (
|
||||
<div className={`sidebar ${isCollapsed ? 'collapsed' : ''} ${isMobileMenuOpen ? 'mobile-open' : ''}`}>
|
||||
<div className="sidebar-logo" style={{ position: 'relative' }}>
|
||||
<FiDroplet />
|
||||
<div className="sidebar-logo">
|
||||
<FiDroplet size={28} color="var(--color-cyan)" />
|
||||
<div className="sidebar-text">
|
||||
<span>HLADINATOR</span>
|
||||
<span style={{ fontWeight: 'bold', letterSpacing: '0.5px', fontSize: '1.1rem' }}>HLADINATOR</span>
|
||||
<small>v1.0</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle Button */}
|
||||
{/* Toggle Button */}
|
||||
<div style={{ display: 'flex', justifyContent: isCollapsed ? 'center' : 'flex-end', marginBottom: '1.5rem', marginTop: isCollapsed ? '1rem' : '-0.5rem' }}>
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
style={{
|
||||
position: 'absolute', right: isCollapsed ? '-16px' : '-16px', top: '50%', transform: 'translateY(-50%)',
|
||||
background: 'var(--bg-card)', border: '1px solid var(--border-color)', color: 'var(--text-main)',
|
||||
borderRadius: '50%', width: '32px', height: '32px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'pointer', zIndex: 10, boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
|
||||
@@ -49,14 +52,42 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
|
||||
</div>
|
||||
|
||||
<div className="nav-links">
|
||||
<div className={`nav-item ${isDetail ? 'active' : ''}`} onClick={() => handleNavigate('/lipno-1')}>
|
||||
<FiStar />
|
||||
{/* Favourites */}
|
||||
<div className={`nav-item ${isFavoritesPage ? 'active' : ''}`} onClick={() => handleNavigate('/favorites')} style={{ position: 'relative' }}>
|
||||
<div style={{ position: 'relative', display: 'flex' }}>
|
||||
<FiStar fill={favorites.length > 0 ? '#f59e0b' : 'none'} color={favorites.length > 0 ? '#f59e0b' : 'currentColor'} />
|
||||
{favorites.length > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-12px',
|
||||
backgroundColor: '#f59e0b',
|
||||
color: '#000',
|
||||
borderRadius: '999px',
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 'bold',
|
||||
minWidth: '16px',
|
||||
height: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 4px',
|
||||
border: '2px solid var(--bg-card)'
|
||||
}}>
|
||||
{favorites.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="sidebar-text">{dict.favorites}</span>
|
||||
</div>
|
||||
|
||||
{/* Lakes & Reservoirs */}
|
||||
<div className={`nav-item ${isOverview ? 'active' : ''}`} onClick={() => handleNavigate('/')}>
|
||||
<FiMenu />
|
||||
<FiDatabase />
|
||||
<span className="sidebar-text">{dict.lakes}</span>
|
||||
</div>
|
||||
|
||||
{/* Map */}
|
||||
<div className={`nav-item ${isMap ? 'active' : ''}`} onClick={() => handleNavigate('/map')}>
|
||||
<FiMap />
|
||||
<span className="sidebar-text">{dict.map}</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FiSearch, FiMenu, FiDroplet } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
interface Props {
|
||||
language: Language;
|
||||
@@ -8,6 +9,8 @@ interface Props {
|
||||
|
||||
const Topbar = ({ language, onToggleMobileMenu }: Props) => {
|
||||
const dict = t[language].topbar;
|
||||
const location = useLocation();
|
||||
const showSearch = location.pathname === '/' || location.pathname === '/favorites';
|
||||
|
||||
return (
|
||||
<div className="topbar">
|
||||
@@ -19,10 +22,12 @@ const Topbar = ({ language, onToggleMobileMenu }: Props) => {
|
||||
<span>Hladinator</span>
|
||||
</div>
|
||||
|
||||
<div className="search-bar">
|
||||
<FiSearch />
|
||||
<input type="text" placeholder={dict.search} />
|
||||
</div>
|
||||
{showSearch && (
|
||||
<div className="search-bar">
|
||||
<FiSearch />
|
||||
<input type="text" placeholder={dict.search} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FiWind, FiSunrise, FiSunset, FiThermometer, FiAlertCircle } from 'react-icons/fi';
|
||||
|
||||
interface WeatherProps {
|
||||
lat: number;
|
||||
lng: number;
|
||||
language: 'cs' | 'en';
|
||||
sensorTemp?: number;
|
||||
windUnit?: 'kmh' | 'ms';
|
||||
}
|
||||
|
||||
interface WeatherData {
|
||||
temp: number;
|
||||
windSpeed: number; // m/s
|
||||
windGusts: number; // m/s
|
||||
windDir: number; // degrees
|
||||
sunrise: string;
|
||||
sunset: string;
|
||||
}
|
||||
|
||||
const getCompassDirection = (degrees: number, language: 'cs' | 'en') => {
|
||||
const directionsEn = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
|
||||
const directionsCs = ['S', 'SV', 'V', 'JV', 'J', 'JZ', 'Z', 'SZ'];
|
||||
const directions = language === 'cs' ? directionsCs : directionsEn;
|
||||
const index = Math.round(((degrees %= 360) < 0 ? degrees + 360 : degrees) / 45) % 8;
|
||||
return directions[index];
|
||||
};
|
||||
|
||||
const formatTime = (isoString: string) => {
|
||||
if (!isoString) return '--:--';
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
export const WeatherWidget = ({ lat, lng, language, sensorTemp, windUnit = 'kmh' }: WeatherProps) => {
|
||||
const [data, setData] = useState<WeatherData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lat || !lng) {
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchWeather = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,wind_speed_10m,wind_direction_10m,wind_gusts_10m&daily=sunrise,sunset&timezone=auto&wind_speed_unit=${windUnit}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch weather');
|
||||
|
||||
const json = await res.json();
|
||||
setData({
|
||||
temp: json.current.temperature_2m,
|
||||
windSpeed: json.current.wind_speed_10m,
|
||||
windGusts: json.current.wind_gusts_10m,
|
||||
windDir: json.current.wind_direction_10m,
|
||||
sunrise: json.daily.sunrise[0],
|
||||
sunset: json.daily.sunset[0]
|
||||
});
|
||||
setError(false);
|
||||
} catch (err) {
|
||||
console.error('Weather fetch error:', err);
|
||||
setError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchWeather();
|
||||
|
||||
// Refresh weather every 15 minutes
|
||||
const interval = setInterval(fetchWeather, 15 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [lat, lng]);
|
||||
|
||||
const dict = {
|
||||
cs: { title: 'Počasí a Vítr (Aktuálně)', error: 'Data nedostupná', wind: 'Vítr', gusts: 'Nárazy', temp: 'Teplota' },
|
||||
en: { title: 'Weather & Wind (Current)', error: 'Data unavailable', wind: 'Wind', gusts: 'Gusts', temp: 'Temp' }
|
||||
}[language];
|
||||
|
||||
if (loading) {
|
||||
return <div className="kpi-card" style={{ minHeight: '120px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)' }}>Loading weather...</div>;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="kpi-card" style={{ opacity: 0.7 }}>
|
||||
<h3 style={{ fontSize: '1rem', color: 'var(--text-muted)', margin: '0 0 0.5rem 0' }}>{dict.title}</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--color-red)' }}>
|
||||
<FiAlertCircle /> {dict.error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>{dict.title}</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '1rem', alignItems: 'center' }}>
|
||||
|
||||
{/* Left Column: Wind */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
|
||||
<div style={{
|
||||
width: '40px', height: '40px', borderRadius: '50%', backgroundColor: 'rgba(0, 195, 255, 0.1)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'var(--color-cyan)', fontSize: '1.2rem',
|
||||
transform: `rotate(${data.windDir}deg)`
|
||||
}} title={`Wind direction: ${data.windDir}°`}>
|
||||
<FiWind style={{ transform: 'rotate(-90deg)' }} /> {/* Assume icon points UP by default, wind from south (180) should point UP. Arrow should point where wind is GOING. */}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', lineHeight: 1.1, color: 'var(--text-main)', whiteSpace: 'nowrap' }}>
|
||||
{data.windSpeed.toFixed(1)} <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'} • {getCompassDirection(data.windDir, language)}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginTop: '4px', whiteSpace: 'nowrap' }}>
|
||||
{dict.gusts}: <span style={{ color: data.windGusts > (windUnit === 'kmh' ? 50 : 13.8) ? 'var(--color-red)' : 'var(--text-main)' }}>{data.windGusts.toFixed(1)} {windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Other Info */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', borderLeft: '1px solid var(--border-color)', paddingLeft: '1rem', whiteSpace: 'nowrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem' }} title={sensorTemp !== undefined ? (language === 'cs' ? 'Měřeno přímo senzorem na hrázi' : 'Measured by sensor at the dam') : 'OpenMeteo API'}>
|
||||
<FiThermometer color="var(--color-orange)" />
|
||||
<span style={{ fontWeight: 'bold' }}>{sensorTemp !== undefined ? sensorTemp.toFixed(1) : data.temp.toFixed(1)} °C</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
|
||||
<FiSunrise color="#f59e0b" />
|
||||
<span>{formatTime(data.sunrise)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
|
||||
<FiSunset color="#f59e0b" />
|
||||
<span>{formatTime(data.sunset)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,273 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ComposedChart, Line, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { FiWind } from 'react-icons/fi';
|
||||
import { type Language } from '../translations';
|
||||
|
||||
interface WindChartProps {
|
||||
lat: number;
|
||||
lng: number;
|
||||
language: Language;
|
||||
timeRange?: '24h' | '7d' | '30d' | '1y' | 'all';
|
||||
windUnit?: 'kmh' | 'ms';
|
||||
}
|
||||
|
||||
interface WindDataPoint {
|
||||
time: string;
|
||||
speed: number;
|
||||
gusts: number;
|
||||
dir: number;
|
||||
dirStr: string;
|
||||
}
|
||||
|
||||
const getCompassDirection = (degrees: number, language: 'cs' | 'en') => {
|
||||
const directionsEn = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
|
||||
const directionsCs = ['S', 'SV', 'V', 'JV', 'J', 'JZ', 'Z', 'SZ'];
|
||||
const directions = language === 'cs' ? directionsCs : directionsEn;
|
||||
const index = Math.round(((degrees %= 360) < 0 ? degrees + 360 : degrees) / 45) % 8;
|
||||
return directions[index];
|
||||
};
|
||||
|
||||
const CustomWindTooltip = ({ active, payload, label, language, windUnit = 'kmh' }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
const date = new Date(label);
|
||||
const dateStr = date.toLocaleDateString(language === 'cs' ? 'cs-CZ' : 'en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
const timeStr = date.toLocaleTimeString(language === 'cs' ? 'cs-CZ' : 'en-GB', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: 'rgba(30, 41, 59, 0.95)', border: '1px solid var(--border-color)', borderRadius: '8px', padding: '12px', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.5)', color: 'var(--text-main)', fontSize: '0.9rem', zIndex: 100 }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '4px' }}>
|
||||
{dateStr} {timeStr}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span style={{ color: 'var(--color-cyan)', fontSize: '1.2rem' }}>●</span>
|
||||
<span>{language === 'cs' ? 'Rychlost větru' : 'Wind Speed'}: <strong>{data.speed} {windUnit === 'kmh' ? 'km/h' : 'm/s'}</strong></span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span style={{ color: 'var(--color-purple)', fontSize: '1.2rem' }}>●</span>
|
||||
<span>{language === 'cs' ? 'Nárazy větru' : 'Wind Gusts'}: <strong>{data.gusts} {windUnit === 'kmh' ? 'km/h' : 'm/s'}</strong></span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '4px', color: 'var(--text-muted)' }}>
|
||||
<FiWind />
|
||||
<span>{language === 'cs' ? 'Směr' : 'Direction'}: <strong>{data.dirStr} ({data.dir}°)</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CustomWindDot = (props: any) => {
|
||||
const { cx, cy, payload } = props;
|
||||
|
||||
if (!cx || !cy || payload.dir === undefined) return null;
|
||||
|
||||
return (
|
||||
<g transform={`translate(${cx},${cy}) rotate(${payload.dir}) scale(1.5)`}>
|
||||
<path
|
||||
d="M0,-6 L-4,4 L0,2 L4,4 Z"
|
||||
fill="var(--color-cyan)"
|
||||
stroke="#1e293b"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'kmh' }: WindChartProps) => {
|
||||
const [data, setData] = useState<WindDataPoint[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentSpeed, setCurrentSpeed] = useState(0);
|
||||
const [maxGust, setMaxGust] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchWind = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
let url = '';
|
||||
let isDaily = false;
|
||||
|
||||
if (timeRange === '1y' || timeRange === 'all') {
|
||||
isDaily = true;
|
||||
const end = new Date();
|
||||
end.setDate(end.getDate() - 1);
|
||||
const endStr = end.toISOString().split('T')[0];
|
||||
|
||||
const start = new Date();
|
||||
if (timeRange === '1y') {
|
||||
start.setDate(start.getDate() - 365);
|
||||
} else {
|
||||
start.setFullYear(2020);
|
||||
}
|
||||
const startStr = start.toISOString().split('T')[0];
|
||||
|
||||
url = `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lng}&start_date=${startStr}&end_date=${endStr}&daily=wind_speed_10m_max,wind_gusts_10m_max,wind_direction_10m_dominant&wind_speed_unit=${windUnit}&timezone=auto`;
|
||||
} else {
|
||||
let pastDays = 7;
|
||||
if (timeRange === '24h') pastDays = 1;
|
||||
if (timeRange === '30d') pastDays = 30;
|
||||
url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&hourly=wind_speed_10m,wind_gusts_10m,wind_direction_10m&past_days=${pastDays}&forecast_days=1&wind_speed_unit=${windUnit}&timezone=auto`;
|
||||
}
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error('Failed to fetch wind data');
|
||||
const json = await res.json();
|
||||
|
||||
const times = isDaily ? json.daily.time : json.hourly.time;
|
||||
const speeds = isDaily ? json.daily.wind_speed_10m_max : json.hourly.wind_speed_10m;
|
||||
const gusts = isDaily ? json.daily.wind_gusts_10m_max : json.hourly.wind_gusts_10m;
|
||||
const dirs = isDaily ? json.daily.wind_direction_10m_dominant : json.hourly.wind_direction_10m;
|
||||
|
||||
const chartData: WindDataPoint[] = [];
|
||||
let maxG = 0;
|
||||
|
||||
const now = new Date();
|
||||
let closestIdx = 0;
|
||||
let minDiff = Infinity;
|
||||
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
const t = new Date(times[i]);
|
||||
const diff = Math.abs(t.getTime() - now.getTime());
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closestIdx = i;
|
||||
}
|
||||
|
||||
if (t.getTime() <= now.getTime() || isDaily) {
|
||||
if (gusts[i] > maxG) maxG = gusts[i];
|
||||
chartData.push({
|
||||
time: times[i],
|
||||
speed: speeds[i] || 0,
|
||||
gusts: gusts[i] || 0,
|
||||
dir: dirs[i] || 0,
|
||||
dirStr: getCompassDirection(dirs[i] || 0, language)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let downsampleFactor = 1;
|
||||
if (timeRange === '7d') downsampleFactor = 3;
|
||||
if (timeRange === '30d') downsampleFactor = 12;
|
||||
if (timeRange === '1y') downsampleFactor = 3;
|
||||
if (timeRange === 'all') downsampleFactor = 14;
|
||||
|
||||
const downsampled = chartData.filter((_, i) => i % downsampleFactor === 0 || i === chartData.length - 1);
|
||||
|
||||
setData(downsampled);
|
||||
setMaxGust(maxG);
|
||||
setCurrentSpeed(speeds[closestIdx] || speeds[speeds.length - 1] || 0);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (lat && lng) {
|
||||
fetchWind();
|
||||
}
|
||||
}, [lat, lng, language, timeRange]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ marginTop: '2rem', display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px', color: 'var(--text-muted)' }}>
|
||||
{language === 'cs' ? 'Načítám data o větru...' : 'Loading wind data...'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '3rem', paddingTop: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '1.1rem', color: 'var(--text-main)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<FiWind style={{ color: 'var(--color-cyan)' }} />
|
||||
{language === 'cs' ? `Aktivita větru (${timeRange === '1y' || timeRange === 'all' ? 'denní maxima' : timeRange})` : `Wind Activity (${timeRange === '1y' || timeRange === 'all' ? 'daily max' : timeRange})`}
|
||||
</h3>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1.5rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{language === 'cs' ? 'Aktuální rychlost' : 'Current Speed'}</span>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.25rem' }}>
|
||||
<span style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--text-main)' }}>{currentSpeed.toFixed(1)}</span>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--color-cyan)' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{language === 'cs' ? 'Max. nárazy' : 'Peak Gusts'}</span>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.25rem' }}>
|
||||
<span style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--text-main)' }}>{maxGust.toFixed(1)}</span>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--color-purple)' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minHeight: '280px', width: '100%', marginTop: '0.5rem' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={data} margin={{ top: 20, right: 0, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorWind" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-cyan)" stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor="var(--color-cyan)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="var(--text-muted)"
|
||||
tick={{fill: 'var(--text-muted)', fontSize: 11}}
|
||||
minTickGap={60}
|
||||
tickFormatter={(v) => {
|
||||
const d = new Date(v);
|
||||
return `${d.getDate()}.${d.getMonth()+1}.`;
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="var(--text-muted)"
|
||||
tick={{fill: 'var(--text-muted)', fontSize: 11}}
|
||||
/>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
|
||||
<Tooltip content={<CustomWindTooltip language={language} windUnit={windUnit} />} />
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="speed"
|
||||
stroke="var(--color-cyan)"
|
||||
strokeWidth={2}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorWind)"
|
||||
isAnimationActive={true}
|
||||
dot={<CustomWindDot />}
|
||||
activeDot={{ r: 6, fill: 'var(--color-cyan)', stroke: '#1e293b', strokeWidth: 2 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="gusts"
|
||||
stroke="var(--color-purple)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
dot={false}
|
||||
isAnimationActive={true}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '1.5rem', fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.5rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ display: 'inline-block', width: '12px', height: '3px', backgroundColor: 'var(--color-cyan)' }}></span>
|
||||
<span>{language === 'cs' ? 'Rychlost větru' : 'Wind Speed'}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ display: 'inline-block', width: '12px', height: '3px', borderTop: '2px dashed var(--color-purple)' }}></span>
|
||||
<span>{language === 'cs' ? 'Nárazy větru' : 'Wind Gusts'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -13,9 +13,8 @@ describe('KpiCards Component', () => {
|
||||
};
|
||||
|
||||
it('renders correctly with negative storageDiff (red)', () => {
|
||||
const { container } = render(<KpiCards data={mockData} language="cs" />);
|
||||
|
||||
// ZÁSOBNÍ PROSTOR card should show -1.81 m
|
||||
render(<KpiCards data={mockData} language="cs" />);
|
||||
// STORAGE SPACE card should show -1.81 m
|
||||
expect(screen.getByText('-1.81 m')).toBeInTheDocument();
|
||||
|
||||
// Because it is negative, it should have the red color style applied
|
||||
@@ -36,6 +35,8 @@ describe('KpiCards Component', () => {
|
||||
const noDiffData = { ...mockData, storageDiff: 0, fullness: 85.5 };
|
||||
render(<KpiCards data={noDiffData} language="cs" />);
|
||||
|
||||
expect(screen.getByText('85.5%')).toBeInTheDocument();
|
||||
const elements = screen.getAllByText('85.5%');
|
||||
expect(elements.length).toBeGreaterThan(0);
|
||||
expect(elements[0]).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
||||
|
||||
const STORAGE_KEY = 'hladinator_favorites';
|
||||
|
||||
const loadFavorites = (): string[] => {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
interface FavoritesContextType {
|
||||
favorites: string[];
|
||||
toggleFavorite: (id: string) => void;
|
||||
isFavorite: (id: string) => boolean;
|
||||
}
|
||||
|
||||
const FavoritesContext = createContext<FavoritesContextType>({
|
||||
favorites: [],
|
||||
toggleFavorite: () => {},
|
||||
isFavorite: () => false,
|
||||
});
|
||||
|
||||
export const FavoritesProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [favorites, setFavorites] = useState<string[]>(loadFavorites);
|
||||
|
||||
const toggleFavorite = useCallback((id: string) => {
|
||||
setFavorites(prev => {
|
||||
const next = prev.includes(id) ? prev.filter(f => f !== id) : [...prev, id];
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isFavorite = useCallback((id: string) => favorites.includes(id), [favorites]);
|
||||
|
||||
return (
|
||||
<FavoritesContext.Provider value={{ favorites, toggleFavorite, isFavorite }}>
|
||||
{children}
|
||||
</FavoritesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFavorites = () => useContext(FavoritesContext);
|
||||
+12
-5
@@ -6,14 +6,15 @@
|
||||
--text-main: #f8fafc; /* White text */
|
||||
--text-muted: #94a3b8; /* Gray text */
|
||||
|
||||
--color-cyan: #06b6d4; /* Hladina / Primary */
|
||||
--color-green: #22c55e; /* Přítok / Positive trend */
|
||||
--color-red: #ef4444; /* Odtok / Negative trend */
|
||||
--color-orange: #f97316; /* Odtok line chart color */
|
||||
--color-cyan: #06b6d4; /* Water level / Primary */
|
||||
--color-green: #22c55e; /* Inflow / Positive trend */
|
||||
--color-red: #ef4444; /* Outflow / Negative trend */
|
||||
--color-orange: #f97316; /* Outflow line chart color */
|
||||
--color-purple: #a855f7; /* Wind gusts line color */
|
||||
|
||||
.kpi-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -161,6 +162,12 @@
|
||||
border-top: 8px solid white;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.kpi-grid-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.map-overlay-panel {
|
||||
top: auto;
|
||||
|
||||
+9
-3
@@ -1,13 +1,19 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { FavoritesProvider } from './hooks/useFavorites'
|
||||
import { HelmetProvider } from 'react-helmet-async'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<HelmetProvider>
|
||||
<BrowserRouter>
|
||||
<FavoritesProvider>
|
||||
<App />
|
||||
</FavoritesProvider>
|
||||
</BrowserRouter>
|
||||
</HelmetProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
+32
-2
@@ -4,7 +4,7 @@ export const t = {
|
||||
en: {
|
||||
sidebar: {
|
||||
favorites: 'Favorites',
|
||||
lakes: 'Lakes',
|
||||
lakes: 'Lakes & Reservoirs',
|
||||
map: 'Map',
|
||||
settings: 'Settings'
|
||||
},
|
||||
@@ -12,6 +12,16 @@ export const t = {
|
||||
search: 'Search river or reservoir (e.g. Lipno)...',
|
||||
updated: 'Last updated:'
|
||||
},
|
||||
seo: {
|
||||
homeTitle: 'Hladinátor - Water levels and flow rates of reservoirs',
|
||||
homeDesc: 'Track current water levels, flow rates, inflow, and weather development on major Czech dams and reservoirs in real time. Data sourced from official river basin authorities.',
|
||||
lakeTitle: '{name} - Water level and flow | Hladinátor',
|
||||
lakeDesc: 'Current water level and statistics for the {name} reservoir. Track water level, flow rate, wind strength, and storage capacity in real time.',
|
||||
favoritesTitle: 'Favorites | Hladinátor',
|
||||
favoritesDesc: 'Your pinned lakes and reservoirs. Track water level, flow rate, and weather development on major Czech dams and reservoirs.',
|
||||
mapTitle: 'Map | Hladinátor',
|
||||
mapDesc: 'Interactive map of all monitored lakes and reservoirs in the Czech Republic.'
|
||||
},
|
||||
kpi: {
|
||||
level: 'WATER LEVEL',
|
||||
flow: 'FLOW RATE',
|
||||
@@ -46,13 +56,18 @@ export const t = {
|
||||
language: 'Language',
|
||||
english: 'English',
|
||||
czech: 'Čeština',
|
||||
windUnits: 'Wind units',
|
||||
windUnitKmh: 'km/h',
|
||||
windUnitMs: 'm/s',
|
||||
contact: 'Contact',
|
||||
contactPlaceholder: 'Your email address',
|
||||
buyCoffee: 'Buy Me a Coffee'
|
||||
}
|
||||
},
|
||||
cs: {
|
||||
sidebar: {
|
||||
favorites: 'Oblíbené',
|
||||
lakes: 'Jezera',
|
||||
lakes: 'Jezera a nádrže',
|
||||
map: 'Mapa',
|
||||
settings: 'Nastavení'
|
||||
},
|
||||
@@ -60,6 +75,16 @@ export const t = {
|
||||
search: 'Hledat tok nebo nádrž (např. Lipno)...',
|
||||
updated: 'Aktualizováno:'
|
||||
},
|
||||
seo: {
|
||||
homeTitle: 'Hladinátor - Aktuální stav přehrad a nádrží',
|
||||
homeDesc: 'Sledujte aktuální vodní stav, průtok, přítok a vývoj počasí na nejvýznamnějších českých přehradách v reálném čase. Oficiální data z povodí.',
|
||||
lakeTitle: '{name} - Stav hladiny a průtok | Hladinátor',
|
||||
lakeDesc: 'Aktuální vodní stav a statistiky pro vodní dílo {name}. Sledujte vývoj hladiny, sílu větru a kapacitu zásobního prostoru v reálném čase.',
|
||||
favoritesTitle: 'Oblíbené | Hladinátor',
|
||||
favoritesDesc: 'Vaše připnuté přehrady a nádrže. Sledujte aktuální vodní stav, průtok a vývoj počasí na vybraných českých přehradách.',
|
||||
mapTitle: 'Mapa | Hladinátor',
|
||||
mapDesc: 'Interaktivní mapa všech sledovaných přehrad a nádrží v České republice.'
|
||||
},
|
||||
kpi: {
|
||||
level: 'HLADINA',
|
||||
flow: 'PRŮTOK',
|
||||
@@ -94,6 +119,11 @@ export const t = {
|
||||
language: 'Jazyk',
|
||||
english: 'English',
|
||||
czech: 'Čeština',
|
||||
windUnits: 'Jednotky větru',
|
||||
windUnitKmh: 'km/h',
|
||||
windUnitMs: 'm/s',
|
||||
contact: 'Kontakt',
|
||||
contactPlaceholder: 'Vaše e-mailová adresa',
|
||||
buyCoffee: 'Kup mi kávu'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
export interface NavigationLimit {
|
||||
level: number;
|
||||
labelCs: string;
|
||||
labelEn: string;
|
||||
type: 'danger' | 'warning';
|
||||
}
|
||||
|
||||
export const NAVIGATION_LIMITS: Record<string, NavigationLimit[]> = {
|
||||
// Orlík
|
||||
'VLOR|2': [
|
||||
{
|
||||
level: 342.50,
|
||||
labelCs: 'Minimální hladina pro lodní výtah Orlík',
|
||||
labelEn: 'Minimum level for Orlík boat lift',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
// Slapy
|
||||
'VLSL|2': [
|
||||
{
|
||||
level: 266.50,
|
||||
labelCs: 'Minimální hladina pro převoz lodí Slapy',
|
||||
labelEn: 'Minimum level for Slapy boat transport',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
// Lipno 1
|
||||
'VLL1|1': [
|
||||
{
|
||||
level: 719.60,
|
||||
labelCs: 'Ukončení značení plavební dráhy',
|
||||
labelEn: 'End of navigation channel marking',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
// Hněvkovice
|
||||
'VLHN|1': [
|
||||
{
|
||||
level: 368.90,
|
||||
labelCs: 'Minimální plavební hladina',
|
||||
labelEn: 'Minimum navigation level',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
// Hracholusky
|
||||
'MZHR|3': [
|
||||
{
|
||||
level: 351.10,
|
||||
labelCs: 'Zkrácení zaručené plavební dráhy',
|
||||
labelEn: 'Shortened guaranteed navigation channel',
|
||||
type: 'warning'
|
||||
}
|
||||
],
|
||||
// Kamýk
|
||||
'VLKA|2': [
|
||||
{
|
||||
level: 283.60,
|
||||
labelCs: 'Minimální plavební hladina',
|
||||
labelEn: 'Minimum navigation level',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
// Vrané
|
||||
'VLVE|2': [
|
||||
{
|
||||
level: 199.30,
|
||||
labelCs: 'Minimální plavební hladina',
|
||||
labelEn: 'Minimum navigation level',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
// Štěchovice
|
||||
'VLST|2': [
|
||||
{
|
||||
level: 217.20,
|
||||
labelCs: 'Minimální plavební hladina',
|
||||
labelEn: 'Minimum navigation level',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
// Kořensko
|
||||
'VLKO|1': [
|
||||
{
|
||||
level: 352.00,
|
||||
labelCs: 'Minimální plavební hladina',
|
||||
labelEn: 'Minimum navigation level',
|
||||
type: 'danger'
|
||||
}
|
||||
]
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user