Display overall statistics
authorDavid Llewellyn-Jones <david@flypig.co.uk>
Sun, 15 Jul 2018 20:27:16 +0000 (21:27 +0100)
committerDavid Llewellyn-Jones <david@flypig.co.uk>
Sun, 15 Jul 2018 20:27:16 +0000 (21:27 +0100)
13 files changed:
qml/pages/DurationEditDialog.qml
qml/pages/JourneyEdit.qml
qml/pages/JourneyInfo.qml
qml/pages/JourneyList.qml
qml/pages/MainPage.qml
qml/pages/Stats.qml
src/harbour-pedalo.cpp
src/journeymodel.cpp
src/journeymodel.h
src/status.cpp
src/status.h
translations/harbour-pedalo-de.ts
translations/harbour-pedalo.ts

index c1a577b..c4e256b 100644 (file)
@@ -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
index 1f61b39..a0d4349 100644 (file)
@@ -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: {
index 0181602..02cea45 100644 (file)
@@ -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
index 1708d20..2b722be 100644 (file)
@@ -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})
 
index 101376a..5c0c16b 100644 (file)
@@ -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.
index f6e319c..39ccc79 100644 (file)
@@ -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
             }
         }
     }
index 216240d..6c0094d 100644 (file)
@@ -36,7 +36,7 @@ int main(int argc, char *argv[])
     qmlRegisterSingletonType<Settings>("harbour.pedalo.settings", 1, 0, "Settings", Settings::provider);
 
     JourneyModel journeys;
-    Status currentStatus;
+    Status currentStatus(journeys);
     Settings::getInstance().setMainStatus(currentStatus);
     Settings::getInstance().loadSettings();
 
index 5b7b5da..1dc7d58 100644 (file)
@@ -122,3 +122,7 @@ QDateTime JourneyModel::epochToDateTime(quint64 epoch) {
     date.setMSecsSinceEpoch(epoch);
     return date;
 }
+
+QList<Journey> const & JourneyModel::getData() const {
+    return journeys;
+}
index 910821c..ded0184 100644 (file)
@@ -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<Journey> const & getData() const;
 signals:
     // General signals
     void journeysChanged();
index 2aa9f7a..b4bb630 100644 (file)
@@ -1,10 +1,12 @@
 #include <QDateTime>
+#include <QDebug>
 
 #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<Journey> 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<Journey> 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<quint64> portions;
+
+    min = qBound(0, min, static_cast<int>(sizeof(base)));
+    max = qBound(0, max, static_cast<int>(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;
+}
+
index f0256e0..8b6b665 100644 (file)
@@ -3,19 +3,30 @@
 
 #include <QObject>
 
+#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
index d479ac1..ec359d7 100644 (file)
@@ -47,6 +47,7 @@
     <name>DurationEditDialog</name>
     <message>
         <source>LTR</source>
+        <extracomment>Can have two values: &quot;LTR&quot; if remaining time in timer item should be written in &quot;[value] [unit]&quot; order i.e. &quot;2 min&quot;, or &quot;RTL&quot; i.e. right-to-left like in Arabic writing systems</extracomment>
         <translation type="unfinished"></translation>
     </message>
     <message>
index 42382ec..8a00fee 100644 (file)
@@ -47,6 +47,7 @@
     <name>DurationEditDialog</name>
     <message>
         <source>LTR</source>
+        <extracomment>Can have two values: &quot;LTR&quot; if remaining time in timer item should be written in &quot;[value] [unit]&quot; order i.e. &quot;2 min&quot;, or &quot;RTL&quot; i.e. right-to-left like in Arabic writing systems</extracomment>
         <translation type="unfinished"></translation>
     </message>
     <message>