feat: Oct-2024 flood replay, simulation hooks, and static replay data
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 30s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 30s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
- header button 'Replay Oct 2024 flood': animates the real Sep 15 - Oct 12 2024 P.1 hourly series (~45 s) through the flood-zone display; zones flood blue as the river climbs to the 5.30 m record and recede as it falls; click again to stop; restores live state when done - replay reads a pre-extracted 15 kB static snapshot (static/flood-2024-p1.json, 335 frames) instead of pulling the 4.6 MB all-time history on every click; falls back to the history API if the snapshot is missing - demo hooks for testing/presentations: ?demo_level=4.4 pins a simulated P.1 level, ?demo_rise=1 animates rising water to 5.30 m; both label the outlook line as SIMULATION with the real level alongside
This commit is contained in:
@@ -182,6 +182,7 @@
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="live-pill"><span class="live-dot"></span> LIVE DATA</div>
|
||||
<button id="replay-2024" type="button">▶ Replay Oct 2024 flood</button>
|
||||
<button id="refresh-button" type="button">↻ Refresh</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -732,6 +733,61 @@
|
||||
return `<div class="popup"><div class="popup-code">Marker</div><h3>${escapeHtml(poi.name)}</h3>${poi.note ? `<div class="popup-th">${escapeHtml(poi.note)}</div>` : ''}${riskHtml}</div>`;
|
||||
}
|
||||
|
||||
async function replayFlood2024() {
|
||||
const button = $('replay-2024');
|
||||
if (state.replayTimer) { // stop a running replay
|
||||
window.clearInterval(state.replayTimer);
|
||||
state.replayTimer = null;
|
||||
state.p1Now = state.p1RealNow;
|
||||
restyleFloodZones();
|
||||
button.textContent = '▶ Replay Oct 2024 flood';
|
||||
$('p1-peak').textContent = state.p1PeakText || '';
|
||||
return;
|
||||
}
|
||||
button.textContent = 'Loading 2024 data…';
|
||||
try {
|
||||
// pre-extracted static snapshot (15 kB); fall back to the live history API
|
||||
let frames;
|
||||
const staticResponse = await fetch('/static/flood-2024-p1.json');
|
||||
if (staticResponse.ok) {
|
||||
frames = await staticResponse.json();
|
||||
} else {
|
||||
const response = await fetch('/measurements/history/P.1?hours=876000&limit=100000');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const rows = await response.json();
|
||||
frames = rows.filter((row) => {
|
||||
const t = row.timestamp;
|
||||
return t >= '2024-09-15' && t <= '2024-10-12' && row.water_level != null;
|
||||
});
|
||||
}
|
||||
if (frames.length < 10) throw new Error('no 2024 data in history');
|
||||
state.p1RealNow = state.p1Now;
|
||||
state.p1PeakText = $('p1-peak').textContent;
|
||||
state.zonesUserHidden = false;
|
||||
await showFloodZones(false);
|
||||
button.textContent = '⏹ Stop replay';
|
||||
let i = 0;
|
||||
state.replayTimer = window.setInterval(() => {
|
||||
const frame = frames[Math.min(i, frames.length - 1)];
|
||||
state.p1Now = Number(frame.water_level);
|
||||
const when = new Date(frame.timestamp).toLocaleString([], { day: 'numeric', month: 'short', hour: '2-digit' });
|
||||
$('p1-peak').textContent = `⏪ 2024 flood replay · ${when} · P.1 at ${state.p1Now.toFixed(2)} m`;
|
||||
restyleFloodZones();
|
||||
i += 1; // 1 h per frame (~45 s total)
|
||||
if (i >= frames.length + 25) { // hold the last frame briefly
|
||||
window.clearInterval(state.replayTimer);
|
||||
state.replayTimer = null;
|
||||
state.p1Now = state.p1RealNow;
|
||||
restyleFloodZones();
|
||||
button.textContent = '▶ Replay Oct 2024 flood';
|
||||
$('p1-peak').textContent = state.p1PeakText || '';
|
||||
}
|
||||
}, 120);
|
||||
} catch (error) {
|
||||
button.textContent = `Replay unavailable: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomMarkers() {
|
||||
try {
|
||||
const response = await fetch('/static/custom-markers.json');
|
||||
@@ -773,8 +829,25 @@
|
||||
const row = p1rows.reduce((best, r) => r.horizon_hours > best.horizon_hours ? r : best);
|
||||
state.p1Stages = row.stages;
|
||||
state.p1Now = row.current_level == null ? null : Number(row.current_level);
|
||||
// Demo hooks: ?demo_level=4.4 pins a simulated P.1 level; ?demo_rise=1 animates
|
||||
// the water from the real level up to the 2024 record (5.30 m).
|
||||
const params = new URLSearchParams(location.search);
|
||||
const demoLevel = Number(params.get('demo_level'));
|
||||
const simulated = Number.isFinite(demoLevel) && demoLevel > 0;
|
||||
if (simulated) state.p1Now = demoLevel;
|
||||
if (params.has('demo_rise') && !state.demoRiseTimer) {
|
||||
let level = state.p1Now ?? 2.2;
|
||||
state.demoRiseTimer = window.setInterval(() => {
|
||||
level = Math.min(level + 0.05, 5.30);
|
||||
state.p1Now = level;
|
||||
$('p1-peak').textContent = `⚠ SIMULATION: rising water · P.1 at ${level.toFixed(2)} m (real: ${Number(row.current_level).toFixed(2)} m)`;
|
||||
restyleFloodZones();
|
||||
if (level >= 5.30) window.clearInterval(state.demoRiseTimer);
|
||||
}, 700);
|
||||
}
|
||||
restyleFloodZones();
|
||||
$('p1-peak').textContent = `Now ${Number(row.current_level).toFixed(2)} m · predicted peak next ${row.horizon_hours} h: ${Number(row.predicted_max_level).toFixed(2)} m`;
|
||||
$('p1-peak').textContent = (simulated ? `⚠ SIMULATION: P.1 at ${demoLevel.toFixed(2)} m (real: ${Number(row.current_level).toFixed(2)} m)` :
|
||||
`Now ${Number(row.current_level).toFixed(2)} m`) + ` · predicted peak next ${row.horizon_hours} h: ${Number(row.predicted_max_level).toFixed(2)} m`;
|
||||
const strip = $('p1-stages');
|
||||
strip.replaceChildren();
|
||||
row.stages.forEach((s) => {
|
||||
@@ -835,6 +908,7 @@
|
||||
|
||||
$('refresh-button').addEventListener('click', loadDashboard);
|
||||
$('zones-toggle').addEventListener('click', toggleFloodZones);
|
||||
$('replay-2024').addEventListener('click', replayFlood2024);
|
||||
$('history-range').addEventListener('change', () => { if (state.selectedStation) loadHistory(state.selectedStation); });
|
||||
loadDashboard();
|
||||
window.setInterval(loadDashboard, 5 * 60 * 1000);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user