1#include "core/aqm_db.h"
2#include "core/globals.h"
11#define PDF_FILE "sensor_data_report.pdf"
12#define PDF_MAX_SENSORS 50
19 double pm25_sum, pm10_sum, co_sum, no2_sum, o3_sum, so2_sum;
20 double pm25_min, pm10_min, co_min, no2_min, o3_min, so2_min;
21 double pm25_max, pm10_max, co_max, no2_max, o3_max, so2_max;
23 int alerts_pm25, alerts_pm10, alerts_co, alerts_no2, alerts_o3, alerts_so2;
35static void init_sensor_stats(SensorStats *stats,
int sensor_id,
const char *name,
const char *model) {
36 stats->sensor_id = sensor_id;
37 snprintf(stats->name,
sizeof(stats->name),
"%s", name ? name :
"");
38 snprintf(stats->model,
sizeof(stats->model),
"%s", model ? model :
"");
39 stats->pm25_sum = stats->pm10_sum = stats->co_sum = stats->no2_sum = stats->o3_sum = stats->so2_sum = 0.0;
40 stats->pm25_min = stats->pm10_min = stats->co_min = stats->no2_min = stats->o3_min = stats->so2_min = 999999.0;
41 stats->pm25_max = stats->pm10_max = stats->co_max = stats->no2_max = stats->o3_max = stats->so2_max = 0.0;
43 stats->alerts_pm25 = stats->alerts_pm10 = stats->alerts_co = stats->alerts_no2 = stats->alerts_o3 = stats->alerts_so2 = 0;
46static void update_sensor_stats(SensorStats *stats,
double pm25,
double pm10,
double co,
double no2,
double o3,
double so2) {
48 stats->pm25_sum += pm25; stats->pm10_sum += pm10; stats->co_sum += co;
49 stats->no2_sum += no2; stats->o3_sum += o3; stats->so2_sum += so2;
50 if (pm25 < stats->pm25_min) stats->pm25_min = pm25;
51 if (pm10 < stats->pm10_min) stats->pm10_min = pm10;
52 if (co < stats->co_min) stats->co_min = co;
53 if (no2 < stats->no2_min) stats->no2_min = no2;
54 if (o3 < stats->o3_min) stats->o3_min = o3;
55 if (so2 < stats->so2_min) stats->so2_min = so2;
56 if (pm25 > stats->pm25_max) stats->pm25_max = pm25;
57 if (pm10 > stats->pm10_max) stats->pm10_max = pm10;
58 if (co > stats->co_max) stats->co_max = co;
59 if (no2 > stats->no2_max) stats->no2_max = no2;
60 if (o3 > stats->o3_max) stats->o3_max = o3;
61 if (so2 > stats->so2_max) stats->so2_max = so2;
62 if (pm25 > limit_pm25) stats->alerts_pm25++;
63 if (pm10 > limit_pm10) stats->alerts_pm10++;
64 if (co > limit_co) stats->alerts_co++;
65 if (no2 > limit_no2) stats->alerts_no2++;
66 if (o3 > limit_o3) stats->alerts_o3++;
67 if (so2 > limit_so2) stats->alerts_so2++;
70static void add_alert(AlertRecord *alerts,
int *alert_count,
int sensor_id,
const char *sensor_name,
71 const char *timestamp,
const char *pollutant,
double value,
double limit) {
72 if (*alert_count >= MAX_ALERTS)
return;
73 alerts[*alert_count].sensor_id = sensor_id;
74 snprintf(alerts[*alert_count].sensor_name,
sizeof(alerts[*alert_count].sensor_name),
"%s", sensor_name);
75 snprintf(alerts[*alert_count].timestamp,
sizeof(alerts[*alert_count].timestamp),
"%s", timestamp);
76 snprintf(alerts[*alert_count].pollutant,
sizeof(alerts[*alert_count].pollutant),
"%s", pollutant);
77 alerts[*alert_count].value = value;
78 alerts[*alert_count].limit = limit;
82static HPDF_Page create_new_page(HPDF_Doc pdf, HPDF_Font font,
int font_size) {
83 HPDF_Page page = HPDF_AddPage(pdf);
84 HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT);
85 HPDF_Page_SetFontAndSize(page, font, font_size);
89static void draw_footer(HPDF_Page page,
int page_num, HPDF_Font font) {
90 HPDF_Page_SetFontAndSize(page, font, 8);
92 snprintf(footer,
sizeof(footer),
"Page %d", page_num);
93 HPDF_Page_BeginText(page);
94 HPDF_Page_TextOut(page, 40, 30, footer);
95 HPDF_Page_TextOut(page, 400, 30,
"Air Quality Monitor Report");
96 HPDF_Page_EndText(page);
99static void draw_cover_page(HPDF_Doc pdf, HPDF_Font font_bold, HPDF_Font font,
100 int total_sensors,
int total_records,
int total_alerts,
101 const char *period_start,
const char *period_end) {
102 HPDF_Page page = HPDF_AddPage(pdf);
103 HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT);
105 HPDF_REAL page_width = HPDF_Page_GetWidth(page);
106 HPDF_REAL center_x = page_width / 2;
109 HPDF_Page_SetFontAndSize(page, font_bold, 24);
110 HPDF_Page_BeginText(page);
111 HPDF_Page_TextOut(page, center_x - 140, 700,
"AIR QUALITY MONITOR");
112 HPDF_Page_EndText(page);
114 HPDF_Page_SetFontAndSize(page, font_bold, 18);
115 HPDF_Page_BeginText(page);
116 HPDF_Page_TextOut(page, center_x - 60, 660,
"Data Report");
117 HPDF_Page_EndText(page);
120 time_t now = time(NULL);
121 const struct tm *tm_info = localtime(&now);
123 strftime(date_str,
sizeof(date_str),
"Generated: %Y-%m-%d %H:%M", tm_info);
124 HPDF_Page_SetFontAndSize(page, font, 12);
125 HPDF_Page_BeginText(page);
126 HPDF_Page_TextOut(page, center_x - 80, 600, date_str);
127 HPDF_Page_EndText(page);
130 HPDF_Page_BeginText(page);
131 char period_str[128];
132 snprintf(period_str,
sizeof(period_str),
"Data Period: %s to %s",
133 period_start && period_start[0] ? period_start :
"N/A",
134 period_end && period_end[0] ? period_end :
"N/A");
135 HPDF_Page_TextOut(page, 100, 550, period_str);
136 HPDF_Page_EndText(page);
139 HPDF_Page_SetRGBStroke(page, 0.3, 0.3, 0.3);
140 HPDF_Page_SetRGBFill(page, 0.95, 0.95, 0.95);
141 HPDF_Page_Rectangle(page, 80, 400, 450, 120);
142 HPDF_Page_FillStroke(page);
144 HPDF_Page_SetFontAndSize(page, font_bold, 14);
145 HPDF_Page_SetRGBFill(page, 0, 0, 0);
146 HPDF_Page_BeginText(page);
147 HPDF_Page_TextOut(page, center_x - 50, 500,
"SUMMARY");
148 HPDF_Page_EndText(page);
150 HPDF_Page_SetFontAndSize(page, font, 12);
151 HPDF_Page_BeginText(page);
153 snprintf(summary,
sizeof(summary),
"Total Sensors: %d", total_sensors);
154 HPDF_Page_TextOut(page, 100, 470, summary);
155 snprintf(summary,
sizeof(summary),
"Total Records: %d", total_records);
156 HPDF_Page_TextOut(page, 100, 445, summary);
157 snprintf(summary,
sizeof(summary),
"Total Alert Events: %d", total_alerts);
158 HPDF_Page_TextOut(page, 100, 420, summary);
159 HPDF_Page_EndText(page);
162static void draw_summary_page(HPDF_Doc pdf, HPDF_Font font_bold, HPDF_Font font,
163 SensorStats *stats,
int sensor_count,
int *page_num) {
164 HPDF_Page page = create_new_page(pdf, font, 10);
167 HPDF_Page_SetFontAndSize(page, font_bold, 16);
168 HPDF_Page_BeginText(page);
169 HPDF_Page_TextOut(page, 40, 780,
"Global Statistics Overview");
170 HPDF_Page_EndText(page);
173 HPDF_Page_SetFontAndSize(page, font, 9);
176 HPDF_Page_SetRGBFill(page, 0.8, 0.8, 0.8);
177 HPDF_Page_Rectangle(page, 40, y - 5, 520, 20);
178 HPDF_Page_FillStroke(page);
179 HPDF_Page_SetRGBFill(page, 0, 0, 0);
180 HPDF_Page_BeginText(page);
181 HPDF_Page_TextOut(page, 45, y,
"Sensor");
182 HPDF_Page_TextOut(page, 150, y,
"Count");
183 HPDF_Page_TextOut(page, 200, y,
"PM2.5 Avg");
184 HPDF_Page_TextOut(page, 280, y,
"PM10 Avg");
185 HPDF_Page_TextOut(page, 350, y,
"CO Avg");
186 HPDF_Page_TextOut(page, 420, y,
"Alerts");
187 HPDF_Page_EndText(page);
190 for (
int i = 0; i < sensor_count && y > 100; i++) {
191 if (stats[i].count == 0)
continue;
193 HPDF_Page_BeginText(page);
194 HPDF_Page_TextOut(page, 45, y, stats[i].name);
196 snprintf(buf,
sizeof(buf),
"%d", stats[i].count);
197 HPDF_Page_TextOut(page, 150, y, buf);
198 snprintf(buf,
sizeof(buf),
"%.2f", stats[i].pm25_sum / stats[i].count);
199 HPDF_Page_TextOut(page, 200, y, buf);
200 snprintf(buf,
sizeof(buf),
"%.2f", stats[i].pm10_sum / stats[i].count);
201 HPDF_Page_TextOut(page, 280, y, buf);
202 snprintf(buf,
sizeof(buf),
"%.2f", stats[i].co_sum / stats[i].count);
203 HPDF_Page_TextOut(page, 350, y, buf);
204 int total_alerts = stats[i].alerts_pm25 + stats[i].alerts_pm10 + stats[i].alerts_co +
205 stats[i].alerts_no2 + stats[i].alerts_o3 + stats[i].alerts_so2;
206 snprintf(buf,
sizeof(buf),
"%d", total_alerts);
207 HPDF_Page_TextOut(page, 420, y, buf);
208 HPDF_Page_EndText(page);
212 draw_footer(page, *page_num, font);
215static void draw_bar_chart(HPDF_Page page, HPDF_Font font,
float x,
float y,
216 const char *label,
double value,
double max_val,
int is_alert) {
217 float bar_width = 200;
218 float bar_height = 15;
219 float filled_width = (max_val > 0) ? (value / max_val) * bar_width : 0;
222 HPDF_Page_SetRGBStroke(page, 0.7, 0.7, 0.7);
223 HPDF_Page_SetRGBFill(page, 0.9, 0.9, 0.9);
224 HPDF_Page_Rectangle(page, x, y, bar_width, bar_height);
225 HPDF_Page_FillStroke(page);
229 HPDF_Page_SetRGBFill(page, 0.9, 0.3, 0.3);
231 HPDF_Page_SetRGBFill(page, 0.3, 0.6, 0.9);
233 HPDF_Page_Rectangle(page, x, y, filled_width, bar_height);
234 HPDF_Page_FillStroke(page);
237 HPDF_Page_SetFontAndSize(page, font, 8);
238 HPDF_Page_SetRGBFill(page, 0, 0, 0);
239 HPDF_Page_BeginText(page);
240 HPDF_Page_TextOut(page, x, y + bar_height + 2, label);
242 snprintf(val_str,
sizeof(val_str),
"%.2f", value);
243 HPDF_Page_TextOut(page, x + bar_width + 5, y + 3, val_str);
244 HPDF_Page_EndText(page);
247static void draw_sensor_detail_page(HPDF_Doc pdf, HPDF_Font font_bold, HPDF_Font font,
248 SensorStats *stats, sqlite3 *db,
int *page_num) {
249 if (stats->count == 0)
return;
251 HPDF_Page page = create_new_page(pdf, font, 10);
255 HPDF_Page_SetFontAndSize(page, font_bold, 16);
256 HPDF_Page_SetRGBFill(page, 0.2, 0.3, 0.5);
257 HPDF_Page_BeginText(page);
259 snprintf(header,
sizeof(header),
"Sensor: %s (ID: %d)", stats->name, stats->sensor_id);
260 HPDF_Page_TextOut(page, 40, 780, header);
261 HPDF_Page_EndText(page);
263 HPDF_Page_SetFontAndSize(page, font, 11);
264 HPDF_Page_SetRGBFill(page, 0, 0, 0);
265 HPDF_Page_BeginText(page);
266 snprintf(header,
sizeof(header),
"Model: %s | Records: %d", stats->model, stats->count);
267 HPDF_Page_TextOut(page, 40, 760, header);
268 HPDF_Page_EndText(page);
272 HPDF_Page_SetFontAndSize(page, font_bold, 12);
273 HPDF_Page_BeginText(page);
274 HPDF_Page_TextOut(page, 40, y,
"Statistics Summary");
275 HPDF_Page_EndText(page);
279 HPDF_Page_SetRGBFill(page, 0.85, 0.85, 0.85);
280 HPDF_Page_Rectangle(page, 40, y - 5, 520, 18);
281 HPDF_Page_FillStroke(page);
282 HPDF_Page_SetRGBFill(page, 0, 0, 0);
283 HPDF_Page_SetFontAndSize(page, font_bold, 9);
284 HPDF_Page_BeginText(page);
285 HPDF_Page_TextOut(page, 45, y,
"Pollutant");
286 HPDF_Page_TextOut(page, 130, y,
"Minimum");
287 HPDF_Page_TextOut(page, 210, y,
"Average");
288 HPDF_Page_TextOut(page, 290, y,
"Maximum");
289 HPDF_Page_TextOut(page, 370, y,
"Limit");
290 HPDF_Page_TextOut(page, 450, y,
"Alerts");
291 HPDF_Page_EndText(page);
294 HPDF_Page_SetFontAndSize(page, font, 9);
296 struct {
const char *name;
double min, avg, max, limit;
int alerts; } pollutants[6] = {
297 {
"PM2.5", stats->pm25_min, stats->pm25_sum/stats->count, stats->pm25_max, limit_pm25, stats->alerts_pm25},
298 {
"PM10", stats->pm10_min, stats->pm10_sum/stats->count, stats->pm10_max, limit_pm10, stats->alerts_pm10},
299 {
"CO", stats->co_min, stats->co_sum/stats->count, stats->co_max, limit_co, stats->alerts_co},
300 {
"NO2", stats->no2_min, stats->no2_sum/stats->count, stats->no2_max, limit_no2, stats->alerts_no2},
301 {
"O3", stats->o3_min, stats->o3_sum/stats->count, stats->o3_max, limit_o3, stats->alerts_o3},
302 {
"SO2", stats->so2_min, stats->so2_sum/stats->count, stats->so2_max, limit_so2, stats->alerts_so2}
305 for (
int i = 0; i < 6; i++) {
307 if (pollutants[i].alerts > 0) {
308 HPDF_Page_SetRGBFill(page, 1.0, 0.9, 0.9);
309 HPDF_Page_Rectangle(page, 40, y - 3, 520, 15);
310 HPDF_Page_FillStroke(page);
313 HPDF_Page_SetRGBFill(page, 0, 0, 0);
314 HPDF_Page_BeginText(page);
315 HPDF_Page_TextOut(page, 45, y, pollutants[i].name);
317 snprintf(buf,
sizeof(buf),
"%.4f", pollutants[i].min);
318 HPDF_Page_TextOut(page, 130, y, buf);
319 snprintf(buf,
sizeof(buf),
"%.4f", pollutants[i].avg);
320 HPDF_Page_TextOut(page, 210, y, buf);
321 snprintf(buf,
sizeof(buf),
"%.4f", pollutants[i].max);
322 HPDF_Page_TextOut(page, 290, y, buf);
323 snprintf(buf,
sizeof(buf),
"%.4f", pollutants[i].limit);
324 HPDF_Page_TextOut(page, 370, y, buf);
325 snprintf(buf,
sizeof(buf),
"%d", pollutants[i].alerts);
326 HPDF_Page_TextOut(page, 450, y, buf);
327 HPDF_Page_EndText(page);
333 HPDF_Page_SetFontAndSize(page, font_bold, 12);
334 HPDF_Page_BeginText(page);
335 HPDF_Page_TextOut(page, 40, y,
"Average Values Chart");
336 HPDF_Page_EndText(page);
340 for (
int i = 0; i < 6; i++) {
341 if (pollutants[i].avg > max_val) max_val = pollutants[i].avg;
343 if (max_val == 0) max_val = 1;
345 for (
int i = 0; i < 6 && y > 100; i++) {
346 draw_bar_chart(page, font, 60, y, pollutants[i].name, pollutants[i].avg, max_val, pollutants[i].alerts > 0);
352 draw_footer(page, *page_num, font);
353 page = create_new_page(pdf, font, 9);
360 HPDF_Page_SetFontAndSize(page, font_bold, 12);
361 HPDF_Page_BeginText(page);
362 HPDF_Page_TextOut(page, 40, y,
"Recent Readings");
363 HPDF_Page_EndText(page);
367 HPDF_Page_SetRGBFill(page, 0.85, 0.85, 0.85);
368 HPDF_Page_Rectangle(page, 40, y - 5, 520, 18);
369 HPDF_Page_FillStroke(page);
370 HPDF_Page_SetRGBFill(page, 0, 0, 0);
371 HPDF_Page_SetFontAndSize(page, font_bold, 8);
372 HPDF_Page_BeginText(page);
373 HPDF_Page_TextOut(page, 45, y,
"Time");
374 HPDF_Page_TextOut(page, 180, y,
"PM2.5");
375 HPDF_Page_TextOut(page, 240, y,
"PM10");
376 HPDF_Page_TextOut(page, 290, y,
"CO");
377 HPDF_Page_TextOut(page, 350, y,
"NO2");
378 HPDF_Page_TextOut(page, 410, y,
"O3");
379 HPDF_Page_TextOut(page, 470, y,
"SO2");
380 HPDF_Page_EndText(page);
384 sqlite3_stmt *res = NULL;
385 const char *sql =
"SELECT measured_at, pm25, pm10, co, no2, o3, so2 FROM readings "
386 "WHERE sensor_id = ? ORDER BY measured_at DESC LIMIT 20;";
388 if (sqlite3_prepare_v2(db, sql, -1, &res, NULL) == SQLITE_OK) {
389 sqlite3_bind_int(res, 1, stats->sensor_id);
390 HPDF_Page_SetFontAndSize(page, font, 8);
393 while (sqlite3_step(res) == SQLITE_ROW && y > 100 && row_count < 20) {
394 const char *time = (
const char *)sqlite3_column_text(res, 0);
395 double pm25 = sqlite3_column_double(res, 1);
396 double pm10 = sqlite3_column_double(res, 2);
397 double co = sqlite3_column_double(res, 3);
398 double no2 = sqlite3_column_double(res, 4);
399 double o3 = sqlite3_column_double(res, 5);
400 double so2 = sqlite3_column_double(res, 6);
403 int has_alert = (pm25 > limit_pm25 || pm10 > limit_pm10 || co > limit_co ||
404 no2 > limit_no2 || o3 > limit_o3 || so2 > limit_so2);
407 HPDF_Page_SetRGBFill(page, 1.0, 0.85, 0.85);
408 HPDF_Page_Rectangle(page, 40, y - 3, 520, 12);
409 HPDF_Page_FillStroke(page);
412 HPDF_Page_SetRGBFill(page, 0, 0, 0);
413 HPDF_Page_BeginText(page);
414 HPDF_Page_TextOut(page, 45, y, time ? time :
"");
416 snprintf(buf,
sizeof(buf),
"%.2f", pm25);
417 HPDF_Page_TextOut(page, 180, y, buf);
418 snprintf(buf,
sizeof(buf),
"%.2f", pm10);
419 HPDF_Page_TextOut(page, 240, y, buf);
420 snprintf(buf,
sizeof(buf),
"%.2f", co);
421 HPDF_Page_TextOut(page, 290, y, buf);
422 snprintf(buf,
sizeof(buf),
"%.4f", no2);
423 HPDF_Page_TextOut(page, 350, y, buf);
424 snprintf(buf,
sizeof(buf),
"%.4f", o3);
425 HPDF_Page_TextOut(page, 410, y, buf);
426 snprintf(buf,
sizeof(buf),
"%.4f", so2);
427 HPDF_Page_TextOut(page, 470, y, buf);
428 HPDF_Page_EndText(page);
433 sqlite3_finalize(res);
436 draw_footer(page, *page_num, font);
439static void draw_alerts_page(HPDF_Doc pdf, HPDF_Font font_bold, HPDF_Font font,
440 AlertRecord *alerts,
int alert_count,
int *page_num) {
441 if (alert_count == 0)
return;
443 HPDF_Page page = create_new_page(pdf, font, 10);
446 HPDF_Page_SetFontAndSize(page, font_bold, 16);
447 HPDF_Page_SetRGBFill(page, 0.8, 0.2, 0.2);
448 HPDF_Page_BeginText(page);
449 HPDF_Page_TextOut(page, 40, 780,
"Alert Summary");
450 HPDF_Page_EndText(page);
452 HPDF_Page_SetFontAndSize(page, font, 11);
453 HPDF_Page_SetRGBFill(page, 0, 0, 0);
454 HPDF_Page_BeginText(page);
456 snprintf(buf,
sizeof(buf),
"Total Alert Events: %d", alert_count);
457 HPDF_Page_TextOut(page, 40, 760, buf);
458 HPDF_Page_EndText(page);
463 HPDF_Page_SetRGBFill(page, 0.9, 0.6, 0.6);
464 HPDF_Page_Rectangle(page, 40, y - 5, 520, 20);
465 HPDF_Page_FillStroke(page);
466 HPDF_Page_SetRGBFill(page, 0, 0, 0);
467 HPDF_Page_SetFontAndSize(page, font_bold, 9);
468 HPDF_Page_BeginText(page);
469 HPDF_Page_TextOut(page, 45, y,
"Time");
470 HPDF_Page_TextOut(page, 180, y,
"Sensor");
471 HPDF_Page_TextOut(page, 280, y,
"Pollutant");
472 HPDF_Page_TextOut(page, 370, y,
"Value");
473 HPDF_Page_TextOut(page, 450, y,
"Limit");
474 HPDF_Page_EndText(page);
477 HPDF_Page_SetFontAndSize(page, font, 9);
479 for (
int i = 0; i < alert_count && i < 50; i++) {
481 draw_footer(page, *page_num, font);
482 page = create_new_page(pdf, font, 9);
487 HPDF_Page_SetRGBFill(page, 0.9, 0.6, 0.6);
488 HPDF_Page_Rectangle(page, 40, y - 5, 520, 20);
489 HPDF_Page_FillStroke(page);
490 HPDF_Page_SetRGBFill(page, 0, 0, 0);
491 HPDF_Page_SetFontAndSize(page, font_bold, 9);
492 HPDF_Page_BeginText(page);
493 HPDF_Page_TextOut(page, 45, y,
"Time");
494 HPDF_Page_TextOut(page, 180, y,
"Sensor");
495 HPDF_Page_TextOut(page, 280, y,
"Pollutant");
496 HPDF_Page_TextOut(page, 370, y,
"Value");
497 HPDF_Page_TextOut(page, 450, y,
"Limit");
498 HPDF_Page_EndText(page);
500 HPDF_Page_SetFontAndSize(page, font, 9);
503 HPDF_Page_BeginText(page);
504 HPDF_Page_TextOut(page, 45, y, alerts[i].timestamp);
505 HPDF_Page_TextOut(page, 180, y, alerts[i].sensor_name);
506 HPDF_Page_TextOut(page, 280, y, alerts[i].pollutant);
508 snprintf(val,
sizeof(val),
"%.4f", alerts[i].value);
509 HPDF_Page_TextOut(page, 370, y, val);
510 snprintf(val,
sizeof(val),
"%.4f", alerts[i].limit);
511 HPDF_Page_TextOut(page, 450, y, val);
512 HPDF_Page_EndText(page);
517 draw_footer(page, *page_num, font);
520void generate_pdf_report(
void) {
522 if (aqm_db_open(&db) != 0)
526 sqlite3_stmt *count_res = NULL;
527 const char *count_sql =
"SELECT COUNT(*), MIN(measured_at), MAX(measured_at) FROM readings;";
528 int total_records = 0;
529 char period_start[32] =
"";
530 char period_end[32] =
"";
532 if (sqlite3_prepare_v2(db, count_sql, -1, &count_res, NULL) == SQLITE_OK) {
533 if (sqlite3_step(count_res) == SQLITE_ROW) {
534 total_records = sqlite3_column_int(count_res, 0);
535 const char *min_date = (
const char *)sqlite3_column_text(count_res, 1);
536 const char *max_date = (
const char *)sqlite3_column_text(count_res, 2);
537 if (min_date) snprintf(period_start,
sizeof(period_start),
"%s", min_date);
538 if (max_date) snprintf(period_end,
sizeof(period_end),
"%s", max_date);
540 sqlite3_finalize(count_res);
544 sqlite3_stmt *sensor_res = NULL;
545 const char *sensor_sql =
"SELECT DISTINCT s.id, s.name FROM readings r JOIN sensors s ON s.id = r.sensor_id ORDER BY s.id;";
546 SensorStats sensor_stats[PDF_MAX_SENSORS];
547 int sensor_count = 0;
549 if (sqlite3_prepare_v2(db, sensor_sql, -1, &sensor_res, NULL) == SQLITE_OK) {
550 while (sqlite3_step(sensor_res) == SQLITE_ROW && sensor_count < PDF_MAX_SENSORS) {
551 int sid = sqlite3_column_int(sensor_res, 0);
552 const char *sname = (
const char *)sqlite3_column_text(sensor_res, 1);
553 init_sensor_stats(&sensor_stats[sensor_count], sid, sname ? sname :
"",
"");
556 sqlite3_finalize(sensor_res);
560 AlertRecord alerts[MAX_ALERTS];
562 int total_alerts = 0;
564 sqlite3_stmt *res = NULL;
566 "SELECT r.id, r.sensor_id, s.name, r.model, r.measured_at, r.pm25, r.pm10, r.co, r.no2, r.o3, r.so2 "
567 "FROM readings r JOIN sensors s ON s.id = r.sensor_id "
568 "ORDER BY r.measured_at ASC;";
570 if (sqlite3_prepare_v2(db, sql, -1, &res, NULL) == SQLITE_OK) {
571 while (sqlite3_step(res) == SQLITE_ROW) {
572 int sensor_id = sqlite3_column_int(res, 1);
573 const char *sname = (
const char *)sqlite3_column_text(res, 2);
574 const char *model = (
const char *)sqlite3_column_text(res, 3);
575 const char *timestamp = (
const char *)sqlite3_column_text(res, 4);
576 double pm25 = sqlite3_column_double(res, 5);
577 double pm10 = sqlite3_column_double(res, 6);
578 double co = sqlite3_column_double(res, 7);
579 double no2 = sqlite3_column_double(res, 8);
580 double o3 = sqlite3_column_double(res, 9);
581 double so2 = sqlite3_column_double(res, 10);
585 for (
int i = 0; i < sensor_count; i++) {
586 if (sensor_stats[i].sensor_id == sensor_id) {
593 if (sensor_stats[sidx].count == 0 && model) {
594 snprintf(sensor_stats[sidx].model,
sizeof(sensor_stats[sidx].model),
"%s", model);
596 update_sensor_stats(&sensor_stats[sidx], pm25, pm10, co, no2, o3, so2);
600 if (pm25 > limit_pm25) {
601 add_alert(alerts, &alert_count, sensor_id, sname, timestamp,
"PM2.5", pm25, limit_pm25);
604 if (pm10 > limit_pm10) {
605 add_alert(alerts, &alert_count, sensor_id, sname, timestamp,
"PM10", pm10, limit_pm10);
609 add_alert(alerts, &alert_count, sensor_id, sname, timestamp,
"CO", co, limit_co);
612 if (no2 > limit_no2) {
613 add_alert(alerts, &alert_count, sensor_id, sname, timestamp,
"NO2", no2, limit_no2);
617 add_alert(alerts, &alert_count, sensor_id, sname, timestamp,
"O3", o3, limit_o3);
620 if (so2 > limit_so2) {
621 add_alert(alerts, &alert_count, sensor_id, sname, timestamp,
"SO2", so2, limit_so2);
625 sqlite3_finalize(res);
629 HPDF_Doc pdf = HPDF_New(NULL, NULL);
631 fprintf(stderr,
"Cannot create PDF document.\n");
636 HPDF_Font font = HPDF_GetFont(pdf,
"Helvetica", NULL);
637 HPDF_Font font_bold = HPDF_GetFont(pdf,
"Helvetica-Bold", NULL);
642 draw_cover_page(pdf, font_bold, font, sensor_count, total_records, total_alerts, period_start, period_end);
646 draw_summary_page(pdf, font_bold, font, sensor_stats, sensor_count, &page_num);
649 for (
int i = 0; i < sensor_count; i++) {
650 draw_sensor_detail_page(pdf, font_bold, font, &sensor_stats[i], db, &page_num);
654 draw_alerts_page(pdf, font_bold, font, alerts, alert_count, &page_num);
656 HPDF_SaveToFile(pdf, PDF_FILE);
660 printf(
"PDF report generated: %s (%d pages)\n", PDF_FILE, page_num);
665void generate_pdf_report(
void) {
666 fputs(
"PDF export is disabled in this build (compile with HAVE_HPDF and link libharu).\n", stderr);