From 5fc520bad80dccf9bf2e0f16552c9f2605417067 Mon Sep 17 00:00:00 2001 From: David Llewellyn-Jones Date: Sun, 15 Jul 2018 21:27:16 +0100 Subject: [PATCH] Display overall statistics --- qml/pages/DurationEditDialog.qml | 2 +- qml/pages/JourneyEdit.qml | 30 ++++++++--- qml/pages/JourneyInfo.qml | 14 +++--- qml/pages/JourneyList.qml | 65 +++++++++++++++++++++--- qml/pages/MainPage.qml | 4 +- qml/pages/Stats.qml | 38 ++++++++------ src/harbour-pedalo.cpp | 2 +- src/journeymodel.cpp | 4 ++ src/journeymodel.h | 2 + src/status.cpp | 83 ++++++++++++++++++++++++++++++- src/status.h | 14 +++++- translations/harbour-pedalo-de.ts | 1 + translations/harbour-pedalo.ts | 1 + 13 files changed, 218 insertions(+), 42 deletions(-) diff --git a/qml/pages/DurationEditDialog.qml b/qml/pages/DurationEditDialog.qml index c1a577b..c4e256b 100644 --- a/qml/pages/DurationEditDialog.qml +++ b/qml/pages/DurationEditDialog.qml @@ -11,6 +11,7 @@ Dialog { property alias minute: timePicker.minute property Item timePicker: timePicker + //: Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems property bool leftToRight: qsTr("LTR") !== "RTL" canAccept: true @@ -45,7 +46,6 @@ Dialog { id: timerLabelColumn anchors.centerIn: parent - //: Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems spacing: -Theme.paddingMedium Row { spacing: Theme.paddingSmall diff --git a/qml/pages/JourneyEdit.qml b/qml/pages/JourneyEdit.qml index 1f61b39..a0d4349 100644 --- a/qml/pages/JourneyEdit.qml +++ b/qml/pages/JourneyEdit.qml @@ -31,18 +31,23 @@ Dialog { SilicaFlickable { id: journeyEditView anchors.fill: parent - contentHeight: journeyEditColumn.implicitHeight + contentHeight: headerItem.height + Theme.paddingLarge + (isPortrait ? + (journeyEditColumnFirst.implicitHeight + journeyEditColumnSecond.implicitHeight) : + Math.max(journeyEditColumnFirst.implicitHeight, journeyEditColumnSecond.implicitHeight)) VerticalScrollDecorator {} + DialogHeader { + id: headerItem + title: journeyEditDialog.title + } + Column { - id: journeyEditColumn + id: journeyEditColumnFirst spacing: Theme.paddingMedium - width: parent.width - - DialogHeader { - title: journeyEditDialog.title - } + width: isPortrait ? parent.width : (parent.width * 0.5) + x: 0 + y: headerItem.height ValueButton { id: startDate @@ -101,6 +106,14 @@ Dialog { }) } } + } + + Column { + id: journeyEditColumnSecond + spacing: Theme.paddingMedium + width: isPortrait ? parent.width : (parent.width * 0.5) + x: isPortrait ? 0 : (parent.width * 0.5) + y: isPortrait ? journeyEditColumnFirst.y + journeyEditColumnFirst.height + Theme.paddingLarge : journeyEditColumnFirst.y TextField { id: faster @@ -122,10 +135,11 @@ Dialog { placeholderText: label text: overtakenby >= 0 ? "" + overtakenby : "" horizontalAlignment: TextInput.AlignLeft - EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.iconSource: "image://theme/icon-m-enter-accept" EnterKey.onClicked: journeyEditDialog.accept() } } + } onAccepted: { diff --git a/qml/pages/JourneyInfo.qml b/qml/pages/JourneyInfo.qml index 0181602..02cea45 100644 --- a/qml/pages/JourneyInfo.qml +++ b/qml/pages/JourneyInfo.qml @@ -39,18 +39,20 @@ Page { SilicaFlickable { id: journeyEditView anchors.fill: parent - contentHeight: journeyEditColumn.implicitHeight + contentHeight: journeyEditColumn.implicitHeight + headerItem.height VerticalScrollDecorator {} + PageHeader { + id: headerItem + title: journeyInfoPage.title + } + Column { id: journeyEditColumn spacing: Theme.paddingLarge - width: parent.width - - PageHeader { - title: journeyInfoPage.title - } + width: isPortrait ? parent.width : parent.width * 0.5 + y: headerItem.height InfoRow { id: startDate diff --git a/qml/pages/JourneyList.qml b/qml/pages/JourneyList.qml index 1708d20..2b722be 100644 --- a/qml/pages/JourneyList.qml +++ b/qml/pages/JourneyList.qml @@ -6,15 +6,56 @@ Page { // The effective value will be restricted by ApplicationWindow.allowedOrientations allowedOrientations: Orientation.All - property int columnwidth: page.width - 2 * Theme.horizontalPageMargin + property int columnwidth: page.width - (2 * Theme.horizontalPageMargin) - 4 * Theme.paddingLarge SilicaListView { id: listView model: journeymodel anchors.fill: parent - header: PageHeader { - title: qsTr("Journey list") + header: Column { + width: page.width + spacing: Theme.paddingLarge + height: implicitHeight + Theme.paddingLarge + + PageHeader { + title: qsTr("Journey list") + } + + Row { + spacing: Theme.paddingLarge + x: Theme.horizontalPageMargin + y: headerItem.height + + Label { + width: columnwidth * 0.34 + text: "Date" + color: Theme.secondaryColor + } + Label { + width: columnwidth * 0.18 + text: "Start" + color: Theme.secondaryColor + } + Label { + width: columnwidth * 0.18 + text: "Length" + color: Theme.secondaryColor + } + Label { + width: columnwidth * 0.15 + text: "+" + color: Theme.secondaryColor + horizontalAlignment: Text.AlignRight + } + Label { + width: columnwidth * 0.15 + text: "-" + color: Theme.secondaryColor + horizontalAlignment: Text.AlignRight + } + } } + delegate: ListItem { id: delegate menu: journeyMenuComponent @@ -24,20 +65,32 @@ Page { x: Theme.horizontalPageMargin Label { - width: columnwidth / 3.0 + width: columnwidth * 0.34 text: Qt.formatDate(journeymodel.epochToDate(start), "d MMM yyyy") color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor } Label { - width: columnwidth / 3.0 + width: columnwidth * 0.18 text: Qt.formatTime(journeymodel.epochToTime(start), "hh:mm") color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor } Label { - width: columnwidth / 3.0 + width: columnwidth * 0.18 text: Qt.formatTime(new Date(0, 0, 0, 0, 0, duration), 'hh:mm') color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor } + Label { + width: columnwidth * 0.15 + text: overtook + color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor + horizontalAlignment: Text.AlignRight + } + Label { + width: columnwidth * 0.15 + text: overtakenby + color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor + horizontalAlignment: Text.AlignRight + } } onClicked: pageStack.push(Qt.resolvedUrl("JourneyInfo.qml"), {title: "Journey info", index: index, start: journeymodel.epochToDateTime(start), duration: duration, overtook: overtook, overtakenby: overtakenby}) diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index 101376a..5c0c16b 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -11,6 +11,8 @@ Page { SilicaFlickable { anchors.fill: parent + VerticalScrollDecorator {} + // PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView PullDownMenu { MenuItem { @@ -20,7 +22,7 @@ Page { } // Tell SilicaFlickable the height of its content. - contentHeight: column.height + contentHeight: column.implicitHeight + Theme.paddingLarge // Place our content in a Column. The PageHeader is always placed at the top // of the page, followed by our content. diff --git a/qml/pages/Stats.qml b/qml/pages/Stats.qml index f6e319c..39ccc79 100644 --- a/qml/pages/Stats.qml +++ b/qml/pages/Stats.qml @@ -11,57 +11,63 @@ Page { SilicaFlickable { id: statsView anchors.fill: parent - contentHeight: statsColumn.implicitHeight + contentHeight: statsColumn.implicitHeight + headerItem.height VerticalScrollDecorator {} + PageHeader { + id: headerItem + title: qsTr("Stats") + } + Column { id: statsColumn spacing: Theme.paddingLarge - width: parent.width - - PageHeader { - title: qsTr("Stats") - } + width: isPortrait ? parent.width : parent.width * 0.5 + y: headerItem.height InfoRow { label: qsTr("Journeys:") - value: "0" + value: currentStatus.getJourneyCount() midlineRatio: 0.7 midlineMin: Theme.fontSizeSmall * 10 - midlineMax: Theme.fontSizeSmall * 15 + midlineMax: Theme.fontSizeSmall * 20 pixelSize: Theme.fontSizeMedium labelTextBold: true + horizontalAlignment: Text.AlignRight } InfoRow { label: qsTr("Time spent cycling:") - value: "0" - midlineRatio: 0.7 + value: currentStatus.getFormattedTime(currentStatus.getTimeSpentCycling(), 0, 5) + midlineRatio: 0.5 midlineMin: Theme.fontSizeSmall * 10 - midlineMax: Theme.fontSizeSmall * 15 + midlineMax: Theme.fontSizeSmall * 20 pixelSize: Theme.fontSizeMedium labelTextBold: true + horizontalAlignment: Text.AlignRight } InfoRow { label: qsTr("Average journey duration:") - value: "0" - midlineRatio: 0.7 + value: currentStatus.getFormattedTime(currentStatus.getAverageDuration(), 1, 5) + midlineRatio: 0.6 midlineMin: Theme.fontSizeSmall * 10 - midlineMax: Theme.fontSizeSmall * 15 + midlineMax: Theme.fontSizeSmall * 20 pixelSize: Theme.fontSizeMedium labelTextBold: true + horizontalAlignment: Text.AlignRight } InfoRow { label: qsTr("Speed percentile:") - value: "0%" + value: Math.round(100.0 - currentStatus.getSpeedPercentile() * 100) + "%" midlineRatio: 0.7 midlineMin: Theme.fontSizeSmall * 10 - midlineMax: Theme.fontSizeSmall * 15 + midlineMax: Theme.fontSizeSmall * 20 pixelSize: Theme.fontSizeMedium labelTextBold: true + horizontalAlignment: Text.AlignRight } } } diff --git a/src/harbour-pedalo.cpp b/src/harbour-pedalo.cpp index 216240d..6c0094d 100644 --- a/src/harbour-pedalo.cpp +++ b/src/harbour-pedalo.cpp @@ -36,7 +36,7 @@ int main(int argc, char *argv[]) qmlRegisterSingletonType("harbour.pedalo.settings", 1, 0, "Settings", Settings::provider); JourneyModel journeys; - Status currentStatus; + Status currentStatus(journeys); Settings::getInstance().setMainStatus(currentStatus); Settings::getInstance().loadSettings(); diff --git a/src/journeymodel.cpp b/src/journeymodel.cpp index 5b7b5da..1dc7d58 100644 --- a/src/journeymodel.cpp +++ b/src/journeymodel.cpp @@ -122,3 +122,7 @@ QDateTime JourneyModel::epochToDateTime(quint64 epoch) { date.setMSecsSinceEpoch(epoch); return date; } + +QList const & JourneyModel::getData() const { + return journeys; +} diff --git a/src/journeymodel.h b/src/journeymodel.h index 910821c..ded0184 100644 --- a/src/journeymodel.h +++ b/src/journeymodel.h @@ -39,6 +39,8 @@ public: Q_INVOKABLE static QDate epochToDate(quint64 epoch); Q_INVOKABLE static QTime epochToTime(quint64 epoch); Q_INVOKABLE static QDateTime epochToDateTime(quint64 epoch); + + QList const & getData() const; signals: // General signals void journeysChanged(); diff --git a/src/status.cpp b/src/status.cpp index 2aa9f7a..b4bb630 100644 --- a/src/status.cpp +++ b/src/status.cpp @@ -1,10 +1,12 @@ #include +#include #include "status.h" -Status::Status(QObject *parent) : QObject(parent), +Status::Status(JourneyModel &journeymodel, QObject *parent) : QObject(parent), cycling(false), - startTime(0u) + startTime(0u), + journeymodel(journeymodel) { } @@ -35,3 +37,80 @@ void Status::startJourney() { setCycling(true); setStartTime(QDateTime::currentMSecsSinceEpoch()); } + +quint64 Status::getJourneyCount() const { + return journeymodel.rowCount(); +} + +quint64 Status::getTimeSpentCycling() const { + quint64 time; + QList const & journeys = journeymodel.getData(); + + time = 0u; + foreach(Journey journey, journeys) { + time += journey.getDuration(); + } + + return time; +} + +double Status::getAverageDuration() const { + quint64 time = getTimeSpentCycling(); + quint64 count = Status::getJourneyCount(); + + return ((double)time / (double)count); +} + +double Status::getSpeedPercentile() const { + quint64 overtook; + quint64 overtakenby; + QList const & journeys = journeymodel.getData(); + double percentile; + + overtook = 0u; + overtakenby = 0u; + foreach(Journey journey, journeys) { + overtook += journey.getOvertook(); + overtakenby += journey.getOvertakenBy(); + } + + percentile = (double)overtook / (double)(overtook + overtakenby); + + return percentile; +} + +QString Status::getFormattedTime(quint64 seconds, int min, int max) { + static const QString plural[5] = {"s", "m", "h", "d", "y"}; + static const QString singular[5] = {"s", "m", "h", "d", "y"}; + static const quint64 base[5] = {60, 60, 24, 365, (quint64)-1}; + quint64 remaining; + quint64 portion; + QString formatted; + QList portions; + + min = qBound(0, min, static_cast(sizeof(base))); + max = qBound(0, max, static_cast(sizeof(base))); + + remaining = seconds; + for (int unit = 0; unit < max; unit++) { + portion = (unit == max - 1) ? remaining : remaining % base[unit]; + portions << portion; + remaining /= base[unit]; + qDebug() << plural[unit] << ": " << portion; + } + + formatted = ""; + for (int unit = max - 1; unit >= min; unit--) { + portion = portions[unit]; + + if (portion != 0) { + if (formatted.length() > 0) { + formatted += " "; + } + formatted += QString::number(portion) + " " + ((portion == 1) ? singular[unit] : plural[unit]); + } + } + + return formatted; +} + diff --git a/src/status.h b/src/status.h index f0256e0..8b6b665 100644 --- a/src/status.h +++ b/src/status.h @@ -3,19 +3,30 @@ #include +#include "journeymodel.h" + class Status : public QObject { Q_OBJECT public: - explicit Status(QObject *parent = nullptr); + explicit Status(JourneyModel &journeymodel, QObject *parent = nullptr); Q_PROPERTY(bool cycling READ getCycling WRITE setCycling NOTIFY cyclingChanged) Q_PROPERTY(quint64 startTime READ getStartTime WRITE setStartTime NOTIFY startTimeChanged) + // Current journey bool getCycling() const; quint64 getStartTime() const; Q_INVOKABLE void startJourney(); Q_INVOKABLE quint64 getDuration() const; + // Statistics + Q_INVOKABLE quint64 getJourneyCount() const; + Q_INVOKABLE quint64 getTimeSpentCycling() const; + Q_INVOKABLE double getAverageDuration() const; + Q_INVOKABLE double getSpeedPercentile() const; + + Q_INVOKABLE static QString getFormattedTime(quint64 seconds, int min, int max); + signals: void cyclingChanged(bool cycling); void startTimeChanged(quint64 startTime); @@ -27,6 +38,7 @@ public slots: private: bool cycling; quint64 startTime; + JourneyModel &journeymodel; }; #endif // STATUS_H diff --git a/translations/harbour-pedalo-de.ts b/translations/harbour-pedalo-de.ts index d479ac1..ec359d7 100644 --- a/translations/harbour-pedalo-de.ts +++ b/translations/harbour-pedalo-de.ts @@ -47,6 +47,7 @@ DurationEditDialog LTR + Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems diff --git a/translations/harbour-pedalo.ts b/translations/harbour-pedalo.ts index 42382ec..8a00fee 100644 --- a/translations/harbour-pedalo.ts +++ b/translations/harbour-pedalo.ts @@ -47,6 +47,7 @@ DurationEditDialog LTR + Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems -- 2.25.1