// SPDX-License-Identifier: GPL-2.0 #include "statsvariables.h" #include "statstranslations.h" #include "core/dive.h" #include "core/divelog.h" #include "core/divemode.h" #include "core/divesite.h" #include "core/gas.h" #include "core/pref.h" #include "core/qthelper.h" // for get_depth_unit() et al. #include "core/string-format.h" #include "core/tag.h" #include "core/trip.h" #include "core/subsurface-time.h" #include #include #include static constexpr double NaN = std::numeric_limits::quiet_NaN(); // Typedefs for year / quarter or month binners using year_quarter = std::pair; using year_month = std::pair; // Small helper template: add an item to an unsorted vector, if its not already there. template static void add_to_vector_unique(std::vector &v, const T &item) { if (std::find(v.begin(), v.end(), item) == v.end()) v.push_back(item); } // Small helper: make a comma separeted list of a vector of QStrings static QString join_strings(const std::vector &v) { QString res; for (const QString &s: v) { if (!res.isEmpty()) res += ", "; res += s; } return res; } // A wrapper around dive site that caches the name of the dive site struct DiveSiteWrapper { const dive_site *ds; QString name; DiveSiteWrapper(const dive_site *ds) : ds(ds), name(ds ? QString::fromStdString(ds->name) : QString()) { } bool operator<(const DiveSiteWrapper &d2) const { if (ds == d2.ds) return false; if (int cmp = QString::compare(name, d2.name, Qt::CaseInsensitive)) return cmp < 0; return ds < d2.ds; // This is just random, try something better. } bool operator==(const DiveSiteWrapper &d2) const { return ds == d2.ds; } bool operator!=(const DiveSiteWrapper &d2) const { return ds != d2.ds; } QString format() const { return ds ? name : StatsTranslations::tr("no divesite"); } }; // A wrapper around dive trips that caches the name and date of the trip and sorts by trip start date struct TripWrapper { const dive_trip *t; QString name; timestamp_t date; TripWrapper(const dive_trip *t) : t(t), name(t ? formatTripTitle(*t) : QString()), date(t ? t->date() : 0) { } bool operator<(const TripWrapper &t2) const { if (t == t2.t) return false; if (!t) return true; if (date == t2.date) { if (int cmp = QString::compare(name, t2.name, Qt::CaseInsensitive)) return cmp < 0; return t < t2.t; // Basically random, but stable. What should we do if name and date are equal? } return date < t2.date; } bool operator==(const TripWrapper &t2) const { return t == t2.t; } bool operator!=(const TripWrapper &t2) const { return t != t2.t; } QString format() const { return t ? name : StatsTranslations::tr("no trip"); } }; // Note: usually I dislike functions defined inside class/struct // declarations ("Java style"). However, for brevity this is done // in this rather template-heavy source file more or less consistently. // Templates to define invalid values for variables and test for said values. // This is used by the binners: returning such a value means "ignore this dive". template T invalid_value(); template<> int invalid_value() { return std::numeric_limits::max(); } template<> double invalid_value() { return NaN; } template<> QString invalid_value() { return QString(); } template<> StatsQuartiles invalid_value() { return { std::vector(), NaN, NaN, NaN, NaN, NaN }; } static bool is_invalid_value(int i) { return i == std::numeric_limits::max(); } static bool is_invalid_value(double d) { return std::isnan(d); } static bool is_invalid_value(const QString &s) { return s.isEmpty(); } // Currently, we don't support invalid dates - should we? static bool is_invalid_value(const year_quarter &) { return false; } static bool is_invalid_value(const DiveSiteWrapper &d) { return !d.ds; } static bool is_invalid_value(const TripWrapper &t) { return !t.t; } static bool is_invalid_value(const StatsOperationResults &res) { return !res.isValid(); } // Describes a gas-content bin. Since we consider O2 and He, these bins // are effectively two-dimensional. However, they are given a linear order // by sorting lexicographically. // This is different from the general type of the gas, which is defined in // gas.h as the gastype enum. The latter does not bin by fine-grained // percentages. struct gas_bin_t { // Depending on the gas content, we format the bins differently. // Eg. in the absence of helium and inreased oxygen, the bin is // formatted as "EAN32". The format is specified by the type enum. enum class Type { Air, Oxygen, EAN, Trimix } type; int o2, he; static gas_bin_t air() { return { Type::Air, 0, 0 }; } static gas_bin_t oxygen() { return { Type::Oxygen, 0, 0 }; } static gas_bin_t ean(int o2) { return { Type::EAN, o2, 0 }; } static gas_bin_t trimix(int o2, int h2) { return { Type::Trimix, o2, h2 }; } }; // We never generate gas_bins of invalid gases. static bool is_invalid_value(const gas_bin_t &) { return false; } static bool is_invalid_value(const std::vector &v) { return v.empty(); } static bool is_invalid_value(const StatsQuartiles &q) { return q.dives.empty(); } bool StatsQuartiles::isValid() const { return !is_invalid_value(*this); } // Define an ordering for gas types // invalid < air < ean (including oxygen) < trimix // The latter two are sorted by (helium, oxygen) // This is in analogy to the dive::get_maximal_gas() function. static bool operator<(const gas_bin_t &t1, const gas_bin_t &t2) { if (t1.type != t2.type) return (int)t1.type < (int)t2.type; switch (t1.type) { default: case gas_bin_t::Type::Oxygen: case gas_bin_t::Type::Air: return false; case gas_bin_t::Type::EAN: return t1.o2 < t2.o2; case gas_bin_t::Type::Trimix: return std::tie(t1.o2, t1.he) < std::tie(t2.o2, t2.he); } } static bool operator==(const gas_bin_t &t1, const gas_bin_t &t2) { return std::tie(t1.type, t1.o2, t1.he) == std::tie(t2.type, t2.o2, t2.he); } static bool operator!=(const gas_bin_t &t1, const gas_bin_t &t2) { return !operator==(t1, t2); } // First, let's define the virtual destructors of our base classes StatsBin::~StatsBin() { } StatsBinner::~StatsBinner() { } QString StatsBinner::unitSymbol() const { return QString(); } StatsVariable::~StatsVariable() { } QString StatsBinner::name() const { return QStringLiteral("N/A"); // Some dummy string that should never reach the UI } QString StatsBinner::formatWithUnit(const StatsBin &bin) const { QString unit = unitSymbol(); QString name = format(bin); return unit.isEmpty() ? std::move(name) : QStringLiteral("%1 %2").arg(name, unit); } QString StatsBinner::formatLowerBound(const StatsBin &bin) const { return QStringLiteral("N/A"); // Some dummy string that should never reach the UI } QString StatsBinner::formatUpperBound(const StatsBin &bin) const { return QStringLiteral("N/A"); // Some dummy string that should never reach the UI } double StatsBinner::lowerBoundToFloat(const StatsBin &bin) const { return 0.0; } double StatsBinner::upperBoundToFloat(const StatsBin &bin) const { return 0.0; } bool StatsBinner::preferBin(const StatsBin &bin) const { return true; } // Default implementation for discrete variables: there are no bins between discrete bins. std::vector StatsBinner::bins_between(const StatsBin &bin1, const StatsBin &bin2) const { return {}; } QString StatsVariable::unitSymbol() const { return {}; } QString StatsVariable::diveCategories(const dive *) const { return QString(); } int StatsVariable::decimals() const { return 0; } double StatsVariable::toFloat(const dive *d) const { return invalid_value(); } QString StatsVariable::nameWithUnit() const { QString s = name(); QString symb = unitSymbol(); return symb.isEmpty() ? std::move(s) : QStringLiteral("%1 [%2]").arg(s, symb); } QString StatsVariable::nameWithBinnerUnit(const StatsBinner &binner) const { QString s = name(); QString symb = binner.unitSymbol(); return symb.isEmpty() ? std::move(s) : QStringLiteral("%1 [%2]").arg(s, symb); } const StatsBinner *StatsVariable::getBinner(int idx) const { std::vector b = binners(); if (b.empty()) return nullptr; return idx >= 0 && idx < (int)b.size() ? b[idx] : b[0]; } std::vector StatsVariable::supportedOperations() const { return {}; } // Attn: The order must correspond to the StatsOperation enum static const char *operation_names[] = { QT_TRANSLATE_NOOP("StatsTranslations", "Median"), QT_TRANSLATE_NOOP("StatsTranslations", "Mean"), QT_TRANSLATE_NOOP("StatsTranslations", "Time-weighted mean"), QT_TRANSLATE_NOOP("StatsTranslations", "Sum"), QT_TRANSLATE_NOOP("StatsTranslations", "Minimum"), QT_TRANSLATE_NOOP("StatsTranslations", "Maximum") }; QStringList StatsVariable::supportedOperationNames() const { std::vector ops = supportedOperations(); QStringList res; res.reserve(ops.size()); for (StatsOperation op: ops) res.push_back(operationName(op)); return res; } StatsOperation StatsVariable::idxToOperation(int idx) const { std::vector ops = supportedOperations(); if (ops.empty()) { qWarning("Stats variable %s does not support operations", qPrintable(name())); return StatsOperation::Median; // oops! } return idx < 0 || idx >= (int)ops.size() ? ops[0] : ops[idx]; } QString StatsVariable::operationName(StatsOperation op) { int idx = (int)op; return idx < 0 || idx >= (int)std::size(operation_names) ? QString() : operation_names[(int)op]; } double StatsVariable::mean(const std::vector &dives) const { StatsOperationResults res = applyOperations(dives); return res.isValid() ? res.mean : invalid_value(); } std::vector StatsVariable::values(const std::vector &dives) const { std::vector vec; vec.reserve(dives.size()); for (dive *d: dives) { double v = toFloat(d); if (!is_invalid_value(v)) vec.push_back({ v, d }); } std::sort(vec.begin(), vec.end(), [](const StatsValue &v1, const StatsValue &v2) { return v1.v < v2.v; }); return vec; } QString StatsVariable::valueWithUnit(const dive *d) const { QLocale loc; double v = toFloat(d); if (is_invalid_value(v)) return QStringLiteral("-"); return QString("%1 %2").arg(loc.toString(v, 'f', decimals()), unitSymbol()); } // Small helper to calculate quartiles - section of intervals of // two consecutive elements in a vector. It's not strictly correct // to interpolate linearly. However, on the one hand we don't know // the actual distribution, on the other hand for a discrete // distribution the quartiles are ranges. So what should we do? static double q1(const StatsValue *v) { return (3.0*v[0].v + v[1].v) / 4.0; } static double q2(const StatsValue *v) { return (v[0].v + v[1].v) / 2.0; } static double q3(const StatsValue *v) { return (v[0].v + 3.0*v[1].v) / 4.0; } StatsQuartiles StatsVariable::quartiles(const std::vector &dives) const { return quartiles(values(dives)); } // This expects the value vector to be sorted! StatsQuartiles StatsVariable::quartiles(const std::vector &vec) { int s = (int)vec.size(); if (s <= 0) return invalid_value(); std::vector dives; dives.reserve(vec.size()); for (const auto &[v, d]: vec) dives.push_back(d); switch (s % 4) { default: // gcc doesn't recognize that we catch all possible values. disappointing. case 0: return { std::move(dives), vec[0].v, q3(&vec[s/4 - 1]), q2(&vec[s/2 - 1]), q1(&vec[s - s/4 - 1]), vec[s - 1].v }; case 1: return { std::move(dives), vec[0].v, vec[s/4].v, vec[s/2].v, vec[s - s/4 - 1].v, vec[s - 1].v }; case 2: return { std::move(dives), vec[0].v, q1(&vec[s/4]), q2(&vec[s/2 - 1]), q3(&vec[s - s/4 - 2]), vec[s - 1].v }; case 3: return { std::move(dives), vec[0].v, q2(&vec[s/4]), vec[s/2].v, q2(&vec[s - s/4 - 2]), vec[s - 1].v }; } } StatsOperationResults StatsVariable::applyOperations(const std::vector &dives) const { StatsOperationResults res; std::vector val = values(dives); double sumTime = 0.0; res.dives.reserve(val.size()); res.median = quartiles(val).q2; if (val.empty()) return res; res.min = std::numeric_limits::max(); res.max = std::numeric_limits::lowest(); for (auto [v, d]: val) { res.dives.push_back(d); res.sum += v; res.mean += v; sumTime += d->duration.seconds; res.timeWeightedMean += v * d->duration.seconds; if (v < res.min) res.min = v; if (v > res.max) res.max = v; } res.mean /= val.size(); res.timeWeightedMean /= sumTime; return res; } StatsOperationResults::StatsOperationResults() : median(0.0), mean(0.0), timeWeightedMean(0.0), sum(0.0), min(0.0), max(0.0) { } bool StatsOperationResults::isValid() const { return !dives.empty(); } double StatsOperationResults::get(StatsOperation op) const { switch (op) { case StatsOperation::Median: return median; case StatsOperation::Mean: return mean; case StatsOperation::TimeWeightedMean: return timeWeightedMean; case StatsOperation::Sum: return sum; case StatsOperation::Min: return min; case StatsOperation::Max: return max; case StatsOperation::Invalid: default: return invalid_value(); } } std::vector StatsVariable::scatter(const StatsVariable &t2, const std::vector &dives) const { std::vector res; res.reserve(dives.size()); for (dive *d: dives) { double v1 = toFloat(d); double v2 = t2.toFloat(d); if (is_invalid_value(v1) || is_invalid_value(v2)) continue; res.push_back({ v1, v2, d }); } std::sort(res.begin(), res.end(), [](const StatsScatterItem &i1, const StatsScatterItem &i2) { return std::tie(i1.x, i1.y) < std::tie(i2.x, i2.y); }); // use std::tie() for lexicographical comparison return res; } template std::vector> bin_convert(const StatsVariable &variable, const StatsBinner &binner, const std::vector &dives, bool fill_empty, DivesToValueFunc func) { std::vector bin_dives = binner.bin_dives(dives, fill_empty); std::vector> res; res.reserve(bin_dives.size()); for (auto &[bin, dives]: bin_dives) { T v = func(dives); if (is_invalid_value(v) && (res.empty() || !fill_empty)) continue; res.push_back({ std::move(bin), std::move(v) }); } if (res.empty()) return res; // Check if we added invalid items at the end. // Note: we added at least one valid item. auto it = res.end() - 1; while (it != res.begin() && is_invalid_value(it->value)) --it; res.erase(it + 1, res.end()); return res; } std::vector StatsVariable::bin_quartiles(const StatsBinner &binner, const std::vector &dives, bool fill_empty) const { return bin_convert(*this, binner, dives, fill_empty, [this](const std::vector &d) { return quartiles(d); }); } std::vector StatsVariable::bin_operations(const StatsBinner &binner, const std::vector &dives, bool fill_empty) const { return bin_convert(*this, binner, dives, fill_empty, [this](const std::vector &d) { return applyOperations(d); }); } std::vector StatsVariable::bin_values(const StatsBinner &binner, const std::vector &dives, bool fill_empty) const { return bin_convert>(*this, binner, dives, fill_empty, [this](const std::vector &d) { return values(d); }); } // Silly template, which spares us defining type() member functions. template struct StatsVariableTemplate : public StatsVariable { Type type() const override { return t; } }; // A simple bin that is based on copyable value and can be initialized from // that value. This template spares us from writing one-line constructors. template struct SimpleBin : public StatsBin { Type value; SimpleBin(const Type &v) : value(v) { } // This must not be called for different types. It will crash with an exception. bool operator<(StatsBin &b) const { return value < dynamic_cast(b).value; } bool operator==(StatsBin &b) const { return value == dynamic_cast(b).value; } }; using IntBin = SimpleBin; using StringBin = SimpleBin; using GasTypeBin = SimpleBin; // A general binner template that works on trivial bins that are based // on a type that is equality and less-than comparable. The derived class // must possess: // - A to_bin_value() function that turns a dive into a value from // which the bins can be constructed. // - A lowerBoundToFloatBase() function that turns the value form // into a double which is understood by the StatsVariable. // The bins must possess: // - A member variable "value" of the type it is constructed with. // Note: this uses the curiously recurring template pattern, which I // dislike, but it is the easiest thing for now. template struct SimpleBinner : public StatsBinner { public: using Type = decltype(Bin::value); std::vector bin_dives(const std::vector &dives, bool fill_empty) const override; const Binner &derived() const { return static_cast(*this); } const Bin &derived_bin(const StatsBin &bin) const { return dynamic_cast(bin); } }; // Wrapper around std::lower_bound that searches for a value in a // vector of pairs. Comparison is made with the first element of the pair. // std::lower_bound does a binary search and this is used to keep a // vector in ascending order. template auto pair_lower_bound(std::vector> &v, const T1 &value) { return std::lower_bound(v.begin(), v.end(), value, [] (const std::pair &entry, const T1 &value) { return entry.first < value; }); } // Register a dive in a (bin_value, value) pair. The second value can be // anything, for example a count or a list of dives. If the bin does not // exist, it is created. The add_dive_func() function increase the second // value accordingly. template void register_bin_value(std::vector> &v, const BinValueType &bin, AddDiveFunc add_dive_func) { // Does that value already exist? auto it = pair_lower_bound(v, bin); if (it == v.end() || it->first != bin) it = v.insert(it, { bin, ValueType() }); // Bin does not exist -> insert at proper location. add_dive_func(it->second); // Register dive } // Turn a (bin-value, value)-pair vector into a (bin, value)-pair vector. // The values are moved out of the first vectors. // If fill_empty is true, missing bins will be completed with a default constructed // value. template std::vector> value_vector_to_bin_vector(const Binner &binner, std::vector> &value_bins, bool fill_empty) { std::vector> res; res.reserve(value_bins.size()); for (const auto &[bin_value, value]: value_bins) { StatsBinPtr b = std::make_unique(bin_value); if (fill_empty && !res.empty()) { // Add empty bins, if any for (StatsBinPtr &bin: binner.bins_between(*res.back().bin, *b)) res.push_back({ std::move(bin), ValueType() }); } res.push_back({ std::move(b), std::move(value)}); } return res; } template std::vector SimpleBinner::bin_dives(const std::vector &dives, bool fill_empty) const { // First, collect a value / dives vector and then produce the final vector // out of that. I wonder if that is premature optimization? using Pair = std::pair>; std::vector value_bins; for (dive *d: dives) { Type value = derived().to_bin_value(d); if (is_invalid_value(value)) continue; register_bin_value(value_bins, value, [d](std::vector &v) { v.push_back(d); }); } // Now, turn that into our result array with allocated bin objects. return value_vector_to_bin_vector(*this, value_bins, fill_empty); } // A simple binner (see above) that works on continuous (or numeric) variables // and can return bin-ranges. The binner must implement an inc() function // that turns a bin into the next-higher bin. template struct SimpleContinuousBinner : public SimpleBinner { using SimpleBinner::derived; std::vector bins_between(const StatsBin &bin1, const StatsBin &bin2) const override; // By default the value gives the lower bound, so the format is the same QString formatLowerBound(const StatsBin &bin) const override { return derived().format(bin); } // For the upper bound, simply go to the next bin QString formatUpperBound(const StatsBin &bin) const override { Bin b = SimpleBinner::derived_bin(bin); derived().inc(b); return formatLowerBound(b); } // Cast to base value type so that the derived class doesn't have to do it double lowerBoundToFloat(const StatsBin &bin) const override { const Bin &b = SimpleBinner::derived_bin(bin); return derived().lowerBoundToFloatBase(b.value); } // For the upper bound, simply go to the next bin double upperBoundToFloat(const StatsBin &bin) const override { Bin b = SimpleBinner::derived_bin(bin); derived().inc(b); return derived().lowerBoundToFloatBase(b.value); } }; // A continuous binner, where the bin is based on an integer value // and subsequent bins are adjacent integers. template struct IntBinner : public SimpleContinuousBinner { void inc(Bin &bin) const { ++bin.value; } }; // An integer based binner, where each bin represents an integer // range with a fixed size. template struct IntRangeBinner : public IntBinner { int bin_size; IntRangeBinner(int size) : bin_size(size) { } QString format(const StatsBin &bin) const override { int value = IntBinner::derived_bin(bin).value; QLocale loc; return StatsTranslations::tr("%1–%2").arg(loc.toString(value * bin_size), loc.toString((value + 1) * bin_size)); } QString formatLowerBound(const StatsBin &bin) const override { int value = IntBinner::derived_bin(bin).value; return QStringLiteral("%L1").arg(value * bin_size); } double lowerBoundToFloatBase(int value) const { return static_cast(value * bin_size); } }; template std::vector SimpleContinuousBinner::bins_between(const StatsBin &bin1, const StatsBin &bin2) const { const Bin &b1 = SimpleBinner::derived_bin(bin1); const Bin &b2 = SimpleBinner::derived_bin(bin2); std::vector res; Bin act = b1; derived().inc(act); while (act.value < b2.value) { res.push_back(std::make_unique(act)); derived().inc(act); } return res; } // A binner template for discrete variables that where each dive can belong to // multiple bins. The bin-type must be less-than comparable. The derived class // must possess: // - A to_bin_values() function that turns a dive into a value from // which the bins can be constructed. // The bins must possess: // - A member variable "value" of the type it is constructed with. template struct MultiBinner : public StatsBinner { public: using Type = decltype(Bin::value); std::vector bin_dives(const std::vector &dives, bool fill_empty) const override; const Binner &derived() const { return static_cast(*this); } const Bin &derived_bin(const StatsBin &bin) const { return dynamic_cast(bin); } }; template std::vector MultiBinner::bin_dives(const std::vector &dives, bool) const { // First, collect a value / dives vector and then produce the final vector // out of that. I wonder if that is premature optimization? using Pair = std::pair>; std::vector value_bins; for (dive *d: dives) { for (const Type &val: derived().to_bin_values(d)) { if (is_invalid_value(val)) continue; register_bin_value(value_bins, val, [d](std::vector &v) { v.push_back(d); }); } } // Now, turn that into our result array with allocated bin objects. return value_vector_to_bin_vector(*this, value_bins, false); } // A binner that works on string-based bins whereby each dive can // produce multiple strings (e.g. dive buddies). The binner must // feature a to_bin_values() function that produces a vector of // QStrings and bins that can be constructed from QStrings. // Other than that, see SimpleBinner. template struct StringBinner : public MultiBinner { public: QString format(const StatsBin &bin) const override { return dynamic_cast(bin).value; } }; // ============ The date of the dive by year, quarter or month ============ // (Note that calendar week is defined differently in different parts of the world and therefore omitted for now) double date_to_double(int year, int month, int day) { struct tm tm = { 0 }; tm.tm_year = year; tm.tm_mon = month; tm.tm_mday = day; timestamp_t t = utc_mktime(&tm); return t / 86400.0; // Turn seconds since 1970 to days since 1970, if that makes sense...? } struct DateYearBinner : public IntBinner { QString name() const override { return StatsTranslations::tr("Yearly"); } QString format(const StatsBin &bin) const override { return QString::number(derived_bin(bin).value); } int to_bin_value(const dive *d) const { return utc_year(d->when); } double lowerBoundToFloatBase(int year) const { return date_to_double(year, 0, 0); } }; using DateQuarterBin = SimpleBin; struct DateQuarterBinner : public SimpleContinuousBinner { QString name() const override { return StatsTranslations::tr("Quarterly"); } QString format(const StatsBin &bin) const override { year_quarter value = derived_bin(bin).value; return StatsTranslations::tr("%1 Q%2").arg(QString::number(value.first), QString::number(value.second)); } // As histogram axis: show full year for new years and then Q2, Q3, Q4. QString formatLowerBound(const StatsBin &bin) const override { year_quarter value = derived_bin(bin).value; return value.second == 1 ? QString::number(value.first) : StatsTranslations::tr("Q%1").arg(QString::number(value.second)); } double lowerBoundToFloatBase(year_quarter value) const { return date_to_double(value.first, (value.second - 1) * 3, 0); } // Prefer bins that show full years bool preferBin(const StatsBin &bin) const override { year_quarter value = derived_bin(bin).value; return value.second == 1; } year_quarter to_bin_value(const dive *d) const { struct tm tm; utc_mkdate(d->when, &tm); int year = tm.tm_year; switch (tm.tm_mon) { case 0 ... 2: return { year, 1 }; case 3 ... 5: return { year, 2 }; case 6 ... 8: return { year, 3 }; default: return { year, 4 }; } } void inc(DateQuarterBin &bin) const { if (++bin.value.second > 4) { bin.value.second = 1; ++bin.value.first; } } }; using DateMonthBin = SimpleBin; struct DateMonthBinner : public SimpleContinuousBinner { QString name() const override { return StatsTranslations::tr("Monthly"); } QString format(const StatsBin &bin) const override { year_month value = derived_bin(bin).value; return QString("%1 %2").arg(monthname(value.second), QString::number(value.first)); } // In histograms, output year for full years, month otherwise QString formatLowerBound(const StatsBin &bin) const override { year_month value = derived_bin(bin).value; return value.second == 0 ? QString::number(value.first) : QString(monthname(value.second)); } double lowerBoundToFloatBase(year_quarter value) const { return date_to_double(value.first, value.second, 0); } // Prefer bins that show full years bool preferBin(const StatsBin &bin) const override { year_month value = derived_bin(bin).value; return value.second == 0; } year_month to_bin_value(const dive *d) const { struct tm tm; utc_mkdate(d->when, &tm); return { tm.tm_year, tm.tm_mon }; } void inc(DateMonthBin &bin) const { if (++bin.value.second > 11) { bin.value.second = 0; ++bin.value.first; } } }; static DateYearBinner date_year_binner; static DateQuarterBinner date_quarter_binner; static DateMonthBinner date_month_binner; struct DateVariable : public StatsVariableTemplate { QString name() const { return StatsTranslations::tr("Date"); } double toFloat(const dive *d) const override { return d->when / 86400.0; } std::vector binners() const override { return { &date_year_binner, &date_quarter_binner, &date_month_binner }; } }; // ============ Dive depth (max and mean), binned in 5, 10, 20 m or 15, 30, 60 ft bins ============ static int dive_depth_in_mm(const dive *d, bool mean) { return mean ? d->meandepth.mm : d->maxdepth.mm; } struct DepthBinner : public IntRangeBinner { bool metric; bool mean; DepthBinner(int bin_size, bool metric, bool mean) : IntRangeBinner(bin_size), metric(metric), mean(mean) { } QString name() const override { QLocale loc; return StatsTranslations::tr("in %1 %2 steps").arg(loc.toString(bin_size), get_depth_unit(metric)); } QString unitSymbol() const override { return get_depth_unit(metric); } int to_bin_value(const dive *d) const { int depth = dive_depth_in_mm(d, mean); return metric ? depth / 1000 / bin_size : lrint(mm_to_feet(depth)) / bin_size; } }; struct DepthVariableBase : public StatsVariableTemplate { bool mean; DepthBinner meter5, meter10, meter20; DepthBinner feet15, feet30, feet60; DepthVariableBase(bool mean) : mean(mean), meter5(5, true, mean), meter10(10, true, mean), meter20(20, true, mean), feet15(15, false, mean), feet30(30, false, mean), feet60(60, false, mean) { } QString unitSymbol() const override { return get_depth_unit(); } int decimals() const override { return 1; } std::vector binners() const override { if (prefs.units.length == units::METERS) return { &meter5, &meter10, &meter20 }; else return { &feet15, &feet30, &feet60 }; } double toFloat(const dive *d) const override { int depth = dive_depth_in_mm(d, mean); return prefs.units.length == units::METERS ? depth / 1000.0 : mm_to_feet(depth); } }; struct MaxDepthVariable : public DepthVariableBase { MaxDepthVariable() : DepthVariableBase(false) { } QString name() const override { return StatsTranslations::tr("Max. Depth"); } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::Sum, StatsOperation::Min, StatsOperation::Max }; } }; struct MeanDepthVariable : public DepthVariableBase { MeanDepthVariable() : DepthVariableBase(true) { } QString name() const override { return StatsTranslations::tr("Mean Depth"); } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max }; } }; // ============ Bottom time, binned in 5, 10, 30 min or 1 h bins ============ struct MinuteBinner : public IntRangeBinner { using IntRangeBinner::IntRangeBinner; QString name() const override { return StatsTranslations::tr("in %1 min steps").arg(bin_size); } QString unitSymbol() const override { return StatsTranslations::tr("min"); } int to_bin_value(const dive *d) const { return d->duration.seconds / 60 / bin_size; } }; struct HourBinner : public IntBinner { QString name() const override { return StatsTranslations::tr("in hours"); } QString format(const StatsBin &bin) const override { return QString::number(derived_bin(bin).value); } QString unitSymbol() const override { return StatsTranslations::tr("h"); } int to_bin_value(const dive *d) const { return d->duration.seconds / 3600; } double lowerBoundToFloatBase(int hour) const { return static_cast(hour); } }; static MinuteBinner minute_binner5(5); static MinuteBinner minute_binner10(10); static MinuteBinner minute_binner30(30); static HourBinner hour_binner; struct DurationVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Duration"); } QString unitSymbol() const override { return StatsTranslations::tr("min"); } int decimals() const override { return 0; } std::vector binners() const override { return { &minute_binner5, &minute_binner10, &minute_binner30, &hour_binner }; } double toFloat(const dive *d) const override { return d->duration.seconds / 60.0; } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::Sum, StatsOperation::Min, StatsOperation::Max }; } }; // ============ SAC, binned in 2, 5, 10 l/min or 0.1, 0.2, 0.4, 0.8 cuft/min bins ============ struct MetricSACBinner : public IntRangeBinner { using IntRangeBinner::IntRangeBinner; QString name() const override { QLocale loc; return StatsTranslations::tr("in %1 %2/min steps").arg(loc.toString(bin_size), get_volume_unit()); } QString unitSymbol() const override { return get_volume_unit(true) + StatsTranslations::tr("/min"); } int to_bin_value(const dive *d) const { if (d->sac <= 0) return invalid_value(); return d->sac / 1000 / bin_size; } }; // "Imperial" SACs are annoying, since we have to bin to sub-integer precision. // We store cuft * 100 as an integer, to avoid troubles with floating point semantics. struct ImperialSACBinner : public IntBinner { int bin_size; ImperialSACBinner(double size) : bin_size(lrint(size * 100.0)) { } QString name() const override { QLocale loc; return StatsTranslations::tr("in %1 %2/min steps").arg(loc.toString(bin_size / 100.0, 'f', 2), get_volume_unit()); } QString format(const StatsBin &bin) const override { int value = derived_bin(bin).value; QLocale loc; return StatsTranslations::tr("%1–%2").arg(loc.toString((value * bin_size) / 100.0, 'f', 2), loc.toString(((value + 1) * bin_size) / 100.0, 'f', 2)); } QString unitSymbol() const override { return get_volume_unit(false) + StatsTranslations::tr("/min"); } QString formatLowerBound(const StatsBin &bin) const override { int value = derived_bin(bin).value; return QStringLiteral("%L1").arg((value * bin_size) / 100.0, 0, 'f', 2); } double lowerBoundToFloatBase(int value) const { return static_cast((value * bin_size) / 100.0); } int to_bin_value(const dive *d) const { if (d->sac <= 0) return invalid_value(); return lrint(ml_to_cuft(d->sac) * 100.0) / bin_size; } }; MetricSACBinner metric_sac_binner2(2); MetricSACBinner metric_sac_binner5(5); MetricSACBinner metric_sac_binner10(10); ImperialSACBinner imperial_sac_binner1(0.1); ImperialSACBinner imperial_sac_binner2(0.2); ImperialSACBinner imperial_sac_binner4(0.4); ImperialSACBinner imperial_sac_binner8(0.8); struct SACVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("SAC"); } QString unitSymbol() const override { return get_volume_unit() + StatsTranslations::tr("/min"); } int decimals() const override { return prefs.units.volume == units::LITER ? 0 : 2; } std::vector binners() const override { if (prefs.units.volume == units::LITER) return { &metric_sac_binner2, &metric_sac_binner5, &metric_sac_binner10 }; else return { &imperial_sac_binner1, &imperial_sac_binner2, &imperial_sac_binner4, &imperial_sac_binner8 }; } double toFloat(const dive *d) const override { if (d->sac <= 0) return invalid_value(); return prefs.units.volume == units::LITER ? d->sac / 1000.0 : ml_to_cuft(d->sac); } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max }; } }; // ============ Water and air temperature, binned in 2, 5, 10, 20 °C/°F bins ============ struct TemperatureBinner : public IntRangeBinner { bool air; bool metric; TemperatureBinner(int bin_size, bool air, bool metric) : IntRangeBinner(bin_size), air(air), metric(metric) { } QString name() const override { QLocale loc; return StatsTranslations::tr("in %1 %2 steps").arg(loc.toString(bin_size), get_temp_unit(metric)); } QString unitSymbol() const override { return get_temp_unit(metric); } int to_bin_value(const struct dive *d) const { temperature_t t = air ? d->airtemp : d->watertemp; if (t.mkelvin <= 0) return invalid_value(); int temp = metric ? static_cast(mkelvin_to_C(t.mkelvin)) : static_cast(mkelvin_to_F(t.mkelvin)); return temp / bin_size; } }; struct TemperatureVariable : public StatsVariableTemplate { TemperatureBinner bin2C, bin5C, bin10C, bin20C; TemperatureBinner bin2F, bin5F, bin10F, bin20F; TemperatureVariable(bool air) : bin2C(2, air, true), bin5C(5, air, true), bin10C(10, air, true), bin20C(20, air, true), bin2F(2, air, false), bin5F(5, air, false), bin10F(10, air, false), bin20F(20, air, false) { } QString unitSymbol() const override { return get_temp_unit(); } int decimals() const override { return 1; } double tempToFloat(temperature_t t) const { if (t.mkelvin <= 0) return invalid_value(); return prefs.units.temperature == units::CELSIUS ? mkelvin_to_C(t.mkelvin) : mkelvin_to_F(t.mkelvin); } std::vector binners() const override { if (prefs.units.temperature == units::CELSIUS) return { &bin2C, &bin5C, &bin10C, &bin20C }; else return { &bin2F, &bin5F, &bin10F, &bin20F }; } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max }; } }; struct WaterTemperatureVariable : TemperatureVariable { WaterTemperatureVariable() : TemperatureVariable(false) { } QString name() const override { return StatsTranslations::tr("Water temperature"); } double toFloat(const dive *d) const override { return tempToFloat(d->watertemp); } }; struct AirTemperatureVariable : TemperatureVariable { AirTemperatureVariable() : TemperatureVariable(true) { } QString name() const override { return StatsTranslations::tr("Air temperature"); } double toFloat(const dive *d) const override { return tempToFloat(d->airtemp); } }; // ============ Weight, binned in 1, 2, 5, 10 kg or 2, 4, 10 or 20 lbs bins ============ struct WeightBinner : public IntRangeBinner { bool metric; WeightBinner(int bin_size, bool metric) : IntRangeBinner(bin_size), metric(metric) { } QString name() const override { QLocale loc; return StatsTranslations::tr("in %1 %2 steps").arg(loc.toString(bin_size), get_weight_unit(metric)); } QString unitSymbol() const override { return get_weight_unit(metric); } int to_bin_value(const dive *d) const { return metric ? d->total_weight().grams / 1000 / bin_size : lrint(grams_to_lbs(d->total_weight().grams)) / bin_size; } }; static WeightBinner weight_binner_1kg(1, true); static WeightBinner weight_binner_2kg(2, true); static WeightBinner weight_binner_5kg(5, true); static WeightBinner weight_binner_10kg(10, true); static WeightBinner weight_binner_2lbs(2, false); static WeightBinner weight_binner_5lbs(4, false); static WeightBinner weight_binner_10lbs(10, false); static WeightBinner weight_binner_20lbs(20, false); struct WeightVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Weight"); } QString unitSymbol() const override { return get_weight_unit(); } int decimals() const override { return 1; } std::vector binners() const override { if (prefs.units.weight == units::KG) return { &weight_binner_1kg, &weight_binner_2kg, &weight_binner_5kg, &weight_binner_10kg }; else return { &weight_binner_2lbs, &weight_binner_5lbs, &weight_binner_10lbs, &weight_binner_20lbs }; } double toFloat(const dive *d) const override { return prefs.units.weight == units::KG ? d->total_weight().grams / 1000.0 : grams_to_lbs(d->total_weight().grams); } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::Sum, StatsOperation::Min, StatsOperation::Max }; } }; // ============ Dive number ============ // Binning dive numbers can't use the IntRangeBinner, // because dive numbers start at 1, not 0! Since 0 means // "no number", these dives aren't registered. struct DiveNrBinner : public IntBinner { int bin_size; DiveNrBinner(int bin_size) : bin_size(bin_size) { } QString format(const StatsBin &bin) const override { int value = derived_bin(bin).value; QLocale loc; return StatsTranslations::tr("%1–%2").arg(loc.toString(value * bin_size + 1), loc.toString((value + 1) * bin_size)); } QString formatLowerBound(const StatsBin &bin) const override { int value = derived_bin(bin).value; return QStringLiteral("%L1").arg(value * bin_size + 1); } double lowerBoundToFloatBase(int value) const { return static_cast(value * bin_size + 1); } QString name() const override { return StatsTranslations::tr("in %L2 steps").arg(bin_size); } int to_bin_value(const dive *d) const { if (d->number <= 0) return invalid_value(); return (d->number - 1) / bin_size; } }; static DiveNrBinner dive_nr_binner_5(5); static DiveNrBinner dive_nr_binner_10(10); static DiveNrBinner dive_nr_binner_20(20); static DiveNrBinner dive_nr_binner_50(50); static DiveNrBinner dive_nr_binner_100(100); static DiveNrBinner dive_nr_binner_200(200); struct DiveNrVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Dive #"); } std::vector binners() const override { if (divelog.dives.size() > 1000) return { &dive_nr_binner_20, &dive_nr_binner_50, &dive_nr_binner_100, &dive_nr_binner_200 }; else return { &dive_nr_binner_5, &dive_nr_binner_10, &dive_nr_binner_20, &dive_nr_binner_50 }; } double toFloat(const dive *d) const override { return d->number >= 0 ? static_cast(d->number) : invalid_value(); } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean }; } }; // ============ Dive mode ============ struct DiveModeBinner : public SimpleBinner { QString format(const StatsBin &bin) const override { return QString(divemode_text_ui[derived_bin(bin).value]); } int to_bin_value(const dive *d) const { int res = (int)d->dcs[0].divemode; return res >= 0 && res < NUM_DIVEMODE ? res : OC; } }; static DiveModeBinner dive_mode_binner; struct DiveModeVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Dive mode"); } QString diveCategories(const dive *d) const override { int mode = (int)d->dcs[0].divemode; return mode >= 0 && mode < NUM_DIVEMODE ? QString(divemode_text_ui[mode]) : QString(); } std::vector binners() const override { return { &dive_mode_binner }; } }; // ============ People, buddies and dive guides ============ struct PeopleBinner : public StringBinner { std::vector to_bin_values(const dive *d) const { std::vector dive_people; for (const QString &s: QString::fromStdString(d->buddy).split(",", SKIP_EMPTY)) dive_people.push_back(s.trimmed()); for (const QString &s: QString::fromStdString(d->diveguide).split(",", SKIP_EMPTY)) dive_people.push_back(s.trimmed()); return dive_people; } }; static PeopleBinner people_binner; struct PeopleVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("People"); } QString diveCategories(const dive *d) const override { QString buddy = QString::fromStdString(d->buddy).trimmed(); QString diveguide = QString::fromStdString(d->diveguide).trimmed(); if (!buddy.isEmpty() && !diveguide.isEmpty()) buddy += ", "; return buddy + diveguide; } std::vector binners() const override { return { &people_binner }; } }; struct BuddyBinner : public StringBinner { std::vector to_bin_values(const dive *d) const { std::vector buddies; for (const QString &s: QString::fromStdString(d->buddy).split(",", SKIP_EMPTY)) buddies.push_back(s.trimmed()); return buddies; } }; static BuddyBinner buddy_binner; struct BuddyVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Buddies"); } QString diveCategories(const dive *d) const override { return QString::fromStdString(d->buddy).trimmed(); } std::vector binners() const override { return { &buddy_binner }; } }; struct DiveGuideBinner : public StringBinner { std::vector to_bin_values(const dive *d) const { std::vector dive_guides; for (const QString &s: QString::fromStdString(d->diveguide).split(",", SKIP_EMPTY)) dive_guides.push_back(s.trimmed()); return dive_guides; } }; static DiveGuideBinner dive_guide_binner; struct DiveGuideVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Dive guides"); } QString diveCategories(const dive *d) const override { return QString::fromStdString(d->diveguide).trimmed(); } std::vector binners() const override { return { &dive_guide_binner }; } }; // ============ Tags ============ struct TagBinner : public StringBinner { std::vector to_bin_values(const dive *d) const { std::vector tags; for (const divetag *tag: d->tags) tags.push_back(QString::fromStdString(tag->name).trimmed()); return tags; } }; static TagBinner tag_binner; struct TagVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Tags"); } QString diveCategories(const dive *d) const override { return QString::fromStdString(taglist_get_tagstring(d->tags)); } std::vector binners() const override { return { &tag_binner }; } }; // ============ Gas type, in 2%, 5%, 10% and 20% steps ============ // This is a bit convoluted: We differentiate between four types: air, pure oxygen, EAN and trimix // The latter two are binned in x% steps. The problem is that we can't use the "simple binner", // because a dive can have more than one cylinder. Moreover, the string-binner might not be optimal // because we don't want to format a string for every gas types, when there are thousands of dives! // Note: when the same dive has multiple cylinders that fall inside a bin, that cylinder will // only be counted once. Thus, depending on the bin size, the number of entries may change! // In addition to a binner with percent-steps also provide a general-type binner (air, nitrox, etc.) // bin gasmix with size given in percent struct gas_bin_t bin_gasmix(struct gasmix mix, int size) { if (gasmix_is_air(mix)) return gas_bin_t::air(); if (get_o2(mix) == 1000) return gas_bin_t::oxygen(); return get_he(mix) == 0 ? gas_bin_t::ean(get_o2(mix) / 10 / size * size) : gas_bin_t::trimix(get_o2(mix) / 10 / size * size, get_he(mix) / 10 / size * size); } struct GasTypeBinner : public MultiBinner { int bin_size; GasTypeBinner(int size) : bin_size(size) { } QString name() const override { return StatsTranslations::tr("in %1% steps").arg(bin_size); } std::vector to_bin_values(const dive *d) const { std::vector res; res.reserve(d->cylinders.size()); for (auto &cyl: d->cylinders) { struct gasmix mix = cyl.gasmix; if (gasmix_is_invalid(mix)) continue; // Add dive to each bin only once. add_to_vector_unique(res, bin_gasmix(mix, bin_size)); } return res; } QString format(const StatsBin &bin) const override { gas_bin_t type = derived_bin(bin).value; QLocale loc; switch (type.type) { default: case gas_bin_t::Type::Air: return StatsTranslations::tr("Air"); case gas_bin_t::Type::Oxygen: return StatsTranslations::tr("Oxygen"); case gas_bin_t::Type::EAN: return StatsTranslations::tr("EAN%1–%2").arg(loc.toString(type.o2), loc.toString(type.o2 + bin_size - 1)); case gas_bin_t::Type::Trimix: return StatsTranslations::tr("%1/%2–%3/%4").arg(loc.toString(type.o2), loc.toString(type.he), loc.toString(type.o2 + bin_size - 1), loc.toString(type.he + bin_size - 1)); } } }; struct GasTypeGeneralBinner : public MultiBinner { using MultiBinner::MultiBinner; QString name() const override { return StatsTranslations::tr("General"); } std::vector to_bin_values(const dive *d) const { std::vector res; res.reserve(d->cylinders.size()); for (auto &cyl: d->cylinders) { struct gasmix mix = cyl.gasmix; if (gasmix_is_invalid(mix)) continue; res.push_back(gasmix_to_type(mix)); } return res; } QString format(const StatsBin &bin) const override { int type = derived_bin(bin).value; return gastype_name((gastype)type); } }; static GasTypeGeneralBinner gas_type_general_binner; static GasTypeBinner gas_type_binner2(2); static GasTypeBinner gas_type_binner5(5); static GasTypeBinner gas_type_binner10(10); static GasTypeBinner gas_type_binner20(20); struct GasTypeVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Gas type"); } QString diveCategories(const dive *d) const override { QString res; std::vector mixes; // List multiple cylinders only once mixes.reserve(d->cylinders.size()); for (auto &cyl: d->cylinders) { struct gasmix mix = cyl.gasmix; if (gasmix_is_invalid(mix)) continue; if (std::find_if(mixes.begin(), mixes.end(), [mix] (gasmix mix2) { return same_gasmix(mix, mix2); }) != mixes.end()) continue; mixes.push_back(mix); if (!res.isEmpty()) res += ", "; res += get_gas_string(mix); } return res; } std::vector binners() const override { return { &gas_type_general_binner, &gas_type_binner2, &gas_type_binner5, &gas_type_binner10, &gas_type_binner20 }; } }; // ============ O2 and H2 content, binned in 2, 5, 10, 20 % bins ============ // Get the gas content in permille of a dive, based on two flags: // - he: get he content, otherwise o2 // - max_he: get cylinder with maximum he content, otherwise with maximum o2 content static int get_gas_content(const struct dive *d, bool he, bool max_he) { if (d->cylinders.empty()) return invalid_value(); // If sorting be He, the second sort criterion is O2 descending, because // we are interested in the "bottom gas": highest He and lowest O2. auto comp = max_he ? [] (const cylinder_t &c1, const cylinder_t &c2) { return std::make_tuple(get_he(c1.gasmix), -get_o2(c1.gasmix)) < std::make_tuple(get_he(c2.gasmix), -get_o2(c2.gasmix)); } : [] (const cylinder_t &c1, const cylinder_t &c2) { return get_o2(c1.gasmix) < get_o2(c2.gasmix); }; auto it = std::max_element(d->cylinders.begin(), d->cylinders.end(), comp); return he ? get_he(it->gasmix) : get_o2(it->gasmix); } // We use the same binner for all gas contents struct GasContentBinner : public IntRangeBinner { bool he; // true if this returns He content, otherwise O2 bool max_he; // true if this takes the gas with maximum helium, otherwise maximum O2 GasContentBinner(int bin_size, bool he, bool max_he) : IntRangeBinner(bin_size), he(he), max_he(max_he) { } QString name() const override { return StatsTranslations::tr("In %L1% steps").arg(bin_size); } QString unitSymbol() const override { return "%"; } int to_bin_value(const struct dive *d) const { int res = get_gas_content(d, he, max_he); // Convert to percent and then bin, but take care not to mangle the invalid value. return is_invalid_value(res) ? res : res / 10 / bin_size; } }; struct GasContentVariable : public StatsVariableTemplate { // In the constructor, generate binners with 2, 5, 10 and 20% bins. GasContentBinner b1, b2, b3, b4; bool he, max_he; GasContentVariable(bool he, bool max_he) : b1(2, he, max_he), b2(5, he, max_he), b3(10, he, max_he), b4(20, he, max_he), he(he), max_he(max_he) { } std::vector binners() const override { return { &b1, &b2, &b3, &b4 }; } QString unitSymbol() const override { return "%"; } int decimals() const override { return 1; } std::vector supportedOperations() const override { return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max }; } double toFloat(const dive *d) const override { int res = get_gas_content(d, he, max_he); // Attn: we have to turn invalid-int into invalid-float. // Perhaps we should signal invalid with an std::optional kind of object? if (is_invalid_value(res)) return invalid_value(); return res / 10.0; } }; struct GasContentO2Variable : GasContentVariable { GasContentO2Variable() : GasContentVariable(false, false) { } QString name() const override { return StatsTranslations::tr("O₂ (max)"); } }; struct GasContentO2HeMaxVariable : GasContentVariable { GasContentO2HeMaxVariable() : GasContentVariable(false, true) { } QString name() const override { return StatsTranslations::tr("O₂ (bottom gas)"); } }; struct GasContentHeVariable : GasContentVariable { GasContentHeVariable() : GasContentVariable(true, true) { } QString name() const override { return StatsTranslations::tr("He (max)"); } }; // ============ Suit ============ struct SuitBinner : public StringBinner { std::vector to_bin_values(const dive *d) const { return { QString::fromStdString(d->suit) }; } }; static SuitBinner suit_binner; struct SuitVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Suit type"); } QString diveCategories(const dive *d) const override { return QString::fromStdString(d->suit); } std::vector binners() const override { return { &suit_binner }; } }; // ============ Weightsystem ============ static std::vector weightsystems(const dive *d) { std::vector res; res.reserve(d->weightsystems.size()); for (auto &ws: d->weightsystems) add_to_vector_unique(res, QString::fromStdString(ws.description).trimmed()); return res; } struct WeightsystemBinner : public StringBinner { std::vector to_bin_values(const dive *d) const { return weightsystems(d); } }; static WeightsystemBinner weightsystem_binner; struct WeightsystemVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Weightsystem"); } QString diveCategories(const dive *d) const override { return join_strings(weightsystems(d)); } std::vector binners() const override { return { &weightsystem_binner }; } }; // ============ Cylinder types ============ static std::vector cylinder_types(const dive *d) { std::vector res; res.reserve(d->cylinders.size()); for (auto &cyl: d->cylinders) add_to_vector_unique(res, QString::fromStdString(cyl.type.description).trimmed()); return res; } struct CylinderTypeBinner : public StringBinner { std::vector to_bin_values(const dive *d) const { return cylinder_types(d); } }; static CylinderTypeBinner cylinder_type_binner; struct CylinderTypeVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Cylinder type"); } QString diveCategories(const dive *d) const override { return join_strings(cylinder_types(d)); } std::vector binners() const override { return { &cylinder_type_binner }; } }; // ============ Location (including trip location) ============ using LocationBin = SimpleBin; struct LocationBinner : public SimpleBinner { QString format(const StatsBin &bin) const override { return derived_bin(bin).value.format(); } const DiveSiteWrapper to_bin_value(const dive *d) const { return DiveSiteWrapper(d->dive_site); } }; static LocationBinner location_binner; struct LocationVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Dive site"); } QString diveCategories(const dive *d) const override { return DiveSiteWrapper(d->dive_site).format(); } std::vector binners() const override { return { &location_binner }; } }; // ============ Dive trip ============ using TripBin = SimpleBin; struct TripBinner : public SimpleBinner { QString format(const StatsBin &bin) const override { return derived_bin(bin).value.format(); } const TripWrapper to_bin_value(const dive *d) const { return TripWrapper(d->divetrip); } }; static TripBinner trip_binner; struct TripVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Dive trip"); } QString diveCategories(const dive *d) const override { return d->divetrip ? formatTripTitle(*d->divetrip) : QString(); } std::vector binners() const override { return { &trip_binner }; } }; // ============ Day of the week ============ struct DayOfWeekBinner : public SimpleBinner { QString format(const StatsBin &bin) const override { return formatDayOfWeek(derived_bin(bin).value); } int to_bin_value(const dive *d) const { return utc_weekday(d->when); } }; static DayOfWeekBinner day_of_week_binner; struct DayOfWeekVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Day of week"); } QString diveCategories(const dive *d) const override { return formatDayOfWeek(utc_weekday(d->when)); } std::vector binners() const override { return { &day_of_week_binner }; } }; // ============ Rating ============ struct RatingBinner : public SimpleBinner { QString format(const StatsBin &bin) const override { return QString("🌟").repeated(derived_bin(bin).value); } int to_bin_value(const dive *d) const { int res = (int)d->rating; return res; } }; static RatingBinner rating_binner; struct RatingVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Rating"); } QString diveCategories(const dive *d) const override { int rating = (int)d->rating; return QString("🌟").repeated(rating); } std::vector binners() const override { return { &rating_binner }; } }; // ============ Visibility ============ struct VisibilityBinner : public SimpleBinner { QString format(const StatsBin &bin) const override { return QString("🌟").repeated(derived_bin(bin).value); } int to_bin_value(const dive *d) const { int res = (int)d->visibility; return res; } }; static VisibilityBinner visibility_binner; struct VisibilityVariable : public StatsVariableTemplate { QString name() const override { return StatsTranslations::tr("Visibility"); } QString diveCategories(const dive *d) const override { int viz = (int)d->visibility; return QString("🌟").repeated(viz); } std::vector binners() const override { return { &visibility_binner }; } }; static DateVariable date_variable; static MaxDepthVariable max_depth_variable; static MeanDepthVariable mean_depth_variable; static DurationVariable duration_variable; static SACVariable sac_variable; static WaterTemperatureVariable water_temperature_variable; static AirTemperatureVariable air_temperature_variable; static WeightVariable weight_variable; static DiveNrVariable dive_nr_variable; static DiveModeVariable dive_mode_variable; static PeopleVariable people_variable; static BuddyVariable buddy_variable; static DiveGuideVariable dive_guide_variable; static TagVariable tag_variable; static GasTypeVariable gas_type_variable; static GasContentO2Variable gas_content_o2_variable; static GasContentO2HeMaxVariable gas_content_o2_he_max_variable; static GasContentHeVariable gas_content_he_variable; static SuitVariable suit_variable; static WeightsystemVariable weightsystem_variable; static CylinderTypeVariable cylinder_type_variable; static LocationVariable location_variable; static TripVariable trip_variable; static DayOfWeekVariable day_of_week_variable; static RatingVariable rating_variable; static VisibilityVariable visibility_variable; const std::vector stats_variables = { &date_variable, &max_depth_variable, &mean_depth_variable, &duration_variable, &sac_variable, &water_temperature_variable, &air_temperature_variable, &weight_variable, &dive_nr_variable, &gas_content_o2_variable, &gas_content_o2_he_max_variable, &gas_content_he_variable, &dive_mode_variable, &people_variable, &buddy_variable, &dive_guide_variable, &tag_variable, &gas_type_variable, &suit_variable, &weightsystem_variable, &cylinder_type_variable, &location_variable, &trip_variable, &day_of_week_variable, &rating_variable, &visibility_variable };