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