]> git.tdb.fi Git - ext/subsurface.git/blob - profile.c
7c72d0b8c2b8be4aa50670f328127dbdb2cacda2
[ext/subsurface.git] / profile.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <stdarg.h>
4 #include <time.h>
5
6 #include "dive.h"
7 #include "display.h"
8 #include "divelist.h"
9
10 int selected_dive = 0;
11
12 #define ROUND_UP(x,y) ((((x)+(y)-1)/(y))*(y))
13
14 /*
15  * When showing dive profiles, we scale things to the
16  * current dive. However, we don't scale past less than
17  * 30 minutes or 90 ft, just so that small dives show
18  * up as such.
19  */
20 static int round_seconds_up(int seconds)
21 {
22         return MAX(30*60, ROUND_UP(seconds, 60*10));
23 }
24
25 static int round_depth_up(depth_t depth)
26 {
27         unsigned mm = depth.mm;
28         /* Minimum 30m */
29         return MAX(30000, ROUND_UP(mm+3000, 10000));
30 }
31
32 typedef struct {
33         double r,g,b;
34         enum {CENTER,LEFT} allign;
35 } text_render_options_t;
36
37 static void plot_text(cairo_t *cr, text_render_options_t *tro,
38                       double x, double y, const char *fmt, ...)
39 {
40         cairo_text_extents_t extents;
41         char buffer[80];
42         va_list args;
43
44         va_start(args, fmt);
45         vsnprintf(buffer, sizeof(buffer), fmt, args);
46         va_end(args);
47
48         cairo_text_extents(cr, buffer, &extents);
49
50         if (tro->allign == CENTER)
51                 x -= extents.width/2 + extents.x_bearing;
52         y += extents.height * 1.2;
53
54         cairo_move_to(cr, x, y);
55         cairo_text_path(cr, buffer);
56         cairo_set_source_rgb(cr, 0, 0, 0);
57         cairo_stroke(cr);
58
59         cairo_move_to(cr, x, y);
60         cairo_set_source_rgb(cr, tro->r, tro->g, tro->b);
61         cairo_show_text(cr, buffer);
62 }
63
64 /*
65  * Find the next maximum point in a 10-minute window.
66  *
67  * We exit early if we hit "enough" of a depth reversal,
68  * which is roughly 10 feet.
69  */
70 static struct sample *next_minmax(struct sample *sample, struct sample *end, int minmax)
71 {
72         const int enough = 3000;
73         struct sample *result;
74         int timelimit, depthlimit;
75
76         if (sample >= end)
77                 return 0;
78
79         timelimit = 24*60*60;
80         depthlimit = sample->depth.mm;
81         result = NULL;
82
83         for (;;) {
84                 int time, depth;
85
86                 sample++;
87                 if (sample >= end)
88                         return NULL;
89                 time = sample->time.seconds;
90                 depth = sample->depth.mm;
91                 if (time > timelimit)
92                         break;
93
94                 if (minmax) {
95                         if (depth <= depthlimit) {
96                                 if (depthlimit - depth > enough)
97                                         break;
98                                 continue;
99                         }
100                 } else {
101                         if (depth >= depthlimit) {
102                                 if (depth - depthlimit > enough)
103                                         break;
104                                 continue;
105                         }
106                 }
107
108                 result = sample;
109                 depthlimit = depth;
110                 /* Look up to ten minutes into the future */
111                 timelimit = time + 600;
112         }
113         return result;
114 }
115
116 /* Scale to 0,0 -> maxx,maxy */
117 #define SCALE(x,y) (x)*maxx/scalex,(y)*maxy/scaley
118
119 void plot_text_samples(struct dive *dive, cairo_t *cr,
120                         double maxx, double maxy,
121                         double scalex, double scaley,
122                         struct sample *a, struct sample *b)
123 {
124         struct sample *max, *min;
125
126         if (b < a)
127                 return;
128         if (b->time.seconds - a->time.seconds < 3*60)
129                 return;
130
131         max = next_minmax(a, b, 1);
132         if (max) {
133                 text_render_options_t tro = {1.0, 0.2, 0.2, CENTER};
134                 int sec = max->time.seconds;
135                 depth_t depth = max->depth;
136                 const char *fmt;
137                 double d;
138
139                 min = next_minmax(max, b, 0);
140                 plot_text_samples(dive, cr, maxx, maxy, scalex, scaley, a, max);
141                 if (min) {
142                         plot_text_samples(dive, cr, maxx, maxy, scalex, scaley, max, min);
143                         plot_text_samples(dive, cr, maxx, maxy, scalex, scaley, min, b);
144                 } else
145                         plot_text_samples(dive, cr, maxx, maxy, scalex, scaley, max, b);
146
147                 switch (output_units.length) {
148                 case METERS:
149                         d = depth.mm / 1000.0;
150                         fmt = "%.1f";
151                         break;
152                 case FEET:
153                         d = to_feet(depth);
154                         fmt = "%.0f";
155                         break;
156                 }
157
158                 plot_text(cr, &tro, SCALE(sec, depth.mm), fmt, d);
159                 return;
160         }
161 }
162
163 static void plot_depth_text(struct dive *dive, cairo_t *cr,
164         double maxx, double maxy)
165 {
166         struct sample *sample, *end;
167         double scalex, scaley;
168         int maxtime, maxdepth;
169
170         /* Get plot scaling limits */
171         maxtime = round_seconds_up(dive->duration.seconds);
172         maxdepth = round_depth_up(dive->maxdepth);
173
174         scalex = maxtime;
175         scaley = maxdepth;
176
177         cairo_set_font_size(cr, 14);
178         cairo_set_source_rgb(cr, 1, 0.2, 0.2);
179
180         /*
181          * We never take the last sample into account.
182          * It should be a surface event anyway, although
183          * there are buggy cases where it isn't..
184          */
185         sample = dive->sample;
186         end = dive->sample + dive->samples - 1;
187
188         plot_text_samples(dive, cr, maxx, maxy, scalex, scaley, sample, end);
189 }
190
191 static void plot_depth_profile(struct dive *dive, cairo_t *cr,
192         double maxx, double maxy)
193 {
194         double scalex, scaley;
195         int begins, sec, depth;
196         int i, samples;
197         struct sample *sample;
198         int maxtime, maxdepth, marker;
199
200         samples = dive->samples;
201         if (!samples)
202                 return;
203
204         cairo_set_line_width(cr, 2);
205
206         /* Get plot scaling limits */
207         maxtime = round_seconds_up(dive->duration.seconds);
208         maxdepth = round_depth_up(dive->maxdepth);
209
210         /* Time markers: every 5 min */
211         scalex = maxtime;
212         scaley = 1.0;
213         for (i = 5*60; i < maxtime; i += 5*60) {
214                 cairo_move_to(cr, SCALE(i, 0));
215                 cairo_line_to(cr, SCALE(i, 1));
216         }
217
218         /* Depth markers: every 30 ft or 10 m*/
219         scalex = 1.0;
220         scaley = maxdepth;
221         switch (output_units.length) {
222         case METERS: marker = 10000; break;
223         case FEET: marker = 9144; break;        /* 30 ft */
224         }
225
226         cairo_set_source_rgba(cr, 1, 1, 1, 0.5);
227         for (i = marker; i < maxdepth; i += marker) {
228                 cairo_move_to(cr, SCALE(0, i));
229                 cairo_line_to(cr, SCALE(1, i));
230         }
231         cairo_stroke(cr);
232
233         /* Show mean depth */
234         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.40);
235         cairo_move_to(cr, SCALE(0, dive->meandepth.mm));
236         cairo_line_to(cr, SCALE(1, dive->meandepth.mm));
237         cairo_stroke(cr);
238
239         scalex = maxtime;
240
241         sample = dive->sample;
242         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
243         begins = sample->time.seconds;
244         cairo_move_to(cr, SCALE(sample->time.seconds, sample->depth.mm));
245         for (i = 1; i < dive->samples; i++) {
246                 sample++;
247                 sec = sample->time.seconds;
248                 if (sec <= maxtime) {
249                         depth = sample->depth.mm;
250                         cairo_line_to(cr, SCALE(sec, depth));
251                 }
252         }
253         scaley = 1.0;
254         cairo_line_to(cr, SCALE(MIN(sec,maxtime), 0));
255         cairo_line_to(cr, SCALE(begins, 0));
256         cairo_close_path(cr);
257         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.20);
258         cairo_fill_preserve(cr);
259         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
260         cairo_stroke(cr);
261 }
262
263 /* gets both the actual start and end pressure as well as the scaling factors */
264 static int get_cylinder_pressure_range(struct dive *dive, double *scalex, double *scaley,
265                                        pressure_t *startp, pressure_t *endp)
266 {
267         int i;
268         int min, max;
269
270         *scalex = round_seconds_up(dive->duration.seconds);
271
272         max = 0;
273         min = 5000000;
274         if (startp)
275                 startp->mbar = endp->mbar = 0;
276
277         for (i = 0; i < dive->samples; i++) {
278                 int mbar;
279                 struct sample *sample = dive->sample + i;
280
281                 /* FIXME! We only track cylinder 0 right now */
282                 if (sample->cylinderindex)
283                         continue;
284                 mbar = sample->cylinderpressure.mbar;
285                 if (!mbar)
286                         continue;
287                 if (mbar < min)
288                         min = mbar;
289                 if (mbar > max)
290                         max = mbar;
291         }
292         if (startp)
293                 startp->mbar = max;
294         if (endp)
295                 endp->mbar = min;
296         if (!max)
297                 return 0;
298         *scaley = max * 1.5;
299         return 1;
300 }
301
302 static void plot_cylinder_pressure(struct dive *dive, cairo_t *cr,
303         double maxx, double maxy)
304 {
305         int i, sec = -1;
306         double scalex, scaley;
307
308         if (!get_cylinder_pressure_range(dive, &scalex, &scaley, NULL, NULL))
309                 return;
310
311         cairo_set_source_rgba(cr, 0.2, 1.0, 0.2, 0.80);
312
313         cairo_move_to(cr, SCALE(0, dive->cylinder[0].start.mbar));
314         for (i = 1; i < dive->samples; i++) {
315                 int mbar;
316                 struct sample *sample = dive->sample + i;
317
318                 mbar = sample->cylinderpressure.mbar;
319                 if (!mbar)
320                         continue;
321                 sec = sample->time.seconds;
322                 if (sec <= dive->duration.seconds)
323                         cairo_line_to(cr, SCALE(sec, mbar));
324         }
325         /*
326          * We may have "surface time" events, in which case we don't go
327          * back to dive duration
328          */
329         if (sec < dive->duration.seconds)
330                 cairo_line_to(cr, SCALE(dive->duration.seconds, dive->cylinder[0].end.mbar));
331         cairo_stroke(cr);
332 }
333
334 /*
335  * Return air usage (in liters).
336  */
337 static double calculate_airuse(struct dive *dive)
338 {
339         double airuse = 0;
340         int i;
341
342         for (i = 0; i < MAX_CYLINDERS; i++) {
343                 cylinder_t *cyl = dive->cylinder + i;
344                 int size = cyl->type.size.mliter;
345                 double kilo_atm;
346
347                 if (!size)
348                         continue;
349
350                 kilo_atm = (cyl->start.mbar - cyl->end.mbar) / 1013250.0;
351
352                 /* Liters of air at 1 atm == milliliters at 1k atm*/
353                 airuse += kilo_atm * size;
354         }
355         return airuse;
356 }
357
358 static void plot_info(struct dive *dive, cairo_t *cr,
359         double maxx, double maxy)
360 {
361         text_render_options_t tro = {0.2, 1.0, 0.2, LEFT};
362         const double liters_per_cuft = 28.317;
363         const char *unit;
364         double airuse;
365
366         airuse = calculate_airuse(dive);
367         if (!airuse)
368                 return;
369
370         /* I really need to start addign some unit setting thing */
371         switch (output_units.volume) {
372         case LITER:
373                 unit = "l";
374                 break;
375         case CUFT:
376                 unit = "cuft";
377                 airuse /= liters_per_cuft;
378                 break;
379         }
380         plot_text(cr, &tro, maxx*0.8, maxy*0.8, "vol: %4.2f %s", airuse, unit);
381         if (dive->duration.seconds) {
382                 double pressure = 1 + (dive->meandepth.mm / 10000.0);
383                 double sac = airuse / pressure * 60 / dive->duration.seconds;
384                 plot_text(cr, &tro, maxx*0.8, maxy*0.85, "SAC: %4.2f %s/min", sac, unit);
385         }
386 }
387
388 static void plot_cylinder_pressure_text(struct dive *dive, cairo_t *cr,
389         double maxx, double maxy)
390 {
391         double scalex, scaley;
392         pressure_t startp, endp;
393
394         cairo_set_font_size(cr, 10);
395
396         if (get_cylinder_pressure_range(dive, &scalex, &scaley,
397                                         &startp, &endp)) {
398                 int start, end;
399                 const char *unit = "bar";
400
401                 switch (output_units.pressure) {
402                 case PASCAL:
403                         start = startp.mbar * 100;
404                         end = startp.mbar * 100;
405                         unit = "pascal";
406                         break;
407                 case BAR:
408                         start = (startp.mbar + 500) / 1000;
409                         end = (endp.mbar + 500) / 1000;
410                         unit = "bar";
411                         break;
412                 case PSI:
413                         start = to_PSI(startp);
414                         end = to_PSI(endp);
415                         unit = "psi";
416                         break;
417                 }
418
419                 text_render_options_t tro = {0.2, 1.0, 0.2, LEFT};
420                 plot_text(cr, &tro, SCALE(0, startp.mbar), "%d %s", start, unit);
421                 plot_text(cr, &tro, SCALE(dive->duration.seconds, endp.mbar),
422                           "%d %s", end, unit);
423         }
424 }
425
426 static void plot(cairo_t *cr, int w, int h, struct dive *dive)
427 {
428         double topx, topy, maxx, maxy;
429         double scalex, scaley;
430
431         topx = w / 20.0;
432         topy = h / 20.0;
433         maxx = (w - 2*topx);
434         maxy = (h - 2*topy);
435         cairo_translate(cr, topx, topy);
436
437         /* Cylinder pressure plot */
438         plot_cylinder_pressure(dive, cr, maxx, maxy);
439
440         /* Depth profile */
441         plot_depth_profile(dive, cr, maxx, maxy);
442
443         /* Text on top of all graphs.. */
444         plot_depth_text(dive, cr, maxx, maxy);
445         plot_cylinder_pressure_text(dive, cr, maxx, maxy);
446
447         /* And info box in the lower right corner.. */
448         plot_info(dive, cr, maxx, maxy);
449
450         /* Bounding box last */
451         scalex = scaley = 1.0;
452         cairo_set_source_rgb(cr, 1, 1, 1);
453         cairo_move_to(cr, SCALE(0,0));
454         cairo_line_to(cr, SCALE(0,1));
455         cairo_line_to(cr, SCALE(1,1));
456         cairo_line_to(cr, SCALE(1,0));
457         cairo_close_path(cr);
458         cairo_stroke(cr);
459
460 }
461
462 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
463 {
464         struct dive *dive = current_dive;
465         cairo_t *cr;
466         int w,h;
467
468         w = widget->allocation.width;
469         h = widget->allocation.height;
470
471         cr = gdk_cairo_create(widget->window);
472         cairo_set_source_rgb(cr, 0, 0, 0);
473         cairo_paint(cr);
474
475         if (dive)
476                 plot(cr, w, h, dive);
477
478         cairo_destroy(cr);
479
480         return FALSE;
481 }
482
483 GtkWidget *dive_profile_widget(void)
484 {
485         GtkWidget *da;
486
487         da = gtk_drawing_area_new();
488         gtk_widget_set_size_request(da, 450, 350);
489         g_signal_connect(da, "expose_event", G_CALLBACK(expose_event), NULL);
490
491         return da;
492 }