···
62
62
fileLabel = parquetFiles.map((p) => path.basename(p)).join(",");
63
63
}
64
64
65
65
-
// For daily resolution, use properly aggregated daily data
65
65
+
// For daily resolution, build proper aggregation with max/avg for all columns
66
66
if (resolution === "day") {
67
67
-
const dailyRows = await queryDailyAggregatesInRange(parquetFiles, start, end);
68
68
-
const header = ["time", "Temperatur Aussen(°C)", "Taupunkt(°C)", "Gefühlte Temperatur", "Niederschlag Tag(mm)", "Windgeschwindigkeit(km/h)", "Böengeschwindigkeit(km/h)"];
69
69
-
const rows = dailyRows.map((r: any) => ({
70
70
-
key: r.day,
71
71
-
time: r.day,
72
72
-
"Temperatur Aussen(°C)": r.tmax,
73
73
-
"Taupunkt(°C)": r.tmin, // Using tmin for dewpoint approximation
74
74
-
"Gefühlte Temperatur": r.tfmax,
75
75
-
"Niederschlag Tag(mm)": r.rain_day,
76
76
-
"Windgeschwindigkeit(km/h)": r.wind_max,
77
77
-
"Böengeschwindigkeit(km/h)": r.gust_max,
78
78
-
}));
79
79
-
return NextResponse.json({ file: fileLabel, header, rows }, { status: 200 });
67
67
+
const parquetPaths = parquetFiles.map((p) => p.replace(/\\/g, "/"));
68
68
+
const colsHints = await discoverMainColumns(parquetPaths);
69
69
+
const conn = await getDuckConn();
70
70
+
const arr = '[' + parquetPaths.map((p) => `'${p}'`).join(',') + ']';
71
71
+
const describeSql = `DESCRIBE SELECT * FROM read_parquet(${arr}, union_by_name=true)`;
72
72
+
const descReader = await conn.runAndReadAll(describeSql);
73
73
+
const cols: any[] = descReader.getRowObjects();
74
74
+
const allNames = cols.map((r: any) => String(r.column_name || r.ColumnName || r.column || ""));
75
75
+
const typedNumericCols = cols
76
76
+
.filter((r: any) => {
77
77
+
const t = String(r.column_type || r.Type || r.type || "").toUpperCase();
78
78
+
return t && !t.includes("VARCHAR") && !t.includes("BOOLEAN") && t !== "";
79
79
+
})
80
80
+
.map((r: any) => String(r.column_name || r.ColumnName || r.column || ""))
81
81
+
.filter((c) => c && c !== "ts" && c !== "Time" && c !== "Zeit");
82
82
+
83
83
+
const seen = new Set<string>();
84
84
+
const orderedCols: string[] = [];
85
85
+
const pushCol = (name?: string | null) => {
86
86
+
if (!name) return;
87
87
+
if (!allNames.includes(name)) return;
88
88
+
if (seen.has(name)) return;
89
89
+
seen.add(name);
90
90
+
orderedCols.push(name);
91
91
+
};
92
92
+
93
93
+
pushCol(colsHints.temp);
94
94
+
for (const c of typedNumericCols) pushCol(c);
95
95
+
const candidateGroups: (string | null)[] = [
96
96
+
colsHints.rainDay,
97
97
+
...colsHints.dailyRainCandidates,
98
98
+
...colsHints.hourlyRainCandidates,
99
99
+
...colsHints.genericRainCandidates,
100
100
+
colsHints.temp,
101
101
+
...colsHints.tempCandidates,
102
102
+
colsHints.dew,
103
103
+
...colsHints.dewCandidates,
104
104
+
colsHints.feelsLike,
105
105
+
...colsHints.feelsLikeCandidates,
106
106
+
colsHints.wind,
107
107
+
...colsHints.windCandidates,
108
108
+
colsHints.gust,
109
109
+
...colsHints.gustCandidates,
110
110
+
];
111
111
+
for (const c of candidateGroups) pushCol(c);
112
112
+
113
113
+
// Use max for temperatures and wind/gust, avg for others
114
114
+
const aggList = orderedCols.map((c) => {
115
115
+
const escaped = c.replace(/"/g, '""');
116
116
+
const isTemp = colsHints.temp === c || colsHints.tempCandidates.includes(c) || c.toLowerCase().includes("temperatur");
117
117
+
const isDew = colsHints.dew === c || colsHints.dewCandidates.includes(c) || c.toLowerCase().includes("taupunkt");
118
118
+
const isFeels = colsHints.feelsLike === c || colsHints.feelsLikeCandidates.includes(c) || c.toLowerCase().includes("gefühl");
119
119
+
const isWind = colsHints.wind === c || colsHints.windCandidates.includes(c);
120
120
+
const isGust = colsHints.gust === c || colsHints.gustCandidates.includes(c);
121
121
+
const isRain = c.toLowerCase().includes("niederschlag") || c.toLowerCase().includes("rain");
122
122
+
123
123
+
const numericExpr = isWind || isGust ? speedExprFor(c) : sqlNum('"' + escaped + '"');
124
124
+
const aggFunc = (isTemp || isDew || isFeels || isWind || isGust) ? "max" : "avg";
125
125
+
return `${aggFunc}(${numericExpr}) AS "${escaped}"`;
126
126
+
}).join(",\n ");
127
127
+
128
128
+
const whereStart = start ? `(ts >= strptime('${start.toISOString().slice(0, 16).replace("T", " ")}', ['%Y-%m-%d %H:%M']))` : "1=1";
129
129
+
const whereEnd = end ? `(ts <= strptime('${end.toISOString().slice(0, 16).replace("T", " ")}', ['%Y-%m-%d %H:%M']))` : "1=1";
130
130
+
const unionSources = parquetPaths.map((p) => `SELECT * FROM read_parquet('${p}')`).join("\nUNION ALL\n");
131
131
+
132
132
+
const sql = `
133
133
+
WITH src AS (
134
134
+
${unionSources}
135
135
+
),
136
136
+
filt AS (
137
137
+
SELECT * FROM src WHERE ts IS NOT NULL AND ${whereStart} AND ${whereEnd}
138
138
+
)
139
139
+
SELECT
140
140
+
strftime(date_trunc('day', ts) + INTERVAL '12 hours', '%Y-%m-%d %H:%M:%S') AS time
141
141
+
${aggList ? ",\n " + aggList : ""}
142
142
+
FROM filt
143
143
+
GROUP BY date_trunc('day', ts)
144
144
+
ORDER BY 1
145
145
+
`;
146
146
+
const reader = await conn.runAndReadAll(sql);
147
147
+
let outRows: any[] = reader.getRowObjects();
148
148
+
outRows = outRows.map((r: any) => ({ key: r.time, ...r }));
149
149
+
const header = ["time", ...orderedCols];
150
150
+
return NextResponse.json({ file: fileLabel, header, rows: outRows }, { status: 200 });
80
151
}
81
152
82
153
const parquetPaths = parquetFiles.map((p) => p.replace(/\\/g, "/"));
···
140
140
{t("statistics.maxDay", "Max day")} : {fmtNum(p.maxDay)} mm<br />
141
141
<span className="text-xs text-gray-600">({fmtDate(p.maxDayDate)})</span>
142
142
</div>
143
143
-
<div>
144
144
-
{t("statistics.minDay", "Min day")} : {fmtNum(p.minDay)} mm<br />
145
145
-
<span className="text-xs text-gray-600">({fmtDate(p.minDayDate)})</span>
146
146
-
</div>
147
143
</div>
148
144
) : (
149
145
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-sm">
150
146
<div>{t("dashboard.total")} : {fmtNum(p.total)} mm</div>
151
147
<div>{t("statistics.maxDay", "Max day")} : {fmtNum(p.maxDay)} mm ({fmtDate(p.maxDayDate)})</div>
152
152
-
<div>{t("statistics.minDay", "Min day")} : {fmtNum(p.minDay)} mm ({fmtDate(p.minDayDate)})</div>
153
148
</div>
154
149
)}
155
150
<div className="mt-2">
···
111
111
FROM casted
112
112
GROUP BY 1
113
113
)
114
114
-
SELECT strftime(d, '%Y-%m-%d %H:%M:%S') AS day,
114
114
+
SELECT strftime(d, '%Y-%m-%d') AS day,
115
115
tmax, tmin, tavg,
116
116
tfmax, tfmin,
117
117
COALESCE(rdaily, rhourly, rgeneric) AS rain_day,
···
251
251
const under10: { date: string; value: number }[] = [];
252
252
253
253
let rainTotal = 0; let rainCnt = 0; let rainMax = Number.NEGATIVE_INFINITY; let rainMaxDate: string | null = null;
254
254
-
let rainMin = Number.POSITIVE_INFINITY; let rainMinDate: string | null = null;
255
254
const rainOver20: { date: string; value: number }[] = [];
256
255
const rainOver30: { date: string; value: number }[] = [];
257
256
let rainDays = 0;
···
287
286
if (rd > 0) rainDays++;
288
287
rainTotal += rd;
289
288
if (rd > rainMax) { rainMax = rd; rainMaxDate = d; }
290
290
-
if (rd < rainMin) { rainMin = rd; rainMinDate = d; }
291
289
if (rd >= 20) rainOver20.push({ date: d, value: rd });
292
290
if (rd >= 30) rainOver30.push({ date: d, value: rd });
293
291
}
···
322
320
total: rainCnt > 0 && Number.isFinite(rainTotal) ? rainTotal : null,
323
321
maxDay: rainCnt > 0 && Number.isFinite(rainMax) ? rainMax : null,
324
322
maxDayDate: rainCnt > 0 ? rainMaxDate : null,
325
325
-
minDay: rainCnt > 0 && Number.isFinite(rainMin) ? rainMin : null,
326
326
-
minDayDate: rainCnt > 0 ? rainMinDate : null,
327
323
over20mm: { count: rainOver20.length, items: rainOver20 },
328
324
over30mm: { count: rainOver30.length, items: rainOver30 },
329
325
} as YearStats["precipitation"];
···
25
25
total: number | null; // sum of daily totals over the period (mm)
26
26
maxDay: number | null; // maximum daily total in the period (mm)
27
27
maxDayDate: string | null; // YYYY-MM-DD
28
28
-
minDay: number | null; // minimum daily total in the period (mm)
29
29
-
minDayDate: string | null; // YYYY-MM-DD
30
28
over20mm: ThresholdList; // days with daily total >= 20 mm
31
29
over30mm: ThresholdList; // days with daily total >= 30 mm
32
30
}