subsurface/qt-models/divesummarymodel.cpp
Dirk Hohndel 16dd16b34b mobile/dive-summary: use 64bit integers for statistics
We still support 32bit ARM platforms, and there long is 32 bits.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2020-03-31 08:42:20 -07:00

283 lines
7.5 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "qt-models/divesummarymodel.h"
#include "core/dive.h"
#include "core/qthelper.h"
#include <QLocale>
#include <QDateTime>
int DiveSummaryModel::rowCount(const QModelIndex &) const
{
return NUM_ROW;
}
int DiveSummaryModel::columnCount(const QModelIndex &) const
{
return (int)results.size();
}
QHash<int, QByteArray> DiveSummaryModel::roleNames() const
{
return { { HEADER_ROLE, "header" },
{ COLUMN0_ROLE, "col0" },
{ COLUMN1_ROLE, "col1" },
{ SECTION_ROLE, "section" } };
}
QVariant DiveSummaryModel::dataDisplay(int row, int col) const
{
if (col >= (int)results.size())
return QVariant();
const Result &res = results[col];
switch (row) {
case DIVES: return res.dives;
case DIVES_EAN: return res.divesEAN;
case DIVES_DEEP: return res.divesDeep;
case PLANS: return res.plans;
case TIME: return res.time;
case TIME_MAX: return res.time_max;
case TIME_AVG: return res.time_avg;
case DEPTH_MAX: return res.depth_max;
case DEPTH_AVG: return res.depth_avg;
case SAC_MIN: return res.sac_min;
case SAC_MAX: return res.sac_max;
case SAC_AVG: return res.sac_avg;
}
return QVariant();
}
QVariant DiveSummaryModel::data(const QModelIndex &index, int role) const
{
if (role == Qt::DisplayRole) // The "normal" case
return dataDisplay(index.row(), index.column());
// The QML case
int row = index.row();
switch (role) {
case HEADER_ROLE:
return headerData(row, Qt::Vertical, Qt::DisplayRole);
case COLUMN0_ROLE:
return dataDisplay(row, 0);
case COLUMN1_ROLE:
return dataDisplay(row, 1);
case SECTION_ROLE:
switch (row) {
case DIVES ... PLANS: return tr("Number of dives");
case TIME ... TIME_AVG: return tr("Time");
case DEPTH_MAX ... DEPTH_AVG: return tr("Depth");
case SAC_MIN ... SAC_AVG: return tr("SAC");
default: return QVariant();
}
}
// The unsupported case
return QVariant();
}
QVariant DiveSummaryModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Vertical || role != Qt::DisplayRole)
return QVariant();
switch (section) {
case DIVES: return tr("Total");
case DIVES_EAN: return tr("EAN dives");
case DIVES_DEEP: return tr("Deep dives (> 39 m)");
case PLANS: return tr("Dive plan(s)");
case TIME: return tr("Total time");
case TIME_MAX: return tr("Max Time");
case TIME_AVG: return tr("Avg time");
case DEPTH_MAX: return tr("Max depth");
case DEPTH_AVG: return tr("Avg max depth");
case SAC_MIN: return tr("Min SAC");
case SAC_MAX: return tr("Max SAC");
case SAC_AVG: return tr("Avg SAC");
}
return QVariant();
}
struct Stats {
Stats();
int dives, divesEAN, divesDeep, diveplans;
int64_t divetime, depth;
int64_t divetimeMax, depthMax, sacMin, sacMax;
int64_t divetimeAvg, depthAvg, sacAvg;
int64_t totalSACTime, totalSacVolume;
};
Stats::Stats() :
dives(0), divesEAN(0), divesDeep(0), diveplans(0),
divetime(0), depth(0), divetimeMax(0), depthMax(0),
sacMin(99999), sacMax(0), totalSACTime(0), totalSacVolume(0)
{
}
static void calculateDive(struct dive *dive, Stats &stats)
{
if (is_dc_planner(&dive->dc)) {
stats.diveplans++;
return;
}
// one more real dive
stats.dives++;
// sum dive in minutes and check for new max.
stats.divetime += dive->duration.seconds;
if (dive->duration.seconds > stats.divetimeMax)
stats.divetimeMax = dive->duration.seconds;
// sum depth in meters, check for new max. and if dive is a deep dive
stats.depth += dive->maxdepth.mm;
if (dive->maxdepth.mm > stats.depthMax)
stats.depthMax = dive->maxdepth.mm;
if (dive->maxdepth.mm > 39000)
stats.divesDeep++;
// sum SAC, check for new min/max.
if (dive->sac) {
stats.totalSACTime += dive->duration.seconds;
stats.totalSacVolume += dive->sac * dive->duration.seconds;
if (dive->sac < stats.sacMin)
stats.sacMin = dive->sac;
if (dive->sac > stats.sacMax)
stats.sacMax = dive->sac;
}
// EAN dive ?
for (int j = 0; j < dive->cylinders.nr; ++j) {
if (dive->cylinders.cylinders[j].gasmix.o2.permille > 210) {
stats.divesEAN++;
break;
}
}
}
// Returns a (first_dive, last_dive) pair
static Stats loopDives(timestamp_t start)
{
Stats stats;
struct dive *dive;
int i;
for_each_dive (i, dive) {
// check if dive is newer than primaryStart (add to first column)
if (dive->when > start)
calculateDive(dive, stats);
}
return stats;
}
static QString timeString(int64_t duration)
{
int64_t hours = duration / 3600;
int64_t minutes = (duration - hours * 3600) / 60;
if (hours >= 100)
return QStringLiteral("%1 h").arg(hours);
else
return QStringLiteral("%1:%2").arg(hours).arg(minutes, 2, 10, QChar('0'));
}
static QString depthString(int64_t depth)
{
return QStringLiteral("%L1").arg(prefs.units.length == units::METERS ? depth / 1000 : lrint(mm_to_feet(depth)));
}
static QString volumeString(int64_t volume)
{
return QStringLiteral("%L1").arg(prefs.units.volume == units::LITER ? volume / 1000 : round(100.0 * ml_to_cuft(volume)) / 100.0);
}
static DiveSummaryModel::Result formatResults(const Stats &stats)
{
DiveSummaryModel::Result res;
if (!stats.dives) {
res.dives = QObject::tr("no dives in period");
res.divesEAN = res.divesDeep = res.plans = QStringLiteral("0");
res.time = res.time_max = res.time_avg = QStringLiteral("0:00");
res.depth_max = res.depth_avg = QStringLiteral("-");
res.sac_min = res.sac_max = res.sac_avg = QStringLiteral("-");
return res;
}
// dives
QLocale loc;
res.dives = loc.toString(stats.dives);
res.divesEAN = loc.toString(stats.divesEAN);
res.divesDeep = loc.toString(stats.divesDeep);
// time
res.time = timeString(stats.divetime);
res.time_max = timeString(stats.divetimeMax);
res.time_avg = timeString(stats.divetime / stats.dives);
// depth
QString unitText = (prefs.units.length == units::METERS) ? " m" : " ft";
res.depth_max = depthString(stats.depthMax) + unitText;
res.depth_avg = depthString(stats.depth / stats.dives) + unitText;
// SAC
if (stats.totalSACTime) {
unitText = (prefs.units.volume == units::LITER) ? " l/min" : " cuft/min";
int64_t avgSac = stats.totalSacVolume / stats.totalSACTime;
res.sac_avg = volumeString(avgSac) + unitText;
res.sac_min = volumeString(stats.sacMin) + unitText;
res.sac_max = volumeString(stats.sacMax) + unitText;
} else {
res.sac_avg = QStringLiteral("-");
res.sac_min = QStringLiteral("-");
res.sac_max = QStringLiteral("-");
}
// Diveplan(s)
res.plans = loc.toString(stats.diveplans);
return res;
}
void DiveSummaryModel::calc(int column, int period)
{
if (column >= (int)results.size())
return;
QDateTime currentTime = QDateTime::currentDateTime();
QDateTime startTime = currentTime;
// Calculate Start of the periods.
switch (period) {
case 0: // having startTime == currentTime is used as special case below
break;
case 1: startTime = currentTime.addMonths(-1);
break;
case 2: startTime = currentTime.addMonths(-3);
break;
case 3: startTime = currentTime.addMonths(-6);
break;
case 4: startTime = currentTime.addYears(-1);
break;
default: qWarning("DiveSummaryModel::calc called with invalid period");
}
timestamp_t start;
if (startTime == currentTime)
start = 0;
else
start = startTime.toMSecsSinceEpoch() / 1000L + gettimezoneoffset();
// Loop over all dives and sum up data
Stats stats = loopDives(start);
results[column] = formatResults(stats);
// For QML always reload column 0, because that works via roles not columns
if (column != 0)
emit dataChanged(index(0, 0), index(NUM_ROW - 1, 0));
emit dataChanged(index(0, column), index(NUM_ROW - 1, column));
}
void DiveSummaryModel::setNumData(int num)
{
beginResetModel();
results.resize(num);
endResetModel();
}