From d22777ed74ae8a2acf2951e48a4e1a074aa8449b Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Tue, 13 Nov 2012 13:27:36 +0000 Subject: [PATCH] UI Nits: Daily/Weekly/Monthly Summary You can now add the summary chart to the diary view to get a summary of the date range currently being summarised on that view. Once the Home view has its own sidebar that selects date ranges you will be able to add it there too and summarise seasons etc. --- src/DBAccess.h | 7 + src/GcCalendar.cpp | 71 +-------- src/GcCalendar.h | 5 +- src/GcWindowRegistry.cpp | 6 +- src/GcWindowRegistry.h | 1 + src/GoldenCheetah.cpp | 3 + src/GoldenCheetah.h | 1 + src/HomeWindow.cpp | 28 ++++ src/HomeWindow.h | 1 + src/MainWindow.cpp | 18 ++- src/MainWindow.h | 2 + src/MetricAggregator.cpp | 12 ++ src/MetricAggregator.h | 2 + src/RideSummaryWindow.cpp | 321 ++++++++++++++++++++++---------------- src/RideSummaryWindow.h | 10 +- src/SummaryMetrics.cpp | 65 ++++++++ src/SummaryMetrics.h | 3 + src/TimeUtils.cpp | 5 +- src/TimeUtils.h | 3 +- 19 files changed, 355 insertions(+), 209 deletions(-) diff --git a/src/DBAccess.h b/src/DBAccess.h index 6a1a26947..14e6a059f 100644 --- a/src/DBAccess.h +++ b/src/DBAccess.h @@ -62,8 +62,15 @@ class DBAccess // Query Records QList getAllMetricsFor(QDateTime start, QDateTime end); + QList getAllMetricsFor(DateRange dr) { + return getAllMetricsFor(QDateTime(dr.from,QTime(0,0,0)), QDateTime(dr.to, QTime(23,59,59))); + } + bool getRide(QString filename, SummaryMetrics &metrics, QColor&color); QList getAllMeasuresFor(QDateTime start, QDateTime end); + QList getAllMeasuresFor(DateRange dr) { + return getAllMeasuresFor(QDateTime(dr.from,QTime(0,0,0)), QDateTime(dr.to, QTime(23,59,59))); + } SummaryMetrics getRideMetrics(QString filename); // for a filename diff --git a/src/GcCalendar.cpp b/src/GcCalendar.cpp index f3ebf24e4..accd7ff3b 100644 --- a/src/GcCalendar.cpp +++ b/src/GcCalendar.cpp @@ -640,7 +640,7 @@ GcCalendar::setSummary() const RideMetric *metric = RideMetricFactory::instance().rideMetric(metricname); - QString value = getAggregated(metricname, results); + QString value = SummaryMetrics::getAggregated(metricname, results, useMetricUnits); // don't show units for time values @@ -670,72 +670,15 @@ GcCalendar::setSummary() // set webview contents summary->page()->mainFrame()->setHtml(summaryText); - } -} - -QString GcCalendar::getAggregated(QString name, QList &results) -{ - // get the metric details, so we can convert etc - const RideMetric *metric = RideMetricFactory::instance().rideMetric(name); - if (!metric) return QString("%1 unknown").arg(name); - - // do we need to convert from metric to imperial? - bool useMetricUnits = (appsettings->value(this, GC_UNIT).toString() == "Metric"); - - // what we will return - double rvalue = 0; - double rcount = 0; // using double to avoid rounding issues with int when dividing - - // loop through and aggregate - foreach (SummaryMetrics rideMetrics, results) { - - // get this value - double value = rideMetrics.getForSymbol(name); - double count = rideMetrics.getForSymbol("workout_time"); // for averaging - + // tell everyone the date range changed + QString name; + if (summarySelect->currentIndex() == 0) name = "Day of " + from.toString("dddd MMMM d"); + else if (summarySelect->currentIndex() == 1) name = QString("Week Commencing %1").arg(from.toString("dddd MMMM d")); + else if (summarySelect->currentIndex() == 2) name = from.toString("MMMM yyyy"); - // check values are bounded, just in case - if (isnan(value) || isinf(value)) value = 0; + emit dateRangeChanged(DateRange(from, to, name)); - // imperial / metric conversion - if (useMetricUnits == false) { - value *= metric->conversion(); - value += metric->conversionSum(); - } - - switch (metric->type()) { - case RideMetric::Total: - rvalue += value; - break; - case RideMetric::Average: - { - // average should be calculated taking into account - // the duration of the ride, otherwise high value but - // short rides will skew the overall average - rvalue += value*count; - rcount += count; - break; - } - case RideMetric::Peak: - { - if (value > rvalue) rvalue = value; - break; - } - } } - - // now compute the average - if (metric->type() == RideMetric::Average) { - if (rcount) rvalue = rvalue / rcount; - } - - - // Format appropriately - QString result; - if (metric->units(useMetricUnits) == "seconds") result = time_to_string(rvalue); - else result = QString("%1").arg(rvalue, 0, 'f', metric->precision()); - - return result; } void diff --git a/src/GcCalendar.h b/src/GcCalendar.h index eef6fe669..c2be5f420 100644 --- a/src/GcCalendar.h +++ b/src/GcCalendar.h @@ -25,6 +25,7 @@ #include #include "MainWindow.h" +#include "TimeUtils.h" #include "GcCalendarModel.h" #include "RideItem.h" #include "RideNavigator.h" @@ -82,9 +83,11 @@ class GcCalendar : public QWidget // not a GcWindow - belongs on sidebar void setSummary(); // set the summary at the bottom // summary metrics aggregator -- refactor later - QString getAggregated(QString name, QList &results); void splitterMoved(int pos, int index); + signals: + void dateRangeChanged(DateRange); + protected: MainWindow *main; RideItem *_ride; diff --git a/src/GcWindowRegistry.cpp b/src/GcWindowRegistry.cpp index e35057c89..674392b66 100644 --- a/src/GcWindowRegistry.cpp +++ b/src/GcWindowRegistry.cpp @@ -67,7 +67,7 @@ GcWindowRegistry GcWindows[] = { { VIEW_HOME, "Performance Manager",GcWindowTypes::PerformanceManager }, { VIEW_HOME, "Collection TreeMap",GcWindowTypes::TreeMap }, { VIEW_HOME, "Weekly Summary",GcWindowTypes::WeeklySummary }, - { VIEW_ANALYSIS, "Summary",GcWindowTypes::RideSummary }, + { VIEW_ANALYSIS, "Activity Summary",GcWindowTypes::RideSummary }, { VIEW_ANALYSIS, "Details",GcWindowTypes::MetadataWindow }, { VIEW_ANALYSIS, "Summary and Details",GcWindowTypes::Summary }, { VIEW_ANALYSIS, "Editor",GcWindowTypes::RideEditor }, @@ -83,6 +83,7 @@ GcWindowRegistry GcWindows[] = { { VIEW_ANALYSIS, "Aerolab Chung Analysis",GcWindowTypes::Aerolab }, { VIEW_DIARY, "Calendar",GcWindowTypes::Diary }, { VIEW_DIARY, "Navigator", GcWindowTypes::ActivityNavigator }, + { VIEW_DIARY, "Summary", GcWindowTypes::DateRangeSummary }, { VIEW_TRAIN, "Telemetry",GcWindowTypes::DialWindow }, { VIEW_TRAIN, "Workout",GcWindowTypes::WorkoutPlot }, { VIEW_TRAIN, "Realtime",GcWindowTypes::RealtimePlot }, @@ -119,7 +120,8 @@ GcWindowRegistry::newGcWindow(GcWinID id, MainWindow *main) //XXX mainWindow wil case GcWindowTypes::PfPv: returning = new PfPvWindow(main); break; case GcWindowTypes::HrPw: returning = new HrPwWindow(main); break; case GcWindowTypes::RideEditor: returning = new RideEditor(main); break; - case GcWindowTypes::RideSummary: returning = new RideSummaryWindow(main); break; + case GcWindowTypes::RideSummary: returning = new RideSummaryWindow(main, true); break; + case GcWindowTypes::DateRangeSummary: returning = new RideSummaryWindow(main, false); break; case GcWindowTypes::Scatter: returning = new ScatterWindow(main, main->home); break; case GcWindowTypes::Summary: returning = new SummaryWindow(main); break; case GcWindowTypes::TreeMap: returning = new TreeMapWindow(main, main->useMetricUnits, main->home); break; diff --git a/src/GcWindowRegistry.h b/src/GcWindowRegistry.h index f352de1eb..7f5e47997 100644 --- a/src/GcWindowRegistry.h +++ b/src/GcWindowRegistry.h @@ -57,6 +57,7 @@ enum gcwinid { RealtimeControls = 29, ActivityNavigator = 30, SpinScanPlot = 31, + DateRangeSummary = 32 }; }; typedef enum GcWindowTypes::gcwinid GcWinID; diff --git a/src/GoldenCheetah.cpp b/src/GoldenCheetah.cpp index 56f6d96a9..7f5b08cd0 100644 --- a/src/GoldenCheetah.cpp +++ b/src/GoldenCheetah.cpp @@ -90,6 +90,7 @@ void GcWindow::setRideItem(RideItem* x) void GcWindow::setDateRange(DateRange dr) { _dr = dr; + emit dateRangeChanged(_dr); } DateRange GcWindow::dateRange() const @@ -154,6 +155,7 @@ GcWindow::GcWindow() qRegisterMetaType("ride"); qRegisterMetaType("type"); qRegisterMetaType("color"); + qRegisterMetaType("dateRange"); setControls(NULL); setRideItem(NULL); setTitle(""); @@ -185,6 +187,7 @@ GcWindow::GcWindow(QWidget *parent) : QFrame(parent), dragState(None) { qRegisterMetaType("ride"); qRegisterMetaType("type"); qRegisterMetaType("color"); + qRegisterMetaType("dateRange"); setParent(parent); setControls(NULL); setRideItem(NULL); diff --git a/src/GoldenCheetah.h b/src/GoldenCheetah.h index f6e0c25be..409f00c12 100644 --- a/src/GoldenCheetah.h +++ b/src/GoldenCheetah.h @@ -23,6 +23,7 @@ #define G_OBJECT Q_PROPERTY(QString instanceName READ instanceName WRITE setInstanceName) #define setInstanceName(x) setProperty("instanceName", x) #define myRideItem property("ride").value() +#define myDateRange property("dateRange").value() #include #include diff --git a/src/HomeWindow.cpp b/src/HomeWindow.cpp index 9c09039a0..37282868b 100644 --- a/src/HomeWindow.cpp +++ b/src/HomeWindow.cpp @@ -202,6 +202,7 @@ HomeWindow::HomeWindow(MainWindow *mainWindow, QString name, QString /* windowti styleChanged(2); connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected())); + connect(this, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange))); connect(mainWindow, SIGNAL(configChanged()), this, SLOT(configChanged())); connect(tabbed, SIGNAL(currentChanged(int)), this, SLOT(tabSelected(int))); connect(tabbed, SIGNAL(tabCloseRequested(int)), this, SLOT(removeChart(int))); @@ -359,6 +360,27 @@ HomeWindow::rideSelected() } } +void +HomeWindow::dateRangeChanged(DateRange dr) +{ + if (amVisible()) { + + for (int i=0; i < charts.count(); i++) { + + // show if its not a tab + if (currentStyle) charts[i]->show(); // keep tabs hidden, show the rest + + // if we are tabbed AND its the current tab then mimic tabselected + // to force the tabwidget to refresh AND set its ride property (see below) + //otherwise just go ahead and notify it of a new ride + if (!currentStyle && charts.count() && i==tabbed->currentIndex()) + tabSelected(tabbed->currentIndex()); + else + charts[i]->setProperty("dateRange", property("dateRange")); + } + } +} + void HomeWindow::tabSelected(int index) { @@ -367,6 +389,7 @@ HomeWindow::tabSelected(int index) if (index >= 0) { charts[index]->show(); charts[index]->setProperty("ride", property("ride")); + charts[index]->setProperty("dateRange", property("dateRange")); controlStack->setCurrentIndex(index); titleEdit->setText(charts[index]->property("title").toString()); } @@ -430,6 +453,7 @@ HomeWindow::styleChanged(int id) tabbed->addTab(charts[i], charts[i]->property("title").toString()); charts[i]->setContentsMargins(0,25,0,0); charts[i]->setResizable(false); // we need to show on tab selection! + charts[i]->setProperty("dateRange", property("dateRange")); charts[i]->hide(); // we need to show on tab selection! break; case 1 : // they are lists in a GridLayout @@ -437,6 +461,7 @@ HomeWindow::styleChanged(int id) charts[i]->setContentsMargins(0,25,0,0); charts[i]->setResizable(false); // we need to show on tab selection! charts[i]->show(); + charts[i]->setProperty("dateRange", property("dateRange")); charts[i]->setProperty("ride", property("ride")); break; case 2 : // thet are in a FlowLayout @@ -444,6 +469,7 @@ HomeWindow::styleChanged(int id) charts[i]->setContentsMargins(0,15,0,0); charts[i]->setResizable(true); // we need to show on tab selection! charts[i]->show(); + charts[i]->setProperty("dateRange", property("dateRange")); charts[i]->setProperty("ride", property("ride")); default: break; @@ -571,6 +597,7 @@ HomeWindow::addChart(GcWindow* newone) RideItem *notconst = (RideItem*)mainWindow->currentRideItem(); newone->setProperty("ride", QVariant::fromValue(notconst)); + newone->setProperty("dateRange", property("dateRange")); // add to tabs switch (currentStyle) { @@ -1219,6 +1246,7 @@ GcWindowDialog::GcWindowDialog(GcWinID type, MainWindow *mainWindow) : mainWindo RideItem *notconst = (RideItem*)mainWindow->currentRideItem(); win->setProperty("ride", QVariant::fromValue(notconst)); + win->setProperty("dateRange", property("dateRange")); layout->setStretch(0, 100); layout->setStretch(1, 50); diff --git a/src/HomeWindow.h b/src/HomeWindow.h index 937fb61f2..2e5f2a09c 100644 --- a/src/HomeWindow.h +++ b/src/HomeWindow.h @@ -55,6 +55,7 @@ class HomeWindow : public GcWindow // GC signals void rideSelected(); + void dateRangeChanged(DateRange); void configChanged(); // QT Widget events and signals diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index c0fa14c9f..1c30ebdc8 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -517,10 +517,6 @@ MainWindow::MainWindow(const QDir &home) : chartTool = new GcWindowTool(this); #endif - // RIGHT SIDEBAR - gcCalendar = new GcCalendar(this); - //gcCalendar->setStyleSheet("background: #B3B4BA;"); - //gcCalendar->setStyleSheet("background: white;"); // TOOLBOX toolBox = new QToolBox(this); @@ -584,6 +580,10 @@ MainWindow::MainWindow(const QDir &home) : // POPULATE TOOLBOX + // do controllers after home windows -- they need their first signals caught + gcCalendar = new GcCalendar(this); + connect(gcCalendar, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange))); + toolBox->addItem(intervalSplitter, QIcon(":images/activity.png"), "Activity History"); toolBox->addItem(gcCalendar, QIcon(":images/toolbar/main/diary.png"), "Calendar"); toolBox->addItem(trainTool->controls(), QIcon(":images/library.png"), "Workout Library"); @@ -1037,6 +1037,15 @@ MainWindow::rideTreeWidgetSelectionChanged() } } +void +MainWindow::dateRangeChanged(DateRange dr) +{ + // we got signalled date range changed, tell the current view + // when we have multiple sidebars that change date we need to connect + // them up individually.... i.e. LTM.... + diaryWindow->setProperty("dateRange", QVariant::fromValue(dr)); +} + void MainWindow::enableSaveButton() { @@ -1326,6 +1335,7 @@ MainWindow::selectDiary() analButtons->hide(); trainTool->getToolbarButtons()->hide(); toolBox->setCurrentIndex(1); + gcCalendar->refresh(); // get that signal with the date range... setStyle(); } diff --git a/src/MainWindow.h b/src/MainWindow.h index 708d0fd15..780387289 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -323,6 +323,8 @@ class MainWindow : public QMainWindow void toggleFullScreen(); #endif + void dateRangeChanged(DateRange); + protected: static QString notesFileName(QString rideFileName); diff --git a/src/MetricAggregator.cpp b/src/MetricAggregator.cpp index a9b9f2432..84a2a4f86 100644 --- a/src/MetricAggregator.cpp +++ b/src/MetricAggregator.cpp @@ -306,6 +306,12 @@ MetricAggregator::writeAsCSV(QString filename) file.close(); } +QList +MetricAggregator::getAllMetricsFor(DateRange dr) +{ + return getAllMetricsFor(QDateTime(dr.from, QTime(0,0,0)), QDateTime(dr.to, QTime(23,59,59))); +} + QList MetricAggregator::getAllMetricsFor(QDateTime start, QDateTime end) { @@ -349,6 +355,12 @@ MetricAggregator::getAllMetricsFor(QString filename) return results; } +QList +MetricAggregator::getAllMeasuresFor(DateRange dr) +{ + return getAllMeasuresFor(QDateTime(dr.from, QTime(0,0,0)), QDateTime(dr.to, QTime(23,59,59))); +} + QList MetricAggregator::getAllMeasuresFor(QDateTime start, QDateTime end) { diff --git a/src/MetricAggregator.h b/src/MetricAggregator.h index fc497d808..f0d6112a1 100644 --- a/src/MetricAggregator.h +++ b/src/MetricAggregator.h @@ -48,7 +48,9 @@ class MetricAggregator : public QObject DBAccess *db() { return dbaccess; } SummaryMetrics getAllMetricsFor(QString filename); // for a single ride QList getAllMetricsFor(QDateTime start, QDateTime end); + QList getAllMetricsFor(DateRange); QList getAllMeasuresFor(QDateTime start, QDateTime end); + QList getAllMeasuresFor(DateRange); SummaryMetrics getRideMetrics(QString filename); void writeAsCSV(QString filename); // export all... diff --git a/src/RideSummaryWindow.cpp b/src/RideSummaryWindow.cpp index c5172c6ca..0593712e3 100644 --- a/src/RideSummaryWindow.cpp +++ b/src/RideSummaryWindow.cpp @@ -32,8 +32,8 @@ #include #include -RideSummaryWindow::RideSummaryWindow(MainWindow *mainWindow) : - GcWindow(mainWindow), mainWindow(mainWindow) +RideSummaryWindow::RideSummaryWindow(MainWindow *mainWindow, bool ridesummary) : + GcWindow(mainWindow), mainWindow(mainWindow), ridesummary(ridesummary) { setInstanceName("Ride Summary Window"); setControls(NULL); @@ -54,9 +54,15 @@ RideSummaryWindow::RideSummaryWindow(MainWindow *mainWindow) : vlayout->addWidget(rideSummary); - connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideItemChanged())); - connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(refresh())); - connect(mainWindow, SIGNAL(intervalsChanged()), this, SLOT(refresh())); + if (ridesummary) { + connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideItemChanged())); + connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(refresh())); + connect(mainWindow, SIGNAL(intervalsChanged()), this, SLOT(refresh())); + } else { + connect(this, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange))); + connect(mainWindow, SIGNAL(rideAdded(RideItem*)), this, SLOT(refresh())); + connect(mainWindow, SIGNAL(rideDeleted(RideItem*)), this, SLOT(refresh())); + } setLayout(vlayout); } @@ -93,15 +99,27 @@ RideSummaryWindow::refresh() { // XXX: activeTab is never equaly to RideSummaryWindow right now because // it's wrapped in the summarySplitter in MainWindow. - if (!myRideItem) { + if (ridesummary && !myRideItem) { rideSummary->page()->mainFrame()->setHtml(""); return; } - RideItem *rideItem = myRideItem; - setSubTitle(rideItem->dateTime.toString(tr("dddd MMMM d, yyyy, h:mm AP"))); + + if (ridesummary) { + RideItem *rideItem = myRideItem; + setSubTitle(rideItem->dateTime.toString(tr("dddd MMMM d, yyyy, h:mm AP"))); + } else { + + if (myDateRange.name != "") setSubTitle(myDateRange.name); + else { + setSubTitle(myDateRange.from.toString("dddd MMMM d") + + " - " + + myDateRange.to.toString("dddd MMMM d")); + } + } rideSummary->page()->mainFrame()->setHtml(htmlSummary()); } + QString RideSummaryWindow::htmlSummary() const { @@ -109,9 +127,11 @@ RideSummaryWindow::htmlSummary() const RideItem *rideItem = myRideItem; RideFile *ride = rideItem->ride(); + QVariant unit = appsettings->value(this, GC_UNIT); + bool useMetricUnits = (unit.toString() == "Metric"); - // ridefile read errors? - if (!ride) { + // ride summary and there were ridefile read errors? + if (ridesummary && !ride) { summary = tr("

Couldn't read file \""); summary += rideItem->fileName + "\":"; QListIterator i(rideItem->errors()); @@ -120,58 +140,48 @@ RideSummaryWindow::htmlSummary() const return summary; } - summary = ("

" + tr("Device Type: ") + ride->deviceType() + "

"); + // always centered + summary = "
"; - QVariant unit = appsettings->value(this, GC_UNIT); + // device summary for ride summary, otherwise how many activities? + if (ridesummary) summary += ("

" + tr("Device Type: ") + ride->deviceType() + "

"); + else summary += ("

" + + QString("%1").arg(data.count()) + (data.count() == 1 ? tr(" activity") : tr(" activities")) + + "

"); - summary += "

"; - - bool metricUnits = (unit.toString() == "Metric"); - - QStringList columnNames = QStringList() << "Totals" <<"Averages" <<"Maximums" <<"Metrics*"; - QStringList totalColumn = QStringList() + // All the metrics we will display + static const QStringList columnNames = QStringList() << "Totals" <<"Averages" <<"Maximums" <<"Metrics*"; + static const QStringList totalColumn = QStringList() << "workout_time" << "time_riding" << "total_distance" << "total_work" << "elevation_gain"; - QStringList averageColumn = QStringList() + static QStringList averageColumn = QStringList() // not const as modified below.. << "average_speed" << "average_power" << "average_hr" << "average_cad"; - // show temp if it is available - if (ride->areDataPresent()->temp || ride->getTag("Temperature", "-") != "-") averageColumn << "average_temp"; - - QStringList maximumColumn = QStringList() + static QStringList maximumColumn = QStringList() // not const as modified below.. << "max_speed" << "max_power" << "max_heartrate" << "max_cadence"; - // show temp if it is available - if (ride->areDataPresent()->temp || ride->getTag("Temperature", "-") != "-") maximumColumn << "max_temp"; + // show average and max temp if it is available (in ride summary mode) + if (ridesummary && (ride->areDataPresent()->temp || ride->getTag("Temperature", "-") != "-")) { + averageColumn << "average_temp"; + maximumColumn << "max_temp"; + } + // users determine the metrics to display QString s = appsettings->value(this, GC_SETTINGS_SUMMARY_METRICS, GC_SETTINGS_SUMMARY_METRICS_DEFAULT).toString(); - - // in case they were set tand then unset if (s == "") s = GC_SETTINGS_SUMMARY_METRICS_DEFAULT; QStringList metricColumn = s.split(","); - /* ORIGINAL HARDCODED (AND NOW DEFAULT METRICS) - QStringList metricColumn = QStringList() - << "skiba_xpower" - << "skiba_relative_intensity" - << "skiba_bike_score" - << "daniels_points" - << "daniels_equivalent_power" - << "trimp_points" - << "aerobic_decoupling"; - */ - - QStringList timeInZones = QStringList() + static const QStringList timeInZones = QStringList() << "time_in_zone_L1" << "time_in_zone_L2" << "time_in_zone_L3" @@ -183,7 +193,7 @@ RideSummaryWindow::htmlSummary() const << "time_in_zone_L9" << "time_in_zone_L10"; - QStringList timeInZonesHR = QStringList() + static const QStringList timeInZonesHR = QStringList() << "time_in_zone_H1" << "time_in_zone_H2" << "time_in_zone_H3" @@ -195,35 +205,41 @@ RideSummaryWindow::htmlSummary() const // Use pre-computed and saved metric values if the ride has not // been edited. Otherwise we need to re-compute every time. + // this is only for ride summary, when showing for a date range + // we already have a summary metrics array SummaryMetrics metrics; RideMetricFactory &factory = RideMetricFactory::instance(); - if (rideItem->isDirty()) { - // make a list if the metrics we want computed - // instead of calculating them all, just do the - // ones we display - QStringList worklist; - worklist += totalColumn; - worklist += averageColumn; - worklist += maximumColumn; - worklist += metricColumn; - worklist += timeInZones; - worklist += timeInZonesHR; - // go calculate them then... - QHash computed = RideMetric::computeMetrics(mainWindow, ride, mainWindow->zones(), mainWindow->hrZones(), worklist); - for(int i = 0; i < worklist.count(); ++i) { - if (worklist[i] != "") { - RideMetricPtr m = computed.value(worklist[i]); - if (m) metrics.setForSymbol(worklist[i], m->value(true)); - else metrics.setForSymbol(worklist[i], 0.00); + if (ridesummary) { + if (rideItem->isDirty()) { + // make a list if the metrics we want computed + // instead of calculating them all, just do the + // ones we display + QStringList worklist; + worklist += totalColumn; + worklist += averageColumn; + worklist += maximumColumn; + worklist += metricColumn; + worklist += timeInZones; + worklist += timeInZonesHR; + + // go calculate them then... + QHash computed = RideMetric::computeMetrics(mainWindow, ride, mainWindow->zones(), mainWindow->hrZones(), worklist); + for(int i = 0; i < worklist.count(); ++i) { + if (worklist[i] != "") { + RideMetricPtr m = computed.value(worklist[i]); + if (m) metrics.setForSymbol(worklist[i], m->value(true)); + else metrics.setForSymbol(worklist[i], 0.00); + } } - } - } else { + } else { - // just use the metricDB versions, nice 'n fast - metrics = mainWindow->metricDB->getRideMetrics(rideItem->fileName); + // just use the metricDB versions, nice 'n fast + metrics = mainWindow->metricDB->getRideMetrics(rideItem->fileName); + } } + // // 3 top columns - total, average, maximums and metrics for entire ride // @@ -257,19 +273,28 @@ RideSummaryWindow::htmlSummary() const s = s.arg(m->name().replace(QRegExp(tr("^(Average|Max) ")), "")); // Add units (if needed) and value (with right precision) - if (m->units(metricUnits) == "seconds") { + if (m->units(useMetricUnits) == "seconds") { s = s.arg(""); // no units - s = s.arg(time_to_string(metrics.getForSymbol(symbol))); + + // get the value - from metrics or from data array + if (ridesummary) s = s.arg(time_to_string(metrics.getForSymbol(symbol))); + else s = s.arg(SummaryMetrics::getAggregated(symbol, data, useMetricUnits)); + } else { - if (m->units(metricUnits) != "") s = s.arg(" (" + m->units(metricUnits) + ")"); + if (m->units(useMetricUnits) != "") s = s.arg(" (" + m->units(useMetricUnits) + ")"); else s = s.arg(""); // temperature is a special case, if it is not present fall back to metadata tag // if that is not present then just display '-' if ((symbol == "average_temp" || symbol == "max_temp") && metrics.getForSymbol(symbol) == RideFile::noTemp) s = s.arg(ride->getTag("Temperature", "-")); - else - s = s.arg(metrics.getForSymbol(symbol) * (metricUnits ? 1 : m->conversion()) + (metricUnits ? 0 : m->conversionSum()), 0, 'f', m->precision()); + else { + + // get the value - from metrics or from data array + if (ridesummary) s = s.arg(metrics.getForSymbol(symbol) * (useMetricUnits ? 1 : m->conversion()) + + (useMetricUnits ? 0 : m->conversionSum()), 0, 'f', m->precision()); + else s = s.arg(SummaryMetrics::getAggregated(symbol, data, useMetricUnits)); + } } summary += s; @@ -283,10 +308,14 @@ RideSummaryWindow::htmlSummary() const // if (rideItem->numZones() > 0) { QVector time_in_zone(rideItem->numZones()); - for (int i = 0; i < rideItem->numZones(); ++i) - time_in_zone[i] = metrics.getForSymbol(timeInZones[i]); + for (int i = 0; i < rideItem->numZones(); ++i) { + + // if using metrics or data + if (ridesummary) time_in_zone[i] = metrics.getForSymbol(timeInZones[i]); + else time_in_zone[i] = SummaryMetrics::getAggregated(timeInZones[i], data, useMetricUnits, true).toDouble(); + } summary += tr("

Power Zones

"); - summary += mainWindow->zones()->summarize(rideItem->zoneRange(), time_in_zone); + summary += mainWindow->zones()->summarize(rideItem->zoneRange(), time_in_zone); //XXX aggregating? } // @@ -294,84 +323,95 @@ RideSummaryWindow::htmlSummary() const // if (rideItem->numHrZones() > 0) { QVector time_in_zone(rideItem->numHrZones()); - for (int i = 0; i < rideItem->numHrZones(); ++i) - time_in_zone[i] = metrics.getForSymbol(timeInZonesHR[i]); + for (int i = 0; i < rideItem->numHrZones(); ++i) { + + // if using metrics or data + if (ridesummary) time_in_zone[i] = metrics.getForSymbol(timeInZonesHR[i]); + else time_in_zone[i] = SummaryMetrics::getAggregated(timeInZonesHR[i], data, useMetricUnits, true).toDouble(); + } + summary += tr("

Heart Rate Zones

"); - summary += mainWindow->hrZones()->summarize(rideItem->hrZoneRange(), time_in_zone); + summary += mainWindow->hrZones()->summarize(rideItem->hrZoneRange(), time_in_zone); //XXX aggregating } - // - // Interval Summary (recalculated on every refresh since they are not cached at present) - // - if (ride->intervals().size() > 0) { - bool firstRow = true; - QString s; - if (appsettings->contains(GC_SETTINGS_INTERVAL_METRICS)) - s = appsettings->value(this, GC_SETTINGS_INTERVAL_METRICS).toString(); - else - s = GC_SETTINGS_INTERVAL_METRICS_DEFAULT; - QStringList intervalMetrics = s.split(","); - summary += "

"+tr("Intervals")+"

\n

\n"; - summary += "intervals()) { - RideFile f(ride->startTime(), ride->recIntSecs()); - for (int i = ride->intervalBegin(interval); i < ride->dataPoints().size(); ++i) { - const RideFilePoint *p = ride->dataPoints()[i]; - if (p->secs >= interval.stop) - break; - f.appendPoint(p->secs, p->cad, p->hr, p->km, p->kph, p->nm, - p->watts, p->alt, p->lon, p->lat, p->headwind, - p->slope, p->temp, p->lrbalance, 0); - } - if (f.dataPoints().size() == 0) { - // Interval empty, do not compute any metrics - continue; - } + // Only get interval summary for a ride summary + if (ridesummary) { - QHash metrics = - RideMetric::computeMetrics(mainWindow, &f, mainWindow->zones(), mainWindow->hrZones(), intervalMetrics); - if (firstRow) { - summary += ""; - summary += ""; + // + // Interval Summary (recalculated on every refresh since they are not cached at present) + // + if (ride->intervals().size() > 0) { + bool firstRow = true; + QString s; + if (appsettings->contains(GC_SETTINGS_INTERVAL_METRICS)) + s = appsettings->value(this, GC_SETTINGS_INTERVAL_METRICS).toString(); + else + s = GC_SETTINGS_INTERVAL_METRICS_DEFAULT; + QStringList intervalMetrics = s.split(","); + summary += "

"+tr("Intervals")+"

\n

\n"; + summary += "

Interval Name
intervals()) { + RideFile f(ride->startTime(), ride->recIntSecs()); + for (int i = ride->intervalBegin(interval); i < ride->dataPoints().size(); ++i) { + const RideFilePoint *p = ride->dataPoints()[i]; + if (p->secs >= interval.stop) + break; + f.appendPoint(p->secs, p->cad, p->hr, p->km, p->kph, p->nm, + p->watts, p->alt, p->lon, p->lat, p->headwind, + p->slope, p->temp, p->lrbalance, 0); + } + if (f.dataPoints().size() == 0) { + // Interval empty, do not compute any metrics + continue; + } + + QHash metrics = + RideMetric::computeMetrics(mainWindow, &f, mainWindow->zones(), mainWindow->hrZones(), intervalMetrics); + if (firstRow) { + summary += ""; + summary += ""; + foreach (QString symbol, intervalMetrics) { + RideMetricPtr m = metrics.value(symbol); + if (!m) continue; + summary += ""; + } + summary += ""; + firstRow = false; + } + if (even) + summary += ""; + else { + QColor color = QApplication::palette().alternateBase().color(); + color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); + summary += ""; + } + even = !even; + summary += ""; foreach (QString symbol, intervalMetrics) { RideMetricPtr m = metrics.value(symbol); if (!m) continue; - summary += ""; + QString s(""); + if (m->units(useMetricUnits) == "seconds") + summary += s.arg(time_to_string(m->value(useMetricUnits))); + else + summary += s.arg(m->value(useMetricUnits), 0, 'f', m->precision()); } summary += ""; - firstRow = false; } - if (even) - summary += ""; - else { - QColor color = QApplication::palette().alternateBase().color(); - color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); - summary += ""; - } - even = !even; - summary += ""; - foreach (QString symbol, intervalMetrics) { - RideMetricPtr m = metrics.value(symbol); - if (!m) continue; - QString s(""); - if (m->units(metricUnits) == "seconds") - summary += s.arg(time_to_string(m->value(metricUnits))); - else - summary += s.arg(m->value(metricUnits), 0, 'f', m->precision()); - } - summary += ""; + summary += "
Interval Name" + m->name(); + if (m->units(useMetricUnits) == "seconds") + ; // don't do anything + else if (m->units(useMetricUnits).size() > 0) + summary += " (" + m->units(useMetricUnits) + ")"; + summary += "
" + interval.name + "" + m->name(); - if (m->units(metricUnits) == "seconds") - ; // don't do anything - else if (m->units(metricUnits).size() > 0) - summary += " (" + m->units(metricUnits) + ")"; - summary += "%1
" + interval.name + "%1
"; } - summary += ""; } - if (!rideItem->errors().empty()) { + // sumarise errors reading file if it was a ride summary + if (ridesummary && !rideItem->errors().empty()) { + summary += tr("

Errors reading file:

    "); QStringListIterator i(rideItem->errors()); while(i.hasNext()) @@ -389,3 +429,14 @@ RideSummaryWindow::htmlSummary() const return summary; } +void RideSummaryWindow::dateRangeChanged(DateRange dr) +{ + if (!amVisible()) return; + + // range didnt change ignore it... + if (dr.from == current.from && dr.to == current.to) return; + else current = dr; + + data = mainWindow->metricDB->getAllMetricsFor(myDateRange); + refresh(); +} diff --git a/src/RideSummaryWindow.h b/src/RideSummaryWindow.h index ea3c005a6..1c34e9e8d 100644 --- a/src/RideSummaryWindow.h +++ b/src/RideSummaryWindow.h @@ -25,6 +25,8 @@ #include #include +#include "SummaryMetrics.h" + class RideSummaryWindow : public GcWindow { @@ -34,12 +36,14 @@ class RideSummaryWindow : public GcWindow public: - RideSummaryWindow(MainWindow *parent); + // two modes - summarise ride or summarise date range + RideSummaryWindow(MainWindow *parent, bool ridesummary = true); protected slots: void refresh(); void rideSelected(); + void dateRangeChanged(DateRange); void rideItemChanged(); void metadataChanged(); @@ -51,6 +55,10 @@ class RideSummaryWindow : public GcWindow QWebView *rideSummary; RideItem *_connected; + bool ridesummary; // do we summarise ride or daterange? + + QList data; // when in date range mode + DateRange current; }; #endif // _GC_RideSummaryWindow_h diff --git a/src/SummaryMetrics.cpp b/src/SummaryMetrics.cpp index 8ac9c7379..d30973ee6 100644 --- a/src/SummaryMetrics.cpp +++ b/src/SummaryMetrics.cpp @@ -99,3 +99,68 @@ SummaryMetrics::getUnitsForSymbol(QString symbol, bool UseMetric) const if (m) return m->units(UseMetric); else return QString("units"); } + +QString SummaryMetrics::getAggregated(QString name, const QList &results, bool useMetricUnits, bool nofmt) +{ + // get the metric details, so we can convert etc + const RideMetric *metric = RideMetricFactory::instance().rideMetric(name); + if (!metric) return QString("%1 unknown").arg(name); + + // what we will return + double rvalue = 0; + double rcount = 0; // using double to avoid rounding issues with int when dividing + + // loop through and aggregate + foreach (SummaryMetrics rideMetrics, results) { + + // get this value + double value = rideMetrics.getForSymbol(name); + double count = rideMetrics.getForSymbol("workout_time"); // for averaging + + + // check values are bounded, just in case + if (isnan(value) || isinf(value)) value = 0; + + // imperial / metric conversion + if (useMetricUnits == false) { + value *= metric->conversion(); + value += metric->conversionSum(); + } + + switch (metric->type()) { + case RideMetric::Total: + rvalue += value; + break; + case RideMetric::Average: + { + // average should be calculated taking into account + // the duration of the ride, otherwise high value but + // short rides will skew the overall average + rvalue += value*count; + rcount += count; + break; + } + case RideMetric::Peak: + { + if (value > rvalue) rvalue = value; + break; + } + } + } + + // now compute the average + if (metric->type() == RideMetric::Average) { + if (rcount) rvalue = rvalue / rcount; + } + + + // Format appropriately + QString result; + if (metric->units(useMetricUnits) == "seconds") { + if (nofmt) result = QString("%1").arg(rvalue); + else result = time_to_string(rvalue); + + } else result = QString("%1").arg(rvalue, 0, 'f', metric->precision()); + + return result; +} diff --git a/src/SummaryMetrics.h b/src/SummaryMetrics.h index f81e3d12f..50007f2b1 100644 --- a/src/SummaryMetrics.h +++ b/src/SummaryMetrics.h @@ -61,6 +61,9 @@ class SummaryMetrics // get unit string to use for this symbol QString getUnitsForSymbol(QString symbol, bool UseMetric) const; + // when passed a list of summary metrics and a name return aggregated value as a string + static QString getAggregated(QString name, const QList &results, bool useMetricUnits, bool nofmt = false); + QMap &values() { return value; } QMap &texts() { return text; } diff --git a/src/TimeUtils.cpp b/src/TimeUtils.cpp index 7590103de..6e1371dda 100644 --- a/src/TimeUtils.cpp +++ b/src/TimeUtils.cpp @@ -97,22 +97,25 @@ QDateTime convertToLocalTime(QString timestamp) } } -DateRange::DateRange(QDate from, QDate to) : QObject() +DateRange::DateRange(QDate from, QDate to, QString name) : QObject() { this->from=from; this->to=to; + this->name=name; } DateRange::DateRange(const DateRange &other) : QObject() { from=other.from; to=other.to; + name=other.name; } DateRange& DateRange::operator=(const DateRange &other) { from=other.from; to=other.to; + name=other.name; emit changed(from, to); return *this; diff --git a/src/TimeUtils.h b/src/TimeUtils.h index 7f6a061f7..2d9f4b492 100644 --- a/src/TimeUtils.h +++ b/src/TimeUtils.h @@ -38,9 +38,10 @@ class DateRange : QObject public: DateRange(const DateRange& other); - DateRange(QDate from = QDate(), QDate to = QDate()); + DateRange(QDate from = QDate(), QDate to = QDate(), QString name =""); DateRange& operator=(const DateRange &); QDate from, to; + QString name; signals: void changed(QDate from, QDate to);