From 4dc94b97b2d4d28bbd7d886d2deecfd0e078a717 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Sat, 16 Nov 2013 21:01:01 +0000 Subject: [PATCH] Athlete Bests on Summary Configurable metrics to show top 10 bests for date range selected. --- src/ConfigDialog.cpp | 3 + src/ConfigDialog.h | 1 + src/Pages.cpp | 193 ++++++++++++++++++++++++++++++++++++++ src/Pages.h | 38 ++++++++ src/RideSummaryWindow.cpp | 48 +++++++++- src/Settings.h | 2 + src/SummaryMetrics.cpp | 46 +++++++++ src/SummaryMetrics.h | 18 ++++ 8 files changed, 348 insertions(+), 1 deletion(-) diff --git a/src/ConfigDialog.cpp b/src/ConfigDialog.cpp index fa2eac2d5..82abef00e 100644 --- a/src/ConfigDialog.cpp +++ b/src/ConfigDialog.cpp @@ -319,6 +319,7 @@ MetricConfig::MetricConfig(QDir home, Zones *zones, Context *context) : home(home), zones(zones), context(context) { // the widgets + bestsPage = new BestsMetricsPage(this); intervalsPage = new IntervalMetricsPage(this); summaryPage = new SummaryMetricsPage(this); @@ -328,6 +329,7 @@ MetricConfig::MetricConfig(QDir home, Zones *zones, Context *context) : mainLayout->setContentsMargins(0,0,0,0); QTabWidget *tabs = new QTabWidget(this); + tabs->addTab(bestsPage, tr("Bests")); tabs->addTab(summaryPage, tr("Summary")); tabs->addTab(intervalsPage, tr("Intervals")); @@ -336,6 +338,7 @@ MetricConfig::MetricConfig(QDir home, Zones *zones, Context *context) : void MetricConfig::saveClicked() { + bestsPage->saveClicked(); summaryPage->saveClicked(); intervalsPage->saveClicked(); } diff --git a/src/ConfigDialog.h b/src/ConfigDialog.h index 2d8eddfcf..309251de7 100644 --- a/src/ConfigDialog.h +++ b/src/ConfigDialog.h @@ -145,6 +145,7 @@ class MetricConfig : public QWidget Zones *zones; Context *context; + BestsMetricsPage *bestsPage; IntervalMetricsPage *intervalsPage; SummaryMetricsPage *summaryPage; }; diff --git a/src/Pages.cpp b/src/Pages.cpp index 395708cf4..977367e36 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -1548,6 +1548,199 @@ IntervalMetricsPage::saveClicked() appsettings->setValue(GC_SETTINGS_INTERVAL_METRICS, metrics.join(",")); } +BestsMetricsPage::BestsMetricsPage(QWidget *parent) : + QWidget(parent), changed(false) +{ + availList = new QListWidget; + availList->setSortingEnabled(true); + availList->setSelectionMode(QAbstractItemView::SingleSelection); + QVBoxLayout *availLayout = new QVBoxLayout; + availLayout->addWidget(new QLabel(tr("Available Metrics"))); + availLayout->addWidget(availList); + selectedList = new QListWidget; + selectedList->setSelectionMode(QAbstractItemView::SingleSelection); + QVBoxLayout *selectedLayout = new QVBoxLayout; + selectedLayout->addWidget(new QLabel(tr("Selected Metrics"))); + selectedLayout->addWidget(selectedList); +#ifndef Q_OS_MAC + upButton = new QToolButton(this); + downButton = new QToolButton(this); + leftButton = new QToolButton(this); + rightButton = new QToolButton(this); + upButton->setArrowType(Qt::UpArrow); + downButton->setArrowType(Qt::DownArrow); + leftButton->setArrowType(Qt::LeftArrow); + rightButton->setArrowType(Qt::RightArrow); + upButton->setFixedSize(20,20); + downButton->setFixedSize(20,20); + leftButton->setFixedSize(20,20); + rightButton->setFixedSize(20,20); +#else + upButton = new QPushButton(tr("Up")); + downButton = new QPushButton(tr("Down")); + leftButton = new QPushButton("<"); + rightButton = new QPushButton(">"); +#endif + QVBoxLayout *buttonGrid = new QVBoxLayout; + QHBoxLayout *upLayout = new QHBoxLayout; + QHBoxLayout *inexcLayout = new QHBoxLayout; + QHBoxLayout *downLayout = new QHBoxLayout; + + upLayout->addStretch(); + upLayout->addWidget(upButton); + upLayout->addStretch(); + + inexcLayout->addStretch(); + inexcLayout->addWidget(leftButton); + inexcLayout->addWidget(rightButton); + inexcLayout->addStretch(); + + downLayout->addStretch(); + downLayout->addWidget(downButton); + downLayout->addStretch(); + + buttonGrid->addStretch(); + buttonGrid->addLayout(upLayout); + buttonGrid->addLayout(inexcLayout); + buttonGrid->addLayout(downLayout); + buttonGrid->addStretch(); + + QHBoxLayout *hlayout = new QHBoxLayout; + hlayout->addLayout(availLayout); + hlayout->addLayout(buttonGrid); + hlayout->addLayout(selectedLayout); + setLayout(hlayout); + + QString s; + if (appsettings->contains(GC_SETTINGS_BESTS_METRICS)) + s = appsettings->value(this, GC_SETTINGS_BESTS_METRICS).toString(); + else + s = GC_SETTINGS_BESTS_METRICS_DEFAULT; + QStringList selectedMetrics = s.split(","); + + const RideMetricFactory &factory = RideMetricFactory::instance(); + for (int i = 0; i < factory.metricCount(); ++i) { + QString symbol = factory.metricName(i); + if (selectedMetrics.contains(symbol)) + continue; + QSharedPointer m(factory.newMetric(symbol)); + QString name = m->name(); + name.replace(tr("™"), tr(" (TM)")); + QListWidgetItem *item = new QListWidgetItem(name); + item->setData(Qt::UserRole, symbol); + availList->addItem(item); + } + foreach (QString symbol, selectedMetrics) { + if (!factory.haveMetric(symbol)) + continue; + QSharedPointer m(factory.newMetric(symbol)); + QString name = m->name(); + name.replace(tr("™"), tr(" (TM)")); + QListWidgetItem *item = new QListWidgetItem(name); + item->setData(Qt::UserRole, symbol); + selectedList->addItem(item); + } + + upButton->setEnabled(false); + downButton->setEnabled(false); + leftButton->setEnabled(false); + rightButton->setEnabled(false); + + connect(upButton, SIGNAL(clicked()), this, SLOT(upClicked())); + connect(downButton, SIGNAL(clicked()), this, SLOT(downClicked())); + connect(leftButton, SIGNAL(clicked()), this, SLOT(leftClicked())); + connect(rightButton, SIGNAL(clicked()), this, SLOT(rightClicked())); + connect(availList, SIGNAL(itemSelectionChanged()), + this, SLOT(availChanged())); + connect(selectedList, SIGNAL(itemSelectionChanged()), + this, SLOT(selectedChanged())); +} + +void +BestsMetricsPage::upClicked() +{ + assert(!selectedList->selectedItems().isEmpty()); + QListWidgetItem *item = selectedList->selectedItems().first(); + int row = selectedList->row(item); + assert(row > 0); + selectedList->takeItem(row); + selectedList->insertItem(row - 1, item); + selectedList->setCurrentItem(item); + changed = true; +} + +void +BestsMetricsPage::downClicked() +{ + assert(!selectedList->selectedItems().isEmpty()); + QListWidgetItem *item = selectedList->selectedItems().first(); + int row = selectedList->row(item); + assert(row < selectedList->count() - 1); + selectedList->takeItem(row); + selectedList->insertItem(row + 1, item); + selectedList->setCurrentItem(item); + changed = true; +} + +void +BestsMetricsPage::leftClicked() +{ + assert(!selectedList->selectedItems().isEmpty()); + QListWidgetItem *item = selectedList->selectedItems().first(); + selectedList->takeItem(selectedList->row(item)); + availList->addItem(item); + changed = true; +} + +void +BestsMetricsPage::rightClicked() +{ + assert(!availList->selectedItems().isEmpty()); + QListWidgetItem *item = availList->selectedItems().first(); + availList->takeItem(availList->row(item)); + selectedList->addItem(item); + changed = true; +} + +void +BestsMetricsPage::availChanged() +{ + rightButton->setEnabled(!availList->selectedItems().isEmpty()); +} + +void +BestsMetricsPage::selectedChanged() +{ + if (selectedList->selectedItems().isEmpty()) { + upButton->setEnabled(false); + downButton->setEnabled(false); + leftButton->setEnabled(false); + return; + } + QListWidgetItem *item = selectedList->selectedItems().first(); + int row = selectedList->row(item); + if (row == 0) + upButton->setEnabled(false); + else + upButton->setEnabled(true); + if (row == selectedList->count() - 1) + downButton->setEnabled(false); + else + downButton->setEnabled(true); + leftButton->setEnabled(true); +} + +void +BestsMetricsPage::saveClicked() +{ + if (!changed) + return; + QStringList metrics; + for (int i = 0; i < selectedList->count(); ++i) + metrics << selectedList->item(i)->data(Qt::UserRole).toString(); + appsettings->setValue(GC_SETTINGS_BESTS_METRICS, metrics.join(",")); +} + SummaryMetricsPage::SummaryMetricsPage(QWidget *parent) : QWidget(parent), changed(false) { diff --git a/src/Pages.h b/src/Pages.h index 383865d23..dcf0a7b29 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -255,6 +255,44 @@ class DevicePage : public QWidget QCheckBox *multiCheck; }; +class BestsMetricsPage : public QWidget +{ + Q_OBJECT + G_OBJECT + + + public: + + BestsMetricsPage(QWidget *parent = NULL); + + public slots: + + void upClicked(); + void downClicked(); + void leftClicked(); + void rightClicked(); + void availChanged(); + void selectedChanged(); + void saveClicked(); + + protected: + + bool changed; + QListWidget *availList; + QListWidget *selectedList; +#ifndef Q_OS_MAC + QToolButton *upButton; + QToolButton *downButton; + QToolButton *leftButton; + QToolButton *rightButton; +#else + QPushButton *upButton; + QPushButton *downButton; + QPushButton *leftButton; + QPushButton *rightButton; +#endif +}; + class IntervalMetricsPage : public QWidget { Q_OBJECT diff --git a/src/RideSummaryWindow.cpp b/src/RideSummaryWindow.cpp index 5b265943f..3a151e7e6 100644 --- a/src/RideSummaryWindow.cpp +++ b/src/RideSummaryWindow.cpp @@ -178,7 +178,6 @@ RideSummaryWindow::refresh() rideSummary->page()->mainFrame()->setHtml(htmlSummary()); } - QString RideSummaryWindow::htmlSummary() const { @@ -248,6 +247,10 @@ RideSummaryWindow::htmlSummary() const if (s == "") s = GC_SETTINGS_SUMMARY_METRICS_DEFAULT; QStringList metricColumn = s.split(","); + s = appsettings->value(this, GC_SETTINGS_BESTS_METRICS, GC_SETTINGS_BESTS_METRICS_DEFAULT).toString(); + if (s == "") s = GC_SETTINGS_BESTS_METRICS_DEFAULT; + QStringList bestsColumn = s.split(","); + static const QStringList timeInZones = QStringList() << "time_in_zone_L1" << "time_in_zone_L2" @@ -380,6 +383,49 @@ RideSummaryWindow::htmlSummary() const } summary += ""; + // + // Bests for the period + // + if (!ridesummary) { + summary += tr("

Athlete Bests

\n"); + + // best headings + summary += ""; + for (int i = 0; i < bestsColumn.count(); ++i) { + summary += ""; + } + // close the table + summary += "
" + ""; + summary = summary.arg(90 / bestsColumn.count()); + + const RideMetric *m = factory.rideMetric(bestsColumn[i]); + summary = summary.arg(m->name()); + + // get top n + QList bests = SummaryMetrics::getBests(context, bestsColumn[i], 10, data, filters, filtered, useMetricUnits); + + QColor color = QApplication::palette().alternateBase().color(); + color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); + + int pos=1; + foreach(SummaryBest best, bests) { + + // alternating shading + if (pos%2) summary += ""; + else summary += ""; + + summary += QString("") + .arg(pos++) + .arg(best.value) + .arg(best.date.toString(tr("dd MMM yy"))); + } + + // close that column + summary += "

%2

%1.%2%3
"; + + } + // // Time In Zones // diff --git a/src/Settings.h b/src/Settings.h index 5eb3048e1..1049ee74b 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -37,11 +37,13 @@ #define GC_TABS_TO_HIDE "mainwindow/tabsToHide" #define GC_ELEVATION_HYSTERESIS "elevationHysteresis" #define GC_SETTINGS_SUMMARY_METRICS "rideSummaryWindow/summaryMetrics" +#define GC_SETTINGS_BESTS_METRICS "rideSummaryWindow/bestsMetrics" #define GC_SETTINGS_INTERVAL_METRICS "rideSummaryWindow/intervalMetrics" #define GC_RIDE_PLOT_SMOOTHING "ridePlot/Smoothing" #define GC_RIDE_PLOT_STACK "ridePlot/Stack" #define GC_PERF_MAN_METRIC "performanceManager/metric" #define GC_HIST_BIN_WIDTH "histogamWindow/binWidth" +#define GC_SETTINGS_BESTS_METRICS_DEFAULT "5s_critical_power,1m_critical_power,5m_critical_power,20m_critical_power,60m_critical_power" #define GC_SETTINGS_SUMMARY_METRICS_DEFAULT "skiba_xpower,skiba_relative_intensity,skiba_bike_score,daniels_points,daniels_equivalent_power,trimp_points,aerobic_decoupling" #define GC_SETTINGS_INTERVAL_METRICS_DEFAULT "workout_time,total_distance,total_work,average_power,skiba_xpower,max_power,average_hr,ninety_five_percent_hr,average_cad,average_speed" #define GC_DATETIME_FORMAT "ddd MMM dd, yyyy, hh:mm AP" diff --git a/src/SummaryMetrics.cpp b/src/SummaryMetrics.cpp index 331ec800a..4e3aef6cf 100644 --- a/src/SummaryMetrics.cpp +++ b/src/SummaryMetrics.cpp @@ -189,3 +189,49 @@ QString SummaryMetrics::getAggregated(Context *context, QString name, const QLis return result; } + +bool summaryBestGreaterThan(const SummaryBest &s1, const SummaryBest &s2) +{ + return s1.nvalue > s2.nvalue; +} + +QList +SummaryMetrics::getBests(Context *context, QString symbol, int n, + const QList &data, + const QStringList &filters, bool filtered, + bool /* useMetricUnits */) +{ + QList results; + + // get the metric details, so we can convert etc + const RideMetric *metric = RideMetricFactory::instance().rideMetric(symbol); + if (!metric) return results; + + // loop through and aggregate + foreach (SummaryMetrics rideMetrics, data) { + + // skip filtered rides + if (filtered && !filters.contains(rideMetrics.getFileName())) continue; + if (context->isfiltered && !context->filters.contains(rideMetrics.getFileName())) continue; + + // get this value + SummaryBest add; + add.nvalue = rideMetrics.getForSymbol(symbol); + add.date = rideMetrics.getRideDate().date(); + + // XXX this needs improving for all cases ... hack for now + add.value = QString("%1").arg(add.nvalue, 0, 'f', metric->precision()); + + results << add; + } + + // now sort + qStableSort(results.begin(), results.end(), summaryBestGreaterThan); + + // truncate + if (results.count() > n) results.erase(results.begin()+n,results.end()); + + // return the array with the right number of entries in #1 - n order + return results; +} + diff --git a/src/SummaryMetrics.h b/src/SummaryMetrics.h index e4048e583..266182989 100644 --- a/src/SummaryMetrics.h +++ b/src/SummaryMetrics.h @@ -24,7 +24,19 @@ #include #include #include + class Context; +class SummaryBest +{ + public: + double nvalue; + QString value; // formatted value + QDate date; + + // for qsort + bool operator< (SummaryBest right) const { return (nvalue < right.nvalue); } +}; + class SummaryMetrics { Q_DECLARE_TR_FUNCTIONS(SummaryMetrics) @@ -69,6 +81,12 @@ class SummaryMetrics const QStringList &filters, bool filtered, bool useMetricUnits, bool nofmt = false); + // get an ordered list pf bests for that symbol + static QList getBests(Context *context, QString symbol, int n, + const QList &results, + const QStringList &filters, bool filtered, + bool useMetricUnits); + QMap &values() { return value; } QMap &texts() { return text; }