subsurface/planner.c
Dirk Hohndel 8a5792d473 Manually add gas changes to a dive
Create a little widget that lists all the gases / tanks we know about and
allow the user to pick one of them.

Turns out that add_event only added events at the end of the list - but we
treat that list as chronologically sorted. So I fixed that little
mis-feature as well.

This does raise the question whether we need the inverse operation
(removing a gas change). And if there are other things that we should be
able to manually edit, now that we have the infrastructure for this neat
little context menu...

See #60 -- this doesn't address all of the issues mentioned there, but at
least deals with the 'headline' of the feature request...

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2013-03-17 20:04:25 -07:00

1365 lines
38 KiB
C

/* planner.c
*
* code that allows us to plan future dives
*
* (c) Dirk Hohndel 2013
*/
#include <libintl.h>
#include <glib/gi18n.h>
#include <unistd.h>
#include <ctype.h>
#include "dive.h"
#include "divelist.h"
#include "display-gtk.h"
int decostoplevels[] = { 0, 3000, 6000, 9000, 12000, 15000, 18000, 21000, 24000, 27000,
30000, 33000, 36000, 39000, 42000, 45000, 48000, 51000, 54000, 57000,
60000, 63000, 66000, 69000, 72000, 75000, 78000, 81000, 84000, 87000,
90000, 100000, 110000, 120000, 130000, 140000, 150000, 160000, 170000,
180000, 190000, 200000, 220000, 240000, 260000, 280000, 300000,
320000, 340000, 360000, 380000
};
double plangflow, plangfhigh;
char *disclaimer;
GtkWidget *planner, *planner_error_bar, *error_label;
#if DEBUG_PLAN
void dump_plan(struct diveplan *diveplan)
{
struct divedatapoint *dp;
struct tm tm;
if (!diveplan) {
printf ("Diveplan NULL\n");
return;
}
utc_mkdate(diveplan->when, &tm);
printf("\nDiveplan @ %04d-%02d-%02d %02d:%02d:%02d (surfpres %dmbar):\n",
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
diveplan->surface_pressure);
dp = diveplan->dp;
while (dp) {
printf("\t%3u:%02u: %dmm gas: %d o2 %d h2\n", FRACTION(dp->time, 60), dp->depth, dp->o2, dp->he);
dp = dp->next;
}
}
#endif
static void on_error_bar_response(GtkWidget *widget, gint response, gpointer data)
{
if (response == GTK_RESPONSE_OK)
{
gtk_widget_destroy(widget);
planner_error_bar = NULL;
error_label = NULL;
}
}
static void show_error(const char *fmt, ...)
{
va_list args;
GError *error;
GtkWidget *box, *container;
gboolean bar_is_visible = TRUE;
va_start(args, fmt);
error = g_error_new_valist(g_quark_from_string("subsurface"), DIVE_ERROR_PLAN, fmt, args);
va_end(args);
if (!planner_error_bar) {
planner_error_bar = gtk_info_bar_new_with_buttons(GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
g_signal_connect(planner_error_bar, "response", G_CALLBACK(on_error_bar_response), NULL);
gtk_info_bar_set_message_type(GTK_INFO_BAR(planner_error_bar), GTK_MESSAGE_ERROR);
bar_is_visible = FALSE;
}
container = gtk_info_bar_get_content_area(GTK_INFO_BAR(planner_error_bar));
if (error_label)
gtk_container_remove(GTK_CONTAINER(container), error_label);
error_label = gtk_label_new(error->message);
gtk_container_add(GTK_CONTAINER(container), error_label);
box = gtk_dialog_get_content_area(GTK_DIALOG(planner));
if (!bar_is_visible)
gtk_box_pack_start(GTK_BOX(box), planner_error_bar, FALSE, FALSE, 0);
gtk_widget_show_all(box);
/* make sure this actually gets shown BEFORE the calculations run */
while (gtk_events_pending())
gtk_main_iteration_do(FALSE);
}
void get_gas_from_events(struct divecomputer *dc, int time, int *o2, int *he)
{
struct event *event = dc->events;
while (event && event->time.seconds <= time) {
if (!strcmp(event->name, "gaschange")) {
*o2 = 10 * event->value & 0xffff;
*he = 10 * event->value >> 16;
}
event = event->next;
}
}
/* simple helper function to compare two permille values with
* (rounded) percent granularity */
static inline gboolean match_percent(int a, int b)
{
return (a + 5) / 10 == (b + 5) / 10;
}
static int get_gasidx(struct dive *dive, int o2, int he)
{
int gasidx = -1;
/* we treat air as 0/0 because it is special */
if (is_air(o2, he))
o2 = 0;
while (++gasidx < MAX_CYLINDERS)
if (match_percent(dive->cylinder[gasidx].gasmix.o2.permille, o2) &&
match_percent(dive->cylinder[gasidx].gasmix.he.permille, he))
return gasidx;
return -1;
}
void get_gas_string(int o2, int he, char *text, int len)
{
if (is_air(o2, he))
snprintf(text, len, _("air"));
else if (he == 0)
snprintf(text, len, _("EAN%d"), (o2 + 5) / 10);
else
snprintf(text, len, "(%d/%d)", (o2 + 5) / 10, (he + 5) / 10);
}
/* returns the tissue tolerance at the end of this (partial) dive */
double tissue_at_end(struct dive *dive, char **cached_datap)
{
struct divecomputer *dc;
struct sample *sample, *psample;
int i, j, t0, t1, gasidx, lastdepth;
int o2, he;
double tissue_tolerance;
if (!dive)
return 0.0;
if (*cached_datap) {
tissue_tolerance = restore_deco_state(*cached_datap);
} else {
tissue_tolerance = init_decompression(dive);
cache_deco_state(tissue_tolerance, cached_datap);
}
dc = &dive->dc;
if (!dc->samples)
return tissue_tolerance;
psample = sample = dc->sample;
lastdepth = t0 = 0;
/* we always start with gas 0 (unless an event tells us otherwise) */
o2 = dive->cylinder[0].gasmix.o2.permille;
he = dive->cylinder[0].gasmix.he.permille;
for (i = 0; i < dc->samples; i++, sample++) {
t1 = sample->time.seconds;
get_gas_from_events(&dive->dc, t0, &o2, &he);
if ((gasidx = get_gasidx(dive, o2, he)) == -1) {
show_error(_("Can't find gas %d/%d"), (o2 + 5) / 10, (he + 5) / 10);
gasidx = 0;
}
if (i > 0)
lastdepth = psample->depth.mm;
for (j = t0; j < t1; j++) {
int depth = interpolate(lastdepth, sample->depth.mm, j - t0, t1 - t0);
tissue_tolerance = add_segment(depth_to_mbar(depth, dive) / 1000.0,
&dive->cylinder[gasidx].gasmix, 1, sample->po2, dive);
}
psample = sample;
t0 = t1;
}
return tissue_tolerance;
}
/* how many seconds until we can ascend to the next stop? */
int time_at_last_depth(struct dive *dive, int o2, int he, int next_stop, char **cached_data_p)
{
int depth, gasidx;
double surface_pressure, tissue_tolerance;
int wait = 0;
struct sample *sample;
if (!dive)
return 0;
surface_pressure = dive->dc.surface_pressure.mbar / 1000.0;
tissue_tolerance = tissue_at_end(dive, cached_data_p);
sample = &dive->dc.sample[dive->dc.samples - 1];
depth = sample->depth.mm;
gasidx = get_gasidx(dive, o2, he);
while (deco_allowed_depth(tissue_tolerance, surface_pressure, dive, 1) > next_stop) {
wait++;
tissue_tolerance = add_segment(depth_to_mbar(depth, dive) / 1000.0,
&dive->cylinder[gasidx].gasmix, 1, sample->po2, dive);
}
return wait;
}
int add_gas(struct dive *dive, int o2, int he)
{
int i;
struct gasmix *mix;
cylinder_t *cyl;
for (i = 0; i < MAX_CYLINDERS; i++) {
cyl = dive->cylinder + i;
mix = &cyl->gasmix;
if (cylinder_nodata(cyl))
break;
if (match_percent(o2, mix->o2.permille) && match_percent(he, mix->he.permille))
return i;
}
if (i == MAX_CYLINDERS) {
show_error(_("Too many gas mixes"));
return -1;
}
mix->o2.permille = o2;
mix->he.permille = he;
/* since air is stored as 0/0 we need to set a name or an air cylinder
* would be seen as unset (by cylinder_nodata()) */
cyl->type.description = strdup(_("Cylinder for planning"));
return i;
}
struct dive *create_dive_from_plan(struct diveplan *diveplan)
{
struct dive *dive;
struct divedatapoint *dp;
struct divecomputer *dc;
struct sample *sample;
int oldo2 = O2_IN_AIR, oldhe = 0;
int oldpo2 = 0;
int lasttime = 0;
if (!diveplan || !diveplan->dp)
return NULL;
#if DEBUG_PLAN & 4
printf("in create_dive_from_plan\n");
dump_plan(diveplan);
#endif
dive = alloc_dive();
dive->when = diveplan->when;
dive->dc.surface_pressure.mbar = diveplan->surface_pressure;
dc = &dive->dc;
dc->model = strdup(_("Simulated Dive"));
dp = diveplan->dp;
/* let's start with the gas given on the first segment */
if (dp->o2 || dp->he) {
oldo2 = dp->o2;
oldhe = dp->he;
}
sample = prepare_sample(dc);
sample->po2 = dp->po2;
finish_sample(dc);
add_gas(dive, oldo2, oldhe);
while (dp) {
int o2 = dp->o2, he = dp->he;
int po2 = dp->po2;
int time = dp->time;
int depth = dp->depth;
if (time == 0) {
/* special entries that just inform the algorithm about
* additional gases that are available */
add_gas(dive, o2, he);
dp = dp->next;
continue;
}
if (!o2 && !he) {
o2 = oldo2;
he = oldhe;
}
/* Check for SetPoint change */
if (oldpo2 != po2) {
if (lasttime)
add_event(dc, lasttime, 20, 0, po2, "SP change"); // SAMPLE_EVENT_PO2
oldpo2 = po2;
}
/* Create new gas, and gas change event if necessary;
* Sadly, we inherited our gaschange event from libdivecomputer which only
* support percentage values, so round the entries */
if (o2 != oldo2 || he != oldhe) {
int plano2 = (o2 + 5) / 10 * 10;
int planhe = (he + 5) / 10 * 10;
int value;
add_gas(dive, plano2, planhe);
value = (plano2 / 10) | ((planhe / 10) << 16);
add_event(dc, lasttime, 25, 0, value, "gaschange"); // SAMPLE_EVENT_GASCHANGE2
oldo2 = o2; oldhe = he;
}
/* Create sample */
sample = prepare_sample(dc);
/* set po2 at beginning of this segment */
/* and keep it valid for last sample - where it likely doesn't matter */
sample[-1].po2 = po2;
sample->po2 = po2;
sample->time.seconds = time;
sample->depth.mm = depth;
finish_sample(dc);
lasttime = time;
dp = dp->next;
}
if (dc->samples <= 1) {
/* not enough there yet to create a dive - most likely the first time is missing */
free(dive);
dive = NULL;
}
#if DEBUG_PLAN & 32
if (dive)
save_dive(stdout, dive);
#endif
return dive;
}
void free_dps(struct divedatapoint *dp)
{
while (dp) {
struct divedatapoint *ndp = dp->next;
free(dp);
dp = ndp;
}
}
struct divedatapoint *create_dp(int time_incr, int depth, int o2, int he, int po2)
{
struct divedatapoint *dp;
dp = malloc(sizeof(struct divedatapoint));
dp->time = time_incr;
dp->depth = depth;
dp->o2 = o2;
dp->he = he;
dp->po2 = po2;
dp->entered = FALSE;
dp->next = NULL;
return dp;
}
struct divedatapoint *get_nth_dp(struct diveplan *diveplan, int idx)
{
struct divedatapoint **ldpp, *dp = diveplan->dp;
int i = 0;
ldpp = &diveplan->dp;
while (dp && i++ < idx) {
ldpp = &dp->next;
dp = dp->next;
}
while (i++ <= idx) {
*ldpp = dp = create_dp(0, 0, 0, 0, 0);
ldpp = &((*ldpp)->next);
}
return dp;
}
void add_duration_to_nth_dp(struct diveplan *diveplan, int idx, int duration, gboolean is_rel)
{
struct divedatapoint *pdp, *dp = get_nth_dp(diveplan, idx);
if (idx > 0) {
pdp = get_nth_dp(diveplan, idx - 1);
if (duration && (is_rel || duration <= pdp->time))
duration += pdp->time;
}
dp->time = duration;
if (duration > 180 * 60)
show_error(_("Warning - extremely long dives can cause long calculation time"));
}
/* this function is ONLY called from the dialog callback - so it
* marks this entry as 'entered'.
* Do NOT call from other parts of the planning code without changing
* that logic */
void add_depth_to_nth_dp(struct diveplan *diveplan, int idx, int depth)
{
struct divedatapoint *dp = get_nth_dp(diveplan, idx);
dp->depth = depth;
dp->entered = TRUE;
}
void add_gas_to_nth_dp(struct diveplan *diveplan, int idx, int o2, int he)
{
struct divedatapoint *dp = get_nth_dp(diveplan, idx);
dp->o2 = o2;
dp->he = he;
}
void add_po2_to_nth_dp(struct diveplan *diveplan, int idx, int po2)
{
struct divedatapoint *dp = get_nth_dp(diveplan, idx);
dp->po2 = po2;
}
void add_to_end_of_diveplan(struct diveplan *diveplan, struct divedatapoint *dp)
{
struct divedatapoint **lastdp = &diveplan->dp;
struct divedatapoint *ldp = *lastdp;
int lasttime = 0;
while (*lastdp) {
ldp = *lastdp;
if (ldp->time > lasttime)
lasttime = ldp->time;
lastdp = &(*lastdp)->next;
}
*lastdp = dp;
if (ldp)
dp->time += lasttime;
}
void plan_add_segment(struct diveplan *diveplan, int duration, int depth, int o2, int he, int po2)
{
struct divedatapoint *dp = create_dp(duration, depth, o2, he, po2);
add_to_end_of_diveplan(diveplan, dp);
}
struct gaschanges {
int depth;
int gasidx;
};
static struct gaschanges *analyze_gaslist(struct diveplan *diveplan, struct dive *dive, int *gaschangenr)
{
int nr = 0;
struct gaschanges *gaschanges = NULL;
struct divedatapoint *dp = diveplan->dp;
while (dp) {
if (dp->time == 0) {
int i = 0, j = 0;
nr++;
gaschanges = realloc(gaschanges, nr * sizeof(struct gaschanges));
while (i < nr - 1) {
if (dp->depth < gaschanges[i].depth) {
memmove(gaschanges + i + 1, gaschanges + i, (nr - i - 1) * sizeof(struct gaschanges));
break;
}
i++;
}
gaschanges[i].depth = dp->depth;
do {
if (dive->cylinder[j].gasmix.o2.permille == dp->o2 &&
dive->cylinder[j].gasmix.he.permille == dp->he) {
gaschanges[i].gasidx = j;
break;
}
j++;
} while (j < MAX_CYLINDERS);
}
dp = dp->next;
}
*gaschangenr = nr;
#if DEBUG_PLAN & 16
for (nr = 0; nr < *gaschangenr; nr++)
printf("gaschange nr %d: @ %5.2lfm gasidx %d (%d/%d)\n", nr, gaschanges[nr].depth / 1000.0,
gaschanges[nr].gasidx, (dive->cylinder[gaschanges[nr].gasidx].gasmix.o2.permille + 5) / 10,
(dive->cylinder[gaschanges[nr].gasidx].gasmix.he.permille + 5) / 10);
#endif
return gaschanges;
}
/* sort all the stops into one ordered list */
static int *sort_stops(int *dstops, int dnr, struct gaschanges *gstops, int gnr)
{
int i, gi, di;
int total = dnr + gnr;
int *stoplevels = malloc(total * sizeof(int));
/* no gaschanges */
if (gnr == 0) {
memcpy(stoplevels, dstops, dnr * sizeof(int));
return stoplevels;
}
i = total - 1;
gi = gnr - 1;
di = dnr - 1;
while (i >= 0) {
if (dstops[di] > gstops[gi].depth) {
stoplevels[i] = dstops[di];
di--;
} else if (dstops[di] == gstops[gi].depth) {
stoplevels[i] = dstops[di];
di--;
gi--;
} else {
stoplevels[i] = gstops[gi].depth;
gi--;
}
i--;
if (di < 0) {
while (gi >= 0)
stoplevels[i--] = gstops[gi--].depth;
break;
}
if (gi < 0) {
while (di >= 0)
stoplevels[i--] = dstops[di--];
break;
}
}
while (i >= 0)
stoplevels[i--] = 0;
#if DEBUG_PLAN & 16
int k;
for (k = gnr + dnr -1; k >= 0; k--) {
printf("stoplevel[%d]: %5.2lfm\n", k, stoplevels[k]/1000.0);
if (stoplevels[k] == 0)
break;
}
#endif
return stoplevels;
}
static void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive)
{
char buffer[20000];
int consumption[MAX_CYLINDERS] = { 0, };
int len, gasidx, lastdepth = 0, lasttime = 0;
struct divedatapoint *dp = diveplan->dp;
int o2, he;
if (!dp)
return;
snprintf(buffer, sizeof(buffer), _("%s\nSubsurface dive plan\nbased on GFlow = %.0f and GFhigh = %.0f\n\n"),
disclaimer, plangflow * 100, plangfhigh * 100);
/* we start with gas 0, then check if that was changed */
o2 = dive->cylinder[0].gasmix.o2.permille;
he = dive->cylinder[0].gasmix.he.permille;
do {
const char *depth_unit;
char gas[64];
double depthvalue;
int decimals;
double used;
int newo2 = o2, newhe = he;
struct divedatapoint *nextdp;
if (dp->time == 0)
continue;
depthvalue = get_depth_units(dp->depth, &decimals, &depth_unit);
/* do we change gas after this segment? We need to look at the gas
* for the next segment (that isn't just a record of available gas !!)
* to find out */
nextdp = dp->next;
while (nextdp && nextdp->time == 0)
nextdp = nextdp->next;
if (nextdp) {
newo2 = nextdp->o2;
newhe = nextdp->he;
if (newhe == 0 && newo2 == 0) {
/* same as last segment */
newo2 = o2;
newhe = he;
}
}
/* do we want to skip this leg as it is devoid of anything useful? */
if (!dp->entered && o2 == newo2 && he == newhe && nextdp && dp->depth != lastdepth && nextdp->depth != dp->depth)
continue;
get_gas_string(o2, he, gas, sizeof(gas));
gasidx = get_gasidx(dive, o2, he);
len = strlen(buffer);
if (dp->depth != lastdepth) {
used = diveplan->bottomsac / 1000.0 * depth_to_mbar((dp->depth + lastdepth) / 2, dive) *
(dp->time - lasttime) / 60;
snprintf(buffer + len, sizeof(buffer) - len, _("Transition to %.*f %s in %d:%02d min - runtime %d:%02u on %s\n"),
decimals, depthvalue, depth_unit,
FRACTION(dp->time - lasttime, 60),
FRACTION(dp->time, 60),
gas);
} else {
/* we use deco SAC rate during the calculated deco stops, bottom SAC rate everywhere else */
int sac = dp->entered ? diveplan->bottomsac : diveplan->decosac;
used = sac / 1000.0 * depth_to_mbar(dp->depth, dive) * (dp->time - lasttime) / 60;
snprintf(buffer + len, sizeof(buffer) - len, _("Stay at %.*f %s for %d:%02d min - runtime %d:%02u on %s\n"),
decimals, depthvalue, depth_unit,
FRACTION(dp->time - lasttime, 60),
FRACTION(dp->time, 60),
gas);
}
if (gasidx != -1)
consumption[gasidx] += used;
get_gas_string(newo2, newhe, gas, sizeof(gas));
if (o2 != newo2 || he != newhe) {
len = strlen(buffer);
snprintf(buffer + len, sizeof(buffer) - len, _("Switch gas to %s\n"), gas);
}
o2 = newo2;
he = newhe;
lasttime = dp->time;
lastdepth = dp->depth;
} while ((dp = dp->next) != NULL);
len = strlen(buffer);
snprintf(buffer + len, sizeof(buffer) - len, _("Gas consumption:\n"));
for (gasidx = 0; gasidx < MAX_CYLINDERS; gasidx++) {
double volume;
const char *unit;
char gas[64];
if (consumption[gasidx] == 0)
continue;
len = strlen(buffer);
volume = get_volume_units(consumption[gasidx], NULL, &unit);
get_gas_string(dive->cylinder[gasidx].gasmix.o2.permille,
dive->cylinder[gasidx].gasmix.he.permille, gas, sizeof(gas));
snprintf(buffer + len, sizeof(buffer) - len, _("%.0f%s of %s\n"), volume, unit, gas);
}
dive->notes = strdup(buffer);
}
void plan(struct diveplan *diveplan, char **cached_datap, struct dive **divep)
{
struct dive *dive;
struct sample *sample;
int wait_time, o2, he, po2;
int ceiling, depth, transitiontime;
int stopidx, gi;
double tissue_tolerance;
struct gaschanges *gaschanges;
int gaschangenr;
int *stoplevels;
set_gf(plangflow, plangfhigh);
if (!diveplan->surface_pressure)
diveplan->surface_pressure = SURFACE_PRESSURE;
if (*divep)
delete_single_dive(dive_table.nr - 1);
*divep = dive = create_dive_from_plan(diveplan);
if (!dive)
return;
record_dive(dive);
sample = &dive->dc.sample[dive->dc.samples - 1];
/* we start with gas 0, then check if that was changed */
o2 = dive->cylinder[0].gasmix.o2.permille;
he = dive->cylinder[0].gasmix.he.permille;
get_gas_from_events(&dive->dc, sample->time.seconds, &o2, &he);
po2 = dive->dc.sample[dive->dc.samples - 1].po2;
depth = dive->dc.sample[dive->dc.samples - 1].depth.mm;
tissue_tolerance = tissue_at_end(dive, cached_datap);
ceiling = deco_allowed_depth(tissue_tolerance, diveplan->surface_pressure / 1000.0, dive, 1);
#if DEBUG_PLAN & 4
printf("gas %d/%d\n", o2, he);
printf("depth %5.2lfm ceiling %5.2lfm\n", depth / 1000.0, ceiling / 1000.0);
#endif
if (depth < ceiling) /* that's not good... */
depth = ceiling;
for (stopidx = 0; stopidx < sizeof(decostoplevels) / sizeof(int); stopidx++)
if (decostoplevels[stopidx] >= depth)
break;
stopidx--;
/* so now we know the first decostop level above us
* NOTE, this could be the surface or a long list of potential stops
* we do NOT start only at the ceiling, as the ceiling may come down
* further during the ascent.
* Next we need to figure out if there are better gases available
* and at which depths we are supposed to switch to them */
gaschanges = analyze_gaslist(diveplan, dive, &gaschangenr);
stoplevels = sort_stops(decostoplevels, stopidx + 1, gaschanges, gaschangenr);
gi = gaschangenr - 1;
stopidx += gaschangenr;
if (depth > stoplevels[stopidx]) {
transitiontime = (depth - stoplevels[stopidx]) / 150;
#if DEBUG_PLAN & 2
printf("transitiontime %d:%02d to depth %5.2lfm\n", FRACTION(transitiontime, 60), stoplevels[stopidx] / 1000.0);
#endif
plan_add_segment(diveplan, transitiontime, stoplevels[stopidx], o2, he, po2);
/* re-create the dive */
delete_single_dive(dive_table.nr - 1);
*divep = dive = create_dive_from_plan(diveplan);
record_dive(dive);
}
while (stopidx > 0) { /* this indicates that we aren't surfacing directly */
if (gi >= 0 && stoplevels[stopidx] == gaschanges[gi].depth) {
o2 = dive->cylinder[gaschanges[gi].gasidx].gasmix.o2.permille;
he = dive->cylinder[gaschanges[gi].gasidx].gasmix.he.permille;
#if DEBUG_PLAN & 16
printf("switch to gas %d (%d/%d) @ %5.2lfm\n", gaschanges[gi].gasidx,
(o2 + 5) / 10, (he + 5) / 10, gaschanges[gi].depth / 1000.0);
#endif
gi--;
}
wait_time = time_at_last_depth(dive, o2, he, stoplevels[stopidx - 1], cached_datap);
/* typically deco plans are done in one minute increments; we may want to
* make this configurable at some point */
wait_time = ((wait_time + 59) / 60) * 60;
#if DEBUG_PLAN & 2
tissue_tolerance = tissue_at_end(dive, cached_datap);
ceiling = deco_allowed_depth(tissue_tolerance, diveplan->surface_pressure / 1000.0, dive, 1);
printf("waittime %d:%02d at depth %5.2lfm; ceiling %5.2lfm\n", FRACTION(wait_time, 60),
stoplevels[stopidx] / 1000.0, ceiling / 1000.0);
#endif
if (wait_time)
plan_add_segment(diveplan, wait_time, stoplevels[stopidx], o2, he, po2);
transitiontime = (stoplevels[stopidx] - stoplevels[stopidx - 1]) / 150;
#if DEBUG_PLAN & 2
printf("transitiontime %d:%02d to depth %5.2lfm\n", FRACTION(transitiontime, 60), stoplevels[stopidx - 1] / 1000.0);
#endif
plan_add_segment(diveplan, transitiontime, stoplevels[stopidx - 1], o2, he, po2);
/* re-create the dive */
delete_single_dive(dive_table.nr - 1);
*divep = dive = create_dive_from_plan(diveplan);
record_dive(dive);
stopidx--;
}
add_plan_to_notes(diveplan, dive);
/* now make the dive visible in the dive list */
report_dives(FALSE, FALSE);
show_and_select_dive(dive);
free(stoplevels);
free(gaschanges);
}
/* and now the UI for all this */
/*
* Get a value in tenths (so "10.2" == 102, "9" = 90)
*
* Return negative for errors.
*/
static int get_tenths(const char *begin, const char **endp)
{
char *end;
int value = strtol(begin, &end, 10);
if (begin == end)
return -1;
value *= 10;
/* Fraction? We only look at the first digit */
if (*end == '.') {
end++;
if (!isdigit(*end))
return -1;
value += *end - '0';
do {
end++;
} while (isdigit(*end));
}
*endp = end;
return value;
}
static int get_thousandths(const char *begin, const char **endp)
{
char *end;
int value = strtol(begin, &end, 10);
if (begin == end)
return -1;
value *= 1000;
/* now deal with up to three more digits after decimal point */
if (*end == '.') {
int factor = 100;
do {
++end;
if (!isdigit(*end))
break;
value += (*end - '0') * factor;
factor /= 10;
} while (factor);
}
*endp = end;
return value;
}
static int get_permille(const char *begin, const char **end)
{
int value = get_tenths(begin, end);
if (value >= 0) {
/* Allow a percentage sign */
if (**end == '%')
++*end;
}
return value;
}
static int validate_gas(const char *text, int *o2_p, int *he_p)
{
int o2, he;
if (!text)
return 0;
while (g_ascii_isspace(*text))
text++;
if (!*text)
return 0;
if (!strcasecmp(text, _("air"))) {
o2 = O2_IN_AIR; he = 0; text += strlen(_("air"));
} else if (!strncasecmp(text, _("ean"), 3)) {
o2 = get_permille(text+3, &text); he = 0;
} else {
o2 = get_permille(text, &text); he = 0;
if (*text == '/')
he = get_permille(text+1, &text);
}
/* We don't want any extra crud */
while (g_ascii_isspace(*text))
text++;
if (*text)
return 0;
/* Validate the gas mix */
if (*text || o2 < 1 || o2 > 1000 || he < 0 || o2+he > 1000)
return 0;
/* Let it rip */
*o2_p = o2;
*he_p = he;
return 1;
}
static int validate_time(const char *text, int *sec_p, int *rel_p)
{
int min, sec, rel;
char *end;
if (!text)
return 0;
while (g_ascii_isspace(*text))
text++;
rel = 0;
if (*text == '+') {
rel = 1;
text++;
while (g_ascii_isspace(*text))
text++;
}
min = strtol(text, &end, 10);
if (text == end)
return 0;
if (min < 0 || min > 1000)
return 0;
/* Ok, minutes look ok */
text = end;
sec = 0;
if (*text == ':') {
text++;
sec = strtol(text, &end, 10);
if (end != text+2)
return 0;
if (sec < 0)
return 0;
text = end;
if (*text == ':') {
if (sec >= 60)
return 0;
min = min*60 + sec;
text++;
sec = strtol(text, &end, 10);
if (end != text+2)
return 0;
if (sec < 0)
return 0;
text = end;
}
}
/* Maybe we should accept 'min' at the end? */
if (g_ascii_isspace(*text))
text++;
if (*text)
return 0;
*sec_p = min*60 + sec;
*rel_p = rel;
return 1;
}
static int validate_depth(const char *text, int *mm_p)
{
int depth, imperial;
if (!text)
return 0;
depth = get_tenths(text, &text);
if (depth < 0)
return 0;
while (g_ascii_isspace(*text))
text++;
imperial = get_units()->length == FEET;
if (*text == 'm') {
imperial = 0;
text++;
} else if (!strcasecmp(text, _("ft"))) {
imperial = 1;
text += 2;
}
while (g_ascii_isspace(*text))
text++;
if (*text)
return 0;
if (imperial) {
depth = feet_to_mm(depth / 10.0);
} else {
depth *= 100;
}
*mm_p = depth;
/* we don't support extreme depths */
if (depth > 400000)
return 0;
return 1;
}
static int validate_po2(const char *text, int *mbar_po2)
{
int po2;
if (!text)
return 0;
po2 = get_tenths(text, &text);
if (po2 < 0)
return 0;
while (g_ascii_isspace(*text))
text++;
while (g_ascii_isspace(*text))
text++;
if (*text)
return 0;
*mbar_po2 = po2 * 100;
return 1;
}
static int validate_volume(const char *text, int *sac)
{
int volume, imperial;
if (!text)
return 0;
volume = get_thousandths(text, &text);
if (volume < 0)
return 0;
while (g_ascii_isspace(*text))
text++;
imperial = get_units()->volume == CUFT;
if (*text == 'l') {
imperial = 0;
text++;
} else if (!strncasecmp(text, _("cuft"), 4)) {
imperial = 1;
text += 4;
}
while (g_ascii_isspace(*text) || *text == '/')
text++;
if (!strncasecmp(text, _("min"), 3))
text += 3;
while (g_ascii_isspace(*text))
text++;
if (*text)
return 0;
if (imperial)
volume = cuft_to_l(volume) + 0.5; /* correct for mcuft -> ml */
*sac = volume;
return 1;
}
static GtkWidget *add_entry_to_box(GtkWidget *box, const char *label)
{
GtkWidget *entry, *frame;
entry = gtk_entry_new();
gtk_entry_set_max_length(GTK_ENTRY(entry), 16);
if (label) {
frame = gtk_frame_new(label);
gtk_container_add(GTK_CONTAINER(frame), entry);
gtk_box_pack_start(GTK_BOX(box), frame, FALSE, FALSE, 0);
} else {
gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 2);
}
return entry;
}
#define MAX_WAYPOINTS 12
GtkWidget *entry_depth[MAX_WAYPOINTS], *entry_duration[MAX_WAYPOINTS], *entry_gas[MAX_WAYPOINTS], *entry_po2[MAX_WAYPOINTS];
int nr_waypoints = 0;
static GtkListStore *gas_model = NULL;
struct diveplan diveplan = {};
char *cache_data = NULL;
struct dive *planned_dive = NULL;
/* make a copy of the diveplan so far and display the corresponding dive */
void show_planned_dive(void)
{
struct diveplan tempplan;
struct divedatapoint *dp, **dpp;
memcpy(&tempplan, &diveplan, sizeof(struct diveplan));
dpp = &tempplan.dp;
dp = diveplan.dp;
while (dp && *dpp) {
*dpp = malloc(sizeof(struct divedatapoint));
memcpy(*dpp, dp, sizeof(struct divedatapoint));
dp = dp->next;
dpp = &(*dpp)->next;
}
#if DEBUG_PLAN & 1
printf("in show_planned_dive:\n");
dump_plan(&tempplan);
#endif
plan(&tempplan, &cache_data, &planned_dive);
free_dps(tempplan.dp);
}
static gboolean gas_focus_out_cb(GtkWidget *entry, GdkEvent *event, gpointer data)
{
const char *gastext;
int o2, he;
int idx = data - NULL;
gastext = gtk_entry_get_text(GTK_ENTRY(entry));
o2 = he = 0;
if (validate_gas(gastext, &o2, &he))
add_string_list_entry(gastext, gas_model);
add_gas_to_nth_dp(&diveplan, idx, o2, he);
show_planned_dive();
return FALSE;
}
static void gas_changed_cb(GtkWidget *combo, gpointer data)
{
const char *gastext;
int o2, he;
int idx = data - NULL;
gastext = get_active_text(GTK_COMBO_BOX(combo));
/* stupidly this gets called for two reasons:
* a) any keystroke into the entry field
* b) mouse selection of a dropdown
* we only care about b) (a) is handled much better with the focus-out event)
* so let's check that the text returned is actually in our model before going on
*/
if (match_list(gas_model, gastext) != MATCH_EXACT)
return;
o2 = he = 0;
if (!validate_gas(gastext, &o2, &he)) {
/* this should never happen as only validated texts should be
* in the dropdown */
show_error(_("Invalid gas for row %d"),idx);
}
add_gas_to_nth_dp(&diveplan, idx, o2, he);
show_planned_dive();
}
static gboolean depth_focus_out_cb(GtkWidget *entry, GdkEvent *event, gpointer data)
{
const char *depthtext;
int depth = -1;
int idx = data - NULL;
depthtext = gtk_entry_get_text(GTK_ENTRY(entry));
if (validate_depth(depthtext, &depth)) {
if (depth > 150000)
show_error(_("Warning - planning very deep dives can take excessive amounts of time"));
add_depth_to_nth_dp(&diveplan, idx, depth);
show_planned_dive();
} else {
/* it might be better to instead change the color of the input field or something */
if (depth == -1)
show_error(_("Invalid depth - could not parse \"%s\""), depthtext);
else
show_error(_("Invalid depth - values deeper than 400m not supported"));
}
return FALSE;
}
static gboolean duration_focus_out_cb(GtkWidget *entry, GdkEvent * event, gpointer data)
{
const char *durationtext;
int duration, is_rel;
int idx = data - NULL;
durationtext = gtk_entry_get_text(GTK_ENTRY(entry));
if (validate_time(durationtext, &duration, &is_rel))
add_duration_to_nth_dp(&diveplan, idx, duration, is_rel);
show_planned_dive();
return FALSE;
}
static gboolean po2_focus_out_cb(GtkWidget *entry, GdkEvent * event, gpointer data)
{
const char *po2text;
int po2;
int idx = data - NULL;
po2text = gtk_entry_get_text(GTK_ENTRY(entry));
if (validate_po2(po2text, &po2))
add_po2_to_nth_dp(&diveplan, idx, po2);
show_planned_dive();
return FALSE;
}
/* Subsurface follows the lead of most divecomputers to use times
* without timezone - so all times are implicitly assumed to be
* local time of the dive location; so in order to give the current
* time in that way we actually need to add the timezone offset */
static timestamp_t current_time_notz(void)
{
time_t now = time(NULL);
struct tm *local = localtime(&now);
return utc_mktime(local);
}
static gboolean starttime_focus_out_cb(GtkWidget *entry, GdkEvent * event, gpointer data)
{
const char *starttimetext;
int starttime, is_rel;
starttimetext = gtk_entry_get_text(GTK_ENTRY(entry));
if (validate_time(starttimetext, &starttime, &is_rel)) {
/* we alway make this relative - either from the current time or from the
* end of the last dive, whichever is later */
timestamp_t cur = current_time_notz();
if (diveplan.lastdive_nr >= 0) {
struct dive *last_dive = get_dive(diveplan.lastdive_nr);
if (last_dive && last_dive->when + last_dive->dc.duration.seconds > cur)
cur = last_dive->when + last_dive->dc.duration.seconds;
}
diveplan.when = cur + starttime;
show_planned_dive();
} else {
/* it might be better to instead change the color of the input field or something */
show_error(_("Invalid starttime"));
}
return FALSE;
}
static gboolean surfpres_focus_out_cb(GtkWidget *entry, GdkEvent * event, gpointer data)
{
const char *surfprestext;
surfprestext = gtk_entry_get_text(GTK_ENTRY(entry));
diveplan.surface_pressure = atoi(surfprestext);
show_planned_dive();
return FALSE;
}
static gboolean sac_focus_out_cb(GtkWidget *entry, GdkEvent * event, gpointer data)
{
const char *sactext;
sactext = gtk_entry_get_text(GTK_ENTRY(entry));
if (validate_volume(sactext, data))
show_planned_dive();
return FALSE;
}
static gboolean gf_focus_out_cb(GtkWidget *entry, GdkEvent * event, gpointer data)
{
const char *gftext;
int gf;
double *gfp = data;
gftext = gtk_entry_get_text(GTK_ENTRY(entry));
if (sscanf(gftext, "%d", &gf) == 1) {
*gfp = gf / 100.0;
show_planned_dive();
}
return FALSE;
}
static GtkWidget *add_gas_combobox_to_box(GtkWidget *box, const char *label, int idx)
{
GtkWidget *frame, *combo;
if (!gas_model) {
gas_model = gtk_list_store_new(1, G_TYPE_STRING);
add_string_list_entry(_("AIR"), gas_model);
add_string_list_entry(_("EAN32"), gas_model);
add_string_list_entry(_("EAN36"), gas_model);
}
combo = combo_box_with_model_and_entry(gas_model);
gtk_widget_add_events(combo, GDK_FOCUS_CHANGE_MASK);
g_signal_connect(gtk_bin_get_child(GTK_BIN(combo)), "focus-out-event", G_CALLBACK(gas_focus_out_cb), NULL + idx);
g_signal_connect(combo, "changed", G_CALLBACK(gas_changed_cb), NULL + idx);
if (label) {
frame = gtk_frame_new(label);
gtk_box_pack_start(GTK_BOX(box), frame, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(frame), combo);
} else {
gtk_box_pack_start(GTK_BOX(box), combo, FALSE, FALSE, 2);
}
return combo;
}
static void add_waypoint_widgets(GtkWidget *box, int idx)
{
GtkWidget *hbox;
hbox = gtk_hbox_new(FALSE, 0);
gtk_box_pack_start(GTK_BOX(box), hbox, FALSE, FALSE, 0);
if (idx == 0) {
entry_depth[idx] = add_entry_to_box(hbox, _("Ending Depth"));
entry_duration[idx] = add_entry_to_box(hbox, _("Segment Time"));
entry_gas[idx] = add_gas_combobox_to_box(hbox, C_("Type of","Gas Used"), idx);
entry_po2[idx] = add_entry_to_box(hbox, _("CC SetPoint"));
} else {
entry_depth[idx] = add_entry_to_box(hbox, NULL);
entry_duration[idx] = add_entry_to_box(hbox, NULL);
entry_gas[idx] = add_gas_combobox_to_box(hbox, NULL, idx);
entry_po2[idx] = add_entry_to_box(hbox, NULL);
}
gtk_widget_add_events(entry_depth[idx], GDK_FOCUS_CHANGE_MASK);
g_signal_connect(entry_depth[idx], "focus-out-event", G_CALLBACK(depth_focus_out_cb), NULL + idx);
gtk_widget_add_events(entry_duration[idx], GDK_FOCUS_CHANGE_MASK);
g_signal_connect(entry_duration[idx], "focus-out-event", G_CALLBACK(duration_focus_out_cb), NULL + idx);
gtk_widget_add_events(entry_po2[idx], GDK_FOCUS_CHANGE_MASK);
g_signal_connect(entry_po2[idx], "focus-out-event", G_CALLBACK(po2_focus_out_cb), NULL + idx);
}
static void add_waypoint_cb(GtkButton *button, gpointer _data)
{
GtkWidget *vbox = _data;
if (nr_waypoints < MAX_WAYPOINTS) {
GtkWidget *ovbox, *dialog;
add_waypoint_widgets(vbox, nr_waypoints);
nr_waypoints++;
ovbox = gtk_widget_get_parent(GTK_WIDGET(button));
dialog = gtk_widget_get_parent(ovbox);
gtk_widget_show_all(dialog);
} else {
show_error(_("Too many waypoints"));
}
}
static void add_entry_with_callback(GtkWidget *box, int length, char *label, char *initialtext,
gboolean (*callback)(GtkWidget *, GdkEvent *, gpointer), gpointer data)
{
GtkWidget *entry = add_entry_to_box(box, label);
gtk_entry_set_max_length(GTK_ENTRY(entry), length);
gtk_entry_set_text(GTK_ENTRY(entry), initialtext);
gtk_widget_add_events(entry, GDK_FOCUS_CHANGE_MASK);
g_signal_connect(entry, "focus-out-event", G_CALLBACK(callback), data);
}
/* set up the dialog where the user can input their dive plan */
void input_plan()
{
GtkWidget *content, *vbox, *hbox, *outervbox, *add_row, *label;
char *bottom_sac, *deco_sac, gflowstring[4], gfhighstring[4];
char *explanationtext = _("<small>Add segments below.\nEach line describes part of the planned dive.\n"
"An entry with depth, time and gas describes a segment that ends "
"at the given depth, takes the given time (if relative, e.g. '+3:30') "
"or ends at the given time (if absolute), and uses the given gas.\n"
"An empty gas means 'use previous gas' (or AIR if no gas was specified).\n"
"An entry that has a depth and a gas given but no time is special; it "
"informs the planner that the gas specified is available for the ascent "
"once the depth given has been reached.\n"
"CC SetPoint specifies CC (rebreather) dives, leave empty for OC.</small>\n");
char *labeltext;
int len;
disclaimer = _("DISCLAIMER / WARNING: THIS IS A NEW IMPLEMENTATION OF THE BUHLMANN "
"ALGORITHM AND A DIVE PLANNER IMPLEMENTION BASED ON THAT WHICH HAS "
"RECEIVED ONLY A LIMITED AMOUNT OF TESTING. WE STRONGLY RECOMMEND NOT TO "
"PLAN DIVES SIMPLY BASED ON THE RESULTS GIVEN HERE.");
if (diveplan.dp)
free_dps(diveplan.dp);
memset(&diveplan, 0, sizeof(diveplan));
diveplan.lastdive_nr = dive_table.nr - 1;
free(cache_data);
cache_data = NULL;
planned_dive = NULL;
planner = gtk_dialog_new_with_buttons(_("Dive Plan - THIS IS JUST A SIMULATION; DO NOT USE FOR DIVING"),
GTK_WINDOW(main_window),
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_STOCK_OK, GTK_RESPONSE_ACCEPT,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
NULL);
content = gtk_dialog_get_content_area (GTK_DIALOG (planner));
outervbox = gtk_vbox_new(FALSE, 2);
gtk_container_add (GTK_CONTAINER (content), outervbox);
len = strlen(explanationtext) + strlen(disclaimer) + sizeof("<span foreground='red'></span>");
labeltext = malloc(len);
snprintf(labeltext, len, "%s<span foreground='red'>%s</span>", explanationtext, disclaimer);
label = gtk_label_new(labeltext);
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
gtk_label_set_width_chars(GTK_LABEL(label), 60);
gtk_box_pack_start(GTK_BOX(outervbox), label, TRUE, TRUE, 0);
vbox = gtk_vbox_new(FALSE, 0);
gtk_box_pack_start(GTK_BOX(outervbox), vbox, TRUE, TRUE, 0);
hbox = gtk_hbox_new(FALSE, 0);
gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
add_entry_with_callback(hbox, 12, _("Dive starts when?"), "+60:00", starttime_focus_out_cb, NULL);
add_entry_with_callback(hbox, 12, _("Surface Pressure (mbar)"), SURFACE_PRESSURE_STRING, surfpres_focus_out_cb, NULL);
hbox = gtk_hbox_new(FALSE, 0);
gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
if (get_units()->volume == CUFT) {
bottom_sac = _("0.7 cuft/min");
deco_sac = _("0.6 cuft/min");
diveplan.bottomsac = 1000 * cuft_to_l(0.7);
diveplan.decosac = 1000 * cuft_to_l(0.6);
} else {
bottom_sac = _("20 l/min");
deco_sac = _("17 l/min");
diveplan.bottomsac = 20000;
diveplan.decosac = 17000;
}
add_entry_with_callback(hbox, 12, _("SAC during dive"), bottom_sac, sac_focus_out_cb, &diveplan.bottomsac);
add_entry_with_callback(hbox, 12, _("SAC during decostop"), deco_sac, sac_focus_out_cb, &diveplan.decosac);
plangflow = prefs.gflow;
plangfhigh = prefs.gfhigh;
snprintf(gflowstring, sizeof(gflowstring), "%3.0f", 100 * plangflow);
snprintf(gfhighstring, sizeof(gflowstring), "%3.0f", 100 * plangfhigh);
add_entry_with_callback(hbox, 5, _("GFlow for plan"), gflowstring, gf_focus_out_cb, &plangflow);
add_entry_with_callback(hbox, 5, _("GFhigh for plan"), gfhighstring, gf_focus_out_cb, &plangfhigh);
diveplan.when = current_time_notz() + 3600;
diveplan.surface_pressure = SURFACE_PRESSURE;
nr_waypoints = 4;
add_waypoint_widgets(vbox, 0);
add_waypoint_widgets(vbox, 1);
add_waypoint_widgets(vbox, 2);
add_waypoint_widgets(vbox, 3);
add_row = gtk_button_new_with_label(_("Add waypoint"));
g_signal_connect(G_OBJECT(add_row), "clicked", G_CALLBACK(add_waypoint_cb), vbox);
gtk_box_pack_start(GTK_BOX(outervbox), add_row, FALSE, FALSE, 0);
gtk_widget_show_all(planner);
if (gtk_dialog_run(GTK_DIALOG(planner)) == GTK_RESPONSE_ACCEPT) {
plan(&diveplan, &cache_data, &planned_dive);
mark_divelist_changed(TRUE);
} else {
if (planned_dive) {
/* we have added a dive during the dynamic construction
* in the dialog; get rid of it */
delete_single_dive(dive_table.nr - 1);
report_dives(FALSE, FALSE);
planned_dive = NULL;
}
}
gtk_widget_destroy(planner);
planner_error_bar = NULL;
error_label = NULL;
set_gf(prefs.gflow, prefs.gfhigh);
}