Air Quality Monitor
Portable C application for collecting, storing, and analyzing air quality sensor readings
Loading...
Searching...
No Matches
generate_pdf_report.c
1#include "core/aqm_db.h"
2#include "core/globals.h"
3#include <stdio.h>
4#include <sqlite3.h>
5#include <string.h>
6#include <time.h>
7
8#ifdef HAVE_HPDF
9#include <hpdf.h>
10
11#define PDF_FILE "sensor_data_report.pdf"
12#define PDF_MAX_SENSORS 50
13#define MAX_ALERTS 200
14
15typedef struct {
16 int sensor_id;
17 char name[64];
18 char model[64];
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;
22 int count;
23 int alerts_pm25, alerts_pm10, alerts_co, alerts_no2, alerts_o3, alerts_so2;
24} SensorStats;
25
26typedef struct {
27 int sensor_id;
28 char sensor_name[64];
29 char timestamp[32];
30 char pollutant[16];
31 double value;
32 double limit;
33} AlertRecord;
34
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;
42 stats->count = 0;
43 stats->alerts_pm25 = stats->alerts_pm10 = stats->alerts_co = stats->alerts_no2 = stats->alerts_o3 = stats->alerts_so2 = 0;
44}
45
46static void update_sensor_stats(SensorStats *stats, double pm25, double pm10, double co, double no2, double o3, double so2) {
47 stats->count++;
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++;
68}
69
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;
79 (*alert_count)++;
80}
81
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);
86 return page;
87}
88
89static void draw_footer(HPDF_Page page, int page_num, HPDF_Font font) {
90 HPDF_Page_SetFontAndSize(page, font, 8);
91 char footer[64];
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);
97}
98
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);
104
105 HPDF_REAL page_width = HPDF_Page_GetWidth(page);
106 HPDF_REAL center_x = page_width / 2;
107
108 // Title
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);
113
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);
118
119 // Date
120 time_t now = time(NULL);
121 const struct tm *tm_info = localtime(&now);
122 char date_str[64];
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);
128
129 // Data period
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);
137
138 // Summary box
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);
143
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);
149
150 HPDF_Page_SetFontAndSize(page, font, 12);
151 HPDF_Page_BeginText(page);
152 char summary[256];
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);
160}
161
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);
165 (*page_num)++;
166
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);
171
172 HPDF_REAL y = 740;
173 HPDF_Page_SetFontAndSize(page, font, 9);
174
175 // Table header
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);
188 y -= 25;
189
190 for (int i = 0; i < sensor_count && y > 100; i++) {
191 if (stats[i].count == 0) continue;
192
193 HPDF_Page_BeginText(page);
194 HPDF_Page_TextOut(page, 45, y, stats[i].name);
195 char buf[32];
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);
209 y -= 15;
210 }
211
212 draw_footer(page, *page_num, font);
213}
214
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;
220
221 // Background bar
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);
226
227 // Filled portion
228 if (is_alert) {
229 HPDF_Page_SetRGBFill(page, 0.9, 0.3, 0.3); // Red for alerts
230 } else {
231 HPDF_Page_SetRGBFill(page, 0.3, 0.6, 0.9); // Blue for normal
232 }
233 HPDF_Page_Rectangle(page, x, y, filled_width, bar_height);
234 HPDF_Page_FillStroke(page);
235
236 // Label and value
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);
241 char val_str[32];
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);
245}
246
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;
250
251 HPDF_Page page = create_new_page(pdf, font, 10);
252 (*page_num)++;
253
254 // Header
255 HPDF_Page_SetFontAndSize(page, font_bold, 16);
256 HPDF_Page_SetRGBFill(page, 0.2, 0.3, 0.5);
257 HPDF_Page_BeginText(page);
258 char header[128];
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);
262
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);
269
270 // Statistics table
271 HPDF_REAL y = 720;
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);
276 y -= 25;
277
278 // Statistics table header
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);
292 y -= 20;
293
294 HPDF_Page_SetFontAndSize(page, font, 9);
295
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}
303 };
304
305 for (int i = 0; i < 6; i++) {
306 // Highlight if there are alerts
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);
311 }
312
313 HPDF_Page_SetRGBFill(page, 0, 0, 0);
314 HPDF_Page_BeginText(page);
315 HPDF_Page_TextOut(page, 45, y, pollutants[i].name);
316 char buf[32];
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);
328 y -= 16;
329 }
330
331 // Bar charts section
332 y -= 20;
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);
337 y -= 30;
338
339 double max_val = 0;
340 for (int i = 0; i < 6; i++) {
341 if (pollutants[i].avg > max_val) max_val = pollutants[i].avg;
342 }
343 if (max_val == 0) max_val = 1;
344
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);
347 y -= 35;
348 }
349
350 // Recent readings table
351 if (y < 250) {
352 draw_footer(page, *page_num, font);
353 page = create_new_page(pdf, font, 9);
354 (*page_num)++;
355 y = 760;
356 } else {
357 y -= 30;
358 }
359
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);
364 y -= 25;
365
366 // Readings table header
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);
381 y -= 18;
382
383 // Query recent readings for this sensor
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;";
387
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);
391
392 int row_count = 0;
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);
401
402 // Check for alerts
403 int has_alert = (pm25 > limit_pm25 || pm10 > limit_pm10 || co > limit_co ||
404 no2 > limit_no2 || o3 > limit_o3 || so2 > limit_so2);
405
406 if (has_alert) {
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);
410 }
411
412 HPDF_Page_SetRGBFill(page, 0, 0, 0);
413 HPDF_Page_BeginText(page);
414 HPDF_Page_TextOut(page, 45, y, time ? time : "");
415 char buf[16];
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);
429
430 y -= 13;
431 row_count++;
432 }
433 sqlite3_finalize(res);
434 }
435
436 draw_footer(page, *page_num, font);
437}
438
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;
442
443 HPDF_Page page = create_new_page(pdf, font, 10);
444 (*page_num)++;
445
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);
451
452 HPDF_Page_SetFontAndSize(page, font, 11);
453 HPDF_Page_SetRGBFill(page, 0, 0, 0);
454 HPDF_Page_BeginText(page);
455 char buf[64];
456 snprintf(buf, sizeof(buf), "Total Alert Events: %d", alert_count);
457 HPDF_Page_TextOut(page, 40, 760, buf);
458 HPDF_Page_EndText(page);
459
460 HPDF_REAL y = 730;
461
462 // Table header
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);
475 y -= 25;
476
477 HPDF_Page_SetFontAndSize(page, font, 9);
478
479 for (int i = 0; i < alert_count && i < 50; i++) {
480 if (y < 100) {
481 draw_footer(page, *page_num, font);
482 page = create_new_page(pdf, font, 9);
483 (*page_num)++;
484 y = 760;
485
486 // Repeat header
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);
499 y -= 25;
500 HPDF_Page_SetFontAndSize(page, font, 9);
501 }
502
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);
507 char val[32];
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);
513
514 y -= 15;
515 }
516
517 draw_footer(page, *page_num, font);
518}
519
520void generate_pdf_report(void) {
521 sqlite3 *db = NULL;
522 if (aqm_db_open(&db) != 0)
523 return;
524
525 // Get data range and count
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] = "";
531
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);
539 }
540 sqlite3_finalize(count_res);
541 }
542
543 // Get unique sensors
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;
548
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 : "", "");
554 sensor_count++;
555 }
556 sqlite3_finalize(sensor_res);
557 }
558
559 // Collect all data and alerts
560 AlertRecord alerts[MAX_ALERTS];
561 int alert_count = 0;
562 int total_alerts = 0;
563
564 sqlite3_stmt *res = NULL;
565 const char *sql =
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;";
569
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);
582
583 // Find sensor index
584 int sidx = -1;
585 for (int i = 0; i < sensor_count; i++) {
586 if (sensor_stats[i].sensor_id == sensor_id) {
587 sidx = i;
588 break;
589 }
590 }
591
592 if (sidx >= 0) {
593 if (sensor_stats[sidx].count == 0 && model) {
594 snprintf(sensor_stats[sidx].model, sizeof(sensor_stats[sidx].model), "%s", model);
595 }
596 update_sensor_stats(&sensor_stats[sidx], pm25, pm10, co, no2, o3, so2);
597 }
598
599 // Check for alerts
600 if (pm25 > limit_pm25) {
601 add_alert(alerts, &alert_count, sensor_id, sname, timestamp, "PM2.5", pm25, limit_pm25);
602 total_alerts++;
603 }
604 if (pm10 > limit_pm10) {
605 add_alert(alerts, &alert_count, sensor_id, sname, timestamp, "PM10", pm10, limit_pm10);
606 total_alerts++;
607 }
608 if (co > limit_co) {
609 add_alert(alerts, &alert_count, sensor_id, sname, timestamp, "CO", co, limit_co);
610 total_alerts++;
611 }
612 if (no2 > limit_no2) {
613 add_alert(alerts, &alert_count, sensor_id, sname, timestamp, "NO2", no2, limit_no2);
614 total_alerts++;
615 }
616 if (o3 > limit_o3) {
617 add_alert(alerts, &alert_count, sensor_id, sname, timestamp, "O3", o3, limit_o3);
618 total_alerts++;
619 }
620 if (so2 > limit_so2) {
621 add_alert(alerts, &alert_count, sensor_id, sname, timestamp, "SO2", so2, limit_so2);
622 total_alerts++;
623 }
624 }
625 sqlite3_finalize(res);
626 }
627
628 // Create PDF
629 HPDF_Doc pdf = HPDF_New(NULL, NULL);
630 if (!pdf) {
631 fprintf(stderr, "Cannot create PDF document.\n");
632 aqm_db_close(db);
633 return;
634 }
635
636 HPDF_Font font = HPDF_GetFont(pdf, "Helvetica", NULL);
637 HPDF_Font font_bold = HPDF_GetFont(pdf, "Helvetica-Bold", NULL);
638
639 int page_num = 0;
640
641 // Cover page
642 draw_cover_page(pdf, font_bold, font, sensor_count, total_records, total_alerts, period_start, period_end);
643 page_num++;
644
645 // Summary page
646 draw_summary_page(pdf, font_bold, font, sensor_stats, sensor_count, &page_num);
647
648 // Sensor detail pages
649 for (int i = 0; i < sensor_count; i++) {
650 draw_sensor_detail_page(pdf, font_bold, font, &sensor_stats[i], db, &page_num);
651 }
652
653 // Alerts page
654 draw_alerts_page(pdf, font_bold, font, alerts, alert_count, &page_num);
655
656 HPDF_SaveToFile(pdf, PDF_FILE);
657 HPDF_Free(pdf);
658 aqm_db_close(db);
659
660 printf("PDF report generated: %s (%d pages)\n", PDF_FILE, page_num);
661}
662
663#else
664
665void generate_pdf_report(void) {
666 fputs("PDF export is disabled in this build (compile with HAVE_HPDF and link libharu).\n", stderr);
667}
668
669#endif