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