mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
A new tab 'Editor' for manually editing ride file data points and associated menu options under 'Tools' for fixing spikes, gaps, GPS errors and adjusting torque values. A revert to saved ride option is also included to 'undo' all changes. The ride editor supports undo/redo as well as cut and paste and "paste special" (to append points or swap columns/overwrite selected data series). The editor also supports search and will automatically highlight anomalous data. When a file is saved, the changes are recorded in a new metadata special field called "Change History" which can be added as a Textbox in the metadata config. The data processors can be run manually or automatically when a ride is opened - these are configured on the ride data tab in the config pane. Significant changes have been introduced in the codebase, the most significant of which are; a RideFileCommand class for modifying ride data has been introduced (as a member of RideFile) and the RideItem class is now a QObject as well as QTreeWidgetItem to enable signalling. The Ride Editor uses a RideFileTableModel that can be re-used in other parts of the code. LTMoutliers class has been introduced in support of anomaly detection in the editor (which highlights anomalies with a wiggly red line). Fixes #103.
445 lines
15 KiB
C++
445 lines
15 KiB
C++
/*
|
|
* 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 "Zones.h"
|
|
#include <QtGui>
|
|
#include <qwt_plot.h>
|
|
#include <qwt_plot_curve.h>
|
|
#include <qwt_plot_grid.h>
|
|
#include <assert.h>
|
|
|
|
WeeklySummaryWindow::WeeklySummaryWindow(bool useMetricUnits,
|
|
MainWindow *mainWindow) :
|
|
QWidget(mainWindow), mainWindow(mainWindow),
|
|
useMetricUnits(useMetricUnits)
|
|
{
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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(mainWindow, SIGNAL(zonesChanged()), this, SLOT(refresh()));
|
|
}
|
|
|
|
void
|
|
WeeklySummaryWindow::refresh()
|
|
{
|
|
if (mainWindow->activeTab() != this)
|
|
return;
|
|
const RideItem *ride = mainWindow->rideItem();
|
|
if (!ride)
|
|
return;
|
|
const QTreeWidgetItem *allRides = mainWindow->allRideItems();
|
|
const Zones *zones = mainWindow->zones();
|
|
QDate wstart = ride->dateTime.date();
|
|
wstart = wstart.addDays(Qt::Monday - wstart.dayOfWeek());
|
|
assert(wstart.dayOfWeek() == Qt::Monday);
|
|
QDate wend = wstart.addDays(7);
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
QSharedPointer<RideMetric> weeklySeconds(factory.newMetric("time_riding"));
|
|
assert(weeklySeconds);
|
|
QSharedPointer<RideMetric> weeklyDistance(factory.newMetric("total_distance"));
|
|
assert(weeklyDistance);
|
|
QSharedPointer<RideMetric> weeklyWork(factory.newMetric("total_work"));
|
|
assert(weeklyWork);
|
|
QSharedPointer<RideMetric> weeklyBS(factory.newMetric("skiba_bike_score"));
|
|
assert(weeklyBS);
|
|
QSharedPointer<RideMetric> weeklyRelIntensity(factory.newMetric("skiba_relative_intensity"));
|
|
assert(weeklyRelIntensity);
|
|
QSharedPointer<RideMetric> weeklyCS(factory.newMetric("daniels_points"));
|
|
assert(weeklyCS);
|
|
|
|
QSharedPointer<RideMetric> dailySeconds[7];
|
|
QSharedPointer<RideMetric> dailyDistance[7];
|
|
QSharedPointer<RideMetric> dailyBS[7];
|
|
QSharedPointer<RideMetric> dailyRI[7];
|
|
QSharedPointer<RideMetric> dailyW[7];
|
|
QSharedPointer<RideMetric> dailyXP[7];
|
|
|
|
for (int i = 0; i < 7; i++) {
|
|
dailySeconds[i] = QSharedPointer<RideMetric>(factory.newMetric("time_riding"));
|
|
assert(dailySeconds[i]);
|
|
dailyDistance[i] = QSharedPointer<RideMetric>(factory.newMetric("total_distance"));
|
|
assert(dailyDistance[i]);
|
|
dailyBS[i] = QSharedPointer<RideMetric>(factory.newMetric("skiba_bike_score"));
|
|
assert(dailyBS[i]);
|
|
dailyRI[i] = QSharedPointer<RideMetric>(factory.newMetric("skiba_relative_intensity"));
|
|
assert(dailyRI[i]);
|
|
dailyW[i] = QSharedPointer<RideMetric>(factory.newMetric("total_work"));
|
|
assert(dailyW[i]);
|
|
dailyXP[i] = QSharedPointer<RideMetric>(factory.newMetric("skiba_xpower"));
|
|
assert(dailyXP[i]);
|
|
}
|
|
|
|
int zone_range = -1;
|
|
QVector<double> time_in_zone;
|
|
int num_zones = -1;
|
|
bool zones_ok = true;
|
|
|
|
for (int i = 0; i < allRides->childCount(); ++i) {
|
|
RideItem *item = (RideItem*) allRides->child(i);
|
|
int day;
|
|
if (
|
|
(item->type() == RIDE_TYPE) &&
|
|
((day = wstart.daysTo(item->dateTime.date())) >= 0) &&
|
|
(day < 7)
|
|
) {
|
|
|
|
item->computeMetrics(); // generates item->ride
|
|
if (!item->ride())
|
|
continue;
|
|
|
|
RideMetricPtr m;
|
|
if ((m = item->metrics.value(weeklySeconds->symbol()))) {
|
|
weeklySeconds->aggregateWith(*m);
|
|
dailySeconds[day]->aggregateWith(*m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyDistance->symbol()))) {
|
|
weeklyDistance->aggregateWith(*m);
|
|
dailyDistance[day]->aggregateWith(*m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyWork->symbol()))) {
|
|
weeklyWork->aggregateWith(*m);
|
|
dailyW[day]->aggregateWith(*m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyCS->symbol())))
|
|
weeklyCS->aggregateWith(*m);
|
|
|
|
if ((m = item->metrics.value(weeklyBS->symbol()))) {
|
|
weeklyBS->aggregateWith(*m);
|
|
dailyBS[day]->aggregateWith(*m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyRelIntensity->symbol()))) {
|
|
weeklyRelIntensity->aggregateWith(*m);
|
|
dailyRI[day]->aggregateWith(*m);
|
|
}
|
|
|
|
if ((m = item->metrics.value("skiba_xpower")))
|
|
dailyXP[day]->aggregateWith(*m);
|
|
|
|
// compute time in zones
|
|
if (zones) {
|
|
if (zone_range == -1) {
|
|
zone_range = item->zoneRange();
|
|
num_zones = item->numZones();
|
|
time_in_zone.clear();
|
|
time_in_zone.resize(num_zones);
|
|
}
|
|
else if (item->zoneRange() != zone_range) {
|
|
zones_ok = false;
|
|
}
|
|
if (zone_range != -1) {
|
|
for (int j = 0; j < num_zones; ++j)
|
|
time_in_zone[j] += item->timeInZone(j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int seconds = ((int) round(weeklySeconds->value(true)));
|
|
int minutes = seconds / 60;
|
|
seconds %= 60;
|
|
int hours = minutes / 60;
|
|
minutes %= 60;
|
|
|
|
QString summary;
|
|
summary =
|
|
tr(
|
|
"<center>"
|
|
"<h2>Week of %1 through %2</h2>"
|
|
"<h2>Summary</h2>"
|
|
"<p>"
|
|
"<table align=\"center\" width=\"60%\" border=0>"
|
|
"<tr><td>Total time riding:</td>"
|
|
" <td align=\"right\">%3:%4:%5</td></tr>"
|
|
"<tr><td>Total distance (%6):</td>"
|
|
" <td align=\"right\">%7</td></tr>"
|
|
"<tr><td>Total work (kJ):</td>"
|
|
" <td align=\"right\">%8</td></tr>"
|
|
"<tr><td>Daily Average work (kJ):</td>"
|
|
" <td align=\"right\">%9</td></tr>"
|
|
)
|
|
.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->value(useMetricUnits), 0, 'f', 1)
|
|
.arg((unsigned) round(weeklyWork->value(useMetricUnits)))
|
|
.arg((unsigned) round(weeklyWork->value(useMetricUnits) / 7));
|
|
|
|
double weeklyBSValue = weeklyBS->value(useMetricUnits);
|
|
bool useBikeScore = (zone_range != -1) && (weeklyBSValue > 0);
|
|
|
|
if (zone_range != -1) {
|
|
if (useBikeScore)
|
|
summary +=
|
|
tr(
|
|
"<tr><td>Total BikeScore:</td>"
|
|
" <td align=\"right\">%1</td></tr>"
|
|
"<tr><td>Total Daniels Points:</td>"
|
|
" <td align=\"right\">%2</td></tr>"
|
|
"<tr><td>Net Relative Intensity:</td>"
|
|
" <td align=\"right\">%3</td></tr>"
|
|
)
|
|
.arg((unsigned) round(weeklyBSValue))
|
|
.arg(weeklyCS->value(useMetricUnits), 0, 'f', 1)
|
|
.arg(weeklyRelIntensity->value(useMetricUnits), 0, 'f', 3);
|
|
|
|
summary +=
|
|
tr(
|
|
"</table>"
|
|
"<h2>Power Zones</h2>"
|
|
);
|
|
if (!zones_ok)
|
|
summary += "Error: Week spans more than one zone range.";
|
|
else {
|
|
summary += zones->summarize(zone_range, time_in_zone);
|
|
}
|
|
}
|
|
|
|
summary += "</center>";
|
|
|
|
// 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]->value(useMetricUnits);
|
|
ydur[i] = dailySeconds[day]->value(useMetricUnits) / 60;
|
|
ybsorw[i] = useBikeScore ? dailyBS[day]->value(useMetricUnits) : dailyW[day]->value(useMetricUnits) / 1000;
|
|
yriorxp[i] = useBikeScore ? dailyRI[day]->value(useMetricUnits) : dailyXP[day]->value(useMetricUnits);
|
|
|
|
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);
|
|
}
|
|
|