mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-15 17:09:56 +00:00
.. the CP plot curve is a terrible mess. Mostly from having multiple significant updates from a number of notable developers; Sean, Dan, Mark and Damien have all made significant contributions. .. But the code contains lots of 'smells' and is very difficult to follow and update .. this update makes no functional changes but is put in place before overhauling the code related to "calculating" and plotting the different curves.
1660 lines
54 KiB
C++
1660 lines
54 KiB
C++
/*
|
|
* Copyright (c) 2006 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 "Athlete.h"
|
|
#include "Zones.h"
|
|
#include "Colors.h"
|
|
#include "CPPlot.h"
|
|
#include <unistd.h>
|
|
#include <QDebug>
|
|
#include <qwt_series_data.h>
|
|
#include <qwt_legend.h>
|
|
#include <qwt_plot_curve.h>
|
|
#include <qwt_plot_grid.h>
|
|
#include <qwt_plot_layout.h>
|
|
#include <qwt_plot_marker.h>
|
|
#include <qwt_scale_engine.h>
|
|
#include <qwt_scale_widget.h>
|
|
#include <qwt_color_map.h>
|
|
#include "CriticalPowerWindow.h"
|
|
#include "RideItem.h"
|
|
#include "LogTimeScaleDraw.h"
|
|
#include "RideFile.h"
|
|
#include "Season.h"
|
|
#include "Settings.h"
|
|
#include "LTMCanvasPicker.h"
|
|
#include "TimeUtils.h"
|
|
|
|
#include <algorithm> // for std::lower_bound
|
|
|
|
CPPlot::CPPlot(QWidget *parent, Context *context, bool rangemode) :
|
|
QwtPlot(parent),
|
|
context(context),
|
|
current(NULL),
|
|
bests(NULL),
|
|
rideSeries(RideFile::watts),
|
|
isFiltered(false),
|
|
shadeMode(2),
|
|
shadeIntervals(true),
|
|
rangemode(rangemode),
|
|
showPercent(false),
|
|
showHeat(false),
|
|
showHeatByDate(false),
|
|
ridePlotStyle(0),
|
|
rideCurve(NULL),
|
|
modelCurve(NULL),
|
|
bestsCurve(NULL),
|
|
curveTitle(NULL)
|
|
{
|
|
setAutoFillBackground(true);
|
|
|
|
setAxisTitle(xBottom, tr("Interval Length"));
|
|
|
|
// Log scale on x-axis
|
|
LogTimeScaleDraw *ld = new LogTimeScaleDraw;
|
|
ld->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
setAxisScaleDraw(xBottom, ld);
|
|
setAxisScaleEngine(xBottom, new QwtLogScaleEngine);
|
|
|
|
//COMMENTED OUT AS CAUSED ISSUES WITH RESIZING
|
|
//QwtScaleDiv div( (double)0.017, (double)60 );
|
|
//div.setTicks(QwtScaleDiv::MajorTick, LogTimeScaleDraw::ticks);
|
|
//setAxisScaleDiv(QwtPlot::xBottom, div);
|
|
|
|
QwtScaleDraw *sd = new QwtScaleDraw;
|
|
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
sd->enableComponent(QwtScaleDraw::Ticks, false);
|
|
sd->enableComponent(QwtScaleDraw::Backbone, false);
|
|
setAxisScaleDraw(yLeft, sd);
|
|
setAxisTitle(yLeft, tr("Average Power (watts)"));
|
|
setAxisMaxMinor(yLeft, 0);
|
|
plotLayout()->setAlignCanvasToScales(true);
|
|
|
|
sd = new QwtScaleDraw;
|
|
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
sd->enableComponent(QwtScaleDraw::Ticks, false);
|
|
sd->enableComponent(QwtScaleDraw::Backbone, false);
|
|
setAxisScaleDraw(yRight, sd);
|
|
setAxisTitle(yRight, tr("Percent of Best"));
|
|
setAxisMaxMinor(yRight, 0);
|
|
|
|
//grid = new QwtPlotGrid();
|
|
//grid->enableX(true);
|
|
//grid->attach(this);
|
|
|
|
zoomer = new penTooltip(static_cast<QwtPlotCanvas*>(this->canvas()));
|
|
zoomer->setMousePattern(QwtEventPattern::MouseSelect1,
|
|
Qt::LeftButton, Qt::ShiftModifier);
|
|
|
|
canvasPicker = new LTMCanvasPicker(this);
|
|
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
|
|
connect(canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), this, SLOT(pointHover(QwtPlotCurve*, int)));
|
|
|
|
configChanged(); // apply colors
|
|
|
|
ecp = new ExtendedCriticalPower(context);
|
|
|
|
extendedModelCurve4 = NULL;
|
|
extendedModelCurve5 = NULL;
|
|
extendedModelCurve6 = NULL;
|
|
heatCurve = NULL;
|
|
heatCurveByDate = NULL;
|
|
|
|
extendedModelCurve_WSecond = NULL;
|
|
extendedModelCurve_WPrime = NULL;
|
|
extendedModelCurve_CP = NULL;
|
|
extendedModelCurve_WPrime_CP = NULL;
|
|
}
|
|
|
|
void
|
|
CPPlot::configChanged()
|
|
{
|
|
QPalette palette;
|
|
palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
|
|
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
|
|
setPalette(palette);
|
|
|
|
axisWidget(QwtPlot::xBottom)->setPalette(palette);
|
|
axisWidget(QwtPlot::yLeft)->setPalette(palette);
|
|
axisWidget(QwtPlot::yRight)->setPalette(palette);
|
|
|
|
setCanvasBackground(GColor(CPLOTBACKGROUND));
|
|
//QPen gridPen(GColor(CPLOTGRID));
|
|
//gridPen.setStyle(Qt::DotLine);
|
|
//grid->setPen(gridPen);
|
|
}
|
|
|
|
void
|
|
CPPlot::setAxisTitle(int axis, QString label)
|
|
{
|
|
// setup the default fonts
|
|
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
|
|
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
|
|
|
QwtText title(label);
|
|
title.setColor(GColor(CPLOTMARKER));
|
|
title.setFont(stGiles);
|
|
QwtPlot::setAxisFont(axis, stGiles);
|
|
QwtPlot::setAxisTitle(axis, title);
|
|
}
|
|
|
|
void
|
|
CPPlot::changeSeason(const QDate &start, const QDate &end)
|
|
{
|
|
// wipe out current - calculate will reinstate
|
|
startDate = (start == QDate()) ? QDate(1900, 1, 1) : start;
|
|
endDate = (end == QDate()) ? QDate(3000, 12, 31) : end;
|
|
|
|
clearCurves();
|
|
}
|
|
|
|
void
|
|
CPPlot::setSeries(CriticalPowerWindow::CriticalSeriesType criticalSeries)
|
|
{
|
|
rideSeries = CriticalPowerWindow::getRideSeries(criticalSeries);
|
|
this->criticalSeries = criticalSeries;
|
|
|
|
// Log scale for all bar Energy
|
|
setAxisScaleEngine(xBottom, new QwtLogScaleEngine);
|
|
LogTimeScaleDraw *ltsd = new LogTimeScaleDraw;
|
|
setAxisScaleDraw(xBottom, ltsd);
|
|
setAxisTitle(xBottom, tr("Interval Length"));
|
|
|
|
switch (criticalSeries) {
|
|
|
|
case CriticalPowerWindow::work:
|
|
setAxisTitle(yLeft, tr("Total work (kJ)"));
|
|
setAxisScaleEngine(xBottom, new QwtLinearScaleEngine);
|
|
setAxisTitle(xBottom, tr("Interval Length (minutes)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::watts_inv_time:
|
|
setAxisTitle(yLeft, tr("Average Power (watts)"));
|
|
setAxisScaleEngine(xBottom, new QwtLinearScaleEngine);
|
|
//setAxisScaleDraw(xBottom, new QwtScaleDraw);
|
|
ltsd->inv_time = true;
|
|
setAxisTitle(xBottom, tr("Interval Length (minutes)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::cad:
|
|
setAxisTitle(yLeft, tr("Average Cadence (rpm)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::hr:
|
|
setAxisTitle(yLeft, tr("Average Heartrate (bpm)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::wattsd:
|
|
setAxisTitle(yLeft, tr("Watts Delta (watts/s)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::cadd:
|
|
setAxisTitle(yLeft, tr("Cadence Delta (rpm/s)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::nmd:
|
|
setAxisTitle(yLeft, tr("Torque Delta (nm/s)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::hrd:
|
|
setAxisTitle(yLeft, tr("Heartrate Delta (bpm/s)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::kphd:
|
|
setAxisTitle(yLeft, tr("Acceleration (m/s/s)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::kph:
|
|
setAxisTitle(yLeft, tr("Average Speed (kph)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::nm:
|
|
setAxisTitle(yLeft, tr("Average Pedal Force (nm)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::NP:
|
|
setAxisTitle(yLeft, tr("Normalized Power (watts)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::aPower:
|
|
setAxisTitle(yLeft, tr("Altitude Power (watts)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::xPower:
|
|
setAxisTitle(yLeft, tr("Skiba xPower (watts)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::wattsKg:
|
|
if (context->athlete->useMetricUnits)
|
|
setAxisTitle(yLeft, tr("Watts per kilo (watts/kg)"));
|
|
else
|
|
setAxisTitle(yLeft, tr("Watts per lb (watts/lb)"));
|
|
break;
|
|
|
|
case CriticalPowerWindow::vam:
|
|
setAxisTitle(yLeft, tr("VAM (meters per hour)"));
|
|
break;
|
|
|
|
default:
|
|
case CriticalPowerWindow::watts:
|
|
setAxisTitle(yLeft, tr("Average Power (watts)"));
|
|
break;
|
|
|
|
}
|
|
|
|
// zap the old curves
|
|
clearCurves();
|
|
}
|
|
|
|
// extract critical power parameters which match the given curve
|
|
// model: maximal power = cp (1 + tau / [t + t0]), where t is the
|
|
// duration of the effort, and t, cp and tau are model parameters
|
|
// the basic critical power model is t0 = 0, but non-zero has
|
|
// been discussed in the literature
|
|
// it is assumed duration = index * seconds
|
|
void
|
|
CPPlot::deriveCPParameters()
|
|
{
|
|
// bounds on anaerobic interval in minutes
|
|
const double t1 = anI1;
|
|
const double t2 = anI2;
|
|
|
|
// bounds on aerobic interval in minutes
|
|
const double t3 = aeI1;
|
|
const double t4 = aeI2;
|
|
|
|
// bounds of these time valus in the data
|
|
int i1, i2, i3, i4;
|
|
|
|
// find the indexes associated with the bounds
|
|
// the first point must be at least the minimum for the anaerobic interval, or quit
|
|
for (i1 = 0; i1 < 60 * t1; i1++)
|
|
if (i1 + 1 >= bests->meanMaxArray(rideSeries).size())
|
|
return;
|
|
// the second point is the maximum point suitable for anaerobicly dominated efforts.
|
|
for (i2 = i1; i2 + 1 <= 60 * t2; i2++)
|
|
if (i2 + 1 >= bests->meanMaxArray(rideSeries).size())
|
|
return;
|
|
// the third point is the beginning of the minimum duration for aerobic efforts
|
|
for (i3 = i2; i3 < 60 * t3; i3++)
|
|
if (i3 + 1 >= bests->meanMaxArray(rideSeries).size())
|
|
return;
|
|
for (i4 = i3; i4 + 1 <= 60 * t4; i4++)
|
|
if (i4 + 1 >= bests->meanMaxArray(rideSeries).size())
|
|
break;
|
|
|
|
// initial estimate of tau
|
|
if (tau == 0)
|
|
tau = 1;
|
|
|
|
// initial estimate of cp (if not already available)
|
|
if (cp == 0)
|
|
cp = 300;
|
|
|
|
// initial estimate of t0: start small to maximize sensitivity to data
|
|
t0 = 0;
|
|
|
|
// lower bound on tau
|
|
const double tau_min = 0.5;
|
|
|
|
// convergence delta for tau
|
|
const double tau_delta_max = 1e-4;
|
|
const double t0_delta_max = 1e-4;
|
|
|
|
// previous loop value of tau and t0
|
|
double tau_prev;
|
|
double t0_prev;
|
|
|
|
// maximum number of loops
|
|
const int max_loops = 100;
|
|
|
|
// loop to convergence
|
|
int iteration = 0;
|
|
do {
|
|
if (iteration ++ > max_loops) {
|
|
QMessageBox::warning(
|
|
NULL, "Warning",
|
|
QString("Maximum number of loops %d exceeded in cp model"
|
|
"extraction").arg(max_loops),
|
|
QMessageBox::Ok,
|
|
QMessageBox::NoButton);
|
|
break;
|
|
}
|
|
|
|
// record the previous version of tau, for convergence
|
|
tau_prev = tau;
|
|
t0_prev = t0;
|
|
|
|
// estimate cp, given tau
|
|
int i;
|
|
cp = 0;
|
|
for (i = i3; i <= i4; i++) {
|
|
double cpn = bests->meanMaxArray(rideSeries)[i] / (1 + tau / (t0 + i / 60.0));
|
|
if (cp < cpn)
|
|
cp = cpn;
|
|
}
|
|
|
|
// if cp = 0; no valid data; give up
|
|
if (cp == 0.0)
|
|
return;
|
|
|
|
// estimate tau, given cp
|
|
tau = tau_min;
|
|
for (i = i1; i <= i2; i++) {
|
|
double taun = (bests->meanMaxArray(rideSeries)[i] / cp - 1) * (i / 60.0 + t0) - t0;
|
|
if (tau < taun)
|
|
tau = taun;
|
|
}
|
|
|
|
// update t0 if we're using that model
|
|
if (model == 2)
|
|
t0 = tau / (bests->meanMaxArray(rideSeries)[1] / cp - 1) - 1 / 60.0;
|
|
|
|
} while ((fabs(tau - tau_prev) > tau_delta_max) ||
|
|
(fabs(t0 - t0_prev) > t0_delta_max)
|
|
);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CPPlot::plotModelCurve(CPPlot *thisPlot, // the plot we're currently displaying
|
|
double cp,
|
|
double tau,
|
|
double t0)
|
|
{
|
|
if (modelCurve) {
|
|
delete modelCurve;
|
|
modelCurve = NULL;
|
|
}
|
|
|
|
if (heatCurve) {
|
|
delete heatCurve;
|
|
heatCurve = NULL;
|
|
}
|
|
if (heatCurveByDate) {
|
|
delete heatCurveByDate;
|
|
heatCurveByDate = NULL;
|
|
}
|
|
|
|
// if there's no cp, then there's nothing to do
|
|
if (cp <= 0)
|
|
return;
|
|
|
|
// if no model, then there's nothing to do
|
|
if (model == 0)
|
|
return;
|
|
|
|
// populate curve data with a CP curve
|
|
const int curve_points = 100;
|
|
double tmin = model == 2 ? 1.00/60.00 : tau; // we want to see the entire curve for 3 model
|
|
double tmax = 180.0;
|
|
QVector<double> cp_curve_power(curve_points);
|
|
QVector<double> cp_curve_time(curve_points);
|
|
|
|
for (int i = 0; i < curve_points; i ++) {
|
|
double x = (double) i / (curve_points - 1);
|
|
double t = pow(tmax, x) * pow(tmin, 1-x);
|
|
|
|
if (criticalSeries == CriticalPowerWindow::work) //this is ENERGY
|
|
cp_curve_power[i] = (cp * t + cp * tau) * 60.0 / 1000.0;
|
|
else
|
|
cp_curve_power[i] = cp * (1 + tau / (t + t0));
|
|
|
|
if (criticalSeries == CriticalPowerWindow::watts_inv_time)
|
|
t = 1.0 / t;
|
|
cp_curve_time[i] = t;
|
|
}
|
|
|
|
// generate a plot
|
|
QString curve_title;
|
|
#if 0 //XXX ?
|
|
if (model == 2) {
|
|
|
|
curve_title.sprintf("CP=%.1f w; W'/CP=%.2f m; t0=%.1f s", cp, tau, 60 * t0);
|
|
|
|
} else {
|
|
#endif
|
|
|
|
if (rideSeries == RideFile::wattsKg)
|
|
curve_title.sprintf("CP=%.2f w/kg; W'=%.2f kJ/kg", cp, cp * tau * 60.0 / 1000.0);
|
|
else
|
|
curve_title.sprintf("CP=%.0f w; W'=%.0f kJ", cp, cp * tau * 60.0 / 1000.0);
|
|
#if 0
|
|
}
|
|
#endif
|
|
|
|
if (curveTitle) {
|
|
delete curveTitle;
|
|
curveTitle = NULL;
|
|
}
|
|
curveTitle = new QwtPlotMarker("");
|
|
curveTitle->setXValue(5);
|
|
|
|
if (rideSeries == RideFile::watts ||
|
|
rideSeries == RideFile::aPower ||
|
|
rideSeries == RideFile::xPower ||
|
|
rideSeries == RideFile::NP ||
|
|
rideSeries == RideFile::wattsKg) {
|
|
|
|
|
|
QwtText text(curve_title, QwtText::PlainText);
|
|
text.setColor(GColor(CPLOTMARKER));
|
|
curveTitle->setLabel(text);
|
|
}
|
|
|
|
if (rideSeries == RideFile::wattsKg)
|
|
curveTitle->setYValue(0.6);
|
|
else
|
|
curveTitle->setYValue(70);
|
|
curveTitle->attach(thisPlot);
|
|
|
|
modelCurve = new QwtPlotCurve(curve_title);
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true)
|
|
modelCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen pen(GColor(CCP));
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 1.0).toDouble();
|
|
pen.setWidth(width);
|
|
pen.setStyle(Qt::DashLine);
|
|
modelCurve->setPen(pen);
|
|
modelCurve->setSamples(cp_curve_time.data(), cp_curve_power.data(), curve_points);
|
|
modelCurve->attach(thisPlot);
|
|
|
|
// draw a heat curve
|
|
if (showHeat && rideSeries == RideFile::watts && bests && bests->heatMeanMaxArray().count()) {
|
|
|
|
// heat curve
|
|
heatCurve = new QwtPlotCurve("heat");
|
|
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true) heatCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
|
|
heatCurve->setBrush(QBrush(GColor(CCP).darker(200)));
|
|
heatCurve->setPen(QPen(Qt::NoPen));
|
|
heatCurve->setZ(-1);
|
|
|
|
// generate samples
|
|
QVector<double> heat;
|
|
QVector<double> time;
|
|
|
|
for (int i=0; i<bests->meanMaxArray(RideFile::watts).count() && i<bests->heatMeanMaxArray().count(); i++) {
|
|
QwtIntervalSample add(i/60.00f, bests->meanMaxArray(RideFile::watts)[i] - bests->heatMeanMaxArray()[i],
|
|
bests->meanMaxArray(RideFile::watts)[i]/* + bests->heatMeanMaxArray()[i]*/);
|
|
time << double(i)/60.00f;
|
|
heat << bests->heatMeanMaxArray()[i];
|
|
}
|
|
heatCurve->setSamples(time, heat);
|
|
heatCurve->setYAxis(yRight);
|
|
setAxisScale(yRight, 0, 100); // always 100
|
|
heatCurve->attach(thisPlot);
|
|
}
|
|
|
|
if (showHeatByDate && bests) {
|
|
// HeatCurveByDate
|
|
heatCurveByDate = new CpPlotCurve("heat by date");
|
|
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true) heatCurveByDate->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
|
|
heatCurveByDate->setPenWidth(1);
|
|
|
|
QwtLinearColorMap *colorMap = new QwtLinearColorMap(Qt::blue, Qt::red);
|
|
heatCurveByDate->setColorMap(colorMap);
|
|
|
|
// generate samples
|
|
QVector<QwtPoint3D> heatByDateSamples;
|
|
|
|
for (int i=0; i<bests->meanMaxArray(rideSeries).count(); i++) {
|
|
QDate date = bests->meanMaxDates(rideSeries)[i];
|
|
double heat = 1000*(bests->start.daysTo(bests->end)-date.daysTo(bests->end))/(bests->start.daysTo(bests->end));
|
|
|
|
QwtPoint3D add(i/60.00f, bests->meanMaxArray(rideSeries)[i], heat);
|
|
|
|
heatByDateSamples << add;
|
|
|
|
}
|
|
heatCurveByDate->setSamples(heatByDateSamples);
|
|
heatCurveByDate->attach(thisPlot);
|
|
|
|
}
|
|
|
|
// Extended CP 4
|
|
if (extendedModelCurve4) {
|
|
delete extendedModelCurve4;
|
|
extendedModelCurve4 = NULL;
|
|
}
|
|
if (extendedModelCurve_WSecond) {
|
|
delete extendedModelCurve_WSecond;
|
|
extendedModelCurve_WSecond = NULL;
|
|
}
|
|
if (extendedModelCurve_WPrime) {
|
|
delete extendedModelCurve_WPrime;
|
|
extendedModelCurve_WPrime = NULL;
|
|
}
|
|
if (extendedModelCurve_CP) {
|
|
delete extendedModelCurve_CP;
|
|
extendedModelCurve_CP = NULL;
|
|
}
|
|
if (extendedModelCurve_WPrime_CP) {
|
|
delete extendedModelCurve_WPrime_CP;
|
|
extendedModelCurve_WPrime_CP = NULL;
|
|
}
|
|
|
|
if (extendedModelCurve5) {
|
|
delete extendedModelCurve5;
|
|
extendedModelCurve5 = NULL;
|
|
}
|
|
|
|
if (extendedModelCurve6) {
|
|
delete extendedModelCurve6;
|
|
extendedModelCurve6 = NULL;
|
|
}
|
|
|
|
|
|
|
|
if (model == 3) {
|
|
if (curveTitle) {
|
|
delete curveTitle;
|
|
curveTitle = NULL;
|
|
}
|
|
//extendedModelCurve4 = ecp->getPlotCurveForExtendedCP_4_3(athleteModeleCP4);
|
|
//extendedModelCurve4->attach(thisPlot);
|
|
|
|
/*extendedModelCurve_WSecond = ecp->getPlotCurveForExtendedCP_4_3_P1(athleteModeleCP4);
|
|
extendedModelCurve_WSecond->attach(thisPlot);
|
|
extendedModelCurve_WPrime = ecp->getPlotCurveForExtendedCP_4_3_WPrime(athleteModeleCP4);
|
|
extendedModelCurve_WPrime->attach(thisPlot);
|
|
extendedModelCurve_CP = ecp->getPlotCurveForExtendedCP_4_3_CP(athleteModeleCP4);
|
|
extendedModelCurve_CP->attach(thisPlot);*/
|
|
|
|
/*extendedCurveTitle = ecp->getPlotMarkerForExtendedCP_4_3(athleteModeleCP4);
|
|
extendedCurveTitle->attach(thisPlot);*/
|
|
|
|
/*for (int level=15;level>0;level--) {
|
|
double best5sec = context->ride->ride()->getWeight() * (23-(15-level)*1);
|
|
double best1min = context->ride->ride()->getWeight() * (12-(15-level)*0.5);
|
|
double best5min = context->ride->ride()->getWeight() * (8-(15-level)*0.33333);
|
|
double best1hour = context->ride->ride()->getWeight() * (6.25-(15-level)*0.25);
|
|
|
|
Model_eCP levelModeleCP5 = ecp->deriveExtendedCP_5_3_ParametersForBest(best5sec, best1min, best5min, best1hour);
|
|
QwtPlotCurve *levelCurve5 = ecp->getPlotLevelForExtendedCP_5_3(levelModeleCP5);
|
|
levelCurve5->attach(thisPlot);
|
|
}*/
|
|
|
|
extendedModelCurve5 = ecp->getPlotCurveForExtendedCP_5_3(athleteModeleCP5);
|
|
extendedModelCurve5->attach(thisPlot);
|
|
|
|
|
|
|
|
/*extendedModelCurve_WSecond = ecp->getPlotCurveForExtendedCP_5_3_WSecond(athleteModeleCP5, false);
|
|
extendedModelCurve_WSecond->attach(thisPlot);
|
|
extendedModelCurve_WPrime = ecp->getPlotCurveForExtendedCP_5_3_WPrime(athleteModeleCP5, false);
|
|
extendedModelCurve_WPrime->attach(thisPlot);
|
|
extendedModelCurve_CP = ecp->getPlotCurveForExtendedCP_5_3_CP(athleteModeleCP5, false);
|
|
extendedModelCurve_CP->attach(thisPlot);*/
|
|
|
|
//extendedModelCurve6 = ecp->getPlotCurveForExtendedCP_6_3(athleteModeleCP6);
|
|
//extendedModelCurve6->attach(thisPlot);
|
|
|
|
/*extendedModelCurve_WSecond = ecp->getPlotCurveForExtendedCP_6_3_WSecond(athleteModeleCP6, false);
|
|
extendedModelCurve_WSecond->attach(thisPlot);
|
|
extendedModelCurve_WPrime = ecp->getPlotCurveForExtendedCP_6_3_WPrime(athleteModeleCP6, false);
|
|
extendedModelCurve_WPrime->attach(thisPlot);
|
|
extendedModelCurve_CP = ecp->getPlotCurveForExtendedCP_6_3_CP(athleteModeleCP6, false);
|
|
extendedModelCurve_CP->attach(thisPlot);*/
|
|
|
|
|
|
curveTitle = ecp->getPlotMarkerForExtendedCP(athleteModeleCP5);
|
|
curveTitle->setXValue(5);
|
|
curveTitle->setYValue(70);
|
|
curveTitle->attach(thisPlot);
|
|
}
|
|
}
|
|
|
|
void
|
|
CPPlot::clearCurves()
|
|
{
|
|
// bests ridefilecache
|
|
if (bests) {
|
|
delete bests;
|
|
bests = NULL;
|
|
}
|
|
|
|
// model curve
|
|
if (modelCurve) {
|
|
delete modelCurve;
|
|
modelCurve = NULL;
|
|
}
|
|
|
|
// ride curve
|
|
if (rideCurve) {
|
|
delete rideCurve;
|
|
rideCurve = NULL;
|
|
}
|
|
|
|
// rainbow curve
|
|
if (bestsCurves.size()) {
|
|
foreach (QwtPlotCurve *curve, bestsCurves)
|
|
delete curve;
|
|
bestsCurves.clear();
|
|
}
|
|
|
|
// rainbow labels
|
|
if (allZoneLabels.size()) {
|
|
foreach (QwtPlotMarker *label, allZoneLabels)
|
|
delete label;
|
|
allZoneLabels.clear();
|
|
}
|
|
|
|
// heat curves
|
|
if (heatCurve) {
|
|
delete heatCurve;
|
|
heatCurve = NULL;
|
|
}
|
|
if (heatCurveByDate) {
|
|
delete heatCurveByDate;
|
|
heatCurveByDate = NULL;
|
|
}
|
|
}
|
|
|
|
// plot the all curve, with shading according to the shade mode
|
|
void
|
|
CPPlot::plotBestsCurve(CPPlot *thisPlot,
|
|
int n_values,
|
|
const double *power_values,
|
|
QColor plotColor,
|
|
bool forcePlotColor)
|
|
{
|
|
QVector<double> energyBests(n_values);
|
|
QVector<double> time_values(n_values);
|
|
// generate an array of time values
|
|
for (int t = 0; t < n_values; t++) {
|
|
double time = (t + 1) / 60.0;
|
|
if (criticalSeries == CriticalPowerWindow::watts_inv_time)
|
|
time = 1.0 / time;
|
|
|
|
time_values[t] = time;
|
|
energyBests[t] = power_values[t] * time_values[t] * 60.0 / 1000.0;
|
|
}
|
|
|
|
// lets work out how we are shading it
|
|
switch(shadeMode) {
|
|
case 0 : // not shading!!
|
|
shadingCP = 0;
|
|
break;
|
|
|
|
case 1 : // value for current date
|
|
// or average for date range if a range
|
|
shadingCP = dateCP;
|
|
break;
|
|
|
|
default:
|
|
case 2 : // derived value
|
|
shadingCP = cp;
|
|
break;
|
|
}
|
|
|
|
// generate zones from shading CP value
|
|
if (shadingCP > 0) {
|
|
QList <int> power_zone;
|
|
int n_zones = context->athlete->zones()->lowsFromCP(&power_zone, (int) int(shadingCP));
|
|
int high = n_values - 1;
|
|
int zone = 0;
|
|
while (zone < n_zones && high > 0) {
|
|
int low = high - 1;
|
|
int nextZone = zone + 1;
|
|
if (nextZone >= power_zone.size())
|
|
low = 0;
|
|
else {
|
|
while ((low > 0) && (power_values[low] < power_zone[nextZone]))
|
|
--low;
|
|
}
|
|
|
|
QColor color = zoneColor(zone, n_zones);
|
|
QString name = context->athlete->zones()->getDefaultZoneName(zone);
|
|
QwtPlotCurve *curve = new QwtPlotCurve(name);
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true)
|
|
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen pen(color.darker(200));
|
|
if (forcePlotColor) // not default
|
|
pen.setColor(plotColor);
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 1.0).toDouble();
|
|
pen.setWidth(width);
|
|
curve->setPen(pen);
|
|
curve->attach(thisPlot);
|
|
|
|
// use a linear gradient
|
|
if (shadeMode && shadingCP) { // 0 value means no shading please - and only if proper value for shadingCP
|
|
color.setAlpha(64);
|
|
QColor color1 = color.darker();
|
|
QLinearGradient linearGradient(0, 0, 0, height());
|
|
linearGradient.setColorAt(0.0, color);
|
|
linearGradient.setColorAt(1.0, color1);
|
|
linearGradient.setSpread(QGradient::PadSpread);
|
|
curve->setBrush(linearGradient); // fill below the line
|
|
}
|
|
|
|
if (criticalSeries == CriticalPowerWindow::work) { // this is Energy mode
|
|
curve->setSamples(time_values.data() + low,
|
|
energyBests.data() + low, high - low + 1);
|
|
} else {
|
|
curve->setSamples(time_values.data() + low,
|
|
power_values + low, high - low + 1);
|
|
}
|
|
bestsCurves.append(curve);
|
|
|
|
if (shadeMode && (criticalSeries != CriticalPowerWindow::work || energyBests[high] > 100.0)) {
|
|
QwtText text(name);
|
|
text.setFont(QFont("Helvetica", 20, QFont::Bold));
|
|
color.setAlpha(255);
|
|
text.setColor(color);
|
|
QwtPlotMarker *label_mark = new QwtPlotMarker();
|
|
// place the text in the geometric mean in time, at a decent power
|
|
double x, y;
|
|
if (criticalSeries == CriticalPowerWindow::work) {
|
|
x = (time_values[low] + time_values[high]) / 2;
|
|
y = (energyBests[low] + energyBests[high]) / 5;
|
|
}
|
|
else {
|
|
x = sqrt(time_values[low] * time_values[high]);
|
|
y = (power_values[low] + power_values[high]) / 5;
|
|
}
|
|
label_mark->setValue(x, y);
|
|
label_mark->setLabel(text);
|
|
label_mark->attach(thisPlot);
|
|
allZoneLabels.append(label_mark);
|
|
}
|
|
|
|
high = low;
|
|
++zone;
|
|
}
|
|
}
|
|
// no zones available: just plot the curve without zones
|
|
else {
|
|
QwtPlotCurve *curve = new QwtPlotCurve(tr("maximal power"));
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true)
|
|
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen pen((plotColor));
|
|
pen.setWidth(appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble());
|
|
curve->setPen(pen);
|
|
QColor brush_color = GColor(CCP);
|
|
brush_color.setAlpha(200);
|
|
//curve->setBrush(QBrush::None); // brush fills below the line
|
|
if (criticalSeries == CriticalPowerWindow::work)
|
|
curve->setSamples(time_values.data(), energyBests.data(), n_values);
|
|
else
|
|
curve->setSamples(time_values.data(), power_values, n_values);
|
|
|
|
curve->attach(thisPlot);
|
|
bestsCurves.append(curve);
|
|
}
|
|
|
|
|
|
double xmin = 0.017;
|
|
double xmax = time_values[n_values - 1];
|
|
|
|
// special min/max
|
|
if (criticalSeries == CriticalPowerWindow::work) {
|
|
// Energy mode is really only interesting in the range where energy is
|
|
// linear in interval duration--up to about 1 hour.
|
|
xmax = 60.0;
|
|
}
|
|
else if (criticalSeries == CriticalPowerWindow::watts_inv_time) {
|
|
xmin = 0.001;
|
|
xmax = 0.3;
|
|
}
|
|
else if (criticalSeries == CriticalPowerWindow::vam) {
|
|
xmin = 4.993;
|
|
}
|
|
|
|
QwtScaleDiv div((double)xmin, (double)xmax);
|
|
if (criticalSeries == CriticalPowerWindow::work)
|
|
div.setTicks(QwtScaleDiv::MajorTick, LogTimeScaleDraw::ticksEnergy);
|
|
else
|
|
div.setTicks(QwtScaleDiv::MajorTick, LogTimeScaleDraw::ticks);
|
|
|
|
thisPlot->setAxisScaleDiv(QwtPlot::xBottom, div);
|
|
|
|
|
|
double ymax;
|
|
if (criticalSeries == CriticalPowerWindow::work) {
|
|
int i = std::lower_bound(time_values.begin(), time_values.end(), 60.0) - time_values.begin();
|
|
ymax = 10 * ceil(energyBests[i] / 10);
|
|
}
|
|
else if (criticalSeries == CriticalPowerWindow::watts_inv_time) {
|
|
ymax = 10 * ceil(power_values[180] / 10);
|
|
}
|
|
else {
|
|
ymax = 100 * ceil(power_values[0] / 100);
|
|
if (ymax == 100)
|
|
ymax = 5 * ceil(power_values[0] / 5);
|
|
}
|
|
thisPlot->setAxisScale(thisPlot->yLeft, 0, ymax);
|
|
|
|
}
|
|
|
|
void
|
|
CPPlot::calculate(RideItem *rideItem)
|
|
{
|
|
clearCurves();
|
|
|
|
// Season Compare Mode
|
|
if (rangemode && context->isCompareDateRanges) return calculateForDateRanges(context->compareDateRanges);
|
|
|
|
if (!rideItem) return;
|
|
|
|
QString fileName = rideItem->fileName;
|
|
QDateTime dateTime = rideItem->dateTime;
|
|
//QDir dir(path);
|
|
QFileInfo file(fileName);
|
|
|
|
// zap any existing ridefilecache then get new one
|
|
if (current) delete current;
|
|
current = new RideFileCache(context, context->athlete->home.absolutePath() + "/" + fileName);
|
|
|
|
// get aggregates - incase not initialised from date change
|
|
if (bests == NULL) bests = new RideFileCache(context, startDate, endDate, isFiltered, files, rangemode);
|
|
|
|
// heat ...
|
|
//! todo qDebug()<<"FTP heat="<<bests->heatMeanMaxArray()[3600];
|
|
//! todo qDebug()<<"3min heat="<<bests->heatMeanMaxArray()[180];
|
|
|
|
//
|
|
// PLOT MODEL CURVE (DERIVED)
|
|
//
|
|
if (rideSeries == RideFile::aPower || rideSeries == RideFile::xPower || rideSeries == RideFile::NP || rideSeries == RideFile::watts || rideSeries == RideFile::wattsKg || rideSeries == RideFile::none) {
|
|
|
|
if (bests->meanMaxArray(rideSeries).size() > 1) {
|
|
// calculate CP model from all-time best data
|
|
cp = tau = t0 = 0;
|
|
deriveCPParameters();
|
|
|
|
if (model == 3) {
|
|
// calculate extended CP model from all-time best data
|
|
//athleteModeleCP2 = ecp->deriveExtendedCP_2_3_Parameters(bests, series, sanI1, sanI2, anI1, anI2, aeI1, aeI2, laeI1, laeI2);
|
|
athleteModeleCP4 = ecp->deriveExtendedCP_4_3_Parameters(true, bests, rideSeries, sanI1, sanI2, anI1, anI2, aeI1, aeI2, laeI1, laeI2);
|
|
athleteModeleCP5 = ecp->deriveExtendedCP_5_3_Parameters(true, bests, rideSeries, sanI1, sanI2, anI1, anI2, aeI1, aeI2, laeI1, laeI2);
|
|
athleteModeleCP6 = ecp->deriveExtendedCP_6_3_Parameters(true, bests, rideSeries, sanI1, sanI2, anI1, anI2, aeI1, aeI2, laeI1, laeI2);
|
|
|
|
}
|
|
}
|
|
|
|
//
|
|
// CP curve only relevant for Energy or Watts (?)
|
|
//
|
|
if (rideSeries == RideFile::aPower || rideSeries == RideFile::NP || rideSeries == RideFile::xPower ||
|
|
rideSeries == RideFile::watts || rideSeries == RideFile::wattsKg || rideSeries == RideFile::none) {
|
|
|
|
if (!modelCurve) plotModelCurve(this, cp, tau, t0);
|
|
else {
|
|
// make sure color reflects latest config
|
|
QPen pen(GColor(CCP));
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 1.0).toDouble();
|
|
pen.setWidth(width);
|
|
pen.setStyle(Qt::DashLine);
|
|
modelCurve->setPen(pen);
|
|
}
|
|
|
|
if (model == 3 && modelCurve) modelCurve->setVisible(false);
|
|
else if (modelCurve) modelCurve->setVisible(true);
|
|
}
|
|
|
|
//
|
|
// PLOT ZONE (RAINBOW) AGGREGATED CURVE
|
|
//
|
|
if (bests->meanMaxArray(rideSeries).size()) {
|
|
int maxNonZero = 0;
|
|
for (int i = 0; i < bests->meanMaxArray(rideSeries).size(); ++i) {
|
|
if (bests->meanMaxArray(rideSeries)[i] > 0) maxNonZero = i;
|
|
}
|
|
|
|
plotBestsCurve(this, maxNonZero, bests->meanMaxArray(rideSeries).constData() + 1, GColor(CCP), false);
|
|
}
|
|
} else {
|
|
|
|
//
|
|
// PLOT BESTS IN SERIES COLOR
|
|
//
|
|
if (bestsCurve) {
|
|
delete bestsCurve;
|
|
bestsCurve = NULL;
|
|
}
|
|
if (bests->meanMaxArray(rideSeries).size()) {
|
|
|
|
int maxNonZero = 0;
|
|
QVector<double> timeArray(bests->meanMaxArray(rideSeries).size());
|
|
for (int i = 0; i < bests->meanMaxArray(rideSeries).size(); ++i) {
|
|
timeArray[i] = i / 60.0;
|
|
if (bests->meanMaxArray(rideSeries)[i] > 0) maxNonZero = i;
|
|
}
|
|
|
|
if (maxNonZero > 1) {
|
|
|
|
bestsCurve = new QwtPlotCurve(dateTime.toString(tr("ddd MMM d, yyyy h:mm AP")));
|
|
bestsCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
|
|
QPen line;
|
|
QColor fill;
|
|
switch (rideSeries) {
|
|
|
|
case RideFile::kphd:
|
|
line.setColor(GColor(CACCELERATION).darker(200));
|
|
fill = (GColor(CACCELERATION));
|
|
break;
|
|
|
|
case RideFile::kph:
|
|
line.setColor(GColor(CSPEED).darker(200));
|
|
fill = (GColor(CSPEED));
|
|
break;
|
|
|
|
case RideFile::cad:
|
|
case RideFile::cadd:
|
|
line.setColor(GColor(CCADENCE).darker(200));
|
|
fill = (GColor(CCADENCE));
|
|
break;
|
|
|
|
case RideFile::nm:
|
|
case RideFile::nmd:
|
|
line.setColor(GColor(CTORQUE).darker(200));
|
|
fill = (GColor(CTORQUE));
|
|
break;
|
|
|
|
case RideFile::hr:
|
|
case RideFile::hrd:
|
|
line.setColor(GColor(CHEARTRATE).darker(200));
|
|
fill = (GColor(CHEARTRATE));
|
|
break;
|
|
|
|
case RideFile::vam:
|
|
line.setColor(GColor(CALTITUDE).darker(200));
|
|
fill = (GColor(CALTITUDE));
|
|
break;
|
|
|
|
default:
|
|
case RideFile::watts: // won't ever get here
|
|
case RideFile::wattsd:
|
|
case RideFile::NP:
|
|
case RideFile::xPower:
|
|
line.setColor(GColor(CPOWER).darker(200));
|
|
fill = (GColor(CPOWER));
|
|
break;
|
|
}
|
|
|
|
// wow, QVector really doesn't have a max/min method!
|
|
double ymax = 0;
|
|
double ymin = 100000;
|
|
foreach(double v, current->meanMaxArray(rideSeries)) {
|
|
if (v > ymax) ymax = v;
|
|
if (v && v < ymin) ymin = v;
|
|
}
|
|
foreach(double v, bests->meanMaxArray(rideSeries)) {
|
|
if (v > ymax) ymax = v;
|
|
if (v&& v < ymin) ymin = v;
|
|
}
|
|
if (ymin == 100000) ymin = 0;
|
|
|
|
// VAM is a bit special
|
|
if (rideSeries == RideFile::vam) {
|
|
if (bests->meanMaxArray(rideSeries).size() > 300)
|
|
ymax = bests->meanMaxArray(rideSeries)[300];
|
|
else
|
|
ymax = 2000;
|
|
}
|
|
|
|
ymax *= 1.1; // bit of headroom
|
|
ymin *= 0.9;
|
|
|
|
// xmax is directly related to the size of the arrays
|
|
double xmax = current->meanMaxArray(rideSeries).size();
|
|
if (bests->meanMaxArray(rideSeries).size() > xmax)
|
|
xmax = bests->meanMaxArray(rideSeries).size();
|
|
xmax /= 60; // its in minutes not seconds
|
|
|
|
setAxisScale(yLeft, ymin, ymax);
|
|
|
|
QwtScaleDiv div((rideSeries == RideFile::vam ? (double) 4.993: (double) 0.017), (double)xmax);
|
|
div.setTicks(QwtScaleDiv::MajorTick, LogTimeScaleDraw::ticks);
|
|
setAxisScaleDiv(QwtPlot::xBottom, div);
|
|
|
|
bestsCurve->setPen(line);
|
|
fill.setAlpha(64);
|
|
// use a linear gradient
|
|
fill.setAlpha(64);
|
|
QColor fill1 = fill.darker();
|
|
QLinearGradient linearGradient(0, 0, 0, height());
|
|
linearGradient.setColorAt(0.0, fill);
|
|
linearGradient.setColorAt(1.0, fill1);
|
|
linearGradient.setSpread(QGradient::PadSpread);
|
|
bestsCurve->setBrush(linearGradient);
|
|
bestsCurve->attach(this);
|
|
bestsCurve->setSamples(timeArray.data() + 1, bests->meanMaxArray(rideSeries).constData() + 1, maxNonZero - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ridePlotStyle == 1)
|
|
calculateCentile(rideItem);
|
|
else if (ridePlotStyle < 2) {
|
|
//
|
|
// PLOT THIS RIDE CURVE
|
|
//
|
|
if (rideCurve) {
|
|
delete rideCurve;
|
|
rideCurve = NULL;
|
|
}
|
|
|
|
if (!rangemode && current->meanMaxArray(rideSeries).size()) {
|
|
int maxNonZero = 0;
|
|
QVector<double> timeArray(current->meanMaxArray(rideSeries).size());
|
|
for (int i = 0; i < current->meanMaxArray(rideSeries).size(); ++i) {
|
|
timeArray[i] = i / 60.0;
|
|
if (current->meanMaxArray(rideSeries)[i] > 0) maxNonZero = i;
|
|
}
|
|
|
|
if (maxNonZero > 1) {
|
|
|
|
rideCurve = new QwtPlotCurve(dateTime.toString(tr("ddd MMM d, yyyy h:mm AP")));
|
|
rideCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
rideCurve->setYAxis(yLeft);
|
|
rideCurve->setBrush(QBrush(Qt::NoBrush));
|
|
setAxisVisible(yRight, false);
|
|
QPen black;
|
|
black.setColor(GColor(CRIDECP));
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 1.0).toDouble();
|
|
black.setWidth(width);
|
|
rideCurve->setPen(black);
|
|
rideCurve->attach(this);
|
|
|
|
if (criticalSeries == CriticalPowerWindow::work) {
|
|
|
|
// Calculate Energy
|
|
QVector<double> energyArray(current->meanMaxArray(RideFile::watts).size());
|
|
for (int i = 0; i <= maxNonZero; ++i) {
|
|
energyArray[i] =
|
|
timeArray[i] *
|
|
current->meanMaxArray(RideFile::watts)[i] * 60.0 / 1000.0;
|
|
}
|
|
rideCurve->setSamples(timeArray.data() + 1, energyArray.constData() + 1, maxNonZero - 1);
|
|
|
|
} else {
|
|
|
|
if (showPercent && bests) {
|
|
|
|
rideCurve->setYAxis(yRight);
|
|
//QColor p = GColor(CRIDECP);
|
|
//p.setAlpha(64);
|
|
//rideCurve->setBrush(QBrush(p));
|
|
setAxisVisible(yRight, true);
|
|
|
|
QVector<double> samples(timeArray.size());
|
|
|
|
for(int i=0; i <samples.size() && i < current->meanMaxArray(rideSeries).size() &&
|
|
i <bests->meanMaxArray(rideSeries).size(); i++) {
|
|
|
|
samples[i] = current->meanMaxArray(rideSeries)[i] /
|
|
bests->meanMaxArray(rideSeries)[i] * 100.00f;
|
|
}
|
|
rideCurve->setSamples(timeArray.data() + 1, samples.data() + 1, maxNonZero -1);
|
|
|
|
int max = rideCurve->maxYValue();
|
|
if (max < 100) max = 100;
|
|
setAxisScale(yRight, 0, max); // always 100
|
|
|
|
} else {
|
|
|
|
rideCurve->setYAxis(yLeft);
|
|
setAxisVisible(yRight, false);
|
|
|
|
// normal
|
|
rideCurve->setSamples(timeArray.data() + 1,
|
|
current->meanMaxArray(rideSeries).constData() + 1, maxNonZero - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
refreshReferenceLines(rideItem);
|
|
|
|
if (!rangemode && context->isCompareIntervals)
|
|
return calculateForIntervals(context->compareIntervals);
|
|
replot();
|
|
}
|
|
|
|
void
|
|
CPPlot::pointHover(QwtPlotCurve *curve, int index)
|
|
{
|
|
if (index >= 0) {
|
|
|
|
double xvalue = curve->sample(index).x();
|
|
double yvalue = curve->sample(index).y();
|
|
QString text, dateStr;
|
|
|
|
// add when to tooltip if its all curve
|
|
if (bestsCurves.contains(curve)) {
|
|
int index = xvalue * 60;
|
|
if (index >= 0 && bests && getBests().count() > index) {
|
|
QDate date = getBestDates()[index];
|
|
dateStr = date.toString("\nddd, dd MMM yyyy");
|
|
}
|
|
}
|
|
|
|
// output the tooltip
|
|
text = QString("%1\n%3 %4%5")
|
|
.arg(interval_to_str(60.0*xvalue))
|
|
.arg(yvalue, 0, 'f', RideFile::decimalsFor(rideSeries))
|
|
.arg(RideFile::unitName(rideSeries, context))
|
|
.arg(dateStr);
|
|
|
|
// set that text up
|
|
zoomer->setText(text);
|
|
return;
|
|
}
|
|
// no point
|
|
zoomer->setText("");
|
|
}
|
|
|
|
void
|
|
CPPlot::clearFilter()
|
|
{
|
|
isFiltered = false;
|
|
files.clear();
|
|
delete bests;
|
|
bests = NULL;
|
|
}
|
|
|
|
void
|
|
CPPlot::setFilter(QStringList list)
|
|
{
|
|
isFiltered = true;
|
|
files = list;
|
|
delete bests;
|
|
bests = NULL;
|
|
}
|
|
|
|
void
|
|
CPPlot::setShowHeat(bool x)
|
|
{
|
|
showHeat = x;
|
|
}
|
|
|
|
void
|
|
CPPlot::setShowPercent(bool x)
|
|
{
|
|
showPercent = x;
|
|
}
|
|
|
|
void
|
|
CPPlot::setShowHeatByDate(bool x)
|
|
{
|
|
showHeatByDate = x;
|
|
}
|
|
|
|
|
|
void
|
|
CPPlot::setShadeMode(int x)
|
|
{
|
|
shadeMode = x;
|
|
}
|
|
|
|
void
|
|
CPPlot::setShadeIntervals(int x)
|
|
{
|
|
shadeIntervals = x;
|
|
}
|
|
|
|
// model parameters!
|
|
void
|
|
CPPlot::setModel(int sanI1, int sanI2, int anI1, int anI2, int aeI1, int aeI2, int laeI1, int laeI2, int model)
|
|
{
|
|
this->anI1 = double(anI1) / double(60.00f);
|
|
this->anI2 = double(anI2) / double(60.00f);
|
|
this->aeI1 = double(aeI1) / double(60.00f);
|
|
this->aeI2 = double(aeI2) / double(60.00f);
|
|
|
|
this->sanI1 = double(sanI1) / double(60.00f);
|
|
this->sanI2 = double(sanI2) / double(60.00f);
|
|
this->laeI1 = double(laeI1) / double(60.00f);
|
|
this->laeI2 = double(laeI2) / double(60.00f);
|
|
|
|
this->model = model;
|
|
|
|
// wipe away previous effort
|
|
clearCurves();
|
|
}
|
|
|
|
void
|
|
CPPlot::refreshReferenceLines(RideItem *rideItem)
|
|
{
|
|
// we only do refs for a specific ride
|
|
if (rangemode) return;
|
|
|
|
// wipe existing
|
|
foreach(QwtPlotMarker *referenceLine, referenceLines) {
|
|
referenceLine->detach();
|
|
delete referenceLine;
|
|
}
|
|
referenceLines.clear();
|
|
|
|
if (!rideItem && !rideItem->ride()) return;
|
|
|
|
// horizontal lines at reference points
|
|
if (rideSeries == RideFile::aPower || rideSeries == RideFile::xPower || rideSeries == RideFile::NP || rideSeries == RideFile::watts || rideSeries == RideFile::wattsKg) {
|
|
|
|
if (rideItem->ride()) {
|
|
foreach(const RideFilePoint *referencePoint, rideItem->ride()->referencePoints()) {
|
|
|
|
if (referencePoint->watts != 0) {
|
|
QwtPlotMarker *referenceLine = new QwtPlotMarker;
|
|
QPen p;
|
|
p.setColor(GColor(CPLOTMARKER));
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 1.0).toDouble();
|
|
p.setWidth(width);
|
|
p.setStyle(Qt::DashLine);
|
|
referenceLine->setLinePen(p);
|
|
referenceLine->setLineStyle(QwtPlotMarker::HLine);
|
|
referenceLine->setYValue(referencePoint->watts);
|
|
referenceLine->attach(this);
|
|
referenceLines.append(referenceLine);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CPPlot::setRidePlotStyle(int index)
|
|
{
|
|
ridePlotStyle = index;
|
|
}
|
|
|
|
void
|
|
CPPlot::calculateCentile(RideItem *rideItem)
|
|
{
|
|
qDebug() << "calculateCentile";
|
|
QTime elapsed;
|
|
elapsed.start();
|
|
|
|
qDebug() << "prepare datas ";
|
|
cpintdata data;
|
|
data.rec_int_ms = (int) round(rideItem->ride()->recIntSecs() * 1000.0);
|
|
double lastsecs = 0;
|
|
bool first = true;
|
|
double offset = 0;
|
|
|
|
foreach (const RideFilePoint *p, rideItem->ride()->dataPoints()) {
|
|
|
|
// get offset to apply on all samples if first sample
|
|
if (first == true) {
|
|
offset = p->secs;
|
|
first = false;
|
|
}
|
|
|
|
// drag back to start at 0s
|
|
double psecs = p->secs - offset;
|
|
|
|
// fill in any gaps in recording - use same dodgy rounding as before
|
|
int count = (psecs - lastsecs - rideItem->ride()->recIntSecs()) / rideItem->ride()->recIntSecs();
|
|
|
|
// gap more than an hour, damn that ride file is a mess
|
|
if (count > 3600) count = 1;
|
|
|
|
for(int i=0; i<count; i++) {
|
|
data.points.append(cpintpoint(round(lastsecs+((i+1)*rideItem->ride()->recIntSecs() *1000.0)/1000), 0));
|
|
}
|
|
|
|
lastsecs = psecs;
|
|
|
|
double secs = round(psecs * 1000.0) / 1000;
|
|
if (secs > 0) {
|
|
if (round(p->value(RideFile::watts))>1400)
|
|
qDebug() << "append point " << round(p->value(RideFile::watts)) ;
|
|
data.points.append(cpintpoint(secs, (int) round(p->value(RideFile::watts))));
|
|
}
|
|
}
|
|
|
|
int total_secs = (int) ceil(rideItem->ride()->dataPoints().back()->secs);
|
|
|
|
QVector < QVector<double> > ride_centiles(10);
|
|
// Initialisation
|
|
for (int i = 0; i < ride_centiles.size(); ++i) {
|
|
ride_centiles[i] = QVector <double>(total_secs);
|
|
}
|
|
|
|
qDebug() << "end prepare datas " << elapsed.elapsed();
|
|
qDebug() << "calcul for first 6min ";
|
|
|
|
// loop through the decritized data from top
|
|
// FIRST 6 MINUTES DO BESTS FOR EVERY SECOND
|
|
// WE DO NOT DO THIS FOR NP or xPower SINCE
|
|
// IT IS WELL KNOWN THAT THEY ARE NOT VALID
|
|
// FOR SUCH SHORT DURATIONS AND IT IS VERY
|
|
// CPU INTENSIVE, SO WE DON'T BOTHER
|
|
|
|
double samplerate = rideItem->ride()->recIntSecs();
|
|
|
|
for (int slice = 1; slice < 360;) {
|
|
int windowsize = slice / samplerate;
|
|
QVector<double> sums(data.points.size()-windowsize+1);
|
|
|
|
int index=0;
|
|
double sum=0;
|
|
|
|
for (int i=0; i<data.points.size(); i++) {
|
|
sum += data.points[i].value;
|
|
|
|
if (i>windowsize-1)
|
|
sum -= data.points[i-windowsize].value;
|
|
|
|
if (i>=windowsize-1) {
|
|
sums[index++] = sum/windowsize;
|
|
}
|
|
|
|
}
|
|
//qSort(sums.begin(), sums.end());
|
|
qSort(sums);
|
|
|
|
qDebug() << "sums (" << slice << ") : " << sums.size() << " max " << sums[sums.size()-1];
|
|
|
|
ride_centiles[9][slice] = sums[sums.size()-1];
|
|
|
|
for (int i = ride_centiles.size()-1; i > 0; --i) {
|
|
sum = 0;
|
|
int count = 0;
|
|
|
|
for (int n = (0.1*i)*sums.size(); n < sums.size()-1 && n < (0.1*(i+1))*sums.size(); ++n) {
|
|
sum += sums[n];
|
|
count++;
|
|
}
|
|
if (sum > 0) {
|
|
if (sum > 0) {
|
|
double avg = sum / count;
|
|
ride_centiles[i-1][slice]=avg;
|
|
}
|
|
} else {
|
|
ride_centiles[i-1][slice]=ride_centiles[i][slice];
|
|
}
|
|
}
|
|
|
|
slice ++;
|
|
}
|
|
|
|
qDebug() << "end calcul for first 6min " << elapsed.elapsed();
|
|
qDebug() << "downsampling to 5s after 6min ";
|
|
|
|
QVector<double> downsampled(0);
|
|
|
|
// moving to 5s samples would INCREASE the work...
|
|
if (rideItem->ride()->recIntSecs() >= 5) {
|
|
samplerate = rideItem->ride()->recIntSecs();
|
|
for (int i=0; i<data.points.size(); i++)
|
|
downsampled.append(data.points[i].value);
|
|
} else {
|
|
// moving to 5s samples is DECREASING the work...
|
|
samplerate = 5;
|
|
// we are downsampling to 5s
|
|
long five=5; // start at 1st 5s sample
|
|
double fivesum=0;
|
|
|
|
int fivecount=0;
|
|
|
|
for (int i=0; i<data.points.size(); i++) {
|
|
if (data.points[i].secs <= five) {
|
|
fivesum += data.points[i].value;
|
|
fivecount++;
|
|
}
|
|
else {
|
|
downsampled.append(fivesum / fivecount);
|
|
fivecount = 1;
|
|
fivesum = data.points[i].value;
|
|
|
|
five += 5;
|
|
}
|
|
}
|
|
}
|
|
|
|
qDebug() << "end downsampling to 5s after 6min " << elapsed.elapsed();
|
|
qDebug() << "calcul for rest of ride ";
|
|
|
|
for (int slice = 360; slice < ride_centiles[9].size();) {
|
|
int windowsize = slice / samplerate;
|
|
QVector<double> sums(downsampled.size()-windowsize+2);
|
|
|
|
|
|
int index=0;
|
|
double sum=0;
|
|
|
|
for (int i=0; i<downsampled.size(); i++) {
|
|
sum += downsampled[i];
|
|
|
|
if (i>windowsize-1)
|
|
sum -= downsampled[i-windowsize];
|
|
if (i>=windowsize-1)
|
|
sums[index++] = sum / windowsize;
|
|
|
|
}
|
|
//qSort(sums.begin(), sums.end());
|
|
qSort(sums);
|
|
|
|
qDebug() << "sums (" << slice << ") : " << sums.size() << " max " << sums[sums.size()-1];
|
|
|
|
|
|
|
|
ride_centiles[9][slice] = sums[sums.size()-1];
|
|
|
|
for (int i = ride_centiles.size()-1; i > 0; --i) {
|
|
sum = 0;
|
|
int count = 0;
|
|
|
|
for (int n = (0.1*i)*sums.size(); n < sums.size() && n < (0.1*(i+1))*sums.size(); ++n) {
|
|
if (sums[n]>0) {
|
|
sum += sums[n];
|
|
count++;
|
|
}
|
|
}
|
|
if (sum > 0) {
|
|
double avg = sum / count;
|
|
ride_centiles[i-1][slice]=avg;
|
|
} else {
|
|
ride_centiles[i-1][slice]=ride_centiles[i][slice];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// increment interval duration we are going to search
|
|
// for next, gaps increase as duration increases to
|
|
// reduce overall work, since we require far less
|
|
// precision as the ride duration increases
|
|
if (slice < 3600) slice +=20; // 20s up to one hour
|
|
else if (slice < 7200) slice +=60; // 1m up to two hours
|
|
else if (slice < 10800) slice += 300; // 5mins up to three hours
|
|
else slice += 600; // 10mins after that
|
|
}
|
|
|
|
qDebug() << "end calcul for rest of ride " << elapsed.elapsed();
|
|
qDebug() << "fill gaps ";
|
|
|
|
/*for (int i = 0; i<ride_centiles.size(); i++) {
|
|
double last=0.0;
|
|
for (int j=ride_centiles[i].size()-1; j; j--) {
|
|
if (ride_centiles[i][j] == 0) ride_centiles[i][j]=last;
|
|
else last = ride_centiles[i][j];
|
|
}
|
|
}*/
|
|
|
|
for (int i = ride_centiles.size()-1; i>=0; i--) {
|
|
double last=0.0;
|
|
for (int j=0; j<ride_centiles[i].size(); j++) {
|
|
if (ride_centiles[i][j] == 0) ride_centiles[i][j]=last;
|
|
else last = ride_centiles[i][j];
|
|
}
|
|
}
|
|
|
|
qDebug() << "end fill gaps " << elapsed.elapsed();
|
|
qDebug() << "plotting ";
|
|
|
|
|
|
for (int i = 0; i<ride_centiles.size(); i++) {
|
|
int maxNonZero = 0;
|
|
QVector<double> timeArray(ride_centiles[i].size());
|
|
for (int j = 0; j < ride_centiles[i].size(); ++j) {
|
|
timeArray[j] = j / 60.0;
|
|
if (ride_centiles[i][j] > 0) maxNonZero = j;
|
|
}
|
|
|
|
if (maxNonZero > 1) {
|
|
|
|
QwtPlotCurve *rideCurve = new QwtPlotCurve(tr("%10 %").arg(i+1));
|
|
rideCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen pen(QColor(250-(i*20),0,00));
|
|
pen.setStyle(Qt::DashLine); // Qt::SolidLine
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 1.0).toDouble();
|
|
pen.setWidth(width);
|
|
rideCurve->setPen(pen);
|
|
rideCurve->attach(this);
|
|
|
|
|
|
rideCurve->setSamples(timeArray.data() + 1, ride_centiles[i].constData() + 1, maxNonZero - 1);
|
|
bestsCurves.append(rideCurve);
|
|
}
|
|
}
|
|
|
|
|
|
qDebug() << "end plotting " << elapsed.elapsed();
|
|
|
|
}
|
|
|
|
void
|
|
CPPlot::calculateForDateRanges(QList<CompareDateRange> compareDateRanges)
|
|
{
|
|
// zap old curves
|
|
clearCurves();
|
|
|
|
// If no range
|
|
if (compareDateRanges.count() == 0) return;
|
|
|
|
int shadeModeOri = shadeMode;
|
|
int modelOri = model;
|
|
|
|
double ymax = 0;
|
|
|
|
model = 0; // no model in compareDateRanges
|
|
|
|
// prepare aggregates
|
|
for (int j = 0; j < compareDateRanges.size(); ++j) {
|
|
|
|
CompareDateRange range = compareDateRanges.at(j);
|
|
|
|
if (range.isChecked()) {
|
|
RideFileCache *cache = range.rideFileCache();
|
|
|
|
if (cache->meanMaxArray(rideSeries).size()) {
|
|
|
|
int maxNonZero = 0;
|
|
int i=0;
|
|
for (; i < cache->meanMaxArray(rideSeries).size(); ++i) {
|
|
if (cache->meanMaxArray(rideSeries)[i] > 0) maxNonZero = i;
|
|
}
|
|
if (i>0) shadeMode = 0;
|
|
|
|
plotBestsCurve(this, maxNonZero, cache->meanMaxArray(rideSeries).constData() + 1, range.color, true);
|
|
|
|
foreach(double v, cache->meanMaxArray(rideSeries)) {
|
|
if (v > ymax) ymax = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
setAxisScale(yLeft, 0, 1.1*ymax);
|
|
shadeMode = shadeModeOri;
|
|
model = modelOri;
|
|
|
|
replot();
|
|
}
|
|
|
|
void
|
|
CPPlot::calculateForIntervals(QList<CompareInterval> compareIntervals)
|
|
{
|
|
if (rangemode) return;
|
|
|
|
// unselect current intervals
|
|
for (int i=0; i<context->athlete->allIntervalItems()->childCount(); i++) {
|
|
context->athlete->allIntervalItems()->child(i)->setSelected(false);
|
|
}
|
|
|
|
// Remove curve from current Ride
|
|
if (rideCurve) {
|
|
delete rideCurve;
|
|
rideCurve = NULL;
|
|
}
|
|
|
|
|
|
// If no intervals
|
|
if (compareIntervals.count() == 0) return;
|
|
|
|
// prepare aggregates
|
|
for (int i = 0; i < compareIntervals.size(); ++i) {
|
|
CompareInterval interval = compareIntervals.at(i);
|
|
|
|
if (interval.isChecked()) {
|
|
|
|
// no data ?
|
|
if (interval.rideFileCache()->meanMaxArray(rideSeries).count() == 0) return;
|
|
|
|
// create curve data arrays
|
|
plotInterval(this, interval.rideFileCache()->meanMaxArray(rideSeries), interval.color);
|
|
}
|
|
}
|
|
|
|
replot();
|
|
}
|
|
|
|
void
|
|
CPPlot::plotInterval(CPPlot *thisPlot, QVector<double> vector, QColor intervalColor)
|
|
{
|
|
QVector<double>x;
|
|
QVector<double>y;
|
|
for (int i=1; i<vector.count(); i++) {
|
|
x << double(i)/60.00f;
|
|
y << vector[i];
|
|
}
|
|
|
|
// create a curve!
|
|
QwtPlotCurve *curve = new QwtPlotCurve();
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true)
|
|
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
|
|
// set its color - based upon index in intervals!
|
|
QPen pen(intervalColor);
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 1.0).toDouble();
|
|
pen.setWidth(width);
|
|
//pen.setStyle(Qt::DotLine);
|
|
intervalColor.setAlpha(64);
|
|
QBrush brush = QBrush(intervalColor);
|
|
if (shadeIntervals) curve->setBrush(brush);
|
|
else curve->setBrush(Qt::NoBrush);
|
|
curve->setPen(pen);
|
|
curve->setSamples(x.data(), y.data(), x.count()-1);
|
|
|
|
// attach and register
|
|
curve->attach(thisPlot);
|
|
|
|
bestsCurves.append(curve);
|
|
}
|