feat: add rivers overview component and sync lake volume data across the dataset

This commit is contained in:
David Fencl
2026-06-08 19:36:54 +02:00
parent ec540e056d
commit 62c861e610
71 changed files with 139421 additions and 29818 deletions
+15 -3
View File
@@ -56,10 +56,21 @@ const lakes = lakesConfig.map(lake => {
const metrics = calculateLakeMetrics(currentLevel, volume, lake);
const cleanText = lake.text.replace(/^VD\s+/, '').replace(/^LG\s+/, '');
const parts = cleanText.split('-').map(p => p.trim());
let name = '';
let river = '';
if (parts.length > 1) {
river = parts[parts.length - 1];
name = parts.slice(0, -1).join(' - ');
} else {
name = parts[0];
}
return {
id: lake.id,
name: lake.text.replace('VD ', '').split('-')[0].trim(),
river: lake.text.includes('-') ? lake.text.split('-')[1].trim() : '',
name,
river,
priority: lake.priority || false,
level: currentLevel.toFixed(2),
capacity: metrics.capacity,
@@ -71,7 +82,8 @@ const lakes = lakesConfig.map(lake => {
navigationForbidden: lake.navigationForbidden || false,
lat: lake.coords[0],
lng: lake.coords[1],
sparkline
sparkline,
type: lake.type || 'lake'
};
});
+79
View File
@@ -0,0 +1,79 @@
import fs from 'fs';
import path from 'path';
function fixExistingData() {
const dataDir = path.resolve('public/data');
if (!fs.existsSync(dataDir)) {
console.error('Data directory does not exist!');
return;
}
const files = fs.readdirSync(dataDir).filter(f => f.endsWith('.json'));
console.log(`Found ${files.length} data files to clean up...`);
files.forEach(file => {
const filePath = path.join(dataDir, file);
try {
const content = fs.readFileSync(filePath, 'utf-8');
const data = JSON.parse(content);
if (!Array.isArray(data)) return;
let lastKnownInflow: number | null = null;
let lastKnownVolume: number | null = null;
let fixCountInflow = 0;
let fixCountVolume = 0;
// First pass (oldest to newest): find and propagate values forward
data.forEach(record => {
// Handle inflow
if (record.inflow !== undefined && record.inflow !== null && record.inflow !== 0) {
lastKnownInflow = record.inflow;
} else if ((record.inflow === undefined || record.inflow === null || record.inflow === 0) && lastKnownInflow !== null) {
record.inflow = lastKnownInflow;
fixCountInflow++;
}
// Handle volume
if (record.volume !== undefined && record.volume !== null && record.volume !== 0) {
lastKnownVolume = record.volume;
} else if ((record.volume === undefined || record.volume === null || record.volume === 0) && lastKnownVolume !== null) {
record.volume = lastKnownVolume;
fixCountVolume++;
}
});
// Second pass (newest to oldest): if there were zeros at the very beginning of the file
// before any non-zero value was found, propagate backwards.
let nextKnownInflow: number | null = null;
let nextKnownVolume: number | null = null;
for (let i = data.length - 1; i >= 0; i--) {
const record = data[i];
if (record.inflow !== undefined && record.inflow !== null && record.inflow !== 0) {
nextKnownInflow = record.inflow;
} else if ((record.inflow === undefined || record.inflow === null || record.inflow === 0) && nextKnownInflow !== null) {
record.inflow = nextKnownInflow;
fixCountInflow++;
}
if (record.volume !== undefined && record.volume !== null && record.volume !== 0) {
nextKnownVolume = record.volume;
} else if ((record.volume === undefined || record.volume === null || record.volume === 0) && nextKnownVolume !== null) {
record.volume = nextKnownVolume;
fixCountVolume++;
}
}
if (fixCountInflow > 0 || fixCountVolume > 0) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
console.log(`[${file}] Cleaned up ${fixCountInflow} inflows and ${fixCountVolume} volumes.`);
}
} catch (e: any) {
console.error(`Error processing file ${file}:`, e.message);
}
});
console.log('Cleanup finished.');
}
fixExistingData();
+19 -1
View File
@@ -8,6 +8,7 @@ export interface LakeConfig {
maxLevel?: number;
storageLevel?: number;
navigationForbidden?: boolean;
type?: 'lake' | 'river';
}
export const lakesConfig: LakeConfig[] = [
@@ -49,5 +50,22 @@ export const lakesConfig: LakeConfig[] = [
{ 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 }
{ 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 },
// Rivers
{ id: "VLCH|2", text: "LG Praha - Malá Chuchle - Vltava", coords: [50.0294, 14.3986], type: 'river' },
{ id: "VLCB|1", text: "LG České Budějovice - Vltava", coords: [48.9712, 14.4714], type: 'river' },
{ id: "BEBE|3", text: "LG Beroun - Berounka", coords: [49.9642, 14.0792], type: 'river' },
{ id: "SANE|2", text: "LG Nespeky - Sázava", coords: [49.8596, 14.5888], type: 'river' },
{ id: "OTPI|1", text: "LG Písek - Otava", coords: [49.3083, 14.1436], type: 'river' },
{ id: "OTSU|1", text: "LG Sušice - Otava", coords: [49.2319, 13.5186], type: 'river' },
{ id: "LUBE|1", text: "LG Bechyně - Lužnice", coords: [49.2931, 14.4758], type: 'river' },
{ id: "LUKL|1", text: "LG Klenovice - Lužnice", coords: [49.3402, 14.7175], type: 'river' },
{ id: "SAZR|2", text: "LG Zruč nad Sázavou - Sázava", coords: [49.7428, 15.1011], type: 'river' },
{ id: "SASV|2", text: "LG Světlá nad Sázavou - Sázava", coords: [49.6677, 15.4048], type: 'river' },
{ id: "SAKA|2", text: "LG Kácov - Sázava", coords: [49.7772, 15.0294], type: 'river' },
{ id: "BEZB|3", text: "LG Zbečno - Berounka", coords: [50.0436, 13.9189], type: 'river' },
{ id: "BEPL|3", text: "LG Plzeň - Bílá Hora - Berounka", coords: [49.7731, 13.3986], type: 'river' },
{ id: "VLVB|1", text: "LG Vyšší Brod - Vltava", coords: [48.6167, 14.3167], type: 'river' }
];
+36 -6
View File
@@ -38,7 +38,11 @@ export function parseDateString(dateStr: string): string | null {
}
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 config = lakesConfig.find(l => l.id === lakeId);
const isRiver = config?.type === 'river';
const URL = isRiver
? `https://www.pvl.cz/portal/sap/cz/pc/Mereni.aspx?oid=${oid}&id=${internalId}`
: `https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?oid=${oid}&id=${internalId}`;
const DATA_FILE = path.resolve(`public/data/${internalId}.json`);
try {
@@ -80,7 +84,8 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
const records: DataRecord[] = [];
let dataTable = null;
$('table').each((i, tbl) => {
if ($(tbl).text().includes('Datum') && $(tbl).text().includes('Odtok')) {
const id = ($(tbl).attr('id') || '').toLowerCase();
if (id.includes('datamereni24hgv') || id.includes('datamerenigv')) {
dataTable = $(tbl);
}
});
@@ -102,9 +107,7 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
records.push({
timestamp: parsedDateStr,
level: parseFloat(levelStr) || 0,
flow: parseFloat(flowStr) || 0,
inflow: 0,
volume: 0
flow: parseFloat(flowStr) || 0
});
}
}
@@ -143,7 +146,19 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
const dataMap = new Map<string, DataRecord>();
existingData.forEach(item => dataMap.set(item.timestamp, item));
records.forEach(item => dataMap.set(item.timestamp, item));
records.forEach(item => {
const existing = dataMap.get(item.timestamp);
if (existing) {
dataMap.set(item.timestamp, {
...existing,
...item,
inflow: item.inflow !== undefined ? item.inflow : existing.inflow,
volume: item.volume !== undefined ? item.volume : existing.volume
});
} else {
dataMap.set(item.timestamp, item);
}
});
const mergedData = Array.from(dataMap.values()).sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
@@ -152,6 +167,9 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
// Propagate previous values if missing (user requested)
let lastKnownTemp: number | null = null;
let lastKnownPrecip: number | null = null;
let lastKnownInflow: number | undefined = undefined;
let lastKnownVolume: number | undefined = undefined;
mergedData.forEach(item => {
if (item.temperature !== undefined && item.temperature !== null) {
lastKnownTemp = item.temperature;
@@ -164,6 +182,18 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
} else if (lastKnownPrecip !== null) {
item.precipitation = lastKnownPrecip;
}
if (item.inflow !== undefined && item.inflow !== null) {
lastKnownInflow = item.inflow;
} else if (lastKnownInflow !== undefined) {
item.inflow = lastKnownInflow;
}
if (item.volume !== undefined && item.volume !== null) {
lastKnownVolume = item.volume;
} else if (lastKnownVolume !== undefined) {
item.volume = lastKnownVolume;
}
});
fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });