]> git.tdb.fi Git - ext/subsurface.git/blob - profile.c
Tweak depth next_minmax() interface
[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 dive *dive, 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 static void plot_depth_text(struct dive *dive, cairo_t *cr,
120         double maxx, double maxy)
121 {
122         struct sample *sample, *end;
123         double scalex, scaley;
124         int maxtime, maxdepth;
125
126         /* Get plot scaling limits */
127         maxtime = round_seconds_up(dive->duration.seconds);
128         maxdepth = round_depth_up(dive->maxdepth);
129
130         scalex = maxtime;
131         scaley = maxdepth;
132
133         cairo_set_font_size(cr, 14);
134         cairo_set_source_rgb(cr, 1, 0.2, 0.2);
135
136         /*
137          * We never take the last sample into account.
138          * It should be a surface event anyway, although
139          * there are buggy cases where it isn't..
140          */
141         sample = dive->sample;
142         end = dive->sample + dive->samples - 1;
143
144         while ((sample = next_minmax(dive, sample, end, 1)) != NULL) {
145                 text_render_options_t tro = {1.0, 0.2, 0.2, CENTER};
146                 int sec = sample->time.seconds;
147                 depth_t depth = sample->depth;
148                 const char *fmt;
149                 double d;
150
151                 switch (output_units.length) {
152                 case METERS:
153                         d = depth.mm / 1000.0;
154                         fmt = "%.1f";
155                         break;
156                 case FEET:
157                         d = to_feet(depth);
158                         fmt = "%.0f";
159                         break;
160                 }
161
162                 plot_text(cr, &tro, SCALE(sec, depth.mm), fmt, d);
163                 sample = next_minmax(dive, sample, end, 0);
164                 if (!sample)
165                         break;
166         }
167 }
168
169 static void plot_depth_profile(struct dive *dive, cairo_t *cr,
170         double maxx, double maxy)
171 {
172         double scalex, scaley;
173         int begins, sec, depth;
174         int i, samples;
175         struct sample *sample;
176         int maxtime, maxdepth, marker;
177
178         samples = dive->samples;
179         if (!samples)
180                 return;
181
182         cairo_set_line_width(cr, 2);
183
184         /* Get plot scaling limits */
185         maxtime = round_seconds_up(dive->duration.seconds);
186         maxdepth = round_depth_up(dive->maxdepth);
187
188         /* Time markers: every 5 min */
189         scalex = maxtime;
190         scaley = 1.0;
191         for (i = 5*60; i < maxtime; i += 5*60) {
192                 cairo_move_to(cr, SCALE(i, 0));
193                 cairo_line_to(cr, SCALE(i, 1));
194         }
195
196         /* Depth markers: every 30 ft or 10 m*/
197         scalex = 1.0;
198         scaley = maxdepth;
199         switch (output_units.length) {
200         case METERS: marker = 10000; break;
201         case FEET: marker = 9144; break;        /* 30 ft */
202         }
203
204         cairo_set_source_rgba(cr, 1, 1, 1, 0.5);
205         for (i = marker; i < maxdepth; i += marker) {
206                 cairo_move_to(cr, SCALE(0, i));
207                 cairo_line_to(cr, SCALE(1, i));
208         }
209         cairo_stroke(cr);
210
211         /* Show mean depth */
212         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.40);
213         cairo_move_to(cr, SCALE(0, dive->meandepth.mm));
214         cairo_line_to(cr, SCALE(1, dive->meandepth.mm));
215         cairo_stroke(cr);
216
217         scalex = maxtime;
218
219         sample = dive->sample;
220         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
221         begins = sample->time.seconds;
222         cairo_move_to(cr, SCALE(sample->time.seconds, sample->depth.mm));
223         for (i = 1; i < dive->samples; i++) {
224                 sample++;
225                 sec = sample->time.seconds;
226                 if (sec <= maxtime) {
227                         depth = sample->depth.mm;
228                         cairo_line_to(cr, SCALE(sec, depth));
229                 }
230         }
231         scaley = 1.0;
232         cairo_line_to(cr, SCALE(MIN(sec,maxtime), 0));
233         cairo_line_to(cr, SCALE(begins, 0));
234         cairo_close_path(cr);
235         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.20);
236         cairo_fill_preserve(cr);
237         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
238         cairo_stroke(cr);
239 }
240
241 /* gets both the actual start and end pressure as well as the scaling factors */
242 static int get_cylinder_pressure_range(struct dive *dive, double *scalex, double *scaley,
243                                        pressure_t *startp, pressure_t *endp)
244 {
245         int i;
246         int min, max;
247
248         *scalex = round_seconds_up(dive->duration.seconds);
249
250         max = 0;
251         min = 5000000;
252         if (startp)
253                 startp->mbar = endp->mbar = 0;
254
255         for (i = 0; i < dive->samples; i++) {
256                 int mbar;
257                 struct sample *sample = dive->sample + i;
258
259                 /* FIXME! We only track cylinder 0 right now */
260                 if (sample->cylinderindex)
261                         continue;
262                 mbar = sample->cylinderpressure.mbar;
263                 if (!mbar)
264                         continue;
265                 if (mbar < min)
266                         min = mbar;
267                 if (mbar > max)
268                         max = mbar;
269         }
270         if (startp)
271                 startp->mbar = max;
272         if (endp)
273                 endp->mbar = min;
274         if (!max)
275                 return 0;
276         *scaley = max * 1.5;
277         return 1;
278 }
279
280 static void plot_cylinder_pressure(struct dive *dive, cairo_t *cr,
281         double maxx, double maxy)
282 {
283         int i, sec = -1;
284         double scalex, scaley;
285
286         if (!get_cylinder_pressure_range(dive, &scalex, &scaley, NULL, NULL))
287                 return;
288
289         cairo_set_source_rgba(cr, 0.2, 1.0, 0.2, 0.80);
290
291         cairo_move_to(cr, SCALE(0, dive->cylinder[0].start.mbar));
292         for (i = 1; i < dive->samples; i++) {
293                 int mbar;
294                 struct sample *sample = dive->sample + i;
295
296                 mbar = sample->cylinderpressure.mbar;
297                 if (!mbar)
298                         continue;
299                 sec = sample->time.seconds;
300                 if (sec <= dive->duration.seconds)
301                         cairo_line_to(cr, SCALE(sec, mbar));
302         }
303         /*
304          * We may have "surface time" events, in which case we don't go
305          * back to dive duration
306          */
307         if (sec < dive->duration.seconds)
308                 cairo_line_to(cr, SCALE(dive->duration.seconds, dive->cylinder[0].end.mbar));
309         cairo_stroke(cr);
310 }
311
312 /*
313  * Return air usage (in liters).
314  */
315 static double calculate_airuse(struct dive *dive)
316 {
317         double airuse = 0;
318         int i;
319
320         for (i = 0; i < MAX_CYLINDERS; i++) {
321                 cylinder_t *cyl = dive->cylinder + i;
322                 int size = cyl->type.size.mliter;
323                 double kilo_atm;
324
325                 if (!size)
326                         continue;
327
328                 kilo_atm = (cyl->start.mbar - cyl->end.mbar) / 1013250.0;
329
330                 /* Liters of air at 1 atm == milliliters at 1k atm*/
331                 airuse += kilo_atm * size;
332         }
333         return airuse;
334 }
335
336 static void plot_info(struct dive *dive, cairo_t *cr,
337         double maxx, double maxy)
338 {
339         text_render_options_t tro = {0.2, 1.0, 0.2, LEFT};
340         const double liters_per_cuft = 28.317;
341         const char *unit;
342         double airuse;
343
344         airuse = calculate_airuse(dive);
345         if (!airuse)
346                 return;
347
348         /* I really need to start addign some unit setting thing */
349         switch (output_units.volume) {
350         case LITER:
351                 unit = "l";
352                 break;
353         case CUFT:
354                 unit = "cuft";
355                 airuse /= liters_per_cuft;
356                 break;
357         }
358         plot_text(cr, &tro, maxx*0.8, maxy*0.8, "vol: %4.2f %s", airuse, unit);
359         if (dive->duration.seconds) {
360                 double pressure = 1 + (dive->meandepth.mm / 10000.0);
361                 double sac = airuse / pressure * 60 / dive->duration.seconds;
362                 plot_text(cr, &tro, maxx*0.8, maxy*0.85, "SAC: %4.2f %s/min", sac, unit);
363         }
364 }
365
366 static void plot_cylinder_pressure_text(struct dive *dive, cairo_t *cr,
367         double maxx, double maxy)
368 {
369         double scalex, scaley;
370         pressure_t startp, endp;
371
372         cairo_set_font_size(cr, 10);
373
374         if (get_cylinder_pressure_range(dive, &scalex, &scaley,
375                                         &startp, &endp)) {
376                 int start, end;
377                 const char *unit = "bar";
378
379                 switch (output_units.pressure) {
380                 case PASCAL:
381                         start = startp.mbar * 100;
382                         end = startp.mbar * 100;
383                         unit = "pascal";
384                         break;
385                 case BAR:
386                         start = (startp.mbar + 500) / 1000;
387                         end = (endp.mbar + 500) / 1000;
388                         unit = "bar";
389                         break;
390                 case PSI:
391                         start = to_PSI(startp);
392                         end = to_PSI(endp);
393                         unit = "psi";
394                         break;
395                 }
396
397                 text_render_options_t tro = {0.2, 1.0, 0.2, LEFT};
398                 plot_text(cr, &tro, SCALE(0, startp.mbar), "%d %s", start, unit);
399                 plot_text(cr, &tro, SCALE(dive->duration.seconds, endp.mbar),
400                           "%d %s", end, unit);
401         }
402 }
403
404 static void plot(cairo_t *cr, int w, int h, struct dive *dive)
405 {
406         double topx, topy, maxx, maxy;
407         double scalex, scaley;
408
409         topx = w / 20.0;
410         topy = h / 20.0;
411         maxx = (w - 2*topx);
412         maxy = (h - 2*topy);
413         cairo_translate(cr, topx, topy);
414
415         /* Cylinder pressure plot */
416         plot_cylinder_pressure(dive, cr, maxx, maxy);
417
418         /* Depth profile */
419         plot_depth_profile(dive, cr, maxx, maxy);
420
421         /* Text on top of all graphs.. */
422         plot_depth_text(dive, cr, maxx, maxy);
423         plot_cylinder_pressure_text(dive, cr, maxx, maxy);
424
425         /* And info box in the lower right corner.. */
426         plot_info(dive, cr, maxx, maxy);
427
428         /* Bounding box last */
429         scalex = scaley = 1.0;
430         cairo_set_source_rgb(cr, 1, 1, 1);
431         cairo_move_to(cr, SCALE(0,0));
432         cairo_line_to(cr, SCALE(0,1));
433         cairo_line_to(cr, SCALE(1,1));
434         cairo_line_to(cr, SCALE(1,0));
435         cairo_close_path(cr);
436         cairo_stroke(cr);
437
438 }
439
440 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
441 {
442         struct dive *dive = current_dive;
443         cairo_t *cr;
444         int w,h;
445
446         w = widget->allocation.width;
447         h = widget->allocation.height;
448
449         cr = gdk_cairo_create(widget->window);
450         cairo_set_source_rgb(cr, 0, 0, 0);
451         cairo_paint(cr);
452
453         if (dive)
454                 plot(cr, w, h, dive);
455
456         cairo_destroy(cr);
457
458         return FALSE;
459 }
460
461 GtkWidget *dive_profile_widget(void)
462 {
463         GtkWidget *da;
464
465         da = gtk_drawing_area_new();
466         gtk_widget_set_size_request(da, 450, 350);
467         g_signal_connect(da, "expose_event", G_CALLBACK(expose_event), NULL);
468
469         return da;
470 }