feat: discharge-driven river animation speed, replay stat tiles, hourly doc render
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 24s
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 15s
Documentation / Validate Documentation (push) Failing after 10s
Documentation / Generate API Documentation (push) Successful in 11s
Documentation / Build Sphinx Documentation (push) Successful in 20s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 24s
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 15s
Documentation / Validate Documentation (push) Failing after 10s
Documentation / Generate API Documentation (push) Successful in 11s
Documentation / Build Sphinx Documentation (push) Successful in 20s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s
- river dash animation now flows at a speed continuously derived from each segment's discharge (period 260/(Q+45) s, clamped 0.5-5.5 s) via inline per-path animation-duration, so it updates live and per-frame during the replay (CSS speed classes removed - setStyle cannot change classes) - the replay drives the Combined discharge and Strongest flow stat tiles each frame (marked '2024 replay'), restored on finish - docs: hour-by-hour detection detail render (22-28 Sep 2024) showing the model alert at 24 Sep 01:00, flooding at 25 Sep 01:00, and the 24 h warning between them; embedded with commentary - Matrix alerts now link to https://water.buildfor.life/ (override via ALERT_DASHBOARD_URL), replacing the Grafana public dashboard link
This commit is contained in:
+3
-3
@@ -155,9 +155,9 @@ class MatrixNotifier:
|
||||
if alert.message:
|
||||
message += f"\n**Details:** {alert.message}\n"
|
||||
|
||||
# Add Grafana public dashboard link
|
||||
grafana_url = "https://metrics.b4l.co.th/public-dashboards/655730aa044f44f49b355d01386018ca"
|
||||
message += f"\n📈 **View Dashboard:** {grafana_url}"
|
||||
# Add live dashboard link
|
||||
dashboard_url = os.getenv("ALERT_DASHBOARD_URL", "https://water.buildfor.life/")
|
||||
message += f"\n📈 **View Dashboard:** {dashboard_url}"
|
||||
|
||||
return self.send_message(message)
|
||||
|
||||
|
||||
+25
-14
@@ -116,12 +116,7 @@
|
||||
opacity: .36; animation: pulse 2.2s ease-out infinite;
|
||||
}
|
||||
@keyframes pulse { 0% { transform: scale(.72); opacity: .55; } 75%,100% { transform: scale(1.35); opacity: 0; } }
|
||||
.flow-line { animation: riverMove 3s linear infinite; }
|
||||
.flow-idle { animation-duration: 5.5s; }
|
||||
.flow-slow { animation-duration: 3s; }
|
||||
.flow-med { animation-duration: 1.9s; }
|
||||
.flow-fast { animation-duration: 1.15s; }
|
||||
.flow-surge { animation-duration: .7s; }
|
||||
.flow-line { animation: riverMove 3s linear infinite; } /* per-segment duration set inline from discharge */
|
||||
@keyframes riverMove { to { stroke-dashoffset: -40; } }
|
||||
@media (prefers-reduced-motion: reduce) { .flow-line, .flow-marker::before { animation: none; } }
|
||||
.line-swatch { width: 24px; height: 4px; border-radius: 2px; flex: none; }
|
||||
@@ -353,12 +348,18 @@
|
||||
return q == null ? 2.5 : Math.max(3, Math.min(9, 2.5 + Math.sqrt(Math.max(0, q)) * .38));
|
||||
}
|
||||
|
||||
function riverSpeedClass(q) {
|
||||
if (q == null) return 'flow-idle';
|
||||
if (q < 25) return 'flow-slow';
|
||||
if (q < 100) return 'flow-med';
|
||||
if (q < 250) return 'flow-fast';
|
||||
return 'flow-surge';
|
||||
// Continuous dash-animation period from discharge: calm water drifts (~5.5 s
|
||||
// per cycle), a flood races (~0.5 s). Applied as inline animation-duration on
|
||||
// each SVG path so it can be updated live (setStyle cannot change classes).
|
||||
function flowDuration(q) {
|
||||
if (q == null) return 5.5;
|
||||
return Math.max(.5, Math.min(5.5, 260 / (Number(q) + 45)));
|
||||
}
|
||||
|
||||
function applyFlowSpeed(layerGroup, flows) {
|
||||
layerGroup.eachLayer((layer) => {
|
||||
if (layer._path) layer._path.style.animationDuration = `${flowDuration(flows.get(layer.feature))}s`;
|
||||
});
|
||||
}
|
||||
|
||||
function riverGauges(stations, readings) {
|
||||
@@ -387,12 +388,13 @@
|
||||
return {
|
||||
color: q == null ? '#69b7d0' : flowColor(q),
|
||||
weight: riverWeight(q), opacity: .92, lineCap: 'round',
|
||||
dashArray: '6 14', className: `flow-line ${riverSpeedClass(q)}`
|
||||
dashArray: '6 14', className: 'flow-line'
|
||||
};
|
||||
}
|
||||
}).addTo(state.map);
|
||||
flow.bringToBack();
|
||||
casing.bringToBack();
|
||||
applyFlowSpeed(flow, flowBySegment);
|
||||
state.layers.push(casing, flow);
|
||||
state.riverNet = riverNetwork;
|
||||
state.riverStations = stations;
|
||||
@@ -410,6 +412,7 @@
|
||||
const q = flows.get(f);
|
||||
return { color: q == null ? '#69b7d0' : flowColor(q), weight: riverWeight(q), opacity: .92 };
|
||||
});
|
||||
applyFlowSpeed(state.riverFlow, flows);
|
||||
}
|
||||
|
||||
function updateStationMarker(code, discharge) {
|
||||
@@ -820,13 +823,21 @@
|
||||
const frame = Math.min(i, n - 1);
|
||||
const readings = new Map();
|
||||
let totalFlow = 0;
|
||||
let peakFlow = null;
|
||||
Object.entries(data.stations).forEach(([code, series]) => {
|
||||
const level = series.level[frame];
|
||||
const discharge = series.discharge[frame];
|
||||
readings.set(code, { water_level: level, discharge });
|
||||
updateStationMarker(code, discharge);
|
||||
if (discharge != null) totalFlow += discharge;
|
||||
if (discharge != null) {
|
||||
totalFlow += discharge;
|
||||
if (!peakFlow || discharge > peakFlow.value) peakFlow = { code, value: discharge };
|
||||
}
|
||||
});
|
||||
// stat tiles follow the replay (restored by loadDashboard on finish)
|
||||
$('total-flow').textContent = totalFlow.toLocaleString(undefined, { maximumFractionDigits: 1 });
|
||||
$('peak-flow').textContent = peakFlow ? peakFlow.value.toFixed(1) : '—';
|
||||
$('peak-station').textContent = peakFlow ? `${peakFlow.code} · m³/s · 2024 replay` : 'No discharge reported';
|
||||
restyleRiver(readings);
|
||||
const p1Level = data.stations['P.1'] ? data.stations['P.1'].level[frame] : null;
|
||||
if (p1Level != null) state.p1Now = Number(p1Level);
|
||||
|
||||
Reference in New Issue
Block a user