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