]> git.tdb.fi Git - ext/subsurface.git/blob - profile.c
Start analyzing depth profile: smoothing and time-based min/max/avg
[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                 int min[3];
36                 int 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 sample *sample, const text_render_options_t *tro)
133 {
134         int sec = sample->time.seconds;
135         depth_t depth = sample->depth;
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 /*
153  * Find the next minimum/maximum point.
154  *
155  * We exit early if we hit "enough" of a depth reversal,
156  * which is roughly 10 feet.
157  */
158 static struct sample *next_minmax(struct sample *sample, struct sample *end, int minmax)
159 {
160         const int enough = 3000;
161         struct sample *result;
162         int depthlimit;
163
164         if (sample >= end)
165                 return 0;
166
167         depthlimit = sample->depth.mm;
168         result = NULL;
169
170         for (;;) {
171                 int depth;
172
173                 sample++;
174                 if (sample >= end)
175                         return NULL;
176                 depth = sample->depth.mm;
177
178                 if (minmax) {
179                         if (depth <= depthlimit) {
180                                 if (depthlimit - depth > enough)
181                                         break;
182                                 continue;
183                         }
184                 } else {
185                         if (depth >= depthlimit) {
186                                 if (depth - depthlimit > enough)
187                                         break;
188                                 continue;
189                         }
190                 }
191
192                 result = sample;
193                 depthlimit = depth;
194         }
195         return result;
196 }
197
198 static void plot_text_samples(struct graphics_context *gc, struct sample *a, struct sample *b)
199 {
200         static const text_render_options_t deep = {14, 1.0, 0.2, 0.2, CENTER, TOP};
201         static const text_render_options_t shallow = {14, 1.0, 0.2, 0.2, CENTER, BOTTOM};
202
203         for (;;) {
204                 if (b <= a)
205                         break;
206                 a = next_minmax(a, b, 1);
207                 if (!a)
208                         break;
209                 render_depth_sample(gc, a, &deep);
210                 a = next_minmax(a, b, 0);
211                 if (!a)
212                         break;
213                 if (a->depth.mm < 2500)
214                         continue;
215                 render_depth_sample(gc, a, &shallow);
216         }
217 }
218
219 static void plot_depth_text(struct dive *dive, struct graphics_context *gc)
220 {
221         struct sample *sample, *end;
222         int maxtime, maxdepth;
223
224         /* Get plot scaling limits */
225         maxtime = round_seconds_up(dive->duration.seconds);
226         maxdepth = round_depth_up(dive->maxdepth);
227
228         gc->scalex = maxtime;
229         gc->scaley = maxdepth;
230
231         sample = dive->sample;
232         end = dive->sample + dive->samples;
233
234         plot_text_samples(gc, sample, end);
235 }
236
237 static void plot_depth_profile(struct dive *dive, struct graphics_context *gc, struct plot_info *pi)
238 {
239         int i;
240         cairo_t *cr = gc->cr;
241         int begins, sec, depth;
242         struct plot_data *entry;
243         int maxtime, maxdepth, marker;
244
245         cairo_set_line_width(gc->cr, 2);
246
247         /* Get plot scaling limits */
248         maxtime = round_seconds_up(dive->duration.seconds);
249         maxdepth = round_depth_up(dive->maxdepth);
250
251         /* Time markers: every 5 min */
252         gc->scalex = maxtime;
253         gc->scaley = 1.0;
254         for (i = 5*60; i < maxtime; i += 5*60) {
255                 move_to(gc, i, 0);
256                 line_to(gc, i, 1);
257         }
258
259         /* Depth markers: every 30 ft or 10 m*/
260         gc->scalex = 1.0;
261         gc->scaley = maxdepth;
262         switch (output_units.length) {
263         case METERS: marker = 10000; break;
264         case FEET: marker = 9144; break;        /* 30 ft */
265         }
266
267         cairo_set_source_rgba(cr, 1, 1, 1, 0.5);
268         for (i = marker; i < maxdepth; i += marker) {
269                 move_to(gc, 0, i);
270                 line_to(gc, 1, i);
271         }
272         cairo_stroke(cr);
273
274         /* Show mean depth */
275         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.40);
276         move_to(gc, 0, dive->meandepth.mm);
277         line_to(gc, 1, dive->meandepth.mm);
278         cairo_stroke(cr);
279
280         gc->scalex = maxtime;
281
282         entry = pi->entry;
283         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
284         begins = entry->sec;
285         move_to(gc, entry->sec, entry->val);
286         for (i = 1; i < pi->nr; i++) {
287                 entry++;
288                 sec = entry->sec;
289                 if (sec <= maxtime) {
290                         depth = entry->val;
291                         line_to(gc, sec, depth);
292                 }
293         }
294         gc->scaley = 1.0;
295         line_to(gc, MIN(sec,maxtime), 0);
296         line_to(gc, begins, 0);
297         cairo_close_path(cr);
298         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.20);
299         cairo_fill_preserve(cr);
300         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
301         cairo_stroke(cr);
302 }
303
304 /* gets both the actual start and end pressure as well as the scaling factors */
305 static int get_cylinder_pressure_range(struct dive *dive, struct graphics_context *gc,
306         pressure_t *startp, pressure_t *endp)
307 {
308         int i;
309         int min, max;
310
311         gc->scalex = round_seconds_up(dive->duration.seconds);
312
313         max = 0;
314         min = 5000000;
315         if (startp)
316                 startp->mbar = endp->mbar = 0;
317
318         for (i = 0; i < dive->samples; i++) {
319                 int mbar;
320                 struct sample *sample = dive->sample + i;
321
322                 /* FIXME! We only track cylinder 0 right now */
323                 if (sample->cylinderindex)
324                         continue;
325                 mbar = sample->cylinderpressure.mbar;
326                 if (!mbar)
327                         continue;
328                 if (mbar < min)
329                         min = mbar;
330                 if (mbar > max)
331                         max = mbar;
332         }
333         if (startp)
334                 startp->mbar = max;
335         if (endp)
336                 endp->mbar = min;
337         if (!max)
338                 return 0;
339         gc->scaley = max * 1.5;
340         return 1;
341 }
342
343 static void plot_cylinder_pressure(struct dive *dive, struct graphics_context *gc)
344 {
345         int i, sec = -1;
346
347         if (!get_cylinder_pressure_range(dive, gc, NULL, NULL))
348                 return;
349
350         cairo_set_source_rgba(gc->cr, 0.2, 1.0, 0.2, 0.80);
351
352         move_to(gc, 0, dive->cylinder[0].start.mbar);
353         for (i = 1; i < dive->samples; i++) {
354                 int mbar;
355                 struct sample *sample = dive->sample + i;
356
357                 mbar = sample->cylinderpressure.mbar;
358                 if (!mbar)
359                         continue;
360                 sec = sample->time.seconds;
361                 if (sec <= dive->duration.seconds)
362                         line_to(gc, sec, mbar);
363         }
364         /*
365          * We may have "surface time" events, in which case we don't go
366          * back to dive duration
367          */
368         if (sec < dive->duration.seconds)
369                 line_to(gc, dive->duration.seconds, dive->cylinder[0].end.mbar);
370         cairo_stroke(gc->cr);
371 }
372
373 /*
374  * Return air usage (in liters).
375  */
376 static double calculate_airuse(struct dive *dive)
377 {
378         double airuse = 0;
379         int i;
380
381         for (i = 0; i < MAX_CYLINDERS; i++) {
382                 cylinder_t *cyl = dive->cylinder + i;
383                 int size = cyl->type.size.mliter;
384                 double kilo_atm;
385
386                 if (!size)
387                         continue;
388
389                 kilo_atm = (cyl->start.mbar - cyl->end.mbar) / 1013250.0;
390
391                 /* Liters of air at 1 atm == milliliters at 1k atm*/
392                 airuse += kilo_atm * size;
393         }
394         return airuse;
395 }
396
397 static void plot_info(struct dive *dive, struct graphics_context *gc)
398 {
399         text_render_options_t tro = {10, 0.2, 1.0, 0.2, LEFT, TOP};
400         const double liters_per_cuft = 28.317;
401         const char *unit;
402         double airuse;
403
404         airuse = calculate_airuse(dive);
405         if (!airuse)
406                 return;
407
408         /* I really need to start addign some unit setting thing */
409         switch (output_units.volume) {
410         case LITER:
411                 unit = "l";
412                 break;
413         case CUFT:
414                 unit = "cuft";
415                 airuse /= liters_per_cuft;
416                 break;
417         }
418         plot_text(gc, &tro, 0.8, 0.8, "vol: %4.2f %s", airuse, unit);
419         if (dive->duration.seconds) {
420                 double pressure = 1 + (dive->meandepth.mm / 10000.0);
421                 double sac = airuse / pressure * 60 / dive->duration.seconds;
422                 plot_text(gc, &tro, 0.8, 0.85, "SAC: %4.2f %s/min", sac, unit);
423         }
424 }
425
426 static void plot_cylinder_pressure_text(struct dive *dive, struct graphics_context *gc)
427 {
428         pressure_t startp, endp;
429
430         if (get_cylinder_pressure_range(dive, gc, &startp, &endp)) {
431                 int start, end;
432                 const char *unit = "bar";
433
434                 switch (output_units.pressure) {
435                 case PASCAL:
436                         start = startp.mbar * 100;
437                         end = startp.mbar * 100;
438                         unit = "pascal";
439                         break;
440                 case BAR:
441                         start = (startp.mbar + 500) / 1000;
442                         end = (endp.mbar + 500) / 1000;
443                         unit = "bar";
444                         break;
445                 case PSI:
446                         start = to_PSI(startp);
447                         end = to_PSI(endp);
448                         unit = "psi";
449                         break;
450                 }
451
452                 text_render_options_t tro = {10, 0.2, 1.0, 0.2, LEFT, TOP};
453                 plot_text(gc, &tro, 0, startp.mbar, "%d %s", start, unit);
454                 plot_text(gc, &tro, dive->duration.seconds, endp.mbar,
455                           "%d %s", end, unit);
456         }
457 }
458
459 static void analyze_plot_info_minmax_minute(struct plot_data *entry, struct plot_data *first, struct plot_data *last, int index)
460 {
461         struct plot_data *p = entry;
462         int time = entry->sec;
463         int seconds = 60*(index+1);
464         int min, max, avg, nr;
465
466         /* Go back 'seconds' in time */
467         while (p > first) {
468                 if (p[-1].sec < time - seconds)
469                         break;
470                 p--;
471         }
472
473         /* Then go forward until we hit an entry past the time */
474         min = max = avg = p->val;
475         nr = 1;
476         while (++p < last) {
477                 int val = p->val;
478                 if (p->sec > time + seconds)
479                         break;
480                 avg += val;
481                 nr ++;
482                 if (val < min)
483                         min = val;
484                 if (val > max)
485                         max = val;
486         }
487         entry->min[index] = min;
488         entry->max[index] = max;
489         entry->avg[index] = (avg + nr/2) / nr;
490 }
491
492 static void analyze_plot_info_minmax(struct plot_data *entry, struct plot_data *first, struct plot_data *last)
493 {
494         analyze_plot_info_minmax_minute(entry, first, last, 0);
495         analyze_plot_info_minmax_minute(entry, first, last, 1);
496         analyze_plot_info_minmax_minute(entry, first, last, 2);
497 }
498
499 static struct plot_info *analyze_plot_info(struct plot_info *pi)
500 {
501         int i;
502         int nr = pi->nr;
503
504         /* Smoothing function: 5-point triangular smooth */
505         for (i = 2; i < nr-2; i++) {
506                 struct plot_data *entry = pi->entry+i;
507                 int val;
508
509                 val = entry[-2].val + 2*entry[-1].val + 3*entry[0].val + 2*entry[1].val + entry[2].val;
510                 entry->smoothed = (val+4) / 9;
511         }
512
513         /* One-, two- and three-minute minmax data */
514         for (i = 0; i < nr; i++) {
515                 struct plot_data *entry = pi->entry +i;
516                 analyze_plot_info_minmax(entry, pi->entry, pi->entry+nr);
517         }
518         
519         return pi;
520 }
521
522 /*
523  * Create a plot-info with smoothing and ranged min/max
524  *
525  * This also makes sure that we have extra empty events on both
526  * sides, so that you can do end-points without having to worry
527  * about it.
528  */
529 static struct plot_info *depth_plot_info(struct dive *dive)
530 {
531         int i, nr = dive->samples + 4, sec;
532         size_t alloc_size = plot_info_size(nr);
533         struct plot_info *pi;
534
535         pi = malloc(alloc_size);
536         if (!pi)
537                 return pi;
538         memset(pi, 0, alloc_size);
539         pi->nr = nr;
540         sec = 0;
541         for (i = 0; i < dive->samples; i++) {
542                 struct sample *sample = dive->sample+i;
543                 struct plot_data *entry = pi->entry + i + 2;
544
545                 sec = entry->sec = sample->time.seconds;
546                 entry->val = sample->depth.mm;
547         }
548         /* Fill in the last two entries with empty values but valid times */
549         i = dive->samples + 2;
550         pi->entry[i].sec = sec + 20;
551         pi->entry[i+1].sec = sec + 40;
552
553         return analyze_plot_info(pi);
554 }
555
556 static void plot(struct graphics_context *gc, int w, int h, struct dive *dive)
557 {
558         double topx, topy;
559         struct plot_info *pi = depth_plot_info(dive);
560
561         topx = w / 20.0;
562         topy = h / 20.0;
563         cairo_translate(gc->cr, topx, topy);
564
565         /*
566          * We can use "cairo_translate()" because that doesn't
567          * scale line width etc. But the actual scaling we need
568          * do set up ourselves..
569          *
570          * Snif. What a pity.
571          */
572         gc->maxx = (w - 2*topx);
573         gc->maxy = (h - 2*topy);
574
575         /* Cylinder pressure plot */
576         plot_cylinder_pressure(dive, gc);
577
578         /* Depth profile */
579         plot_depth_profile(dive, gc, pi);
580
581         /* Text on top of all graphs.. */
582         plot_depth_text(dive, gc);
583         plot_cylinder_pressure_text(dive, gc);
584
585         /* And info box in the lower right corner.. */
586         gc->scalex = gc->scaley = 1.0;
587         plot_info(dive, gc);
588
589         /* Bounding box last */
590         cairo_set_source_rgb(gc->cr, 1, 1, 1);
591         move_to(gc, 0, 0);
592         line_to(gc, 0, 1);
593         line_to(gc, 1, 1);
594         line_to(gc, 1, 0);
595         cairo_close_path(gc->cr);
596         cairo_stroke(gc->cr);
597
598 }
599
600 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
601 {
602         struct dive *dive = current_dive;
603         struct graphics_context gc;
604         int w,h;
605
606         w = widget->allocation.width;
607         h = widget->allocation.height;
608
609         gc.cr = gdk_cairo_create(widget->window);
610         cairo_set_source_rgb(gc.cr, 0, 0, 0);
611         cairo_paint(gc.cr);
612
613         if (dive)
614                 plot(&gc, w, h, dive);
615
616         cairo_destroy(gc.cr);
617
618         return FALSE;
619 }
620
621 GtkWidget *dive_profile_widget(void)
622 {
623         GtkWidget *da;
624
625         da = gtk_drawing_area_new();
626         gtk_widget_set_size_request(da, 450, 350);
627         g_signal_connect(da, "expose_event", G_CALLBACK(expose_event), NULL);
628
629         return da;
630 }