feat: implement sensor glitch detection for water levels and update data cleaning logic

This commit is contained in:
David Fencl
2026-06-08 19:45:37 +02:00
parent 62c861e610
commit f8a7be7fa3
28 changed files with 222 additions and 64 deletions
+74 -7
View File
@@ -121,12 +121,34 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
.catch(err => console.error(err));
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
const staticConfig = lakesConfig.find(l => l.id.split('|')[0] === internalId);
fetch(`/data/${internalId}.json?t=${Date.now()}`)
.then(res => res.json())
.then(json => {
let lastValidLevel: number | null = null;
const formattedData = json.map((item: any) => {
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
let level = item.level === null || isNaN(item.level) ? 0 : item.level;
// Outlier/sensor glitch detection
if (level > 0) {
if (staticConfig && staticConfig.minLevel && staticConfig.maxLevel) {
const minAllowed = staticConfig.minLevel - 5;
const maxAllowed = staticConfig.maxLevel + 5;
if (level < minAllowed || level > maxAllowed) {
// Glitch detected, fallback to last known valid level
level = lastValidLevel !== null ? lastValidLevel : (staticConfig.minLevel + staticConfig.maxLevel) / 2;
} else {
lastValidLevel = level;
}
} else {
lastValidLevel = level;
}
} else if (lastValidLevel !== null) {
// Level is 0 or less, fallback
level = lastValidLevel;
}
return {
timestamp: item.timestamp,
@@ -134,7 +156,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
}),
level: item.level === null || isNaN(item.level) ? 0 : item.level,
level: level,
outflow: outflow,
inflow: item.inflow || 0,
volume: item.volume || 0,
@@ -393,13 +415,58 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
{/* 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 }} />
<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: 11 }} />
))}
{!isRiver && 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 }} />
)}
{!isRiver && 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 }} />
{!isRiver && staticConfig?.maxLevel && staticConfig?.storageLevel && Math.abs(staticConfig.maxLevel - staticConfig.storageLevel) < 0.05 ? (
<ReferenceLine
yAxisId="left"
y={staticConfig.maxLevel}
stroke="var(--color-orange)"
strokeDasharray="3 3"
label={{
position: 'insideBottomRight',
value: language === 'cs'
? `Max. retenční / zásobní hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)`
: `Max retention / storage level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`,
fill: 'var(--color-orange)',
fontSize: 11
}}
/>
) : (
<>
{!isRiver && staticConfig?.maxLevel && (
<ReferenceLine
yAxisId="left"
y={staticConfig.maxLevel}
stroke="var(--color-orange)"
strokeDasharray="3 3"
label={{
position: 'insideBottomRight',
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: 11
}}
/>
)}
{!isRiver && 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: 11
}}
/>
)}
</>
)}
<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-red)" strokeWidth={2} dot={false} isAnimationActive={animate} />