diff --git a/src/Pages.cpp b/src/Pages.cpp index 3619f1b06..1530c715c 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -19,11 +19,13 @@ ConfigurationPage::ConfigurationPage(MainWindow *main) : main(main) QWidget *config = new QWidget(this); QVBoxLayout *configLayout = new QVBoxLayout(config); colorsPage = new ColorsPage(main); + summaryMetrics = new SummaryMetricsPage; intervalMetrics = new IntervalMetricsPage; metadataPage = new MetadataPage(main); tabs->addTab(config, tr("Basic Settings")); tabs->addTab(colorsPage, tr("Colors")); + tabs->addTab(summaryMetrics, tr("Summary Metrics")); tabs->addTab(intervalMetrics, tr("Interval Metrics")); tabs->addTab(metadataPage, tr("Ride Data")); @@ -227,6 +229,7 @@ void ConfigurationPage::saveClicked() { colorsPage->saveClicked(); + summaryMetrics->saveClicked(); intervalMetrics->saveClicked(); metadataPage->saveClicked(); } @@ -930,6 +933,187 @@ IntervalMetricsPage::saveClicked() settings->setValue(GC_SETTINGS_INTERVAL_METRICS, metrics.join(",")); } + +SummaryMetricsPage::SummaryMetricsPage(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); + upButton = new QPushButton("Move up"); + downButton = new QPushButton("Move down"); + leftButton = new QPushButton("Exclude"); + rightButton = new QPushButton("Include"); + 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; + boost::shared_ptr settings = GetApplicationSettings(); + if (settings->contains(GC_SETTINGS_SUMMARY_METRICS)) + s = settings->value(GC_SETTINGS_SUMMARY_METRICS).toString(); + else + s = GC_SETTINGS_SUMMARY_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 +SummaryMetricsPage::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 +SummaryMetricsPage::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 +SummaryMetricsPage::leftClicked() +{ + assert(!selectedList->selectedItems().isEmpty()); + QListWidgetItem *item = selectedList->selectedItems().first(); + selectedList->takeItem(selectedList->row(item)); + availList->addItem(item); + changed = true; +} + +void +SummaryMetricsPage::rightClicked() +{ + assert(!availList->selectedItems().isEmpty()); + QListWidgetItem *item = availList->selectedItems().first(); + availList->takeItem(availList->row(item)); + selectedList->addItem(item); + changed = true; +} + +void +SummaryMetricsPage::availChanged() +{ + rightButton->setEnabled(!availList->selectedItems().isEmpty()); +} + +void +SummaryMetricsPage::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 +SummaryMetricsPage::saveClicked() +{ + if (!changed) + return; + QStringList metrics; + for (int i = 0; i < selectedList->count(); ++i) + metrics << selectedList->item(i)->data(Qt::UserRole).toString(); + boost::shared_ptr settings = GetApplicationSettings(); + settings->setValue(GC_SETTINGS_SUMMARY_METRICS, metrics.join(",")); +} + MetadataPage::MetadataPage(MainWindow *main) : main(main) { QVBoxLayout *layout = new QVBoxLayout(this); diff --git a/src/Pages.h b/src/Pages.h index 3f023dd7f..5beea0636 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -35,6 +35,7 @@ class QHBoxLayout; class QVBoxLayout; class ColorsPage; class IntervalMetricsPage; +class SummaryMetricsPage; class MetadataPage; class KeywordsPage; class FieldsPage; @@ -66,6 +67,7 @@ class ConfigurationPage : public QWidget private: MainWindow *main; ColorsPage *colorsPage; + SummaryMetricsPage *summaryMetrics; IntervalMetricsPage *intervalMetrics; MetadataPage *metadataPage; @@ -228,6 +230,36 @@ class IntervalMetricsPage : public QWidget QPushButton *rightButton; }; +class SummaryMetricsPage : public QWidget +{ + Q_OBJECT + + public: + + SummaryMetricsPage(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; + QPushButton *upButton; + QPushButton *downButton; + QPushButton *leftButton; + QPushButton *rightButton; +}; + + class KeywordsPage : public QWidget { Q_OBJECT diff --git a/src/RideSummaryWindow.cpp b/src/RideSummaryWindow.cpp index 07fcd5e73..d9a5f3c13 100644 --- a/src/RideSummaryWindow.cpp +++ b/src/RideSummaryWindow.cpp @@ -108,7 +108,25 @@ RideSummaryWindow::htmlSummary() const NULL }; - const char *metricColumn[] = { + QString s; + + if (settings->contains(GC_SETTINGS_SUMMARY_METRICS)) + s = settings->value(GC_SETTINGS_SUMMARY_METRICS).toString(); + else + s = GC_SETTINGS_SUMMARY_METRICS_DEFAULT; + QStringList metricColumnList = s.split(","); + + char **metricColumnTmp; + // Copy QStringList to char ** + metricColumnTmp = new char*[metricColumnList.size() + 1]; + for (int i = 0; i < metricColumnList.size(); i++) { + metricColumnTmp[i] = new char[strlen(metricColumnList.at(i).toStdString().c_str())+1]; + memcpy(metricColumnTmp[i], metricColumnList.at(i).toStdString().c_str(), strlen(metricColumnList.at(i).toStdString().c_str())+1); + } + metricColumnTmp[metricColumnList.size()] = NULL; + char const **metricColumn = (const char**)metricColumnTmp; + + /*const char *metricColumn[] = { "skiba_xpower", "skiba_relative_intensity", "skiba_bike_score", @@ -117,7 +135,7 @@ RideSummaryWindow::htmlSummary() const "trimp_points", "aerobic_decoupling", NULL - }; + };*/ summary += ""; for (int i = 0; i < columns; ++i) { @@ -135,6 +153,7 @@ RideSummaryWindow::htmlSummary() const for (int j = 0;; ++j) { const char *symbol = metricsList[j]; if (!symbol) break; + RideMetricPtr m = rideItem->metrics.value(symbol); QString name = m->name().replace(QRegExp(tr("^Average ")), ""); if (m->units(metricUnits) == "seconds" || m->units(metricUnits) == tr("seconds")) { diff --git a/src/Settings.h b/src/Settings.h index b6909538b..7d569828d 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -31,11 +31,13 @@ #define GC_SETTINGS_SUMMARYSPLITTER_SIZES "mainwindow/summarysplittersizes" #define GC_SETTINGS_CALENDAR_SIZES "mainwindow/calendarSizes" #define GC_TABS_TO_HIDE "mainwindow/tabsToHide" +#define GC_SETTINGS_SUMMARY_METRICS "rideSummaryWindow/summaryMetrics" #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_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" #define GC_UNIT "unit" diff --git a/src/TRIMPPoints.cpp b/src/TRIMPPoints.cpp index 755f33823..2ace95e9b 100644 --- a/src/TRIMPPoints.cpp +++ b/src/TRIMPPoints.cpp @@ -293,6 +293,58 @@ public: RideMetric *clone() const { return new TRIMPZonalPoints(*this); } }; + +// RPE is the rate of percieved exercion (borg scale). +// Is a numerical value the riders give in "average" fatigue of the training session he percieved. +// +// Calculate the session RPE that is the product of RPE * time (minutes) of training/race ride. I +// We have 3 different "training load" parameters: +// - internal load (TRIMPS) +// - external load (bikescore/TSS) +// - perceived load (session RPE) +// +class SessionRPE : public RideMetric { + Q_DECLARE_TR_FUNCTIONS(TRIMPPoints) + + double score; + + public: + + SessionRPE() : score(0.0) + { + setSymbol("session_rpe"); +#ifdef ENABLE_METRICS_TRANSLATION + setInternalName("Session RPE"); + } + void initialize() { +#endif + setName(tr("Session RPE")); + setMetricUnits(""); + setImperialUnits(""); + setType(RideMetric::Total); + } + void compute(const RideFile *rideFile, + const Zones *, int , + const HrZones *hrZones, int hrZoneRange, + const QHash &deps) + { + // use RPE value in ride metadata + double rpe = rideFile->getTag("RPE", "0.0").toDouble(); + + assert(deps.contains("workout_time")); + const RideMetric *workoutTimeMetric = deps.value("workout_time"); + assert(workoutTimeMetric); + + double secs = workoutTimeMetric->value(true); + + // ok lets work the score out + score = ((secs == 0.0 || rpe == 0) ? 0.0 : secs/60 *rpe); + setValue(score); + } + + RideMetric *clone() const { return new SessionRPE(*this); } +}; + static bool added() { QVector deps; deps.append("workout_time"); @@ -312,6 +364,10 @@ static bool added() { deps.append("time_in_zone_H4"); deps.append("time_in_zone_H5"); RideMetricFactory::instance().addMetric(TRIMPZonalPoints(), &deps); + + deps.clear(); + deps.append("workout_time"); + RideMetricFactory::instance().addMetric(SessionRPE(), &deps); return true; }