feat: implement automated data scraping and history generation pipeline for PVL reservoir levels
This commit is contained in:
@@ -63,6 +63,11 @@ const lakes = lakesConfig.map(lake => {
|
||||
if (volume === 0) volume = lake.maxVolume || 0;
|
||||
}
|
||||
|
||||
let storageDiff = 0;
|
||||
if (lake.storageLevel && currentLevel > 0) {
|
||||
storageDiff = Number((currentLevel - lake.storageLevel).toFixed(2));
|
||||
}
|
||||
|
||||
return {
|
||||
id: lake.id,
|
||||
name: lake.text.replace('VD ', '').split('-')[0].trim(),
|
||||
@@ -70,6 +75,7 @@ const lakes = lakesConfig.map(lake => {
|
||||
priority: lake.priority || false,
|
||||
level: currentLevel.toFixed(2),
|
||||
capacity: capacity,
|
||||
storageDiff: storageDiff,
|
||||
inflow: inflow.toFixed(1),
|
||||
outflow: currentFlow.toFixed(1),
|
||||
volume: volume,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DATA_DIR = path.resolve(process.cwd(), 'public/data');
|
||||
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
console.error("Data directory not found.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(DATA_DIR).filter(f => f.endsWith('.json') && f !== 'lakes_index.json');
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(DATA_DIR, file);
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
if (data.length === 0) return;
|
||||
|
||||
// The oldest record currently in DB
|
||||
const oldest = data[0];
|
||||
const oldestTime = new Date(oldest.timestamp).getTime();
|
||||
|
||||
const newRecords: any[] = [];
|
||||
|
||||
let currentLevel = oldest.level;
|
||||
let currentFlow = oldest.flow;
|
||||
let currentInflow = oldest.inflow || (Math.random() * 10 + 5);
|
||||
|
||||
// Generate 720 records (30 days * 24 hours) going BACKWARDS
|
||||
for (let i = 1; i <= 720; i++) {
|
||||
const d = new Date(oldestTime - i * 60 * 60 * 1000);
|
||||
|
||||
// random walk for level
|
||||
currentLevel = currentLevel + (Math.random() - 0.5) * 0.05;
|
||||
|
||||
// random walk for outflow and inflow
|
||||
currentFlow = Math.max(0, currentFlow + (Math.random() - 0.5) * 2);
|
||||
currentInflow = Math.max(0, currentInflow + (Math.random() - 0.5) * 2);
|
||||
|
||||
// Temperature: daily sine wave (colder at night, warmer in day) + noise
|
||||
const hour = d.getHours();
|
||||
const tempBase = 18; // base 18C
|
||||
const tempDay = Math.sin(((hour - 6) / 24) * Math.PI * 2) * 8; // cold morning, warm afternoon
|
||||
const randomTemp = tempBase + tempDay + (Math.random() - 0.5) * 2;
|
||||
|
||||
// Precipitation: rare spikes
|
||||
const randomPrecip = Math.random() > 0.95 ? Math.random() * 15 : 0;
|
||||
|
||||
newRecords.push({
|
||||
timestamp: d.toISOString(),
|
||||
level: currentLevel,
|
||||
flow: currentFlow,
|
||||
inflow: currentInflow,
|
||||
volume: oldest.volume, // volume changes too slow, keep constant for mock
|
||||
temperature: randomTemp,
|
||||
precipitation: randomPrecip
|
||||
});
|
||||
}
|
||||
|
||||
// Combine: newRecords are older, so reverse them to make chronological (oldest first), then add real data
|
||||
const allRecords = [...newRecords.reverse(), ...data];
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(allRecords, null, 2));
|
||||
console.log(`Generated 30 days of realistic mock history for ${file}`);
|
||||
});
|
||||
+16
-12
@@ -3,19 +3,23 @@ export interface LakeConfig {
|
||||
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 },
|
||||
{ id: "VLL2|1", text: "VD Lipno II - Vltava", coords: [48.6250, 14.3180], maxVolume: 1.5, minLevel: 510.0, maxLevel: 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 },
|
||||
{ id: "VLKO|1", text: "VD Kořensko - Vltava", coords: [49.2550, 14.3980], maxVolume: 2.8, minLevel: 352.0, maxLevel: 353.5 },
|
||||
{ id: "VLOR|2", text: "VD Orlík - Vltava", priority: true, coords: [49.6060, 14.1700], maxVolume: 716.5, minLevel: 330.00, maxLevel: 354.00 },
|
||||
{ id: "UHKA|2", text: "VD Kamýk - Vltava", coords: [49.6360, 14.2540], maxVolume: 12.8, minLevel: 283.0, maxLevel: 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 },
|
||||
{ id: "VLST|2", text: "VD Štěchovice - Vltava", coords: [49.8450, 14.4120], maxVolume: 11.2, minLevel: 217.0, maxLevel: 219.5 },
|
||||
{ id: "VRSN|2", text: "VD Vrané - Vltava", coords: [49.9340, 14.3850], maxVolume: 11.1, minLevel: 198.5, maxLevel: 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 },
|
||||
{ id: "MARI|1", text: "VD Římov - Malše", priority: true, coords: [48.8470, 14.4870], maxVolume: 33.8, minLevel: 458.0, maxLevel: 471.0 },
|
||||
{ id: "MZHR|3", text: "VD Hracholusky - Mže", priority: true, coords: [49.7890, 13.1550], maxVolume: 56.7, minLevel: 360.0, maxLevel: 373.0 }
|
||||
{ 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 }
|
||||
];
|
||||
|
||||
@@ -9,6 +9,10 @@ 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
|
||||
@@ -46,6 +50,8 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
|
||||
|
||||
let currentInflow = 0;
|
||||
let currentVolume = 0;
|
||||
let currentTemp: number | null = null;
|
||||
let currentPrecip: number | null = null;
|
||||
|
||||
$('table').each((i, tbl) => {
|
||||
const text = $(tbl).text();
|
||||
@@ -55,6 +61,14 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
|
||||
const valStr = $(r).find('td').eq(1).text().trim().replace(/\s/g, '').replace(',', '.');
|
||||
if (label.includes('Přítok')) currentInflow = parseFloat(valStr) || 0;
|
||||
if (label.includes('Objem')) currentVolume = parseFloat(valStr) || 0;
|
||||
if (label.includes('Teplota')) {
|
||||
const v = parseFloat(valStr);
|
||||
if (!isNaN(v)) currentTemp = v;
|
||||
}
|
||||
if (label.includes('Srážky')) {
|
||||
const v = parseFloat(valStr);
|
||||
if (!isNaN(v)) currentPrecip = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -97,6 +111,8 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
|
||||
// Apply current values to the latest record
|
||||
records[0].inflow = currentInflow;
|
||||
records[0].volume = currentVolume;
|
||||
if (currentTemp !== null) records[0].temperature = currentTemp;
|
||||
if (currentPrecip !== null) records[0].precipitation = currentPrecip;
|
||||
}
|
||||
|
||||
let existingData: DataRecord[] = [];
|
||||
@@ -113,6 +129,23 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
|
||||
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
|
||||
});
|
||||
|
||||
// Propagate previous values if missing (user requested)
|
||||
let lastKnownTemp: number | null = null;
|
||||
let lastKnownPrecip: number | null = null;
|
||||
mergedData.forEach(item => {
|
||||
if (item.temperature !== undefined && item.temperature !== null) {
|
||||
lastKnownTemp = item.temperature;
|
||||
} else if (lastKnownTemp !== null) {
|
||||
item.temperature = lastKnownTemp;
|
||||
}
|
||||
|
||||
if (item.precipitation !== undefined && item.precipitation !== null) {
|
||||
lastKnownPrecip = item.precipitation;
|
||||
} else if (lastKnownPrecip !== null) {
|
||||
item.precipitation = lastKnownPrecip;
|
||||
}
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(mergedData, null, 2), 'utf-8');
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const minutes = parseInt(args[0], 10) || 10;
|
||||
const intervalMs = minutes * 60 * 1000;
|
||||
|
||||
console.log(`\n⏱️ HLADINATOR Watcher spuštěn!`);
|
||||
console.log(`Budu automaticky stahovat nová data každých ${minutes} minut.\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`);
|
||||
} catch (error: any) {
|
||||
console.error(`[${new Date().toLocaleTimeString('cs-CZ')}] ❌ Chyba při aktualizaci:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Spustit ihned po zapnutí
|
||||
runUpdate();
|
||||
|
||||
// A pak periodicky v zadaném intervalu
|
||||
setInterval(runUpdate, intervalMs);
|
||||
Reference in New Issue
Block a user