]> git.tdb.fi Git - ext/subsurface.git/blob - profile.c
551918b35518e8cfe2f0e1d8b69cbc62a863e46b
[ext/subsurface.git] / profile.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <stdarg.h>
4 #include <string.h>
5 #include <time.h>
6
7 #include "dive.h"
8 #include "display.h"
9 #include "divelist.h"
10
11 int selected_dive = 0;
12
13 /*
14  * Cairo scaling really is horribly horribly mis-designed.
15  *
16  * Which is sad, because I really like Cairo otherwise. But
17  * the fact that the line width is scaled with the same scale
18  * as the coordinate system is a f*&%ing disaster. So we
19  * can't use it, and instead have this butt-ugly wrapper thing..
20  */
21 struct graphics_context {
22         cairo_t *cr;
23         double maxx, maxy;
24         double leftx, rightx;
25         double topy, bottomy;
26 };
27
28 /* Plot info with smoothing and one-, two- and three-minute minimums and maximums */
29 struct plot_info {
30         int nr;
31         int maxtime;
32         struct plot_data {
33                 int sec;
34                 int val;
35                 int smoothed;
36                 struct plot_data *min[3];
37                 struct plot_data *max[3];
38                 int avg[3];
39         } entry[];
40 };
41 #define plot_info_size(nr) (sizeof(struct plot_info) + (nr)*sizeof(struct plot_data))
42
43 /* Scale to 0,0 -> maxx,maxy */
44 #define SCALEX(gc,x)  (((x)-gc->leftx)/(gc->rightx-gc->leftx)*gc->maxx)
45 #define SCALEY(gc,y)  (((y)-gc->topy)/(gc->bottomy-gc->topy)*gc->maxy)
46 #define SCALE(gc,x,y) SCALEX(gc,x),SCALEY(gc,y)
47
48 static void move_to(struct graphics_context *gc, double x, double y)
49 {
50         cairo_move_to(gc->cr, SCALE(gc, x, y));
51 }
52
53 static void line_to(struct graphics_context *gc, double x, double y)
54 {
55         cairo_line_to(gc->cr, SCALE(gc, x, y));
56 }
57
58 #define ROUND_UP(x,y) ((((x)+(y)-1)/(y))*(y))
59
60 /*
61  * When showing dive profiles, we scale things to the
62  * current dive. However, we don't scale past less than
63  * 30 minutes or 90 ft, just so that small dives show
64  * up as such.
65  */
66 static int round_seconds_up(int seconds)
67 {
68         return MAX(30*60, ROUND_UP(seconds, 60*10));
69 }
70
71 static int round_depth_up(depth_t depth)
72 {
73         unsigned mm = depth.mm;
74         /* Minimum 30m */
75         return MAX(30000, ROUND_UP(mm+3000, 10000));
76 }
77
78 typedef struct {
79         int size;
80         double r,g,b;
81         enum {CENTER,LEFT} halign;
82         enum {MIDDLE,TOP,BOTTOM} valign;
83 } text_render_options_t;
84
85 static void plot_text(struct graphics_context *gc, const text_render_options_t *tro,
86                       double x, double y, const char *fmt, ...)
87 {
88         cairo_t *cr = gc->cr;
89         cairo_text_extents_t extents;
90         double dx, dy;
91         char buffer[80];
92         va_list args;
93
94         va_start(args, fmt);
95         vsnprintf(buffer, sizeof(buffer), fmt, args);
96         va_end(args);
97
98         cairo_set_font_size(cr, tro->size);
99         cairo_text_extents(cr, buffer, &extents);
100         dx = 0;
101         switch (tro->halign) {
102         case CENTER:
103                 dx = -(extents.width/2 + extents.x_bearing);
104                 break;
105         case LEFT:
106                 dx = 0;
107                 break;
108         }
109         switch (tro->valign) {
110         case TOP:
111                 dy = extents.height * 1.2;
112                 break;
113         case BOTTOM:
114                 dy = -extents.height * 0.8;
115                 break;
116         case MIDDLE:
117                 dy = 0;
118                 break;
119         }
120
121         move_to(gc, x, y);
122         cairo_rel_move_to(cr, dx, dy);
123
124         cairo_text_path(cr, buffer);
125         cairo_set_source_rgb(cr, 0, 0, 0);
126         cairo_stroke(cr);
127
128         move_to(gc, x, y);
129         cairo_rel_move_to(cr, dx, dy);
130
131         cairo_set_source_rgb(cr, tro->r, tro->g, tro->b);
132         cairo_show_text(cr, buffer);
133 }
134
135 static void render_depth_sample(struct graphics_context *gc, struct plot_data *entry, const text_render_options_t *tro)
136 {
137         int sec = entry->sec;
138         depth_t depth = { entry->val };
139         const char *fmt;
140         double d;
141
142         switch (output_units.length) {
143         case METERS:
144                 d = depth.mm / 1000.0;
145                 fmt = "%.1f";
146                 break;
147         case FEET:
148                 d = to_feet(depth);
149                 fmt = "%.0f";
150                 break;
151         }
152         plot_text(gc, tro, sec, depth.mm, fmt, d);
153 }
154
155 static void plot_text_samples(struct graphics_context *gc, struct plot_info *pi)
156 {
157         static const text_render_options_t deep = {14, 1.0, 0.2, 0.2, CENTER, TOP};
158         static const text_render_options_t shallow = {14, 1.0, 0.2, 0.2, CENTER, BOTTOM};
159         int i;
160
161         for (i = 0; i < pi->nr; i++) {
162                 struct plot_data *entry = pi->entry + i;
163
164                 if (entry->val < 2000)
165                         continue;
166
167                 if (entry == entry->max[2])
168                         render_depth_sample(gc, entry, &deep);
169
170                 if (entry == entry->min[2])
171                         render_depth_sample(gc, entry, &shallow);
172         }
173 }
174
175 static void plot_depth_text(struct dive *dive, struct graphics_context *gc, struct plot_info *pi)
176 {
177         int maxtime, maxdepth;
178
179         /* Get plot scaling limits */
180         maxtime = round_seconds_up(dive->duration.seconds);
181         maxdepth = round_depth_up(dive->maxdepth);
182
183         gc->leftx = 0; gc->rightx = maxtime;
184         gc->topy = 0; gc->bottomy = maxdepth;
185
186         plot_text_samples(gc, pi);
187 }
188
189 static void plot_smoothed_profile(struct graphics_context *gc, struct plot_info *pi)
190 {
191         int i;
192         struct plot_data *entry = pi->entry;
193
194         cairo_set_source_rgba(gc->cr, 1, 0.2, 0.2, 0.20);
195         move_to(gc, entry->sec, entry->smoothed);
196         for (i = 1; i < pi->nr; i++) {
197                 entry++;
198                 line_to(gc, entry->sec, entry->smoothed);
199         }
200         cairo_stroke(gc->cr);
201 }
202
203 static void plot_minmax_profile_minute(struct graphics_context *gc, struct plot_info *pi,
204                                 int index, double a)
205 {
206         int i;
207         struct plot_data *entry = pi->entry;
208
209         cairo_set_source_rgba(gc->cr, 1, 0.2, 1, a);
210         move_to(gc, entry->sec, entry->min[index]->val);
211         for (i = 1; i < pi->nr; i++) {
212                 entry++;
213                 line_to(gc, entry->sec, entry->min[index]->val);
214         }
215         for (i = 1; i < pi->nr; i++) {
216                 line_to(gc, entry->sec, entry->max[index]->val);
217                 entry--;
218         }
219         cairo_close_path(gc->cr);
220         cairo_fill(gc->cr);
221 }
222
223 static void plot_minmax_profile(struct graphics_context *gc, struct plot_info *pi)
224 {
225         plot_minmax_profile_minute(gc, pi, 2, 0.1);
226         plot_minmax_profile_minute(gc, pi, 1, 0.1);
227         plot_minmax_profile_minute(gc, pi, 0, 0.1);
228 }
229
230 static void plot_depth_profile(struct dive *dive, struct graphics_context *gc, struct plot_info *pi)
231 {
232         int i;
233         cairo_t *cr = gc->cr;
234         int begins, sec, depth;
235         struct plot_data *entry;
236         int maxtime, maxdepth, marker;
237
238         cairo_set_line_width(gc->cr, 2);
239
240         /* Get plot scaling limits */
241         maxtime = round_seconds_up(dive->duration.seconds);
242         maxdepth = round_depth_up(dive->maxdepth);
243
244         /* Time markers: every 5 min */
245         gc->leftx = 0; gc->rightx = maxtime;
246         gc->topy = 0; gc->bottomy = 1.0;
247         for (i = 5*60; i < maxtime; i += 5*60) {
248                 move_to(gc, i, 0);
249                 line_to(gc, i, 1);
250         }
251
252         /* Depth markers: every 30 ft or 10 m*/
253         gc->leftx = 0; gc->rightx = 1.0;
254         gc->topy = 0; gc->bottomy = maxdepth;
255         switch (output_units.length) {
256         case METERS: marker = 10000; break;
257         case FEET: marker = 9144; break;        /* 30 ft */
258         }
259
260         cairo_set_source_rgba(cr, 1, 1, 1, 0.5);
261         for (i = marker; i < maxdepth; i += marker) {
262                 move_to(gc, 0, i);
263                 line_to(gc, 1, i);
264         }
265         cairo_stroke(cr);
266
267         /* Show mean depth */
268         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.40);
269         move_to(gc, 0, dive->meandepth.mm);
270         line_to(gc, 1, dive->meandepth.mm);
271         cairo_stroke(cr);
272
273         gc->leftx = 0; gc->rightx = maxtime;
274
275         plot_smoothed_profile(gc, pi);
276         plot_minmax_profile(gc, pi);
277
278         entry = pi->entry;
279         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
280         begins = entry->sec;
281         move_to(gc, entry->sec, entry->val);
282         for (i = 1; i < pi->nr; i++) {
283                 entry++;
284                 sec = entry->sec;
285                 if (sec <= maxtime) {
286                         depth = entry->val;
287                         line_to(gc, sec, depth);
288                 }
289         }
290         gc->topy = 0; gc->bottomy = 1.0;
291         line_to(gc, MIN(sec,maxtime), 0);
292         line_to(gc, begins, 0);
293         cairo_close_path(cr);
294         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.20);
295         cairo_fill_preserve(cr);
296         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
297         cairo_stroke(cr);
298 }
299
300 static void plot_temperature_profile(struct dive *dive, struct graphics_context *gc)
301 {
302         int i;
303         cairo_t *cr = gc->cr;
304         int begins = 0, sec = 0;
305         int maxtime, mintemp, maxtemp;
306
307         /* Get plot scaling limits */
308         maxtime = round_seconds_up(dive->duration.seconds);
309         mintemp = INT_MAX;
310         maxtemp = 0;
311         for (i = 0; i < dive->samples; i++) {
312                 struct sample *sample = dive->sample+i;
313                 int mkelvin = sample->temperature.mkelvin;
314                 if (!mkelvin)
315                         continue;
316                 if (!begins) {
317                         begins = mkelvin;
318                         sec = sample->time.seconds;
319                 }
320                 if (mkelvin > maxtemp)
321                         maxtemp = mkelvin;
322                 if (mkelvin < mintemp)
323                         mintemp = mkelvin;
324         }
325         if (mintemp >= maxtemp)
326                 return;
327
328         gc->leftx = 0; gc->rightx = maxtime;
329         /* Show temperatures in roughly the lower third */
330         gc->topy = maxtemp + (maxtemp - mintemp)*2;
331         gc->bottomy = mintemp - (maxtemp - mintemp)/2;
332
333         cairo_set_source_rgba(cr, 0.2, 0.2, 1.0, 0.8);
334         move_to(gc, sec, begins);
335         for (i = 0; i < dive->samples; i++) {
336                 struct sample *sample = dive->sample+i;
337                 int mkelvin = sample->temperature.mkelvin;
338                 if (!mkelvin)
339                         mkelvin = begins;
340                 line_to(gc, sample->time.seconds, mkelvin);
341                 begins = mkelvin;
342         }
343         cairo_stroke(cr);
344 }
345
346 /* gets both the actual start and end pressure as well as the scaling factors */
347 static int get_cylinder_pressure_range(struct dive *dive, struct graphics_context *gc,
348         pressure_t *startp, pressure_t *endp)
349 {
350         int i;
351         int min, max;
352
353         gc->leftx = 0; gc->rightx = round_seconds_up(dive->duration.seconds);
354
355         max = 0;
356         min = 5000000;
357         if (startp)
358                 startp->mbar = endp->mbar = 0;
359
360         for (i = 0; i < dive->samples; i++) {
361                 int mbar;
362                 struct sample *sample = dive->sample + i;
363
364                 /* FIXME! We only track cylinder 0 right now */
365                 if (sample->cylinderindex)
366                         continue;
367                 mbar = sample->cylinderpressure.mbar;
368                 if (!mbar)
369                         continue;
370                 if (mbar < min)
371                         min = mbar;
372                 if (mbar > max)
373                         max = mbar;
374         }
375         if (startp)
376                 startp->mbar = max;
377         if (endp)
378                 endp->mbar = min;
379         if (!max)
380                 return 0;
381         gc->topy = 0; gc->bottomy = max * 1.5;
382         return 1;
383 }
384
385 static void plot_cylinder_pressure(struct dive *dive, struct graphics_context *gc)
386 {
387         int i, sec = -1;
388
389         if (!get_cylinder_pressure_range(dive, gc, NULL, NULL))
390                 return;
391
392         cairo_set_source_rgba(gc->cr, 0.2, 1.0, 0.2, 0.80);
393
394         move_to(gc, 0, dive->cylinder[0].start.mbar);
395         for (i = 1; i < dive->samples; i++) {
396                 int mbar;
397                 struct sample *sample = dive->sample + i;
398
399                 mbar = sample->cylinderpressure.mbar;
400                 if (!mbar)
401                         continue;
402                 sec = sample->time.seconds;
403                 if (sec <= dive->duration.seconds)
404                         line_to(gc, sec, mbar);
405         }
406         /*
407          * We may have "surface time" events, in which case we don't go
408          * back to dive duration
409          */
410         if (sec < dive->duration.seconds)
411                 line_to(gc, dive->duration.seconds, dive->cylinder[0].end.mbar);
412         cairo_stroke(gc->cr);
413 }
414
415 /*
416  * Return air usage (in liters).
417  */
418 static double calculate_airuse(struct dive *dive)
419 {
420         double airuse = 0;
421         int i;
422
423         for (i = 0; i < MAX_CYLINDERS; i++) {
424                 cylinder_t *cyl = dive->cylinder + i;
425                 int size = cyl->type.size.mliter;
426                 double kilo_atm;
427
428                 if (!size)
429                         continue;
430
431                 kilo_atm = (cyl->start.mbar - cyl->end.mbar) / 1013250.0;
432
433                 /* Liters of air at 1 atm == milliliters at 1k atm*/
434                 airuse += kilo_atm * size;
435         }
436         return airuse;
437 }
438
439 static void plot_info(struct dive *dive, struct graphics_context *gc)
440 {
441         text_render_options_t tro = {10, 0.2, 1.0, 0.2, LEFT, TOP};
442         const double liters_per_cuft = 28.317;
443         const char *unit, *desc;
444         double airuse;
445
446         airuse = calculate_airuse(dive);
447         if (!airuse)
448                 return;
449
450         /* I really need to start addign some unit setting thing */
451         switch (output_units.volume) {
452         case LITER:
453                 unit = "l";
454                 break;
455         case CUFT:
456                 unit = "cuft";
457                 airuse /= liters_per_cuft;
458                 break;
459         }
460         plot_text(gc, &tro, 0.8, 0.8, "vol: %4.2f %s", airuse, unit);
461         if (dive->duration.seconds) {
462                 double pressure = 1 + (dive->meandepth.mm / 10000.0);
463                 double sac = airuse / pressure * 60 / dive->duration.seconds;
464                 plot_text(gc, &tro, 0.8, 0.85, "SAC: %4.2f %s/min", sac, unit);
465         }
466         desc = dive->cylinder[0].type.description;
467         if (desc || dive->cylinder[0].gasmix.o2.permille) {
468                 int o2 = dive->cylinder[0].gasmix.o2.permille / 10;
469                 if (!desc)
470                         desc = "";
471                 if (!o2)
472                         o2 = 21;
473                 plot_text(gc, &tro, 0.8, 0.9, "%s (%d%%)", desc, o2);
474         }
475 }
476
477 static void plot_cylinder_pressure_text(struct dive *dive, struct graphics_context *gc)
478 {
479         pressure_t startp, endp;
480
481         if (get_cylinder_pressure_range(dive, gc, &startp, &endp)) {
482                 int start, end;
483                 const char *unit = "bar";
484
485                 switch (output_units.pressure) {
486                 case PASCAL:
487                         start = startp.mbar * 100;
488                         end = startp.mbar * 100;
489                         unit = "pascal";
490                         break;
491                 case BAR:
492                         start = (startp.mbar + 500) / 1000;
493                         end = (endp.mbar + 500) / 1000;
494                         unit = "bar";
495                         break;
496                 case PSI:
497                         start = to_PSI(startp);
498                         end = to_PSI(endp);
499                         unit = "psi";
500                         break;
501                 }
502
503                 text_render_options_t tro = {10, 0.2, 1.0, 0.2, LEFT, TOP};
504                 plot_text(gc, &tro, 0, startp.mbar, "%d %s", start, unit);
505                 plot_text(gc, &tro, dive->duration.seconds, endp.mbar,
506                           "%d %s", end, unit);
507         }
508 }
509
510 static void analyze_plot_info_minmax_minute(struct plot_data *entry, struct plot_data *first, struct plot_data *last, int index)
511 {
512         struct plot_data *p = entry;
513         int time = entry->sec;
514         int seconds = 90*(index+1);
515         struct plot_data *min, *max;
516         int avg, nr;
517
518         /* Go back 'seconds' in time */
519         while (p > first) {
520                 if (p[-1].sec < time - seconds)
521                         break;
522                 p--;
523         }
524
525         /* Then go forward until we hit an entry past the time */
526         min = max = p;
527         avg = p->val;
528         nr = 1;
529         while (++p < last) {
530                 int val = p->val;
531                 if (p->sec > time + seconds)
532                         break;
533                 avg += val;
534                 nr ++;
535                 if (val < min->val)
536                         min = p;
537                 if (val > max->val)
538                         max = p;
539         }
540         entry->min[index] = min;
541         entry->max[index] = max;
542         entry->avg[index] = (avg + nr/2) / nr;
543 }
544
545 static void analyze_plot_info_minmax(struct plot_data *entry, struct plot_data *first, struct plot_data *last)
546 {
547         analyze_plot_info_minmax_minute(entry, first, last, 0);
548         analyze_plot_info_minmax_minute(entry, first, last, 1);
549         analyze_plot_info_minmax_minute(entry, first, last, 2);
550 }
551
552 static struct plot_info *analyze_plot_info(struct plot_info *pi)
553 {
554         int i;
555         int nr = pi->nr;
556
557         /* Smoothing function: 5-point triangular smooth */
558         for (i = 2; i < nr-2; i++) {
559                 struct plot_data *entry = pi->entry+i;
560                 int val;
561
562                 val = entry[-2].val + 2*entry[-1].val + 3*entry[0].val + 2*entry[1].val + entry[2].val;
563                 entry->smoothed = (val+4) / 9;
564         }
565
566         /* One-, two- and three-minute minmax data */
567         for (i = 0; i < nr; i++) {
568                 struct plot_data *entry = pi->entry +i;
569                 analyze_plot_info_minmax(entry, pi->entry, pi->entry+nr);
570         }
571         
572         return pi;
573 }
574
575 /*
576  * Create a plot-info with smoothing and ranged min/max
577  *
578  * This also makes sure that we have extra empty events on both
579  * sides, so that you can do end-points without having to worry
580  * about it.
581  */
582 static struct plot_info *depth_plot_info(struct dive *dive)
583 {
584         int i, nr = dive->samples + 4, sec;
585         size_t alloc_size = plot_info_size(nr);
586         struct plot_info *pi;
587
588         pi = malloc(alloc_size);
589         if (!pi)
590                 return pi;
591         memset(pi, 0, alloc_size);
592         pi->nr = nr;
593         sec = 0;
594         for (i = 0; i < dive->samples; i++) {
595                 struct sample *sample = dive->sample+i;
596                 struct plot_data *entry = pi->entry + i + 2;
597
598                 sec = entry->sec = sample->time.seconds;
599                 entry->val = sample->depth.mm;
600         }
601         /* Fill in the last two entries with empty values but valid times */
602         i = dive->samples + 2;
603         pi->entry[i].sec = sec + 20;
604         pi->entry[i+1].sec = sec + 40;
605
606         return analyze_plot_info(pi);
607 }
608
609 static void plot(struct graphics_context *gc, int w, int h, struct dive *dive)
610 {
611         double topx, topy;
612         struct plot_info *pi = depth_plot_info(dive);
613
614         topx = w / 20.0;
615         topy = h / 20.0;
616         cairo_translate(gc->cr, topx, topy);
617
618         /*
619          * We can use "cairo_translate()" because that doesn't
620          * scale line width etc. But the actual scaling we need
621          * do set up ourselves..
622          *
623          * Snif. What a pity.
624          */
625         gc->maxx = (w - 2*topx);
626         gc->maxy = (h - 2*topy);
627
628         /* Cylinder pressure plot */
629         plot_cylinder_pressure(dive, gc);
630
631         /* Depth profile */
632         plot_depth_profile(dive, gc, pi);
633
634         /* Temperature profile */
635         plot_temperature_profile(dive, gc);
636
637         /* Text on top of all graphs.. */
638         plot_depth_text(dive, gc, pi);
639         plot_cylinder_pressure_text(dive, gc);
640
641         /* And info box in the lower right corner.. */
642         gc->leftx = 0; gc->rightx = 1.0;
643         gc->topy = 0; gc->bottomy = 1.0;
644         plot_info(dive, gc);
645
646         /* Bounding box last */
647         cairo_set_source_rgb(gc->cr, 1, 1, 1);
648         move_to(gc, 0, 0);
649         line_to(gc, 0, 1);
650         line_to(gc, 1, 1);
651         line_to(gc, 1, 0);
652         cairo_close_path(gc->cr);
653         cairo_stroke(gc->cr);
654
655 }
656
657 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
658 {
659         struct dive *dive = current_dive;
660         struct graphics_context gc;
661         int w,h;
662
663         w = widget->allocation.width;
664         h = widget->allocation.height;
665
666         gc.cr = gdk_cairo_create(widget->window);
667         cairo_set_source_rgb(gc.cr, 0, 0, 0);
668         cairo_paint(gc.cr);
669
670         if (dive)
671                 plot(&gc, w, h, dive);
672
673         cairo_destroy(gc.cr);
674
675         return FALSE;
676 }
677
678 GtkWidget *dive_profile_widget(void)
679 {
680         GtkWidget *da;
681
682         da = gtk_drawing_area_new();
683         gtk_widget_set_size_request(da, 350, 250);
684         g_signal_connect(da, "expose_event", G_CALLBACK(expose_event), NULL);
685
686         return da;
687 }