]> git.tdb.fi Git - ext/subsurface.git/blob - profile.c
Add a notebook for cylinder information
[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;
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 }
418
419 static void plot_cylinder_pressure_text(struct dive *dive, struct graphics_context *gc)
420 {
421         pressure_t startp, endp;
422
423         if (get_cylinder_pressure_range(dive, gc, &startp, &endp)) {
424                 int start, end;
425                 const char *unit = "bar";
426
427                 switch (output_units.pressure) {
428                 case PASCAL:
429                         start = startp.mbar * 100;
430                         end = startp.mbar * 100;
431                         unit = "pascal";
432                         break;
433                 case BAR:
434                         start = (startp.mbar + 500) / 1000;
435                         end = (endp.mbar + 500) / 1000;
436                         unit = "bar";
437                         break;
438                 case PSI:
439                         start = to_PSI(startp);
440                         end = to_PSI(endp);
441                         unit = "psi";
442                         break;
443                 }
444
445                 text_render_options_t tro = {10, 0.2, 1.0, 0.2, LEFT, TOP};
446                 plot_text(gc, &tro, 0, startp.mbar, "%d %s", start, unit);
447                 plot_text(gc, &tro, dive->duration.seconds, endp.mbar,
448                           "%d %s", end, unit);
449         }
450 }
451
452 static void analyze_plot_info_minmax_minute(struct plot_data *entry, struct plot_data *first, struct plot_data *last, int index)
453 {
454         struct plot_data *p = entry;
455         int time = entry->sec;
456         int seconds = 90*(index+1);
457         struct plot_data *min, *max;
458         int avg, nr;
459
460         /* Go back 'seconds' in time */
461         while (p > first) {
462                 if (p[-1].sec < time - seconds)
463                         break;
464                 p--;
465         }
466
467         /* Then go forward until we hit an entry past the time */
468         min = max = p;
469         avg = p->val;
470         nr = 1;
471         while (++p < last) {
472                 int val = p->val;
473                 if (p->sec > time + seconds)
474                         break;
475                 avg += val;
476                 nr ++;
477                 if (val < min->val)
478                         min = p;
479                 if (val > max->val)
480                         max = p;
481         }
482         entry->min[index] = min;
483         entry->max[index] = max;
484         entry->avg[index] = (avg + nr/2) / nr;
485 }
486
487 static void analyze_plot_info_minmax(struct plot_data *entry, struct plot_data *first, struct plot_data *last)
488 {
489         analyze_plot_info_minmax_minute(entry, first, last, 0);
490         analyze_plot_info_minmax_minute(entry, first, last, 1);
491         analyze_plot_info_minmax_minute(entry, first, last, 2);
492 }
493
494 static struct plot_info *analyze_plot_info(struct plot_info *pi)
495 {
496         int i;
497         int nr = pi->nr;
498
499         /* Smoothing function: 5-point triangular smooth */
500         for (i = 2; i < nr-2; i++) {
501                 struct plot_data *entry = pi->entry+i;
502                 int val;
503
504                 val = entry[-2].val + 2*entry[-1].val + 3*entry[0].val + 2*entry[1].val + entry[2].val;
505                 entry->smoothed = (val+4) / 9;
506         }
507
508         /* One-, two- and three-minute minmax data */
509         for (i = 0; i < nr; i++) {
510                 struct plot_data *entry = pi->entry +i;
511                 analyze_plot_info_minmax(entry, pi->entry, pi->entry+nr);
512         }
513         
514         return pi;
515 }
516
517 /*
518  * Create a plot-info with smoothing and ranged min/max
519  *
520  * This also makes sure that we have extra empty events on both
521  * sides, so that you can do end-points without having to worry
522  * about it.
523  */
524 static struct plot_info *depth_plot_info(struct dive *dive)
525 {
526         int i, nr = dive->samples + 4, sec;
527         size_t alloc_size = plot_info_size(nr);
528         struct plot_info *pi;
529
530         pi = malloc(alloc_size);
531         if (!pi)
532                 return pi;
533         memset(pi, 0, alloc_size);
534         pi->nr = nr;
535         sec = 0;
536         for (i = 0; i < dive->samples; i++) {
537                 struct sample *sample = dive->sample+i;
538                 struct plot_data *entry = pi->entry + i + 2;
539
540                 sec = entry->sec = sample->time.seconds;
541                 entry->val = sample->depth.mm;
542         }
543         /* Fill in the last two entries with empty values but valid times */
544         i = dive->samples + 2;
545         pi->entry[i].sec = sec + 20;
546         pi->entry[i+1].sec = sec + 40;
547
548         return analyze_plot_info(pi);
549 }
550
551 static void plot(struct graphics_context *gc, int w, int h, struct dive *dive)
552 {
553         double topx, topy;
554         struct plot_info *pi = depth_plot_info(dive);
555
556         topx = w / 20.0;
557         topy = h / 20.0;
558         cairo_translate(gc->cr, topx, topy);
559
560         /*
561          * We can use "cairo_translate()" because that doesn't
562          * scale line width etc. But the actual scaling we need
563          * do set up ourselves..
564          *
565          * Snif. What a pity.
566          */
567         gc->maxx = (w - 2*topx);
568         gc->maxy = (h - 2*topy);
569
570         /* Cylinder pressure plot */
571         plot_cylinder_pressure(dive, gc);
572
573         /* Depth profile */
574         plot_depth_profile(dive, gc, pi);
575
576         /* Text on top of all graphs.. */
577         plot_depth_text(dive, gc, pi);
578         plot_cylinder_pressure_text(dive, gc);
579
580         /* And info box in the lower right corner.. */
581         gc->scalex = gc->scaley = 1.0;
582         plot_info(dive, gc);
583
584         /* Bounding box last */
585         cairo_set_source_rgb(gc->cr, 1, 1, 1);
586         move_to(gc, 0, 0);
587         line_to(gc, 0, 1);
588         line_to(gc, 1, 1);
589         line_to(gc, 1, 0);
590         cairo_close_path(gc->cr);
591         cairo_stroke(gc->cr);
592
593 }
594
595 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
596 {
597         struct dive *dive = current_dive;
598         struct graphics_context gc;
599         int w,h;
600
601         w = widget->allocation.width;
602         h = widget->allocation.height;
603
604         gc.cr = gdk_cairo_create(widget->window);
605         cairo_set_source_rgb(gc.cr, 0, 0, 0);
606         cairo_paint(gc.cr);
607
608         if (dive)
609                 plot(&gc, w, h, dive);
610
611         cairo_destroy(gc.cr);
612
613         return FALSE;
614 }
615
616 GtkWidget *dive_profile_widget(void)
617 {
618         GtkWidget *da;
619
620         da = gtk_drawing_area_new();
621         gtk_widget_set_size_request(da, 450, 350);
622         g_signal_connect(da, "expose_event", G_CALLBACK(expose_event), NULL);
623
624         return da;
625 }