/* * Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "WeeklySummaryWindow.h" #include "DaysScaleDraw.h" #include "MainWindow.h" #include "RideItem.h" #include "RideMetric.h" #include "SummaryMetrics.h" #include "MetricAggregator.h" #include "Zones.h" #include "Units.h" // for MILES_PER_KM #include #include #include #include #include #include WeeklySummaryWindow::WeeklySummaryWindow(bool useMetricUnits, MainWindow *mainWindow) : GcWindow(mainWindow), mainWindow(mainWindow), useMetricUnits(useMetricUnits) { setInstanceName("Weekly Summary Window"); setControls(NULL); QGridLayout *glayout = new QGridLayout; // set up the weekly distance / duration plot: weeklyPlot = new QwtPlot(); weeklyPlot->enableAxis(QwtPlot::yRight, true); weeklyPlot->setAxisMaxMinor(QwtPlot::xBottom,0); weeklyPlot->setAxisScaleDraw(QwtPlot::xBottom, new DaysScaleDraw()); QFont weeklyPlotAxisFont = weeklyPlot->axisFont(QwtPlot::yLeft); weeklyPlotAxisFont.setPointSize(weeklyPlotAxisFont.pointSize() * 0.9f); weeklyPlot->setAxisFont(QwtPlot::xBottom, weeklyPlotAxisFont); weeklyPlot->setAxisFont(QwtPlot::yLeft, weeklyPlotAxisFont); weeklyPlot->setAxisFont(QwtPlot::yRight, weeklyPlotAxisFont); weeklyPlot->canvas()->setFrameStyle(QFrame::NoFrame); weeklyPlot->setCanvasBackground(Qt::white); weeklyDistCurve = new QwtPlotCurve(); weeklyDistCurve->setStyle(QwtPlotCurve::Steps); QPen pen(Qt::SolidLine); weeklyDistCurve->setPen(pen); weeklyDistCurve->setBrush(Qt::red); weeklyDistCurve->setRenderHint(QwtPlotItem::RenderAntialiased); weeklyDistCurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left weeklyDistCurve->attach(weeklyPlot); weeklyDurationCurve = new QwtPlotCurve(); weeklyDurationCurve->setStyle(QwtPlotCurve::Steps); weeklyDurationCurve->setPen(pen); weeklyDurationCurve->setBrush(QColor(255,200,0,255)); weeklyDurationCurve->setRenderHint(QwtPlotItem::RenderAntialiased); weeklyDurationCurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left weeklyDurationCurve->setYAxis(QwtPlot::yRight); weeklyDurationCurve->attach(weeklyPlot); // set up the weekly bike score plot: weeklyBSPlot = new QwtPlot(); weeklyBSPlot->enableAxis(QwtPlot::yRight, true); weeklyBSPlot->setAxisMaxMinor(QwtPlot::xBottom,0); weeklyBSPlot->setAxisScaleDraw(QwtPlot::xBottom, new DaysScaleDraw()); QwtText textLabel = QwtText(); weeklyBSPlot->setAxisFont(QwtPlot::xBottom, weeklyPlotAxisFont); weeklyBSPlot->setAxisFont(QwtPlot::yLeft, weeklyPlotAxisFont); weeklyBSPlot->setAxisFont(QwtPlot::yRight, weeklyPlotAxisFont); weeklyBSPlot->canvas()->setFrameStyle(QFrame::NoFrame); weeklyBSPlot->setCanvasBackground(Qt::white); weeklyBSCurve = new QwtPlotCurve(); weeklyBSCurve->setStyle(QwtPlotCurve::Steps); weeklyBSCurve->setPen(pen); weeklyBSCurve->setBrush(Qt::blue); weeklyBSCurve->setRenderHint(QwtPlotItem::RenderAntialiased); weeklyBSCurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left weeklyBSCurve->attach(weeklyBSPlot); weeklyRICurve = new QwtPlotCurve(); weeklyRICurve->setStyle(QwtPlotCurve::Steps); weeklyRICurve->setPen(pen); weeklyRICurve->setBrush(Qt::green); weeklyRICurve->setRenderHint(QwtPlotItem::RenderAntialiased); weeklyRICurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left weeklyRICurve->setYAxis(QwtPlot::yRight); weeklyRICurve->attach(weeklyBSPlot); // set baseline curves to obscure linewidth variations along baseline pen.setWidth(2); weeklyBaselineCurve = new QwtPlotCurve(); weeklyBaselineCurve->setPen(pen); weeklyBaselineCurve->attach(weeklyPlot); weeklyBSBaselineCurve = new QwtPlotCurve(); weeklyBSBaselineCurve->setPen(pen); weeklyBSBaselineCurve->attach(weeklyBSPlot); QwtPlotGrid *grid = new QwtPlotGrid(); grid->enableX(false); QPen gridPen; gridPen.setStyle(Qt::DotLine); grid->setPen(gridPen); grid->attach(weeklyPlot); QwtPlotGrid *grid1 = new QwtPlotGrid(); grid1->enableX(false); gridPen.setStyle(Qt::DotLine); grid1->setPen(gridPen); grid1->attach(weeklyBSPlot); weeklySummary = new QTextEdit; weeklySummary->setReadOnly(true); weeklySummary->setStyleSheet("QTextEdit { border: 0px}"); QFont defaultFont; // mainwindow sets up the defaults.. we need to apply defaultFont.setPointSize(defaultFont.pointSize()-2); weeklySummary->setFont(defaultFont); glayout->addWidget(weeklySummary,0,0,1,-1); // row, col, rowspan, colspan. -1 == fill to edge glayout->addWidget(weeklyPlot,1,0); glayout->addWidget(weeklyBSPlot,1,1); glayout->setRowStretch(0, 3); // stretch factor of summary glayout->setRowStretch(1, 2); // stretch factor of weekly plots glayout->setColumnStretch(0, 1); // stretch first column glayout->setColumnStretch(1, 1); // stretch second column glayout->setRowMinimumHeight(0, 180); // minimum height of weekly summary glayout->setRowMinimumHeight(1, 120); // minimum height of weekly plots setLayout(glayout); //connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(refresh())); connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(refresh())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(refresh())); connect(mainWindow, SIGNAL(rideAdded(RideItem*)), this, SLOT(refresh())); connect(mainWindow, SIGNAL(rideDeleted(RideItem*)), this, SLOT(refresh())); } void WeeklySummaryWindow::refresh() { // not active, or null ride item then do nothing if (!amVisible()) return; const RideItem *ride = myRideItem; if (!ride) return; // work through the rides const QTreeWidgetItem *allRides = mainWindow->allRideItems(); // start / end of week QDate wstart = ride->dateTime.date(); wstart = wstart.addDays(Qt::Monday - wstart.dayOfWeek()); assert(wstart.dayOfWeek() == Qt::Monday); // weekly values QVector time_in_zone; // max 10 zones supported QVector time_in_hrzone; // max 10 zones supported double weeklyRides=0; double weeklySeconds=0; double weeklyDistance=0; double weeklyW=0; double weeklyBS=0; double weeklyCS=0; double weeklyRI=0; // daily values double dailyRides[7]; double dailySeconds[7]; double dailyDistance[7]; double dailyW[7]; double dailyBS[7]; double dailyXP[7]; double dailyRI[7]; // init daily values to zero for(int i=0; i<7; i++) dailyRides[i] = dailySeconds[i] = dailyDistance[i] = dailyW[i] = dailyBS[i] = dailyXP[i] = dailyRI[i] = 0; // how many zones did we see? int num_zones = -1; int zone_range = -1; int num_hrzones = -1; int hrzone_range = -1; for (int i = 0; i < allRides->childCount(); ++i) { QTreeWidgetItem *twi = allRides->child(i); RideItem *item = static_cast(twi); int day; if (((day = wstart.daysTo(item->dateTime.date())) >= 0) && (day < 7)) { // one more ride for this day dailyRides[day]++; weeklyRides++; // get metrics from metricDB (always in metric units) SummaryMetrics metrics = mainWindow->metricDB->getRideMetrics(item->fileName); // increment daily values dailySeconds[day] += metrics.getForSymbol("time_riding"); dailyDistance[day] += metrics.getForSymbol("total_distance") * (useMetricUnits ? 1 : MILES_PER_KM); dailyW[day] += metrics.getForSymbol("total_work"); dailyBS[day] += metrics.getForSymbol("skiba_bike_score"); dailyXP[day] += metrics.getForSymbol("skiba_power"); // average RI for the day if (dailyRides[day] > 1) dailyRI[day] = ((dailyRI[day] * (dailyRides[day]-1)) + metrics.getForSymbol("skiba_relative_intensity")) / dailyRides[day]; else dailyRI[day] = metrics.getForSymbol("skiba_relative_intensity"); // increment weekly values weeklySeconds += metrics.getForSymbol("time_riding"); weeklyDistance += metrics.getForSymbol("total_distance") * (useMetricUnits ? 1 : MILES_PER_KM); weeklyCS += metrics.getForSymbol("daniels_points"); weeklyW += metrics.getForSymbol("total_work"); weeklyBS += metrics.getForSymbol("skiba_bike_score"); // average RI for the week if (weeklyRides > 1) weeklyRI = ((weeklyRI * (weeklyRides-1)) + metrics.getForSymbol("skiba_relative_intensity")) / weeklyRides; else weeklyRI = metrics.getForSymbol("skiba_relative_intensity"); // compute time in zones // Power if (num_zones < item->numZones()) { num_zones = item->numZones(); time_in_zone.resize(num_zones); } zone_range = item->zoneRange(); for (int j=0; jnumZones(); j++) { QString symbol = QString("time_in_zone_L%1").arg(j+1); time_in_zone[j] += metrics.getForSymbol(symbol); } // HR if (num_hrzones < item->numHrZones()) { num_hrzones = item->numHrZones(); time_in_hrzone.resize(num_hrzones); } hrzone_range = item->hrZoneRange(); for (int j=0; jnumHrZones(); j++) { QString symbol = QString("time_in_zone_H%1").arg(j+1); time_in_hrzone[j] += metrics.getForSymbol(symbol); } } } // break duration into hours, mins and seconds int seconds = round(weeklySeconds); int minutes = seconds / 60; seconds %= 60; int hours = minutes / 60; minutes %= 60; QString summary; summary = tr( "
" "

Week of %1 through %2

" "

Summary

" "

" "" "" " " "" " " "" " " "" " ") .arg(wstart.toString(tr("MM/dd/yyyy"))) .arg(wstart.addDays(6).toString(tr("MM/dd/yyyy"))) .arg(hours) .arg(minutes, 2, 10, QLatin1Char('0')) .arg(seconds, 2, 10, QLatin1Char('0')) .arg(useMetricUnits ? "km" : "miles") .arg(weeklyDistance, 0, 'f', 1) .arg((unsigned) round(weeklyW)) .arg((unsigned) round(weeklyW/ 7)); bool useBikeScore = (weeklyBS > 0); if (num_zones != -1) { if (useBikeScore) summary += tr("" " " "" " " "" " ") .arg((unsigned) round(weeklyBS)) .arg(weeklyCS, 0, 'f', 1) .arg(weeklyRI, 0, 'f', 3); if (zone_range >= 0) { summary += tr( "
Total time riding:%3:%4:%5
Total distance (%6):%7
Total work (kJ):%8
Daily Average work (kJ):%9
Total BikeScore:%1
Total Daniels Points:%2
Net Relative Intensity:%3
" "

Power Zones

"); summary += mainWindow->zones()->summarize(zone_range, time_in_zone); } else { summary += "No zones configured - zone summary not available."; } if (hrzone_range >= 0) { summary += tr( "" "

Heart Rate Zones

"); summary += mainWindow->hrZones()->summarize(hrzone_range, time_in_hrzone); } } summary += "
"; // set the axis labels of the weekly plots QwtText textLabel = QwtText(useMetricUnits ? "km" : "miles"); QFont weeklyPlotAxisTitleFont = textLabel.font(); weeklyPlotAxisTitleFont.setPointSize(10); weeklyPlotAxisTitleFont.setBold(true); textLabel.setFont(weeklyPlotAxisTitleFont); weeklyPlot->setAxisTitle(QwtPlot::yLeft, textLabel); textLabel.setText("Minutes"); weeklyPlot->setAxisTitle(QwtPlot::yRight, textLabel); textLabel.setText(useBikeScore ? "BikeScore" : "kJoules"); weeklyBSPlot->setAxisTitle(QwtPlot::yLeft, textLabel); textLabel.setText(useBikeScore ? "Intensity" : "xPower"); weeklyBSPlot->setAxisTitle(QwtPlot::yRight, textLabel); // for the daily distance/duration and bikescore plots: // first point: establish zero position // points 2N, 2N+1: Nth day (N from 1 to 7), up then down // 16th point: move to draw baseline off right of plot double xdist[16]; double xdur[16]; double xbsorw[16]; double xriorxp[16]; double ydist[16]; // daily distance double ydur[16]; // daily total time double ybsorw[16]; // daily minutes double yriorxp[16]; // daily relative intensity // data for a "baseline" curve to draw a baseline double xbaseline[] = {0, 8}; double ybaseline[] = {0, 0}; weeklyBaselineCurve->setData(xbaseline, ybaseline, 2); weeklyBSBaselineCurve->setData(xbaseline, ybaseline, 2); const double bar_width = 0.3; int i = 0; xdist[i] = xdur[i] = xbsorw[i] = xriorxp[i] = 0; ydist[i] = ydur[i] = ybsorw[i] = yriorxp[i] = 0; for(int day = 0; day < 7; day++){ double x; i++; xdist[i] = x = day + 1 - bar_width; xdist[i + 1] = x += bar_width; xdur[i] = x; xdur[i + 1] = x += bar_width; xbsorw[i] = x = day + 1 - bar_width; xbsorw[i + 1] = x += bar_width; xriorxp[i] = x; xriorxp[i + 1] = x += bar_width; ydist[i] = dailyDistance[day]; ydur[i] = dailySeconds[day] / 60; ybsorw[i] = useBikeScore ? dailyBS[day] : dailyW[day] / 1000; yriorxp[i] = useBikeScore ? dailyRI[day] : dailyXP[day]; i++; ydist[i] = 0; ydur[i] = 0; ybsorw[i] = 0; yriorxp[i] = 0; } // sweep a baseline off the right of the plot i++; xdist[i] = xdur[i] = xbsorw[i] = xriorxp[i] = 8; ydist[i] = ydur[i] = ybsorw[i] = yriorxp[i] = 0; // Distance/Duration plot: weeklyDistCurve->setData(xdist, ydist, 16); weeklyPlot->setAxisScale(QwtPlot::yLeft, 0, weeklyDistCurve->maxYValue()*1.1, 0); weeklyPlot->setAxisScale(QwtPlot::xBottom, 0.5, 7.5, 0); weeklyPlot->setAxisTitle(QwtPlot::yLeft, useMetricUnits ? "Kilometers" : "Miles"); weeklyDurationCurve->setData(xdur, ydur, 16); weeklyPlot->setAxisScale(QwtPlot::yRight, 0, weeklyDurationCurve->maxYValue()*1.1, 0); weeklyPlot->replot(); // BikeScore/Relative Intensity plot weeklyBSCurve->setData(xbsorw, ybsorw, 16); weeklyBSPlot->setAxisScale(QwtPlot::yLeft, 0, weeklyBSCurve->maxYValue()*1.1, 0); weeklyBSPlot->setAxisScale(QwtPlot::xBottom, 0.5, 7.5, 0); // set axis minimum for relative intensity double RImin = -1; for(int i = 1; i < 16; i += 2) if (yriorxp[i] > 0 && ((RImin < 0) || (yriorxp[i] < RImin))) RImin = yriorxp[i]; if (RImin < 0) RImin = 0; RImin *= 0.8; for (int i = 0; i < 16; i ++) if (yriorxp[i] < RImin) yriorxp[i] = RImin; weeklyRICurve->setBaseline(RImin); weeklyRICurve->setData(xriorxp, yriorxp, 16); weeklyBSPlot->setAxisScale(QwtPlot::yRight, RImin, weeklyRICurve->maxYValue()*1.1, 0); weeklyBSPlot->replot(); weeklySummary->setHtml(summary); }