summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Berthold Stoeger <bstoeger@mail.tuwien.ac.at>2020-02-08 12:06:57 +0100
committerGravatar Dirk Hohndel <dirk@hohndel.org>2020-02-08 10:29:36 -0800
commit1a85b0e941b57f4f3c3406a42be78d4d6642d17a (patch)
tree983d895fa634113ed4290f6c5d92306116a0aae7
parent48ccd114fcb5e1ca0a692c096143c2f24d8254d3 (diff)
downloadsubsurface-1a85b0e941b57f4f3c3406a42be78d4d6642d17a.tar.gz
mobile/summary: create DiveSummaryModel
Instead of passing the dive summary via a completely unstructured QStringList to QML, implement a dynamic model. For potential reuse on desktop (though somewhat unlikely) the model has two interfaces, one for QtWidgets and one for QML. The former is based on columns, whereas the later is based on roles. The number of columns is set dynamically. The roles currently support access to two columns. If more columns should be accessed from QML, more roles have to be added manually. This commit only creates the model and hooks it into QMLs global context, but does not yet change the QML page. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at> Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
-rw-r--r--packaging/ios/Subsurface-mobile.pro2
-rw-r--r--qt-models/CMakeLists.txt2
-rw-r--r--qt-models/divesummarymodel.cpp258
-rw-r--r--qt-models/divesummarymodel.h58
-rw-r--r--subsurface-helper.cpp2
5 files changed, 322 insertions, 0 deletions
diff --git a/packaging/ios/Subsurface-mobile.pro b/packaging/ios/Subsurface-mobile.pro
index 15ae8ff88..5c2c9a84a 100644
--- a/packaging/ios/Subsurface-mobile.pro
+++ b/packaging/ios/Subsurface-mobile.pro
@@ -115,6 +115,7 @@ SOURCES += ../../subsurface-mobile-main.cpp \
../../mobile-widgets/qmlmanager.cpp \
../../mobile-widgets/themeinterface.cpp \
../../qt-models/divelistmodel.cpp \
+ ../../qt-models/divesummarymodel.cpp \
../../qt-models/diveplotdatamodel.cpp \
../../qt-models/gpslistmodel.cpp \
../../qt-models/completionmodels.cpp \
@@ -251,6 +252,7 @@ HEADERS += \
../../mobile-widgets/themeinterface.h \
../../map-widget/qmlmapwidgethelper.h \
../../qt-models/divelistmodel.h \
+ ../../qt-models/divesummarymodel.h \
../../qt-models/diveplotdatamodel.h \
../../qt-models/gpslistmodel.h \
../../qt-models/divelocationmodel.h \
diff --git a/qt-models/CMakeLists.txt b/qt-models/CMakeLists.txt
index 8a469a5c2..8cdeecd25 100644
--- a/qt-models/CMakeLists.txt
+++ b/qt-models/CMakeLists.txt
@@ -53,6 +53,8 @@ set(SUBSURFACE_DESKTOP_MODELS_LIB_SRCS
set(SUBSURFACE_MOBILE_MODELS_LIB_SRCS
divelistmodel.cpp
divelistmodel.h
+ divesummarymodel.cpp
+ divesummarymodel.h
gpslistmodel.cpp
gpslistmodel.h
messagehandlermodel.cpp
diff --git a/qt-models/divesummarymodel.cpp b/qt-models/divesummarymodel.cpp
new file mode 100644
index 000000000..f7110fbf4
--- /dev/null
+++ b/qt-models/divesummarymodel.cpp
@@ -0,0 +1,258 @@
+// 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" } };
+}
+
+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);
+ }
+
+ // 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;
+ long divetime, depth;
+ long divetimeMax, depthMax, sacMin, sacMax;
+ long divetimeAvg, depthAvg, sacAvg;
+ long 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(long duration)
+{
+ long hours = duration / 3600;
+ long 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(long depth)
+{
+ return QStringLiteral("%L1").arg(prefs.units.length == units::METERS ? depth / 1000 : lrint(mm_to_feet(depth)));
+}
+
+static QString volumeString(long 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";
+ long 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 localTime;
+
+ // Calculate Start of the 2 periods.
+ timestamp_t now, start;
+ now = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset();
+ start = (period == 0) ? 0 : now - period * 30 * 24 * 60 * 60;
+
+ // 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();
+}
diff --git a/qt-models/divesummarymodel.h b/qt-models/divesummarymodel.h
new file mode 100644
index 000000000..13d37ba17
--- /dev/null
+++ b/qt-models/divesummarymodel.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0
+#ifndef DIVESUMMARYMODEL_H
+#define DIVESUMMARYMODEL_H
+
+#include <QAbstractTableModel>
+#include <vector>
+
+class DiveSummaryModel : public QAbstractTableModel {
+ Q_OBJECT
+public:
+ enum Row {
+ DIVES,
+ DIVES_EAN,
+ DIVES_DEEP,
+ PLANS,
+ TIME,
+ TIME_MAX,
+ TIME_AVG,
+ DEPTH_MAX,
+ DEPTH_AVG,
+ SAC_MIN,
+ SAC_MAX,
+ SAC_AVG,
+ NUM_ROW
+ };
+
+ // Roles for QML. Amazingly it appears that QML before Qt 5.12 *cannot*
+ // display run-of-the-mill tabular data.
+ // Therefore we transform a fixed number of columns, including the header
+ // into roles that can be displayed by QML. The mind boggles.
+ enum QMLRoles {
+ HEADER_ROLE = Qt::UserRole + 1,
+ COLUMN0_ROLE,
+ COLUMN1_ROLE,
+ };
+
+ struct Result {
+ QString dives, divesEAN, divesDeep, plans;
+ QString time, time_max, time_avg;
+ QString depth_max, depth_avg;
+ QString sac_min, sac_max, sac_avg;
+ };
+
+ Q_INVOKABLE void setNumData(int num);
+ Q_INVOKABLE void calc(int column, int period);
+private:
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ QVariant dataDisplay(int row, int col) const;
+
+ std::vector<Result> results;
+};
+
+#endif
diff --git a/subsurface-helper.cpp b/subsurface-helper.cpp
index 9ae89561f..1ef93102d 100644
--- a/subsurface-helper.cpp
+++ b/subsurface-helper.cpp
@@ -17,6 +17,7 @@
#include "mobile-widgets/qmlmanager.h"
#include "mobile-widgets/qmlinterface.h"
#include "qt-models/divelistmodel.h"
+#include "qt-models/divesummarymodel.h"
#include "qt-models/gpslistmodel.h"
#include "qt-models/messagehandlermodel.h"
#include "profile-widget/qmlprofile.h"
@@ -195,6 +196,7 @@ void register_qml_types(QQmlEngine *engine)
REGISTER_TYPE(QMLManager, "QMLManager");
REGISTER_TYPE(QMLProfile, "QMLProfile");
REGISTER_TYPE(DiveImportedModel, "DCImportModel");
+ REGISTER_TYPE(DiveSummaryModel, "DiveSummaryModel");
#endif // not SUBSURFACE_MOBILE
REGISTER_TYPE(MapWidgetHelper, "MapWidgetHelper");