Files
GoldenCheetah/src/CPPlot.cpp
Mark Liversedge 774267afb4 CP Code Wash before Refactor
.. 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.
2014-03-24 11:08:07 +00:00

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);
}