Fixed CPU overuse. (LP: 1582930)
[psensor.git] / src / graph.c
1 /*
2  * Copyright (C) 2010-2014 jeanfi@gmail.com
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License as
6  * published by the Free Software Foundation; either version 2 of the
7  * License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17  * 02110-1301 USA
18  */
19 #include <stdlib.h>
20 #include <string.h>
21
22 #include <sys/time.h>
23
24 #include <glib/gi18n.h>
25 #include <gtk/gtk.h>
26
27 #include <math.h>
28
29 #include <cfg.h>
30 #include <graph.h>
31 #include <plog.h>
32 #include <psensor.h>
33
34 /* horizontal padding */
35 static const int GRAPH_H_PADDING = 4;
36 /* vertical padding */
37 static const int GRAPH_V_PADDING = 4;
38
39 bool is_smooth_curves_enabled;
40
41 struct graph_info {
42         /* Horizontal position of the central area (curves) */
43         int g_xoff;
44         /* Vertical position of the central area (curves) */
45         int g_yoff;
46
47         /* Width of the central area (curves) */
48         int g_width;
49         /* Height of the central area (curves) */
50         int g_height;
51
52         /* Height of the drawing canvas */
53         int height;
54         /* Width of the drawing canvas */
55         int width;
56 };
57
58 static GtkStyleContext *style;
59 /* Foreground color of the current desktop theme */
60 static GdkRGBA theme_fg_color;
61 /* Background color of the current desktop theme */
62 static GdkRGBA theme_bg_color;
63
64 static void update_theme(GtkWidget *w)
65 {
66         style = gtk_widget_get_style_context(w);
67
68         gtk_style_context_get_background_color(style,
69                                                GTK_STATE_FLAG_NORMAL,
70                                                &theme_bg_color);
71         gtk_style_context_get_color(style,
72                                     GTK_STATE_FLAG_NORMAL,
73                                     &theme_fg_color);
74 }
75
76 static struct psensor **list_filter_graph_enabled(struct psensor **sensors)
77 {
78         int n, i;
79         struct psensor **result, **cur, *s;
80
81         if (!sensors)
82                 return NULL;
83
84         n = psensor_list_size(sensors);
85         result = malloc((n+1) * sizeof(struct psensor *));
86
87         for (cur = sensors, i = 0; *cur; cur++) {
88                 s = *cur;
89
90                 if (config_is_sensor_graph_enabled(s->id))
91                         result[i++] = s;
92         }
93
94         result[i] = NULL;
95
96         return result;
97 }
98
99 /* Return the end time of the graph i.e. the more recent measure.  If
100  * no measure are available, return 0.
101  * If Bezier curves are used return the measure n-3 to avoid to
102  * display a part of the curve outside the graph area.
103  */
104 static time_t get_graph_end_time_s(struct psensor **sensors)
105 {
106         time_t ret, t;
107         struct psensor *s;
108         struct measure *measures;
109         int i, n;
110
111         ret = 0;
112         while (*sensors) {
113                 s = *sensors;
114                 measures = s->measures;
115
116                 if (is_smooth_curves_enabled)
117                         n = 2;
118                 else
119                         n = 0;
120
121                 for (i = s->values_max_length - 1; i >= 0; i--) {
122                         if (measures[i].value != UNKNOWN_DBL_VALUE) {
123                                 if (!n) {
124                                         t = measures[i].time.tv_sec;
125
126                                         if (t > ret) {
127                                                 ret = t;
128                                                 break;
129                                         }
130                                 } else {
131                                         n--;
132                                 }
133                         }
134                         i--;
135                 }
136
137                 sensors++;
138         }
139
140         return ret;
141 }
142
143 static time_t get_graph_begin_time_s(struct config *cfg, time_t etime)
144 {
145         if (!etime)
146                 return 0;
147
148         return etime - cfg->graph_monitoring_duration * 60;
149 }
150
151 static double
152 compute_y(double value, double min, double max, int height, int off)
153 {
154         double t = value - min;
155
156         return height - ((double)height * (t / (max - min))) + off;
157 }
158
159 static char *time_to_str(time_t s)
160 {
161         char *str;
162         /* note: localtime returns a static field, no free required */
163         struct tm *tm = localtime(&s);
164
165         if (!tm)
166                 return NULL;
167
168         str = malloc(6);
169         strftime(str, 6, "%H:%M", tm);
170
171         return str;
172 }
173
174 static void draw_left_region(cairo_t *cr, struct graph_info *info)
175 {
176         cairo_set_source_rgb(cr,
177                              theme_bg_color.red,
178                              theme_bg_color.green,
179                              theme_bg_color.blue);
180
181         cairo_rectangle(cr, 0, 0, info->g_xoff, info->height);
182         cairo_fill(cr);
183 }
184
185 static void draw_right_region(cairo_t *cr, struct graph_info *info)
186 {
187         cairo_set_source_rgb(cr,
188                              theme_bg_color.red,
189                              theme_bg_color.green,
190                              theme_bg_color.blue);
191
192
193         cairo_rectangle(cr,
194                         info->g_xoff + info->g_width,
195                         0,
196                         info->g_xoff + info->g_width + GRAPH_H_PADDING,
197                         info->height);
198         cairo_fill(cr);
199 }
200
201 static void
202 draw_graph_background(cairo_t *cr,
203                       struct config *config,
204                       struct graph_info *info)
205 {
206         struct color *bgcolor;
207
208         bgcolor = config->graph_bgcolor;
209
210         if (config->alpha_channel_enabled)
211                 cairo_set_source_rgba(cr,
212                                       theme_bg_color.red,
213                                       theme_bg_color.green,
214                                       theme_bg_color.blue,
215                                       config->graph_bg_alpha);
216         else
217                 cairo_set_source_rgb(cr,
218                                      theme_bg_color.red,
219                                      theme_bg_color.green,
220                                      theme_bg_color.blue);
221
222         cairo_rectangle(cr, info->g_xoff, 0, info->g_width, info->height);
223         cairo_fill(cr);
224
225         if (config->alpha_channel_enabled)
226                 cairo_set_source_rgba(cr,
227                                       bgcolor->red,
228                                       bgcolor->green,
229                                       bgcolor->blue,
230                                       config->graph_bg_alpha);
231         else
232                 cairo_set_source_rgb(cr,
233                                      bgcolor->red,
234                                      bgcolor->green,
235                                      bgcolor->blue);
236
237         cairo_rectangle(cr,
238                         info->g_xoff,
239                         info->g_yoff,
240                         info->g_width,
241                         info->g_height);
242         cairo_fill(cr);
243 }
244
245 /* setup dash style */
246 static double dashes[] = {
247         1.0,            /* ink */
248         2.0,            /* skip */
249 };
250 static int ndash = sizeof(dashes) / sizeof(dashes[0]);
251
252 static void draw_background_lines(cairo_t *cr,
253                                   int min, int max,
254                                   struct config *config,
255                                   struct graph_info *info)
256 {
257         int i;
258         double x, y;
259         struct color *color;
260
261         color = config->graph_fgcolor;
262
263         /* draw background lines */
264         cairo_set_line_width(cr, 1);
265         cairo_set_dash(cr, dashes, ndash, 0);
266         cairo_set_source_rgb(cr, color->red, color->green, color->blue);
267
268         /* vertical lines representing time steps */
269         for (i = 0; i <= 5; i++) {
270                 x = i * ((double)info->g_width / 5) + info->g_xoff;
271                 cairo_move_to(cr, x, info->g_yoff);
272                 cairo_line_to(cr, x, info->g_yoff + info->g_height);
273         }
274
275         /* horizontal lines draws a line for each 10C step */
276         for (i = min; i < max; i++) {
277                 if (i % 10 == 0) {
278                         y = compute_y(i,
279                                       min,
280                                       max,
281                                       info->g_height,
282                                       info->g_yoff);
283
284                         cairo_move_to(cr, info->g_xoff, y);
285                         cairo_line_to(cr, info->g_xoff + info->g_width, y);
286                 }
287         }
288
289         cairo_stroke(cr);
290
291         /* back to normal line style */
292         cairo_set_dash(cr, NULL, 0, 0);
293 }
294
295 /* Keys: sensor identifier.
296  *
297  * Values: array of time_t. Each time_t is corresponding to a sensor
298  * measure which has been used as the start point of a Bezier curve.
299  */
300 static GHashTable *times;
301
302 static void draw_sensor_smooth_curve(struct psensor *s,
303                                      cairo_t *cr,
304                                      double min,
305                                      double max,
306                                      int bt,
307                                      int et,
308                                      struct graph_info *info)
309 {
310         int i, dt, vdt, j, k, found;
311         double x[4], y[4], v;
312         time_t t, t0, *stimes;
313         GdkRGBA *color;
314
315         if (!times)
316                 times = g_hash_table_new_full(g_str_hash,
317                                               g_str_equal,
318                                               free,
319                                               free);
320
321         stimes = g_hash_table_lookup(times, s->id);
322
323         color = config_get_sensor_color(s->id);
324
325         cairo_set_source_rgb(cr,
326                              color->red,
327                              color->green,
328                              color->blue);
329         gdk_rgba_free(color);
330
331         /* search the index of the first measure used as a start point
332          * of a Bezier curve. The start and end points of the Bezier
333          * curves must be preserved to ensure the same overall shape
334          * of the graph. */
335         i = 0;
336         if (stimes) {
337                 while (i < s->values_max_length) {
338                         t = s->measures[i].time.tv_sec;
339                         v = s->measures[i].value;
340
341                         found = 0;
342                         if (v != UNKNOWN_DBL_VALUE && t) {
343                                 k = 0;
344                                 while (stimes[k]) {
345                                         if (t == stimes[k]) {
346                                                 found = 1;
347                                                 break;
348                                         }
349                                         k++;
350                                 }
351                         }
352
353                         if (found)
354                                 break;
355
356                         i++;
357                 }
358         }
359
360         stimes = malloc((s->values_max_length + 1) * sizeof(time_t));
361         memset(stimes, 0, (s->values_max_length + 1) * sizeof(time_t));
362         g_hash_table_insert(times, strdup(s->id), stimes);
363
364         if (i == s->values_max_length)
365                 i = 0;
366
367         k = 0;
368         dt = et - bt;
369         while (i < s->values_max_length) {
370                 j = 0;
371                 t = 0;
372                 while (i < s->values_max_length && j < 4) {
373                         t = s->measures[i].time.tv_sec;
374                         v = s->measures[i].value;
375
376                         if (v == UNKNOWN_DBL_VALUE || !t) {
377                                 i++;
378                                 continue;
379                         }
380
381                         vdt = t - bt;
382
383                         x[0 + j] = ((double)vdt * info->g_width)
384                                 / dt + info->g_xoff;
385                         y[0 + j] = compute_y(v,
386                                              min,
387                                              max,
388                                              info->g_height,
389                                              info->g_yoff);
390
391                         if (j == 0)
392                                 t0 = t;
393
394                         i++;
395                         j++;
396                 }
397
398                 if (j == 4) {
399                         cairo_move_to(cr, x[0], y[0]);
400                         cairo_curve_to(cr, x[1], y[1], x[2], y[3], x[3], y[3]);
401                         stimes[k++] = t0;
402                         i--;
403                 }
404         }
405
406         cairo_stroke(cr);
407 }
408
409 static void draw_sensor_curve(struct psensor *s,
410                               cairo_t *cr,
411                               double min,
412                               double max,
413                               int bt,
414                               int et,
415                               struct graph_info *info)
416 {
417         int first, i, t, dt, vdt;
418         double v, x, y;
419         GdkRGBA *color;
420
421         color = config_get_sensor_color(s->id);
422         cairo_set_source_rgb(cr,
423                              color->red,
424                              color->green,
425                              color->blue);
426         gdk_rgba_free(color);
427
428         dt = et - bt;
429         first = 1;
430         for (i = 0; i < s->values_max_length; i++) {
431                 t = s->measures[i].time.tv_sec;
432                 v = s->measures[i].value;
433
434                 if (v == UNKNOWN_DBL_VALUE || !t)
435                         continue;
436
437                 vdt = t - bt;
438
439                 x = ((double)vdt * info->g_width) / dt + info->g_xoff;
440
441                 y = compute_y(v, min, max, info->g_height, info->g_yoff);
442
443                 if (first) {
444                         cairo_move_to(cr, x, y);
445                         first = 0;
446                 } else {
447                         cairo_line_to(cr, x, y);
448                 }
449
450         }
451         cairo_stroke(cr);
452 }
453
454 static void display_no_graphs_warning(cairo_t *cr, int x, int y)
455 {
456         char *msg;
457
458         msg = strdup(_("No graphs enabled"));
459
460         cairo_select_font_face(cr,
461                                "sans-serif",
462                                CAIRO_FONT_SLANT_NORMAL,
463                                CAIRO_FONT_WEIGHT_NORMAL);
464         cairo_set_font_size(cr, 18.0);
465
466         cairo_move_to(cr, x, y);
467         cairo_show_text(cr, msg);
468
469         free(msg);
470 }
471
472 void
473 graph_update(struct psensor **sensors,
474              GtkWidget *w_graph,
475              struct config *config,
476              GtkWidget *window)
477 {
478         int et, bt, width, height, g_width, g_height;
479         double min_rpm, max_rpm, mint, maxt, min, max;
480         char *strmin, *strmax;
481         /* horizontal and vertical offset of the graph */
482         int g_xoff, g_yoff, no_graphs, use_celsius;
483         cairo_surface_t *cst;
484         cairo_t *cr, *cr_pixmap;
485         char *str_btime, *str_etime;
486         cairo_text_extents_t te_btime, te_etime, te_max, te_min;
487         struct psensor **sensor_cur, **enabled_sensors;
488         GtkAllocation galloc;
489         GtkStyleContext *style_ctx;
490         struct graph_info info;
491
492         if (!gtk_widget_is_drawable(w_graph))
493                 return;
494
495         if (!style)
496                 update_theme(window);
497
498         enabled_sensors = list_filter_graph_enabled(sensors);
499
500         min_rpm = get_min_rpm(enabled_sensors);
501         max_rpm = get_max_rpm(enabled_sensors);
502
503         if (config_get_temperature_unit() == CELSIUS)
504                 use_celsius = 1;
505         else
506                 use_celsius = 0;
507
508         mint = get_min_temp(enabled_sensors);
509         strmin = psensor_value_to_str(SENSOR_TYPE_TEMP, mint, use_celsius);
510
511         maxt = get_max_temp(enabled_sensors);
512         strmax = psensor_value_to_str(SENSOR_TYPE_TEMP, maxt, use_celsius);
513
514         et = get_graph_end_time_s(enabled_sensors);
515         bt = get_graph_begin_time_s(config, et);
516
517         str_btime = time_to_str(bt);
518         str_etime = time_to_str(et);
519
520         gtk_widget_get_allocation(w_graph, &galloc);
521         width = galloc.width;
522         info.width = galloc.width;
523         height = galloc.height;
524         info.height = height;
525
526
527         cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
528         cr = cairo_create(cst);
529
530         cairo_select_font_face(cr,
531                                "sans-serif",
532                                CAIRO_FONT_SLANT_NORMAL,
533                                CAIRO_FONT_WEIGHT_NORMAL);
534         cairo_set_font_size(cr, 10.0);
535
536         cairo_text_extents(cr, str_etime, &te_etime);
537         cairo_text_extents(cr, str_btime, &te_btime);
538         cairo_text_extents(cr, strmax, &te_max);
539         cairo_text_extents(cr, strmin, &te_min);
540
541         g_yoff = GRAPH_V_PADDING;
542         info.g_yoff = g_yoff;
543
544         g_height = height - GRAPH_V_PADDING;
545         if (te_etime.height > te_btime.height)
546                 g_height -= GRAPH_V_PADDING + te_etime.height + GRAPH_V_PADDING;
547         else
548                 g_height -= GRAPH_V_PADDING + te_btime.height + GRAPH_V_PADDING;
549
550         info.g_height = g_height;
551
552         if (te_min.width > te_max.width)
553                 g_xoff = (2 * GRAPH_H_PADDING) + te_max.width;
554         else
555                 g_xoff = (2 * GRAPH_H_PADDING) + te_min.width;
556
557         info.g_xoff = g_xoff;
558
559         g_width = width - g_xoff - GRAPH_H_PADDING;
560         info.g_width = g_width;
561
562         draw_graph_background(cr, config, &info);
563
564         /* Set the color for text drawing */
565         cairo_set_source_rgb(cr,
566                              theme_fg_color.red,
567                              theme_fg_color.green,
568                              theme_fg_color.blue);
569
570         /* draw graph begin time */
571         cairo_move_to(cr, g_xoff, height - GRAPH_V_PADDING);
572         cairo_show_text(cr, str_btime);
573         free(str_btime);
574
575         /* draw graph end time */
576         cairo_move_to(cr,
577                       width - te_etime.width - GRAPH_H_PADDING,
578                       height - GRAPH_V_PADDING);
579         cairo_show_text(cr, str_etime);
580         free(str_etime);
581
582         draw_background_lines(cr, mint, maxt, config, &info);
583
584         /* .. and finaly draws the temperature graphs */
585         if (bt && et) {
586                 sensor_cur = enabled_sensors;
587
588                 cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
589                 cairo_set_line_width(cr, 1);
590                 no_graphs = 1;
591                 while (*sensor_cur) {
592                         struct psensor *s = *sensor_cur;
593
594                         no_graphs = 0;
595                         if (s->type & SENSOR_TYPE_RPM) {
596                                 min = min_rpm;
597                                 max = max_rpm;
598                         } else if (s->type & SENSOR_TYPE_PERCENT) {
599                                 min = 0;
600                                 max = get_max_value(enabled_sensors,
601                                                     SENSOR_TYPE_PERCENT);
602                         } else {
603                                 min = mint;
604                                 max = maxt;
605                         }
606
607                         if (is_smooth_curves_enabled)
608                                 draw_sensor_smooth_curve(s, cr,
609                                                          min, max,
610                                                          bt, et,
611                                                          &info);
612                         else
613                                 draw_sensor_curve(s, cr,
614                                                   min, max,
615                                                   bt, et,
616                                                   &info);
617
618                         sensor_cur++;
619                 }
620
621                 if (no_graphs)
622                         display_no_graphs_warning(cr,
623                                                   g_xoff + 12,
624                                                   g_height / 2);
625         }
626
627         draw_left_region(cr, &info);
628         draw_right_region(cr, &info);
629
630         /* draw min and max temp */
631         cairo_set_source_rgb(cr,
632                              theme_fg_color.red,
633                              theme_fg_color.green,
634                              theme_fg_color.blue);
635
636         cairo_move_to(cr, GRAPH_H_PADDING, te_max.height + GRAPH_V_PADDING);
637         cairo_show_text(cr, strmax);
638         free(strmax);
639
640         cairo_move_to(cr,
641                       GRAPH_H_PADDING, height - (te_min.height / 2) - g_yoff);
642         cairo_show_text(cr, strmin);
643         free(strmin);
644
645         cr_pixmap = gdk_cairo_create(gtk_widget_get_window(w_graph));
646
647         if (cr_pixmap) {
648                 if (config->alpha_channel_enabled)
649                         cairo_set_operator(cr_pixmap, CAIRO_OPERATOR_SOURCE);
650
651                 cairo_set_source_surface(cr_pixmap, cst, 0, 0);
652                 cairo_paint(cr_pixmap);
653         }
654
655         free(enabled_sensors);
656
657         cairo_destroy(cr_pixmap);
658         cairo_surface_destroy(cst);
659         cairo_destroy(cr);
660 }
661
662 int compute_values_max_length(struct config *c)
663 {
664         int n, duration, interval;
665
666         duration = c->graph_monitoring_duration * 60;
667         interval = c->sensor_update_interval;
668
669         n = 3 + ceil((((double)duration) / interval) + 0.5) + 3;
670
671         return n;
672 }