Planner: Improve Gas Handling in CCR Mode.

This has become a bit of a catch-all overhaul of a large portion of the
planner - I started out wanting to improve the CCR mode, but then as I
started pulling all the other threads that needed addressing started to
come with it.

Improve how the gas selection is handled when planning dives in CCR
mode, by making the type (OC / CCR) of segments dependent on the gas use
type that was set for the selected gas.
Add a preference to allow the user to chose to use OC gases as diluent,
in a similar fashion to the original implementation.
Hide gases that cannot be used in the currently selected dive mode in
all drop downs.
Include usage type in gas names if this is needed.
Hide columns and disable elements in the 'Dive planner points' table if
they can they can not be edited in the curently selected dive mode.
Visually identify gases and usage types that are not appropriate for the
currently selected dive mode.
Move the 'Dive mode' selection to the top of the planner view, to
accommodate the fact that this is a property of the dive and not a
planner setting.
Show a warning instead of the dive plan if the plan contains gases that
are not usable in the selected dive mode.
Fix the data entry for the setpoint in the 'Dive planner points' table.
Fix problems with enabling / disabling planner settings when switching
between dive modes.
Refactor some names to make them more appropriate for their current
usage.

One point that is still open is to hide gas usage graphs in the planner
profile if the gas isn't used for OC, as there is no way to meaningfully
interpolate such usage.

Signed-off-by: Michael Keller <github@ike.ch>
This commit is contained in:
Michael Keller 2024-05-15 17:23:39 +12:00
parent 7106c4d5f0
commit 2d8e343221
63 changed files with 678 additions and 383 deletions

View file

@ -1625,6 +1625,35 @@ static bool cylinder_in_use(const struct dive *dive, int idx)
return cylinder_has_data(dive->cylinders[idx]);
}
bool is_cylinder_use_appropriate(const struct divecomputer &dc, const cylinder_t &cyl, bool allowNonUsable)
{
switch (cyl.cylinder_use) {
case OC:
if (dc.divemode == FREEDIVE)
return false;
break;
case OXYGEN:
if (!allowNonUsable)
return false;
case DILUENT:
if (dc.divemode != CCR)
return false;
break;
case NOT_USED:
if (!allowNonUsable)
return false;
break;
default:
return false;
}
return true;
}
/*
* Merging cylinder information is non-trivial, because the two dive computers
* may have different ideas of what the different cylinder indexing is.
@ -2451,7 +2480,7 @@ int dive::mbar_to_depth(int mbar) const
if (!surface_pressure.mbar)
surface_pressure.mbar = SURFACE_PRESSURE;
return rel_mbar_to_depth(mbar - surface_pressure.mbar);
}

View file

@ -146,6 +146,7 @@ struct dive_or_trip {
extern void cylinder_renumber(struct dive &dive, int mapping[]);
extern int same_gasmix_cylinder(const cylinder_t &cyl, int cylid, const struct dive *dive, bool check_unused);
extern bool is_cylinder_use_appropriate(const struct divecomputer &dc, const cylinder_t &cyl, bool allowNonUsable);
/* Data stored when copying a dive */
struct dive_paste_data {

View file

@ -35,7 +35,7 @@ struct divecomputer {
pressure_t surface_pressure;
enum divemode_t divemode = OC; // dive computer type: OC(default) or CCR
uint8_t no_o2sensors = 0; // rebreathers: number of O2 sensors used
int salinity = 0; // kg per 10000 l
int salinity = 0; // kg per 10000 l
std::string model, serial, fw_version;
uint32_t deviceid = 0, diveid = 0;
// Note: ve store samples, events and extra_data in std::vector<>s.

View file

@ -4,4 +4,6 @@
enum divemode_t {OC, CCR, PSCR, FREEDIVE, NUM_DIVEMODE, UNDEF_COMP_TYPE}; // Flags (Open-circuit and Closed-circuit-rebreather) for setting dive computer type
#define IS_REBREATHER_MODE(divemode) ((divemode) == CCR || (divemode) == PSCR)
#endif

View file

@ -380,14 +380,17 @@ static int setpoint_change(struct dive *dive, int cylinderid)
}
}
static std::vector<gaschanges> analyze_gaslist(struct diveplan *diveplan, struct dive *dive, int depth, int *asc_cylinder, bool ccr)
static std::vector<gaschanges> analyze_gaslist(struct diveplan *diveplan, struct dive *dive, int dcNr, int depth, int *asc_cylinder, bool ccr, bool &inappropriate_cylinder_use)
{
size_t nr = 0;
std::vector<gaschanges> gaschanges;
struct divedatapoint *dp = diveplan->dp;
struct divedatapoint *best_ascent_dp = NULL;
bool total_time_zero = true;
const divecomputer *dc = dive->get_dc(dcNr);
while (dp) {
inappropriate_cylinder_use = inappropriate_cylinder_use || !is_cylinder_use_appropriate(*dc, *dive->get_cylinder(dp->cylinderid), false);
if (dp->time == 0 && total_time_zero && (ccr == (bool) setpoint_change(dive, dp->cylinderid))) {
if (dp->depth.mm <= depth) {
int i = 0;
@ -661,7 +664,7 @@ bool plan(struct deco_state *ds, struct diveplan *diveplan, struct dive *dive, i
bool o2break_next = false;
int break_cylinder = -1, breakfrom_cylinder = 0;
bool last_segment_min_switch = false;
bool error = false;
planner_error_t error = PLAN_OK;
bool decodive = false;
int first_stop_depth = 0;
int laststoptime = timestep;
@ -744,7 +747,11 @@ bool plan(struct deco_state *ds, struct diveplan *diveplan, struct dive *dive, i
/* Find the gases available for deco */
std::vector<gaschanges> gaschanges = analyze_gaslist(diveplan, dive, depth, &best_first_ascend_cylinder, divemode == CCR && !prefs.dobailout);
bool inappropriate_cylinder_use = false;
std::vector<gaschanges> gaschanges = analyze_gaslist(diveplan, dive, dcNr, depth, &best_first_ascend_cylinder, divemode == CCR && !prefs.dobailout, inappropriate_cylinder_use);
if (inappropriate_cylinder_use) {
error = PLAN_ERROR_INAPPROPRIATE_GAS;
}
/* Find the first potential decostopdepth above current depth */
for (stopidx = 0; stopidx < decostoplevelcount; stopidx++)
@ -880,7 +887,7 @@ bool plan(struct deco_state *ds, struct diveplan *diveplan, struct dive *dive, i
current_cylinder = 0;
}
reset_regression(ds);
while (1) {
while (error == PLAN_OK) {
/* We will break out when we hit the surface */
do {
/* Ascend to next stop depth */
@ -1005,7 +1012,8 @@ bool plan(struct deco_state *ds, struct diveplan *diveplan, struct dive *dive, i
laststoptime = new_clock - clock;
/* Finish infinite deco */
if (laststoptime >= 48 * 3600 && depth >= 6000) {
error = true;
error = PLAN_ERROR_TIMEOUT;
break;
}
@ -1070,7 +1078,7 @@ bool plan(struct deco_state *ds, struct diveplan *diveplan, struct dive *dive, i
* if the ascent rate is slower, which is completely nonsensical.
* Assume final ascent takes 20s, which is the time taken to ascend at 9m/min from 3m */
ds->deco_time = clock - bottom_time - (M_OR_FT(3,10) * ( prefs.last_stop ? 2 : 1)) / last_ascend_rate + 20;
} while (!is_final_plan);
} while (!is_final_plan && error == PLAN_OK);
decostoptable[decostopcounter].depth = 0;
plan_add_segment(diveplan, clock - previous_point_time, 0, current_cylinder, po2, false, divemode);
@ -1112,24 +1120,31 @@ static int get_decimals(const char *begin, const char **endp, const unsigned dec
return -1;
/* Fraction? We only look at the first digit */
if (*end == '.') {
unsigned fraction = 0;
for (unsigned i = 0; i < decimals; i++) {
value *= 10;
if (*end == '.')
end++;
unsigned fraction = 0;
for (unsigned i = 0; i < decimals; i++) {
value *= 10;
unsigned digit = 0;
if (isdigit(*end)) {
digit = *end - '0';
end++;
if (!isdigit(*end))
return -1;
fraction = 10 * fraction + (*end - '0');
} else if (*end != '\0') {
return -1;
}
value += fraction;
do {
end++;
} while (isdigit(*end));
fraction = 10 * fraction + digit;
}
value += fraction;
do {
end++;
} while (isdigit(*end));
*endp = end;
return value;
}
@ -1209,5 +1224,9 @@ int validate_po2(const char *text, int *mbar_po2)
return 0;
*mbar_po2 = po2 * 10;
if (*mbar_po2 < 160)
*mbar_po2 = 160;
return 1;
}

View file

@ -34,11 +34,17 @@ struct diveplan {
struct deco_state_cache;
typedef enum {
PLAN_OK,
PLAN_ERROR_TIMEOUT,
PLAN_ERROR_INAPPROPRIATE_GAS,
} planner_error_t;
extern int validate_gas(const char *text, struct gasmix *gas);
extern int validate_po2(const char *text, int *mbar_po2);
extern int get_cylinderid_at_time(struct dive *dive, struct divecomputer *dc, duration_t time);
extern bool diveplan_empty(struct diveplan *diveplan);
extern void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_disclaimer, bool error);
extern void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_disclaimer, planner_error_t error);
extern const char *get_planner_disclaimer();
extern void free_dps(struct diveplan *diveplan);

View file

@ -96,7 +96,7 @@ extern std::string get_planner_disclaimer_formatted()
return format_string_std(get_planner_disclaimer(), deco);
}
void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_disclaimer, bool error)
void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_disclaimer, planner_error_t error)
{
std::string buf;
std::string icdbuf;
@ -122,12 +122,31 @@ void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_d
if (!dp)
return;
if (error) {
if (error != PLAN_OK) {
const char *message;
switch (error) {
case PLAN_ERROR_TIMEOUT:
message = translate("gettextFromC", "Decompression calculation aborted due to excessive time");
break;
case PLAN_ERROR_INAPPROPRIATE_GAS:
message = translate("gettextFromC", "One or more tanks with a tank use type inappropriate for the selected dive mode are included in the dive plan. "
"Please change them to appropriate tanks to enable the generation of a dive plan.");
break;
default:
message = translate("gettextFromC", "An error occurred during dive plan generation");
break;
}
buf += format_string_std("<span style='color: red;'>%s </span> %s<br/>",
translate("gettextFromC", "Warning:"),
translate("gettextFromC", "Decompression calculation aborted due to excessive time"));
translate("gettextFromC", "Warning:"), message);
// TODO: avoid copy
dive->notes = buf;
dive->notes = strdup(buf.c_str());
return;
}

View file

@ -93,6 +93,7 @@ preferences::preferences() :
vpmb_conservatism(3),
zoomed_plot(false),
infobox(true),
allowOcGasAsDiluent(false),
coordinates_traditional(true),
unit_system(METRIC),
units(SI_UNITS)

View file

@ -201,6 +201,7 @@ struct preferences {
int vpmb_conservatism;
bool zoomed_plot;
bool infobox;
bool allowOcGasAsDiluent;
// ********** Units **********
bool coordinates_traditional;

View file

@ -1151,19 +1151,51 @@ QString get_gas_string(struct gasmix gas)
return result;
}
QStringList get_dive_gas_list(const struct dive *d)
QString get_dive_gas(const struct dive *d, int dcNr, int cylinderId)
{
QStringList list;
for (auto [i, cyl]: enumerated_range(d->cylinders)) {
/* Check if we have the same gasmix two or more times
* If yes return more verbose string */
int same_gas = same_gasmix_cylinder(cyl, i, d, true);
if (same_gas == -1)
list.push_back(get_gas_string(cyl.gasmix));
else
list.push_back(get_gas_string(cyl.gasmix) + QStringLiteral(" (%1 %2 ").arg(gettextFromC::tr("cyl.")).arg(i + 1) +
QString::fromStdString(cyl.type.description) + ")");
const cylinder_t *cyl = d->get_cylinder(cylinderId);
const divecomputer *dc = d->get_dc(dcNr);
bool showUse = (dc->divemode == CCR) | !is_cylinder_use_appropriate(*dc, *cyl, false);
QString gasType = get_gas_string(cyl->gasmix);
QString gasName;
/* Check if we have the same gasmix two or more times
* If yes return more verbose string */
int same_gas = same_gasmix_cylinder(*cyl, cylinderId, d, true);
if (same_gas != -1) {
gasType += QString(" (%1 %2").arg(gettextFromC::tr("cyl.")).arg(cylinderId + 1);
gasName = QString::fromStdString(d->get_cylinder(cylinderId)->type.description);
}
if (showUse) {
if (gasName.isNull())
gasType += " (";
else
gasType += ", ";
gasType += gettextFromC::tr(cylinderuse_text[d->get_cylinder(cylinderId)->cylinder_use]);
}
if (!gasName.isNull())
gasType += QString(", %1").arg(gasName);
if (!gasName.isNull() || showUse)
gasType += ")";
return gasType;
}
std::vector<std::pair<int, QString>> get_dive_gas_list(const struct dive *d, int dcNr, bool showOnlyAppropriate)
{
const divecomputer *dc = d->get_dc(dcNr);
std::vector<std::pair<int, QString>> list;
for (unsigned int i = 0; i < d->cylinders.size(); i++) {
if (showOnlyAppropriate && !is_cylinder_use_appropriate(*dc, *d->get_cylinder(i), false))
continue;
list.push_back(std::pair(i, get_dive_gas(d, dcNr, i)));
}
return list;
}

View file

@ -28,7 +28,8 @@ enum watertypes {FRESHWATER, BRACKISHWATER, EN13319WATER, SALTWATER, DC_WATERTYP
QString distance_string(int distanceInMeters);
bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out = 0);
QString get_gas_string(struct gasmix gas);
QStringList get_dive_gas_list(const struct dive *d);
QString get_dive_gas(const struct dive *d, int dcNr, int cylinderId);
std::vector<std::pair<int, QString>> get_dive_gas_list(const struct dive *d, int dcNr, bool showOnlyAppropriate);
QStringList stringToList(const QString &s);
void read_hashes();
void write_hashes();

View file

@ -14,6 +14,7 @@ qPrefTechnicalDetails *qPrefTechnicalDetails::instance()
void qPrefTechnicalDetails::loadSync(bool doSync)
{
disk_allowOcGasAsDiluent(doSync);
disk_calcalltissues(doSync);
disk_calcceiling(doSync);
disk_calcceiling3m(doSync);
@ -100,6 +101,8 @@ void qPrefTechnicalDetails::disk_gflow(bool doSync)
}
}
HANDLE_PREFERENCE_BOOL(TechnicalDetails, "allowOcGasAsDiluent", allowOcGasAsDiluent);
HANDLE_PREFERENCE_BOOL(TechnicalDetails, "gf_low_at_maxdepth", gf_low_at_maxdepth);
HANDLE_PREFERENCE_BOOL(TechnicalDetails, "InfoBox", infobox);

View file

@ -7,6 +7,7 @@
class qPrefTechnicalDetails : public QObject {
Q_OBJECT
Q_PROPERTY(bool allowOcGasAsDiluent READ allowOcGasAsDiluent WRITE set_allowOcGasAsDiluent NOTIFY allowOcGasAsDiluentChanged)
Q_PROPERTY(bool calcalltissues READ calcalltissues WRITE set_calcalltissues NOTIFY calcalltissuesChanged)
Q_PROPERTY(bool calcceiling READ calcceiling WRITE set_calcceiling NOTIFY calcceilingChanged)
Q_PROPERTY(bool calcceiling3m READ calcceiling3m WRITE set_calcceiling3m NOTIFY calcceiling3mChanged)
@ -44,6 +45,7 @@ public:
static void sync() { loadSync(true); }
public:
static bool allowOcGasAsDiluent() { return prefs.allowOcGasAsDiluent; }
static bool calcalltissues() { return prefs.calcalltissues; }
static bool calcceiling() { return prefs.calcceiling; }
static bool calcceiling3m() { return prefs.calcceiling3m; }
@ -73,6 +75,7 @@ public:
static bool infobox() { return prefs.infobox; }
public slots:
static void set_allowOcGasAsDiluent(bool value);
static void set_calcalltissues(bool value);
static void set_calcceiling(bool value);
static void set_calcceiling3m(bool value);
@ -102,6 +105,7 @@ public slots:
static void set_infobox(bool value);
signals:
void allowOcGasAsDiluentChanged(bool value);
void calcalltissuesChanged(bool value);
void calcceilingChanged(bool value);
void calcceiling3mChanged(bool value);
@ -133,6 +137,7 @@ signals:
private:
qPrefTechnicalDetails() {}
static void disk_allowOcGasAsDiluent(bool doSync);
static void disk_calcalltissues(bool doSync);
static void disk_calcceiling(bool doSync);
static void disk_calcceiling3m(bool doSync);