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