This commit is contained in:
David Fencl
2026-06-09 20:45:08 +02:00
parent c4cad149ea
commit c8fe97078d
56 changed files with 50749 additions and 48947 deletions
+540 -456
View File
File diff suppressed because it is too large Load Diff
+477 -391
View File
File diff suppressed because it is too large Load Diff
+522 -436
View File
File diff suppressed because it is too large Load Diff
+996 -978
View File
File diff suppressed because it is too large Load Diff
+1124 -1106
View File
File diff suppressed because it is too large Load Diff
+1122 -1104
View File
File diff suppressed because it is too large Load Diff
+1119 -1101
View File
File diff suppressed because it is too large Load Diff
+947 -947
View File
File diff suppressed because it is too large Load Diff
+1152 -1134
View File
File diff suppressed because it is too large Load Diff
+1121 -1103
View File
File diff suppressed because it is too large Load Diff
+1125 -1107
View File
File diff suppressed because it is too large Load Diff
+1011 -1011
View File
File diff suppressed because it is too large Load Diff
+1006 -1006
View File
File diff suppressed because it is too large Load Diff
+1139 -1121
View File
File diff suppressed because it is too large Load Diff
+1160 -1142
View File
File diff suppressed because it is too large Load Diff
+512 -428
View File
File diff suppressed because it is too large Load Diff
+454 -381
View File
File diff suppressed because it is too large Load Diff
+1135 -1117
View File
File diff suppressed because it is too large Load Diff
+1177 -1159
View File
File diff suppressed because it is too large Load Diff
+1174 -1156
View File
File diff suppressed because it is too large Load Diff
+1139 -1112
View File
File diff suppressed because it is too large Load Diff
+494 -408
View File
File diff suppressed because it is too large Load Diff
+535 -451
View File
File diff suppressed because it is too large Load Diff
+1030 -1012
View File
File diff suppressed because it is too large Load Diff
+1131 -1113
View File
File diff suppressed because it is too large Load Diff
+515 -429
View File
File diff suppressed because it is too large Load Diff
+454 -379
View File
File diff suppressed because it is too large Load Diff
+1128 -1110
View File
File diff suppressed because it is too large Load Diff
+516 -430
View File
File diff suppressed because it is too large Load Diff
+521 -435
View File
File diff suppressed because it is too large Load Diff
+1107 -1089
View File
File diff suppressed because it is too large Load Diff
+1118 -1100
View File
File diff suppressed because it is too large Load Diff
+1134 -1116
View File
File diff suppressed because it is too large Load Diff
+1068 -1050
View File
File diff suppressed because it is too large Load Diff
+1102 -1084
View File
File diff suppressed because it is too large Load Diff
+1133 -1106
View File
File diff suppressed because it is too large Load Diff
+1177 -1159
View File
File diff suppressed because it is too large Load Diff
+1156 -1129
View File
File diff suppressed because it is too large Load Diff
+1126 -1108
View File
File diff suppressed because it is too large Load Diff
+1183 -1165
View File
File diff suppressed because it is too large Load Diff
+464 -391
View File
File diff suppressed because it is too large Load Diff
+498 -412
View File
File diff suppressed because it is too large Load Diff
+1152 -1134
View File
File diff suppressed because it is too large Load Diff
+1096 -1078
View File
File diff suppressed because it is too large Load Diff
+1147 -1138
View File
File diff suppressed because it is too large Load Diff
+1104 -1086
View File
File diff suppressed because it is too large Load Diff
+1159 -1141
View File
File diff suppressed because it is too large Load Diff
+1123 -1123
View File
File diff suppressed because it is too large Load Diff
+1179 -1179
View File
File diff suppressed because it is too large Load Diff
+1139 -1130
View File
File diff suppressed because it is too large Load Diff
+518 -434
View File
File diff suppressed because it is too large Load Diff
+1123 -1105
View File
File diff suppressed because it is too large Load Diff
+1126 -1108
View File
File diff suppressed because it is too large Load Diff
+97 -97
View File
@@ -35,9 +35,9 @@
"name": "Lipno II", "name": "Lipno II",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "561.07", "level": "561.06",
"capacity": 64.4, "capacity": 64.4,
"storageDiff": -1.63, "storageDiff": -1.64,
"inflow": "11.2", "inflow": "11.2",
"outflow": "0.0", "outflow": "0.0",
"volume": 1.03, "volume": 1.03,
@@ -46,8 +46,6 @@
"lat": 48.625, "lat": 48.625,
"lng": 14.318, "lng": 14.318,
"sparkline": [ "sparkline": [
561.14,
561.13,
561.12, 561.12,
561.12, 561.12,
561.11, 561.11,
@@ -57,7 +55,9 @@
561.09, 561.09,
561.08, 561.08,
561.07, 561.07,
561.07 561.07,
561.06,
561.06
], ],
"type": "lake" "type": "lake"
}, },
@@ -77,8 +77,6 @@
"lat": 49.183, "lat": 49.183,
"lng": 14.444, "lng": 14.444,
"sparkline": [ "sparkline": [
369.52,
369.51,
369.51, 369.51,
369.52, 369.52,
369.52, 369.52,
@@ -88,6 +86,8 @@
369.51, 369.51,
369.51, 369.51,
369.51, 369.51,
369.51,
369.51,
369.51 369.51
], ],
"type": "lake" "type": "lake"
@@ -108,7 +108,6 @@
"lat": 49.255, "lat": 49.255,
"lng": 14.398, "lng": 14.398,
"sparkline": [ "sparkline": [
352.56,
352.55, 352.55,
352.55, 352.55,
352.55, 352.55,
@@ -119,6 +118,7 @@
352.54, 352.54,
352.54, 352.54,
352.54, 352.54,
352.54,
352.54 352.54
], ],
"type": "lake" "type": "lake"
@@ -128,11 +128,11 @@
"name": "Orlík", "name": "Orlík",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "345.20", "level": "345.19",
"capacity": 72.7, "capacity": 72.7,
"storageDiff": -4.7, "storageDiff": -4.71,
"inflow": "27.6", "inflow": "27.6",
"outflow": "56.5", "outflow": "73.6",
"volume": 520.93, "volume": 520.93,
"maxVolume": 716.5, "maxVolume": 716.5,
"navigationForbidden": false, "navigationForbidden": false,
@@ -142,7 +142,6 @@
345.19, 345.19,
345.19, 345.19,
345.19, 345.19,
345.19,
345.2, 345.2,
345.21, 345.21,
345.21, 345.21,
@@ -150,7 +149,8 @@
345.21, 345.21,
345.21, 345.21,
345.2, 345.2,
345.2 345.2,
345.19
], ],
"type": "lake" "type": "lake"
}, },
@@ -159,11 +159,11 @@
"name": "Slapy", "name": "Slapy",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "269.76", "level": "269.74",
"capacity": 96.6, "capacity": 96.6,
"storageDiff": -0.84, "storageDiff": -0.86,
"inflow": "33.8", "inflow": "33.8",
"outflow": "257.4", "outflow": "293.6",
"volume": 260.04, "volume": 260.04,
"maxVolume": 269.3, "maxVolume": 269.3,
"navigationForbidden": false, "navigationForbidden": false,
@@ -180,8 +180,8 @@
269.79, 269.79,
269.79, 269.79,
269.79, 269.79,
269.79, 269.76,
269.76 269.74
], ],
"type": "lake" "type": "lake"
}, },
@@ -190,18 +190,17 @@
"name": "Štěchovice", "name": "Štěchovice",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "216.32", "level": "216.58",
"capacity": 67.9, "capacity": 67.9,
"storageDiff": -3.08, "storageDiff": -2.82,
"inflow": "49.2", "inflow": "49.2",
"outflow": "46.7", "outflow": "60.8",
"volume": 7.6, "volume": 7.6,
"maxVolume": 11.2, "maxVolume": 11.2,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.845, "lat": 49.845,
"lng": 14.412, "lng": 14.412,
"sparkline": [ "sparkline": [
216.51,
216.49, 216.49,
216.49, 216.49,
216.46, 216.46,
@@ -212,7 +211,8 @@
216.39, 216.39,
216.37, 216.37,
216.35, 216.35,
216.32 216.32,
216.58
], ],
"type": "lake" "type": "lake"
}, },
@@ -256,7 +256,7 @@
"capacity": 56.4, "capacity": 56.4,
"storageDiff": -1.35, "storageDiff": -1.35,
"inflow": "1.0", "inflow": "1.0",
"outflow": "0.0", "outflow": "2.5",
"volume": 31.99, "volume": 31.99,
"maxVolume": 56.7, "maxVolume": 56.7,
"navigationForbidden": false, "navigationForbidden": false,
@@ -267,13 +267,13 @@
352.75, 352.75,
352.75, 352.75,
352.75, 352.75,
352.75,
352.75,
352.76, 352.76,
352.75, 352.75,
352.75, 352.75,
352.75, 352.75,
352.75, 352.75,
352.75,
352.75,
352.75 352.75
], ],
"type": "lake" "type": "lake"
@@ -283,9 +283,9 @@
"name": "Švihov (Želivka)", "name": "Švihov (Želivka)",
"river": "", "river": "",
"priority": true, "priority": true,
"level": "375.11", "level": "375.10",
"capacity": 90.2, "capacity": 90.2,
"storageDiff": -1.89, "storageDiff": -1.9,
"inflow": "0.9", "inflow": "0.9",
"outflow": "0.0", "outflow": "0.0",
"volume": 240.42, "volume": 240.42,
@@ -305,7 +305,7 @@
375.11, 375.11,
375.11, 375.11,
375.11, 375.11,
375.11 375.1
], ],
"type": "lake" "type": "lake"
}, },
@@ -314,19 +314,17 @@
"name": "Kamýk", "name": "Kamýk",
"river": "", "river": "",
"priority": false, "priority": false,
"level": "282.96", "level": "283.02",
"capacity": 77.3, "capacity": 77.3,
"storageDiff": -1.64, "storageDiff": -1.58,
"inflow": "26.3", "inflow": "26.3",
"outflow": "39.4", "outflow": "39.5",
"volume": 9.89, "volume": 9.89,
"maxVolume": 12.8, "maxVolume": 12.8,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.638, "lat": 49.638,
"lng": 14.258, "lng": 14.258,
"sparkline": [ "sparkline": [
282.97,
282.98,
282.98, 282.98,
282.97, 282.97,
282.98, 282.98,
@@ -336,7 +334,9 @@
282.97, 282.97,
282.97, 282.97,
282.97, 282.97,
282.96 282.96,
282.95,
283.02
], ],
"type": "lake" "type": "lake"
}, },
@@ -345,9 +345,9 @@
"name": "Vrané", "name": "Vrané",
"river": "", "river": "",
"priority": false, "priority": false,
"level": "199.33", "level": "199.34",
"capacity": 82.4, "capacity": 82.4,
"storageDiff": -0.77, "storageDiff": -0.76,
"inflow": "47.8", "inflow": "47.8",
"outflow": "39.5", "outflow": "39.5",
"volume": 9.15, "volume": 9.15,
@@ -356,8 +356,6 @@
"lat": 49.939, "lat": 49.939,
"lng": 14.391, "lng": 14.391,
"sparkline": [ "sparkline": [
199.33,
199.32,
199.32, 199.32,
199.32, 199.32,
199.32, 199.32,
@@ -367,7 +365,9 @@
199.3, 199.3,
199.3, 199.3,
199.32, 199.32,
199.33 199.33,
199.33,
199.34
], ],
"type": "lake" "type": "lake"
}, },
@@ -380,7 +380,7 @@
"capacity": 42.6, "capacity": 42.6,
"storageDiff": -1.08, "storageDiff": -1.08,
"inflow": "0.5", "inflow": "0.5",
"outflow": "0.6", "outflow": "0.7",
"volume": 2.43, "volume": 2.43,
"maxVolume": 5.7, "maxVolume": 5.7,
"navigationForbidden": true, "navigationForbidden": true,
@@ -449,8 +449,6 @@
"lat": 50.063, "lat": 50.063,
"lng": 13.931, "lng": 13.931,
"sparkline": [ "sparkline": [
292.88,
292.88,
292.88, 292.88,
0, 0,
292.88, 292.88,
@@ -460,6 +458,8 @@
292.88, 292.88,
292.88, 292.88,
292.88, 292.88,
292.88,
292.88,
292.88 292.88
], ],
"type": "lake" "type": "lake"
@@ -511,8 +511,6 @@
"lat": 49.715, "lat": 49.715,
"lng": 13.364, "lng": 13.364,
"sparkline": [ "sparkline": [
313.43,
313.43,
313.43, 313.43,
0, 0,
313.42, 313.42,
@@ -522,6 +520,8 @@
313.43, 313.43,
0, 0,
313.43, 313.43,
313.42,
313.43,
0 0
], ],
"type": "lake" "type": "lake"
@@ -566,15 +566,13 @@
"capacity": 88.4, "capacity": 88.4,
"storageDiff": -0.3, "storageDiff": -0.3,
"inflow": "0.6", "inflow": "0.6",
"outflow": "0.0", "outflow": "0.3",
"volume": 1.68, "volume": 1.68,
"maxVolume": 1.9, "maxVolume": 1.9,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.507, "lat": 49.507,
"lng": 15.263, "lng": 15.263,
"sparkline": [ "sparkline": [
447.09,
447.09,
447.09, 447.09,
447.09, 447.09,
447.09, 447.09,
@@ -584,6 +582,8 @@
447.1, 447.1,
447.1, 447.1,
447.1, 447.1,
447.1,
447.1,
447.1 447.1
], ],
"type": "lake" "type": "lake"
@@ -597,7 +597,7 @@
"capacity": 100, "capacity": 100,
"storageDiff": -1.47, "storageDiff": -1.47,
"inflow": "0.3", "inflow": "0.3",
"outflow": "0.0", "outflow": "0.5",
"volume": 2.91, "volume": 2.91,
"maxVolume": 2.3, "maxVolume": 2.3,
"navigationForbidden": true, "navigationForbidden": true,
@@ -605,8 +605,8 @@
"lng": 12.639, "lng": 12.639,
"sparkline": [ "sparkline": [
530.64, 530.64,
530.64, 530.63,
530.64, 530.63,
530.63, 530.63,
530.63, 530.63,
530.63, 530.63,
@@ -717,9 +717,9 @@
"name": "Obecnice", "name": "Obecnice",
"river": "", "river": "",
"priority": false, "priority": false,
"level": "563.65", "level": "0.00",
"capacity": 76.7, "capacity": 76.7,
"storageDiff": -0.9, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.46, "volume": 0.46,
@@ -739,7 +739,7 @@
563.65, 563.65,
563.65, 563.65,
563.65, 563.65,
563.65 0
], ],
"type": "lake" "type": "lake"
}, },
@@ -748,9 +748,9 @@
"name": "Strž", "name": "Strž",
"river": "", "river": "",
"priority": false, "priority": false,
"level": "588.39", "level": "0.00",
"capacity": 32, "capacity": 32,
"storageDiff": -0.21, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "0.1", "outflow": "0.1",
"volume": 0.32, "volume": 0.32,
@@ -759,9 +759,6 @@
"lat": 49.791, "lat": 49.791,
"lng": 14.004, "lng": 14.004,
"sparkline": [ "sparkline": [
588.38,
588.38,
588.38,
588.38, 588.38,
0, 0,
588.39, 588.39,
@@ -770,7 +767,10 @@
588.39, 588.39,
588.39, 588.39,
0, 0,
588.39 588.39,
588.39,
588.39,
0
], ],
"type": "lake" "type": "lake"
}, },
@@ -852,8 +852,6 @@
"lat": 48.784, "lat": 48.784,
"lng": 14.735, "lng": 14.735,
"sparkline": [ "sparkline": [
534.71,
534.71,
534.72, 534.72,
0, 0,
534.72, 534.72,
@@ -863,6 +861,8 @@
534.72, 534.72,
0, 0,
534.72, 534.72,
534.72,
534.72,
0 0
], ],
"type": "lake" "type": "lake"
@@ -883,9 +883,6 @@
"lat": 49.575, "lat": 49.575,
"lng": 15.952, "lng": 15.952,
"sparkline": [ "sparkline": [
580.53,
580.53,
580.53,
0, 0,
0, 0,
580.53, 580.53,
@@ -894,6 +891,9 @@
580.53, 580.53,
580.53, 580.53,
580.53, 580.53,
580.53,
580.53,
580.53,
580.53 580.53
], ],
"type": "lake" "type": "lake"
@@ -1217,15 +1217,13 @@
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "54.7", "outflow": "54.2",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 50.0294, "lat": 50.0294,
"lng": 14.3986, "lng": 14.3986,
"sparkline": [ "sparkline": [
46,
47,
47, 47,
47, 47,
46, 46,
@@ -1235,6 +1233,8 @@
47, 47,
46, 46,
46, 46,
46,
46,
46 46
], ],
"type": "river" "type": "river"
@@ -1244,18 +1244,17 @@
"name": "České Budějovice", "name": "České Budějovice",
"river": "Vltava", "river": "Vltava",
"priority": false, "priority": false,
"level": "103.00", "level": "106.00",
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "6.9", "outflow": "6.7",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 48.9712, "lat": 48.9712,
"lng": 14.4714, "lng": 14.4714,
"sparkline": [ "sparkline": [
99,
99, 99,
98, 98,
98, 98,
@@ -1266,7 +1265,8 @@
97, 97,
98, 98,
102, 102,
103 103,
106
], ],
"type": "river" "type": "river"
}, },
@@ -1275,19 +1275,17 @@
"name": "Beroun", "name": "Beroun",
"river": "Berounka", "river": "Berounka",
"priority": false, "priority": false,
"level": "94.00", "level": "95.00",
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "7.9", "outflow": "8.4",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.9642, "lat": 49.9642,
"lng": 14.0792, "lng": 14.0792,
"sparkline": [ "sparkline": [
100,
99,
98, 98,
97, 97,
96, 96,
@@ -1297,7 +1295,9 @@
93, 93,
93, 93,
93, 93,
94 94,
95,
95
], ],
"type": "river" "type": "river"
}, },
@@ -1310,7 +1310,7 @@
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "3.8", "outflow": "3.7",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
@@ -1319,7 +1319,7 @@
"sparkline": [ "sparkline": [
43, 43,
43, 43,
43, 42,
42, 42,
42, 42,
42, 42,
@@ -1337,19 +1337,17 @@
"name": "Písek", "name": "Písek",
"river": "Otava", "river": "Otava",
"priority": false, "priority": false,
"level": "42.00", "level": "45.00",
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "4.7", "outflow": "5.1",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.3083, "lat": 49.3083,
"lng": 14.1436, "lng": 14.1436,
"sparkline": [ "sparkline": [
44,
45,
46, 46,
44, 44,
43, 43,
@@ -1359,7 +1357,9 @@
44, 44,
43, 43,
43, 43,
42 42,
42,
45
], ],
"type": "river" "type": "river"
}, },
@@ -1399,19 +1399,17 @@
"name": "Bechyně", "name": "Bechyně",
"river": "Lužnice", "river": "Lužnice",
"priority": false, "priority": false,
"level": "82.00", "level": "83.00",
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "2.7", "outflow": "2.8",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 49.2931, "lat": 49.2931,
"lng": 14.4758, "lng": 14.4758,
"sparkline": [ "sparkline": [
83,
81,
81, 81,
84, 84,
86, 86,
@@ -1421,7 +1419,9 @@
83, 83,
86, 86,
85, 85,
82 82,
80,
83
], ],
"type": "river" "type": "river"
}, },
@@ -1450,7 +1450,7 @@
50, 50,
50, 50,
50, 50,
50, 51,
51, 51,
51 51
], ],
@@ -1481,8 +1481,8 @@
67, 67,
67, 67,
67, 67,
67, 66,
67, 66,
66 66
], ],
"type": "river" "type": "river"
@@ -1558,15 +1558,13 @@
"capacity": 0, "capacity": 0,
"storageDiff": 0, "storageDiff": 0,
"inflow": "0.0", "inflow": "0.0",
"outflow": "5.8", "outflow": "5.7",
"volume": 0, "volume": 0,
"maxVolume": 0, "maxVolume": 0,
"navigationForbidden": false, "navigationForbidden": false,
"lat": 50.0436, "lat": 50.0436,
"lng": 13.9189, "lng": 13.9189,
"sparkline": [ "sparkline": [
150,
150,
150, 150,
150, 150,
150, 150,
@@ -1576,6 +1574,8 @@
148, 148,
148, 148,
148, 148,
147,
147,
147 147
], ],
"type": "river" "type": "river"
@@ -1596,8 +1596,6 @@
"lat": 49.7731, "lat": 49.7731,
"lng": 13.3986, "lng": 13.3986,
"sparkline": [ "sparkline": [
96,
95,
94, 94,
93, 93,
93, 93,
@@ -1607,6 +1605,8 @@
90, 90,
89, 89,
89, 89,
89,
89,
89 89
], ],
"type": "river" "type": "river"
+6 -4
View File
@@ -25,9 +25,10 @@ async function backfill() {
try { try {
const lat = lake.coords[0]; const lat = lake.coords[0];
const lon = lake.coords[1]; const lon = lake.coords[1];
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&past_days=7&hourly=temperature_2m,precipitation&timezone=GMT`; // Fetch maximum past days supported by the forecast API (92 days)
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&past_days=92&hourly=temperature_2m,precipitation&timezone=GMT`;
const res = await axios.get(url, { timeout: 10000 }); const res = await axios.get(url, { timeout: 15000 });
const hourly = res.data.hourly; const hourly = res.data.hourly;
// Build lookup map for O(1) matching: '2026-06-02T04:00' -> { temp, precip } // Build lookup map for O(1) matching: '2026-06-02T04:00' -> { temp, precip }
@@ -43,9 +44,10 @@ async function backfill() {
let updatedCount = 0; let updatedCount = 0;
for (const record of data) { for (const record of data) {
// record.timestamp is like "2026-06-02T04:00:00.000Z" // record.timestamp is like "2026-06-02T04:10:00.000Z"
// Open-Meteo time is like "2026-06-02T04:00" // Open-Meteo time is like "2026-06-02T04:00"
const hourKey = record.timestamp.substring(0, 16); // Extract up to minutes // Convert to hourly key to match weatherMap
const hourKey = record.timestamp.substring(0, 13) + ':00';
if (weatherMap.has(hourKey)) { if (weatherMap.has(hourKey)) {
const w = weatherMap.get(hourKey); const w = weatherMap.get(hourKey);
+38 -18
View File
@@ -200,15 +200,27 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
// Outlier/sensor glitch detection // Outlier/sensor glitch detection
if (level > 0) { if (level > 0) {
let isGlitch = false;
if (staticConfig && staticConfig.minLevel && staticConfig.maxLevel) { if (staticConfig && staticConfig.minLevel && staticConfig.maxLevel) {
const minAllowed = staticConfig.minLevel - 5; const minAllowed = staticConfig.minLevel - 5;
const maxAllowed = staticConfig.maxLevel + 5; const maxAllowed = staticConfig.maxLevel + 5;
if (level < minAllowed || level > maxAllowed) { if (level < minAllowed || level > maxAllowed) {
// Glitch detected, fallback to last known valid level isGlitch = true;
level = lastValidLevel !== null ? lastValidLevel : (staticConfig.minLevel + staticConfig.maxLevel) / 2;
} else {
lastValidLevel = level;
} }
}
// Check rate of change: sudden spikes/drops
if (!isGlitch && lastValidLevel !== null) {
const isRiver = staticConfig?.type === 'river';
const maxAllowedDelta = isRiver ? 50 : 0.5; // 50 cm for rivers, 0.5 m for reservoirs
if (Math.abs(level - lastValidLevel) > maxAllowedDelta) {
isGlitch = true;
}
}
if (isGlitch) {
// Glitch detected, fallback to last known valid level
level = lastValidLevel !== null ? lastValidLevel : (staticConfig && staticConfig.minLevel && staticConfig.maxLevel ? (staticConfig.minLevel + staticConfig.maxLevel) / 2 : level);
} else { } else {
lastValidLevel = level; lastValidLevel = level;
} }
@@ -394,15 +406,29 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
e.preventDefault(); e.preventDefault();
const isTouchEvent = 'touches' in e; const isTouchEvent = 'touches' in e;
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
// Calculate the center based on the middle of the actual data values to keep the graph line centered
let dataCenter = (currentDomain[1] + currentDomain[0]) / 2;
if (axis === 'left') {
const levels = chartData.map(d => d.level).filter(v => v !== null && v !== undefined && !isNaN(v));
if (levels.length > 0) {
dataCenter = (Math.min(...levels) + Math.max(...levels)) / 2;
}
} else {
const flows = chartData.flatMap(d => [d.outflow, d.inflow]).filter(v => v !== null && v !== undefined && !isNaN(v));
if (flows.length > 0) {
dataCenter = (Math.min(...flows) + Math.max(...flows)) / 2;
}
}
if (isTouchEvent && e.touches.length === 2) { if (isTouchEvent && e.touches.length === 2) {
const touch1 = e.touches[0]; const touch1 = e.touches[0];
const touch2 = e.touches[1]; const touch2 = e.touches[1];
const dist = Math.abs(touch1.clientY - touch2.clientY); const dist = Math.abs(touch1.clientY - touch2.clientY);
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
const onTouchMove = (moveEvent: TouchEvent) => { const onTouchMove = (moveEvent: TouchEvent) => {
if (moveEvent.touches.length === 2) { if (moveEvent.touches.length === 2) {
moveEvent.preventDefault(); // Stop native page zooming moveEvent.preventDefault(); // Stop native page zooming
@@ -411,10 +437,9 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
const currentDist = Math.abs(mTouch1.clientY - mTouch2.clientY); const currentDist = Math.abs(mTouch1.clientY - mTouch2.clientY);
if (currentDist > 5) { if (currentDist > 5) {
const factor = dist / currentDist; const factor = dist / currentDist;
const center = (currentDomain[0] + currentDomain[1]) / 2;
const range = currentDomain[1] - currentDomain[0]; const range = currentDomain[1] - currentDomain[0];
const newMin = center - (range * factor) / 2; const newMin = dataCenter - (range * factor) / 2;
const newMax = center + (range * factor) / 2; const newMax = dataCenter + (range * factor) / 2;
if (axis === 'left') { if (axis === 'left') {
setLeftCustomDomain([newMin, newMax]); setLeftCustomDomain([newMin, newMax]);
@@ -436,12 +461,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
} }
const startY = isTouchEvent ? e.touches[0].clientY : e.clientY; const startY = isTouchEvent ? e.touches[0].clientY : e.clientY;
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
const initialRange = currentDomain[1] - currentDomain[0]; const initialRange = currentDomain[1] - currentDomain[0];
const center = (currentDomain[1] + currentDomain[0]) / 2;
const onMove = (moveEvent: MouseEvent | TouchEvent) => { const onMove = (moveEvent: MouseEvent | TouchEvent) => {
if ('touches' in moveEvent) { if ('touches' in moveEvent) {
@@ -451,8 +471,8 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
const deltaY = clientY - startY; const deltaY = clientY - startY;
const factor = Math.pow(2.5, deltaY / 150); const factor = Math.pow(2.5, deltaY / 150);
const newMin = center - (initialRange * factor) / 2; const newMin = dataCenter - (initialRange * factor) / 2;
const newMax = center + (initialRange * factor) / 2; const newMax = dataCenter + (initialRange * factor) / 2;
if (axis === 'left') { if (axis === 'left') {
setLeftCustomDomain([newMin, newMax]); setLeftCustomDomain([newMin, newMax]);