Files
GoldenCheetah/src/Charts/CriticalPowerWindow.cpp
Alejandro Martinez fbae10f103 Avoid crash in CP chart when perspective is not set
This was reported at the forum and it is reproducible with
the supplied home-perspectives.xml, it was likely generated
by an older version, but lets get defensive just in case.
2022-09-05 12:39:08 -03:00

2021 lines
66 KiB
C++

/*
* Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net)
* Copyright (c) 2009 Dan Connelly (@djconnel)
* Copyright (c) 2014 Damien Grauser (Damien.Grauser@pev-geneve.ch)
* Copyright (c) 2014 Mark Liversedge (liversedge@gmail.com)
*
* 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 "CriticalPowerWindow.h"
#include "Settings.h"
#include "SearchFilterBox.h"
#include "CPPlot.h"
#include "Context.h"
#include "Athlete.h"
#include "RideItem.h"
#include "TimeUtils.h"
#include "IntervalItem.h"
#include "GcOverlayWidget.h"
#include "MUWidget.h"
#include "HelpWhatsThis.h"
#include "AbstractView.h" // stylesheet
#include "RideCache.h"
#include <qwt_picker.h>
#include <qwt_picker_machine.h>
#include <qwt_plot_picker.h>
#include <qwt_plot_curve.h>
#include <qwt_series_data.h>
#include <qwt_scale_div.h>
#include <qwt_compat.h>
#include <QFile>
#include "Season.h"
#include "SeasonParser.h"
#include "Colors.h"
#include "Zones.h"
#include <QXmlInputSource>
#include <QXmlSimpleReader>
#include <QFileDialog>
CriticalPowerWindow::CriticalPowerWindow(Context *context, bool rangemode) :
GcChartWindow(context), _dateRange("{00000000-0000-0000-0000-000000000001}"), context(context), currentRide(NULL), rangemode(rangemode), isfiltered(false), stale(true), useCustom(false), useToToday(false), active(false), hoverCurve(NULL), firstShow(true)
{
//
// reveal controls widget
//
// layout reveal controls
QHBoxLayout *revealLayout = new QHBoxLayout;
revealLayout->setContentsMargins(0,0,0,0);
QLabel *rSeriesLabel = new QLabel(tr("Data Series"), this);
rSeriesSelector = new QxtStringSpinBox();
rPercent = new QCheckBox(this);
rPercent->setText(tr("Percentage of Best"));
rHeat = new QCheckBox(this);
rHeat->setText(tr("Show Heat"));
rDelta = new QCheckBox(this);
rDelta->setText(tr("Delta compare"));
rDelta->hide();
rDeltaPercent = new QCheckBox(this);
rDeltaPercent->setText(tr("as percentage"));
rDeltaPercent->hide();
QVBoxLayout *checks = new QVBoxLayout;
checks->addStretch();
checks->addWidget(rPercent);
checks->addWidget(rHeat);
checks->addWidget(rDelta);
checks->addWidget(rDeltaPercent);
checks->addStretch();
revealLayout->addStretch();
revealLayout->addWidget(rSeriesLabel);
revealLayout->addWidget(rSeriesSelector);
revealLayout->addSpacing(20);
revealLayout->addLayout(checks);
revealLayout->addStretch();
setRevealLayout(revealLayout);
//
// main plot area
//
QVBoxLayout *mainLayout = new QVBoxLayout();
setChartLayout(mainLayout);
cpPlot = new CPPlot(this, context, rangemode);
mainLayout->addWidget(cpPlot);
HelpWhatsThis *help = new HelpWhatsThis(cpPlot);
if (rangemode) cpPlot->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::ChartTrends_Critical_MM));
else cpPlot->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::ChartRides_Critical_MM));
// if we're plotting a veloclinic plot we can adjust CP to see what happens
CPLabel = new QLabel(tr("Critical Power "), this);
CPEdit = new QLineEdit(this);
CPEdit->setFixedWidth(50);
CPSlider = new QSlider(Qt::Horizontal);
CPSlider->setTickInterval(50);
CPSlider->setMinimum(100);
CPSlider->setMaximum(500);
CPSlider->setFocusPolicy(Qt::NoFocus);
QHBoxLayout *cpediting = new QHBoxLayout();
cpediting->addStretch();
cpediting->addWidget(CPLabel);
cpediting->addWidget(CPEdit);
cpediting->addWidget(CPSlider);
cpediting->addStretch();
CPEdit->hide();
CPSlider->hide();
CPLabel->hide();
mainLayout->addLayout(cpediting);
connect(CPEdit, SIGNAL(textChanged(QString)), this, SLOT(setSliderFromEdit()));
connect(CPSlider, SIGNAL(valueChanged(int)), this, SLOT(setEditFromSlider()));
//
// Chart settings
//
// controls widget and layout
QTabWidget *settingsTabs = new QTabWidget(this);
HelpWhatsThis *helpTabs = new HelpWhatsThis(settingsTabs);
if (rangemode) settingsTabs->setWhatsThis(helpTabs->getWhatsThisText(HelpWhatsThis::ChartTrends_Critical_MM));
else settingsTabs->setWhatsThis(helpTabs->getWhatsThisText(HelpWhatsThis::ChartRides_Critical_MM));
QWidget *settingsWidget = new QWidget(this);
settingsWidget->setContentsMargins(0,0,0,0);
settingsTabs->addTab(settingsWidget, tr("Basic"));
HelpWhatsThis *helpSettings = new HelpWhatsThis(settingsWidget);
if (rangemode) settingsWidget->setWhatsThis(helpSettings->getWhatsThisText(HelpWhatsThis::ChartTrends_Critical_MM_Config_Settings));
else settingsWidget->setWhatsThis(helpSettings->getWhatsThisText(HelpWhatsThis::ChartRides_Critical_MM_Config_Settings));
QFormLayout *cl = new QFormLayout(settingsWidget);;
cl->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
QWidget *modelWidget = new QWidget(this);
modelWidget->setContentsMargins(0,0,0,0);
settingsTabs->addTab(modelWidget, tr("Model"));
HelpWhatsThis *helpModel = new HelpWhatsThis(modelWidget);
if (rangemode) modelWidget->setWhatsThis(helpModel->getWhatsThisText(HelpWhatsThis::ChartTrends_Critical_MM_Config_Model));
else modelWidget->setWhatsThis(helpModel->getWhatsThisText(HelpWhatsThis::ChartRides_Critical_MM_Config_Model));
QFormLayout *mcl = new QFormLayout(modelWidget);;
mcl->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
// add additional menu items before setting
// controls since the menu is SET from setControls
QAction *showsettings = new QAction(tr("Chart Settings..."));
addAction(showsettings);
QAction *exportData = new QAction(tr("Export Chart Data..."), this);
addAction(exportData);
setControls(settingsTabs);
// filter / searchbox
searchBox = new SearchFilterBox(this, context);
connect(searchBox, SIGNAL(searchClear()), cpPlot, SLOT(clearFilter()));
connect(searchBox, SIGNAL(searchResults(QStringList)), cpPlot, SLOT(setFilter(QStringList)));
connect(searchBox, SIGNAL(searchClear()), this, SLOT(filterChanged()));
connect(searchBox, SIGNAL(searchResults(QStringList)), this, SLOT(filterChanged()));
cl->addRow(new QLabel(tr("Filter")), searchBox);
cl->addWidget(new QLabel("")); //spacing
HelpWhatsThis *searchHelp = new HelpWhatsThis(searchBox);
searchBox->setWhatsThis(searchHelp->getWhatsThisText(HelpWhatsThis::SearchFilterBox));
// series
seriesCombo = new QComboBox(this);
addSeries();
// data -- season / daterange edit
cComboSeason = new QComboBox(this);
seasons = context->athlete->seasons;
resetSeasons();
QLabel *label = new QLabel(tr("Date range"));
QLabel *label2 = new QLabel(tr("Date range"));
if (rangemode) {
cComboSeason->hide();
label2->hide();
}
cl->addRow(label2, cComboSeason);
dateSetting = new DateSettingsEdit(this);
HelpWhatsThis *dateSettingHelp = new HelpWhatsThis(dateSetting);
dateSetting->setWhatsThis(dateSettingHelp->getWhatsThisText(HelpWhatsThis::ChartTrends_DateRange));
cl->addRow(label, dateSetting);
if (rangemode == false) {
dateSetting->hide();
label->hide();
}
cl->addWidget(new QLabel("")); //spacing
cl->addRow(new QLabel(tr("Data series")), seriesCombo);
// shading
shadeCheck = new QCheckBox(this);
QLabel *shading = new QLabel(tr("Zone Shading"));
shadeCheck->setChecked(true);
cl->addRow(shading, shadeCheck);
showPPCheck = new QCheckBox(this);
showPPCheck->setChecked(false); // default off
QLabel *pp = new QLabel(tr("Show Power Profile"));
cl->addRow(pp, showPPCheck);
showGridCheck = new QCheckBox(this);
showGridCheck->setChecked(true); // default on
QLabel *gridify = new QLabel(tr("Show grid"));
cl->addRow(gridify, showGridCheck);
showTestCheck = new QCheckBox(this);
showTestCheck->setChecked(true); // default on
QLabel *testify = new QLabel(tr("Show Performance Tests"));
cl->addRow(testify, showTestCheck);
showBestCheck = new QCheckBox(this);
showBestCheck->setChecked(true); // default on
QLabel *bestify = new QLabel(tr("Show Bests"));
cl->addRow(bestify, showBestCheck);
filterBestCheck = new QCheckBox(this);
filterBestCheck->setChecked(false); // default off
QLabel *filterify = new QLabel(tr("Filter Unique Bests"));
cl->addRow(filterify, filterBestCheck);
showEffortCheck = new QCheckBox(this);
showEffortCheck->setChecked(false); // default off
QLabel *heaties = new QLabel(tr("Show Sustained Efforts"));
cl->addRow(heaties, showEffortCheck);
showPowerIndexCheck = new QCheckBox(this);
showPowerIndexCheck->setChecked(false); // default off
QLabel *indexify = new QLabel(tr("Show as Power Index"));
cl->addRow(indexify, showPowerIndexCheck);
showPercentCheck = new QCheckBox(this);
showPercentCheck->setChecked(false); // default off
QLabel *percentify = new QLabel(tr("Show as percentage"));
cl->addRow(percentify, showPercentCheck);
showHeatCheck = new QCheckBox(this);
showHeatCheck->setChecked(false); // default off
QLabel *efforts = new QLabel(tr("Show curve heat"));
cl->addRow(efforts, showHeatCheck);
showHeatByDateCheck = new QCheckBox(this);
showHeatByDateCheck->setChecked(false); // default off
QLabel *heatiesByDate = new QLabel(tr("Show curve heat by date"));
cl->addRow(heatiesByDate, showHeatByDateCheck);
shadeIntervalsCheck = new QCheckBox(this);
shadeIntervalsCheck->setChecked(true); // default on
QLabel *shadies = new QLabel(tr("Shade Intervals"));
cl->addRow(shadies, shadeIntervalsCheck);
showCSLinearCheck = new QCheckBox(this);
showCSLinearCheck->setChecked(true); // default on
showCSLinearLabel = new QLabel(tr("Show time scale linear"));
cl->addRow(showCSLinearLabel, showCSLinearCheck);
ridePlotStyleCombo = new QComboBox(this);
ridePlotStyleCombo->addItem(tr("Activity Mean Max"));
ridePlotStyleCombo->addItem(tr("Activity Centile"));
ridePlotStyleCombo->addItem(tr("No Activity"));
cl->addWidget(new QLabel("")); //spacing
cl->addRow(new QLabel(tr("Current Activity")), ridePlotStyleCombo);
// model config
// 2 or 3 point model ?
modelCombo= new QComboBox(this);
modelCombo->addItem(tr("None"));
modelCombo->addItem(tr("2 parameter"));
modelCombo->addItem(tr("3 parameter"));
modelCombo->addItem(tr("Extended CP"));
#if 0 // disable until model fitting errors are fixed (!!!)
modelCombo->addItem(tr("Multicomponent"));
modelCombo->addItem(tr("Ward-Smith"));
#endif
modelCombo->setCurrentIndex(1);
mcl->addRow(new QLabel(tr("Model")), modelCombo);
fitCombo= new QComboBox(this);
fitCombo->addItem(tr("Envelope"));
fitCombo->addItem(tr("Least Squares (LMA)"));
fitCombo->addItem(tr("Linear Regression (Work)"));
fitCombo->setCurrentIndex(0); // default to envelope, backwards compatibility
mcl->addRow(new QLabel(tr("Curve Fit")), fitCombo);
fitdataCombo= new QComboBox(this);
fitdataCombo->addItem(tr("MMP bests"));
fitdataCombo->addItem(tr("Performance tests"));
fitdataCombo->setCurrentIndex(0); // default to MMP, backwards compatibility
mcl->addRow(new QLabel(tr("Data to fit")), fitdataCombo);
mcl->addRow(new QLabel(tr(" ")));
modelDecayLabel = new QLabel(tr("CP and W' Decay"));
modelDecayCheck = new QCheckBox(this);
mcl->addRow(modelDecayLabel, modelDecayCheck);
mcl->addRow(new QLabel(tr(" ")));
intervalLabel = new QLabel(tr("Search Interval"));
secondsLabel = new QLabel(tr("(seconds)"));
mcl->addRow(intervalLabel, secondsLabel);
anLabel = new QLabel(tr("Anaerobic"));
anI1SpinBox = new QDoubleSpinBox(this);
anI1SpinBox->setDecimals(0);
anI1SpinBox->setMinimum(0);
anI1SpinBox->setMaximum(3600);
anI1SpinBox->setSingleStep(1.0);
anI1SpinBox->setAlignment(Qt::AlignRight);
anI1SpinBox->setValue(180); // 3 minutes
anI2SpinBox = new QDoubleSpinBox(this);
anI2SpinBox->setDecimals(0);
anI2SpinBox->setMinimum(0);
anI2SpinBox->setMaximum(3600);
anI2SpinBox->setSingleStep(1.0);
anI2SpinBox->setAlignment(Qt::AlignRight);
anI2SpinBox->setValue(360); // 6 minutes
QHBoxLayout *anLayout = new QHBoxLayout;
anLayout->addWidget(anI1SpinBox);
anLayout->addWidget(anI2SpinBox);
mcl->addRow(anLabel, anLayout);
aeLabel = new QLabel(tr("Aerobic"));
aeI1SpinBox = new QDoubleSpinBox(this);
aeI1SpinBox->setDecimals(0);
aeI1SpinBox->setMinimum(0.0);
aeI1SpinBox->setMaximum(3600);
aeI1SpinBox->setSingleStep(1.0);
aeI1SpinBox->setAlignment(Qt::AlignRight);
aeI1SpinBox->setValue(420); // 7 minutes
aeI2SpinBox = new QDoubleSpinBox(this);
aeI2SpinBox->setDecimals(0);
aeI2SpinBox->setMinimum(0.0);
aeI2SpinBox->setMaximum(3600);
aeI2SpinBox->setSingleStep(1.0);
aeI2SpinBox->setAlignment(Qt::AlignRight);
aeI2SpinBox->setValue(1800); // 30 minutes
QHBoxLayout *aeLayout = new QHBoxLayout;
aeLayout->addWidget(aeI1SpinBox);
aeLayout->addWidget(aeI2SpinBox);
mcl->addRow(aeLabel, aeLayout);
sanI1SpinBox = new QDoubleSpinBox(this);
sanI1SpinBox->setDecimals(0);
sanI1SpinBox->setMinimum(15);
sanI1SpinBox->setMaximum(45);
sanI1SpinBox->setSingleStep(1.0);
sanI1SpinBox->setAlignment(Qt::AlignRight);
sanI1SpinBox->setValue(30); // 30 secs
sanI2SpinBox = new QDoubleSpinBox(this);
sanI2SpinBox->setDecimals(0);
sanI2SpinBox->setMinimum(15);
sanI2SpinBox->setMaximum(300);
sanI2SpinBox->setSingleStep(1.0);
sanI2SpinBox->setAlignment(Qt::AlignRight);
sanI2SpinBox->setValue(60); // 100 secs
sanLabel = new QLabel(tr("Short anaerobic"));
QHBoxLayout *sanLayout = new QHBoxLayout();
sanLayout->addWidget(sanI1SpinBox);
sanLayout->addWidget(sanI2SpinBox);
mcl->addRow(sanLabel, sanLayout);
laeI1SpinBox = new QDoubleSpinBox(this);
laeI1SpinBox->setDecimals(0);
laeI1SpinBox->setMinimum(3000);
laeI1SpinBox->setMaximum(9000);
laeI1SpinBox->setSingleStep(1.0);
laeI1SpinBox->setAlignment(Qt::AlignRight);
laeI1SpinBox->setValue(3000);
laeI2SpinBox = new QDoubleSpinBox();
laeI2SpinBox->setDecimals(0);
laeI2SpinBox->setMinimum(4000);
laeI2SpinBox->setMaximum(30000);
laeI2SpinBox->setSingleStep(1.0);
laeI2SpinBox->setAlignment(Qt::AlignRight);
laeI2SpinBox->setValue(30000);
laeLabel = new QLabel(tr("Long aerobic"));
QHBoxLayout *laeLayout = new QHBoxLayout();
laeLayout->addWidget(laeI1SpinBox);
laeLayout->addWidget(laeI2SpinBox);
mcl->addRow(laeLabel, laeLayout);
mcl->addRow(new QLabel(""), new QLabel(""));
velo1 = new QRadioButton(tr("Exponential"));
velo1->setChecked(true);
mcl->addRow(vlabel = new QLabel(tr("Variant")), velo1);
velo2 = new QRadioButton(tr("Linear feedback"));
mcl->addRow(new QLabel(""), velo2);
velo3 = new QRadioButton(tr("Regeneration"));
mcl->addRow(new QLabel(""), velo3);
// point 2 + 3 -or- point 1 + 2 in a 2 point model
grid = new QwtPlotGrid();
grid->enableX(false); // not needed
grid->enableY(true);
grid->setZ(-20);
QwtValueList ytick[QwtScaleDiv::NTickTypes];
for (double i=0.0; i<=2500; i+= 100) ytick[QwtScaleDiv::MajorTick]<<i;
cpPlot->setAxisScaleDiv(QwtPlot::yLeft,QwtScaleDiv(0.0,2500.0,ytick));
grid->attach(cpPlot);
// the model helper -- showing model parameters etc
helper = new QWidget(this);
helper->setAutoFillBackground(true);
QGridLayout *gridLayout = new QGridLayout(helper);
gridLayout->setColumnStretch(0, 40);
gridLayout->setColumnStretch(1, 30);
gridLayout->setColumnStretch(2, 20);
// create the labels
titleBlank = new QLabel(tr(""), this);
titleValue = new QLabel(tr("Value"), this);
titleRank = new QLabel(tr("Rank"), this);
wprimeTitle = new QLabel(tr("W'"), this);
wprimeValue = new QLabel(tr("0 kJ"), this);
wprimeRank = new QLabel(tr("n/a"), this);
cpTitle = new QLabel(tr("CP"), this);
cpValue = new QLabel(tr("0 w"), this);
cpRank = new QLabel(tr("n/a"), this);
pmaxTitle = new QLabel(tr("Pmax"), this);
pmaxValue = new QLabel(tr("0 w"), this);
pmaxRank = new QLabel(tr("n/a"), this);
eiTitle = new QLabel(tr("Endurance Index"), this);
eiValue = new QLabel(tr("n/a"), this);
summary = new QLabel(tr(""), this);
// autofill
titleBlank->setAutoFillBackground(true);
titleValue->setAutoFillBackground(true);
titleRank->setAutoFillBackground(true);
wprimeTitle->setAutoFillBackground(true);
wprimeValue->setAutoFillBackground(true);
wprimeRank->setAutoFillBackground(true);
cpTitle->setAutoFillBackground(true);
cpValue->setAutoFillBackground(true);
cpRank->setAutoFillBackground(true);
pmaxTitle->setAutoFillBackground(true);
pmaxValue->setAutoFillBackground(true);
pmaxRank->setAutoFillBackground(true);
eiTitle->setAutoFillBackground(true);
eiValue->setAutoFillBackground(true);
summary->setAutoFillBackground(true);
// align all centered
titleBlank->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
titleValue->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
titleRank->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
wprimeTitle->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
wprimeValue->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
wprimeRank->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
cpTitle->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
cpValue->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
cpRank->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
pmaxTitle->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
pmaxValue->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
pmaxRank->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
eiTitle->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
eiValue->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
// add to grid
gridLayout->addWidget(titleBlank, 0, 0);
gridLayout->addWidget(titleValue, 0, 1);
gridLayout->addWidget(titleRank, 0, 2);
gridLayout->addWidget(cpTitle, 1, 0);
gridLayout->addWidget(cpValue, 1, 1);
gridLayout->addWidget(cpRank, 1, 2);
gridLayout->addWidget(wprimeTitle, 2, 0);
gridLayout->addWidget(wprimeValue, 2, 1);
gridLayout->addWidget(wprimeRank, 2, 2);
gridLayout->addWidget(pmaxTitle, 3, 0);
gridLayout->addWidget(pmaxValue, 3, 1);
gridLayout->addWidget(pmaxRank, 3, 2);
gridLayout->addWidget(eiTitle, 4, 0);
gridLayout->addWidget(eiValue, 4, 1);
gridLayout->addWidget(summary, 5, 0, 1, 3);
#ifdef GC_HAVE_MUMODEL
addHelper(QString(tr("Motor Unit Model")), new MUWidget(this, context));
#endif
addHelper(QString(tr("Model")), helper);
GcChartWindow::overlayWidget->move(100,100);
if (rangemode) {
connect(this, SIGNAL(dateRangeChanged(DateRange)), SLOT(dateRangeChanged(DateRange)));
// Compare
connect(context, SIGNAL(compareDateRangesStateChanged(bool)), SLOT(forceReplot()));
connect(context, SIGNAL(compareDateRangesChanged()), SLOT(forceReplot()));
connect(this, SIGNAL(perspectiveFilterChanged(QString)), this, SLOT(perspectiveFilterChanged()));
connect(this, SIGNAL(perspectiveChanged(Perspective*)), this, SLOT(perspectiveFilterChanged()));
} else {
// when working on a ride we can select intervals!
connect(cComboSeason, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int)));
connect(context, SIGNAL(intervalSelected()), this, SLOT(intervalSelected()));
connect(context, SIGNAL(intervalsChanged()), this, SLOT(intervalsChanged()));
connect(context, SIGNAL(intervalHover(IntervalItem*)), this, SLOT(intervalHover(IntervalItem*)));
// Compare
connect(context, SIGNAL(compareIntervalsStateChanged(bool)), SLOT(forceReplot()));
connect(context, SIGNAL(compareIntervalsChanged()), SLOT(forceReplot()));
}
connect(seriesCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setSeries(int)));
connect(ridePlotStyleCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setPlotType(int)));
connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected()));
connect(context, SIGNAL(configChanged(qint32)), cpPlot, SLOT(configChanged(qint32)));
connect(exportData, SIGNAL(triggered()), this, SLOT(exportData()));
connect(showsettings, SIGNAL(triggered()), this, SIGNAL(showControls()));
// model updated?
connect(modelCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(modelChanged()));
connect(fitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(fitChanged()));
connect(fitdataCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(fitChanged()));
connect(seriesCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(modelParametersChanged()));
connect(anI1SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(anI2SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(aeI1SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(aeI2SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(sanI1SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(sanI2SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(laeI1SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(laeI2SpinBox, SIGNAL(valueChanged(double)), this, SLOT(modelParametersChanged()));
connect(modelDecayCheck, SIGNAL(toggled(bool)), this, SLOT(modelParametersChanged()));
connect(velo1, SIGNAL(toggled(bool)), this, SLOT(modelParametersChanged()));
connect(velo2, SIGNAL(toggled(bool)), this, SLOT(modelParametersChanged()));
connect(velo3, SIGNAL(toggled(bool)), this, SLOT(modelParametersChanged()));
// redraw on config change -- this seems the simplest approach
connect(context, SIGNAL(filterChanged()), this, SLOT(forceReplot()));
connect(context, SIGNAL(homeFilterChanged()), this, SLOT(forceReplot()));
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
connect(context, SIGNAL(rideSaved(RideItem*)), this, SLOT(refreshRideSaved(RideItem*)));
connect(context, SIGNAL(rideAdded(RideItem*)), this, SLOT(newRideAdded(RideItem*)));
connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(newRideAdded(RideItem*)));
connect(seasons, SIGNAL(seasonsChanged()), this, SLOT(resetSeasons()));
connect(shadeCheck, SIGNAL(stateChanged(int)), this, SLOT(shadingSelected(int)));
connect(shadeIntervalsCheck, SIGNAL(stateChanged(int)), this, SLOT(shadeIntervalsChanged(int)));
connect(showEffortCheck, SIGNAL(stateChanged(int)), this, SLOT(showEffortChanged(int)));
connect(showPPCheck, SIGNAL(stateChanged(int)), this, SLOT(showPPChanged(int)));
connect(showHeatCheck, SIGNAL(stateChanged(int)), this, SLOT(showHeatChanged(int)));
connect(showCSLinearCheck, SIGNAL(stateChanged(int)), this, SLOT(showCSLinearChanged(int)));
connect(rSeriesSelector, SIGNAL(valueChanged(int)), this, SLOT(rSeriesSelectorChanged(int)));
connect(rHeat, SIGNAL(stateChanged(int)), this, SLOT(rHeatChanged(int)));
connect(rDelta, SIGNAL(stateChanged(int)), this, SLOT(rDeltaChanged()));
connect(rDeltaPercent, SIGNAL(stateChanged(int)), this, SLOT(rDeltaChanged()));
connect(showHeatByDateCheck, SIGNAL(stateChanged(int)), this, SLOT(showHeatByDateChanged(int)));
connect(showPercentCheck, SIGNAL(stateChanged(int)), this, SLOT(showPercentChanged(int)));
connect(showPowerIndexCheck, SIGNAL(stateChanged(int)), this, SLOT(showPowerIndexChanged(int)));
connect(showTestCheck, SIGNAL(stateChanged(int)), this, SLOT(showTestChanged(int)));
connect(showBestCheck, SIGNAL(stateChanged(int)), this, SLOT(showBestChanged(int)));
connect(filterBestCheck, SIGNAL(stateChanged(int)), this, SLOT(filterBestChanged(int)));
connect(showGridCheck, SIGNAL(stateChanged(int)), this, SLOT(showGridChanged(int)));
connect(rPercent, SIGNAL(stateChanged(int)), this, SLOT(rPercentChanged(int)));
connect(dateSetting, SIGNAL(useCustomRange(DateRange)), this, SLOT(useCustomRange(DateRange)));
connect(dateSetting, SIGNAL(useThruToday()), this, SLOT(useThruToday()));
connect(dateSetting, SIGNAL(useStandardRange()), this, SLOT(useStandardRange()));
// if refresh is in progress give CPPlot a chance to redraw when CPX updating
connect(context, SIGNAL(refreshUpdate(QDate)), cpPlot, SLOT(refreshUpdate(QDate)));
connect(context, SIGNAL(refreshEnd()), cpPlot, SLOT(refreshEnd()));
// set date range for bests and model
if (!rangemode) seasonSelected(cComboSeason->currentIndex());
// set widgets and model parameters
modelChanged();
configChanged(CONFIG_APPEARANCE); // get colors set
}
// veloclinic stuff
void
CriticalPowerWindow::setSliderFromEdit()
{
int value = CPEdit->text().toInt();
CPSlider->setValue(value);
}
void
CriticalPowerWindow::setEditFromSlider()
{
CPEdit->setText(QString("%1").arg(CPSlider->value()));
// replot with this value if we're a velo plot
if (series() == veloclinicplot) {
// replot the charts using this new value
cpPlot->setVeloCP(CPSlider->value());
// force replot...
if (rangemode) {
// Refresh aggregated curve
stale = true;
dateRangeChanged(myDateRange);
} else {
Season season = seasons->seasons.at(cComboSeason->currentIndex());
// Refresh aggregated curve (ride added/filter changed)
cpPlot->setDateRange(season.getStart(), season.getEnd());
// if visible make the changes visible
if (amVisible() && myRideItem) cpPlot->setRide(myRideItem);
}
}
}
void
CriticalPowerWindow::configChanged(qint32)
{
if (rangemode) setProperty("color", GColor(CTRENDPLOTBACKGROUND));
else setProperty("color", GColor(CPLOTBACKGROUND));
// tinted palette for headings etc
QPalette palette;
if (rangemode) palette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
else palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
palette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
setPalette(palette);
// inverted palette for data etc
QPalette whitepalette;
if (rangemode) {
whitepalette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
whitepalette.setBrush(QPalette::Background, QBrush(GColor(CTRENDPLOTBACKGROUND)));
whitepalette.setColor(QPalette::WindowText, GCColor::invertColor(GColor(CTRENDPLOTBACKGROUND)));
whitepalette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
whitepalette.setColor(QPalette::Text, GCColor::invertColor(GColor(CTRENDPLOTBACKGROUND)));
} else {
whitepalette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
whitepalette.setBrush(QPalette::Background, QBrush(GColor(CPLOTBACKGROUND)));
whitepalette.setColor(QPalette::WindowText, GCColor::invertColor(GColor(CPLOTBACKGROUND)));
whitepalette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
whitepalette.setColor(QPalette::Text, GCColor::invertColor(GColor(CPLOTBACKGROUND)));
}
QFont font;
font.setPointSize(12); // reasonably big
titleBlank->setFont(font);
titleValue->setFont(font);
titleRank->setFont(font);
wprimeTitle->setFont(font);
wprimeValue->setFont(font);
wprimeRank->setFont(font);
cpTitle->setFont(font);
cpValue->setFont(font);
cpRank->setFont(font);
pmaxTitle->setFont(font);
pmaxValue->setFont(font);
pmaxRank->setFont(font);
eiTitle->setFont(font);
eiValue->setFont(font);
summary->setFont(font);
helper->setPalette(palette);
titleBlank->setPalette(palette);
titleValue->setPalette(palette);
titleRank->setPalette(palette);
wprimeTitle->setPalette(palette);
wprimeValue->setPalette(whitepalette);
wprimeRank->setPalette(whitepalette);
cpTitle->setPalette(palette);
cpValue->setPalette(whitepalette);
cpRank->setPalette(whitepalette);
pmaxTitle->setPalette(palette);
pmaxValue->setPalette(whitepalette);
pmaxRank->setPalette(whitepalette);
eiTitle->setPalette(palette);
eiValue->setPalette(whitepalette);
summary->setPalette(whitepalette);
CPEdit->setPalette(whitepalette);
CPLabel->setPalette(whitepalette);
CPSlider->setPalette(whitepalette);
#ifndef Q_OS_MAC
QString style = QString("QSpinBox { background: %1; }").arg(GCColor::alternateColor(GColor(CPLOTBACKGROUND)).name());
CPEdit->setStyleSheet(style);
//CPLabel->setStyleSheet(style);
//CPSlider->setStyleSheet(style);
if (dpiXFactor > 1) {
helper->setStyleSheet(QString("background: %1; color: %2;").arg(GColor(CPLOTBACKGROUND).name())
.arg(GColor(CPLOTMARKER).name()));
}
// do after cascade above
summary->setStyleSheet(QString("background-color: %1; color: %2;").arg(GColor(CPLOTBACKGROUND).name()).arg(QColor(Qt::gray).name()));
#endif
QPen gridPen(GColor(CPLOTGRID));
grid->setPen(gridPen);
// set ride
rideSelected();
}
void
CriticalPowerWindow::fitChanged()
{
if (active == true) return;
// gets a replot
modelParametersChanged();
}
void
CriticalPowerWindow::modelChanged()
{
// we changed from/to a 2 or 3 parameter model
// so lets set some sensible defaults, these are
// based on advice from our exercise physiologist friends
// for best results in predicting both W' and CP and providing
// a reasonable fit for durations < 2mins.
active = true;
// hide veloclinic's variation for everyone
// it will get shown for model 4 below
vlabel->hide();
velo1->hide();
velo2->hide();
velo3->hide();
// multimodels use envelope, always
// others should use LM, but user can choose
if (modelCombo->currentIndex() > 2) {
fitCombo->setCurrentIndex(0);
fitdataCombo->setCurrentIndex(0);
fitCombo->setEnabled(false);
fitdataCombo->setEnabled(false);
} else {
fitCombo->setCurrentIndex(1);
fitdataCombo->setCurrentIndex(1);
fitCombo->setEnabled(true);
fitdataCombo->setEnabled(true);
}
// disable linear regression fit for all models
// except CP2, this is a bit of a hack, but works
QStandardItem *item=qobject_cast<QStandardItemModel*>(fitCombo->model())->item(2);
item->setFlags(modelCombo->currentIndex() != 1 ? item->flags() & ~Qt::ItemIsEnabled: item->flags() | Qt::ItemIsEnabled);
switch (modelCombo->currentIndex()) {
case 0 : // None
intervalLabel->hide();
secondsLabel->hide();
sanLabel->hide();
sanI1SpinBox->hide();
sanI2SpinBox->hide();
anLabel->hide();
anI1SpinBox->hide();
anI2SpinBox->hide();
aeLabel->hide();
aeI1SpinBox->hide();
aeI2SpinBox->hide();
laeLabel->hide();
laeI1SpinBox->hide();
laeI2SpinBox->hide();
modelDecayCheck->hide();
modelDecayLabel->hide();
// No default values !
break;
case 4 : // Veloclinic Model uses 2 parameter classic but
// also lets you select a variation ..
vlabel->show();
velo1->show();
velo2->show();
velo3->show();
modelDecayCheck->hide();
modelDecayLabel->hide();
// intentional fallthrough
// and drop through into case 1 below ...
case 1 : // Classic 2 param model 2-20 default (per literature)
intervalLabel->show();
secondsLabel->show();
anLabel->show();
sanLabel->hide();
sanI1SpinBox->hide();
sanI2SpinBox->hide();
anLabel->show();
anI1SpinBox->show();
anI2SpinBox->show();
aeLabel->show();
aeI1SpinBox->show();
aeI2SpinBox->show();
laeLabel->hide();
laeI1SpinBox->hide();
laeI2SpinBox->hide();
modelDecayCheck->hide();
modelDecayLabel->hide();
// Default values: class 2-3mins 10-20 model
anI1SpinBox->setValue(120);
anI2SpinBox->setValue(200);
aeI1SpinBox->setValue(720);
aeI2SpinBox->setValue(1200);
break;
case 2 : // 3 param model: 30-60 model
case 5 : // WS model: 30-60 model
intervalLabel->show();
secondsLabel->show();
sanLabel->hide();
sanI1SpinBox->hide();
sanI2SpinBox->hide();
anLabel->show();
anI1SpinBox->show();
anI2SpinBox->show();
aeLabel->show();
aeI1SpinBox->show();
aeI2SpinBox->show();
laeLabel->hide();
laeI1SpinBox->hide();
laeI2SpinBox->hide();
modelDecayLabel->show();
modelDecayCheck->show();
// Default values
anI1SpinBox->setValue(180);
anI2SpinBox->setValue(240);
aeI1SpinBox->setValue(600);
aeI2SpinBox->setValue(1200);
break;
case 3 : // ExtendedCP
intervalLabel->show();
secondsLabel->show();
sanLabel->show();
sanI1SpinBox->show();
sanI2SpinBox->show();
anLabel->show();
anI1SpinBox->show();
anI2SpinBox->show();
aeLabel->show();
aeI1SpinBox->show();
aeI2SpinBox->show();
laeLabel->show();
laeI1SpinBox->show();
laeI2SpinBox->show();
modelDecayCheck->hide();
modelDecayLabel->hide();
// Default values
sanI1SpinBox->setValue(20);
sanI2SpinBox->setValue(90);
anI1SpinBox->setValue(120);
anI2SpinBox->setValue(300);
aeI1SpinBox->setValue(420);
aeI2SpinBox->setValue(1800);
laeI1SpinBox->setValue(4000);
laeI2SpinBox->setValue(30000);
break;
}
active = false;
// update the plot.
modelParametersChanged();
}
// kind of tedious but return index into a radio button group
// for the button that is actually checked.
int
CriticalPowerWindow::variant() const
{
if (velo1->isChecked()) return 0;
if (velo2->isChecked()) return 1;
if (velo3->isChecked()) return 2;
return 0; // default
}
void
CriticalPowerWindow::setVariant(int index)
{
switch (index) {
case 0 : velo1->setChecked(true); velo2->setChecked(false); velo3->setChecked(false); break;
case 1 : velo1->setChecked(false); velo2->setChecked(true); velo3->setChecked(false); break;
case 2 : velo1->setChecked(false); velo2->setChecked(false); velo3->setChecked(true); break;
}
}
void
CriticalPowerWindow::modelParametersChanged()
{
if (active == true) return;
// need a helper any more ?
if (seriesCombo->currentIndex() >= 0) {
CriticalSeriesType series = static_cast<CriticalSeriesType>(seriesCombo->itemData(seriesCombo->currentIndex()).toInt());
updateOptions(series);
}
// tell the plot
cpPlot->setModel(sanI1SpinBox->value(),
sanI2SpinBox->value(),
anI1SpinBox->value(),
anI2SpinBox->value(),
aeI1SpinBox->value(),
aeI2SpinBox->value(),
laeI1SpinBox->value(),
laeI2SpinBox->value(),
modelCombo->currentIndex(),
variant(),
fitCombo->currentIndex(),
fitdataCombo->currentIndex(),
modelDecay());
// and apply
if (amVisible() && myRideItem != NULL) {
cpPlot->setRide(myRideItem);
}
}
void
CriticalPowerWindow::refreshRideSaved(RideItem *item)
{
if (!item) return;
// if the saved ride is in the aggregated time period
QDate date = item->dateTime.date();
// in rangemode ?
if (rangemode && date >= cpPlot->startDate &&
date <= cpPlot->endDate) {
// force a redraw next time visible
cpPlot->setDateRange(cpPlot->startDate, cpPlot->endDate);
}
if (!rangemode) {
// reset date range for bests and model
seasonSelected(cComboSeason->currentIndex());
}
}
void
CriticalPowerWindow::forceReplot()
{
stale = true; // we must become stale
if ((rangemode && context->isCompareDateRanges) || (!rangemode && context->isCompareIntervals)) {
// hide in compare mode
helperWidget()->hide();
rPercent->hide();
rHeat->hide();
rDelta->show();
rDeltaPercent->show();
} else {
// show helper if we're showing power
CriticalSeriesType series = static_cast<CriticalSeriesType>(seriesCombo->itemData(seriesCombo->currentIndex()).toInt());
updateOptions(series);
// these are allowed outside of compare mode
rPercent->show();
rHeat->show();
rDelta->hide();
rDeltaPercent->hide();
}
if (rangemode) {
// force replot...
dateRangeChanged(myDateRange);
} else {
// if visible make the changes visible
// rideSelected is easiest way
if (amVisible()) rideSelected();
}
repaint();
}
void
CriticalPowerWindow::newRideAdded(RideItem *here)
{
// mine just got Zapped, a new rideitem would not be my current item
if (here == currentRide) currentRide = NULL;
// any plots we already have are now stale
if (!rangemode) {
Season season = seasons->seasons.at(cComboSeason->currentIndex());
stale = true;
if ((here->dateTime.date() >= season.getStart() || season.getStart() == QDate())
&& (here->dateTime.date() <= season.getEnd() || season.getEnd() == QDate())) {
// replot
forceReplot();
}
} else {
forceReplot();
}
}
void
CriticalPowerWindow::intervalSelected()
{
if (rangemode) return; // do nothing for ranges!
// in compare mode we don't plot intervals from the sidebar
if (!rangemode && context->isCompareIntervals) {
// wipe away any we might have
foreach(QwtPlotCurve *p, intervalCurves) {
if (p) {
p->detach();
delete p;
}
}
return;
}
// nothing to plot
if (!amVisible() || myRideItem == NULL) return;
// if the array hasn't been initialised properly then clean it up
// this is because intervalsChanged gets called when selecting rides
if (intervalCurves.count() != myRideItem->intervals().count()) {
// wipe away what we got, even if not visible
// clear any interval curves -- even if we are not visible
foreach(QwtPlotCurve *p, intervalCurves) {
if (p) {
p->detach();
delete p;
}
}
// clear, resize to interval count and set to null
intervalCurves.clear();
if (myRideItem && myRideItem->ride() && myRideItem->intervals().count())
for (int i=0; i< myRideItem->intervals().count(); i++)
intervalCurves << NULL;
}
// which itervals are selected?
int i=0;
foreach (IntervalItem*p, myRideItem->intervals()) {
if (p != NULL) {
if (p->selected == true) {
showIntervalCurve(p, i); // set it all up
} else {
hideIntervalCurve(i); // in case its shown at present
}
}
i++;
}
cpPlot->replot();
}
// user hovered over an interval
void
CriticalPowerWindow::intervalHover(IntervalItem* x)
{
if (myRideItem == NULL) return;
// ignore in compare mode
if (!amVisible() || context->isCompareIntervals) return;
// do we need to fill with nulls ?
if (intervalCurves.count() == 0 && myRideItem && myRideItem->ride() && myRideItem->intervals().count())
for (int i=0; i< myRideItem->intervals().count(); i++)
intervalCurves << NULL;
// only one interval can be hovered at any one time
// so we always use the same curve to ensure we don't leave
// any nasty artefacts behind. And its always gray :)
// first lets see what interval this actually is?
int index = myRideItem->intervals().indexOf(x);
if (index >=0 && index < intervalCurves.count()) {
// lazy for now just reuse existing
if (intervalCurves[index] == NULL) {
// get the data setup
// but if there is no data for the ride series
// selected they will still be null
showIntervalCurve(x, index); // set it all up
hideIntervalCurve(index); // in case its shown at present
}
// wipe what we have
if (hoverCurve != NULL) {
hoverCurve->detach();
delete hoverCurve;
hoverCurve = NULL;
}
// still NULL so they have no data
if (intervalCurves[index] == NULL) return;
// clone the data
QVector<QPointF> array;
for (size_t i=0; i<intervalCurves[index]->data()->size(); i++) array << intervalCurves[index]->data()->sample(i);
QPen pen(Qt::gray);
double width = appsettings->value(this, GC_LINEWIDTH, 0.5).toDouble();
pen.setWidth(width);
// create the hover curve
hoverCurve = new QwtPlotCurve("Interval");
hoverCurve->setPen(pen);
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true) hoverCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
hoverCurve->setYAxis(QwtPlot::yLeft);
hoverCurve->setSamples(array);
hoverCurve->setVisible(true);
hoverCurve->setZ(100);
hoverCurve->attach(cpPlot);
cpPlot->replot();
}
}
void
CriticalPowerWindow::hideIntervalCurve(int index)
{
if (rangemode) return; // do nothing for ranges!
if (intervalCurves[index] != NULL) {
intervalCurves[index]->detach(); // in case
}
}
void
CriticalPowerWindow::showIntervalCurve(IntervalItem *current, int index)
{
if (rangemode) return; // do nothing for ranges!
// is this interval being created as we plot ?
if (index == intervalCurves.count()) intervalCurves << NULL;
// we already made it?
if (intervalCurves[index] != NULL) {
intervalCurves[index]->detach(); // in case
intervalCurves[index]->attach(cpPlot);
return;
}
// ack, we need to create the curve for this interval
// make a ridefile
RideFile f(myRideItem->ride());
foreach(RideFilePoint *p, myRideItem->ride()->dataPoints()) {
if ((p->secs+f.recIntSecs()) >= current->start && p->secs <= (current->stop+f.recIntSecs())) {
f.appendPoint(p->secs, p->cad, p->hr, p->km, p->kph, p->nm,
p->watts, p->alt, p->lon, p->lat, p->headwind,
p->slope, p->temp, p->lrbalance,
p->lte, p->rte, p->lps, p->rps,
p->lpco, p->rpco, p->lppb, p->rppb, p->lppe, p->rppe, p->lpppb, p->rpppb, p->lpppe, p->rpppe,
p->smo2, p->thb,
p->rvert, p->rcad, p->rcontact, p->tcore, 0);
}
}
// for xpower and acceleration et al
f.recalculateDerivedSeries();
// compute the mean max, this is BLAZINGLY fast, thanks to Mark Rages'
// mean-max computer. Does a 11hr ride in 150ms
QVector<float>vector;
MeanMaxComputer thread1(&f, vector, getRideSeries(series())); thread1.run();
thread1.wait();
// no data!
if (vector.count() == 0) return;
// create curve data arrays
QVector<double>y;
RideFileCache::doubleArray(y, vector, getRideSeries(series()));
QVector<double>x;
for (int i=0; i<vector.count(); i++) {
x << double(i)/60.00f;
}
// create a curve!
QwtPlotCurve *curve = new QwtPlotCurve();
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true)
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
// set its color - based upon index in intervals!
QColor intervalColor;
int count=myRideItem->intervals().count();
intervalColor.setHsv(index * (255/count), 255,255);
QPen pen(intervalColor);
double width = appsettings->value(this, GC_LINEWIDTH, 0.5).toDouble();
pen.setWidth(width);
//pen.setStyle(Qt::DotLine);
intervalColor.setAlpha(64);
QBrush brush = QBrush(intervalColor);
if (shadeIntervalsCheck->isChecked()) curve->setBrush(brush);
else curve->setBrush(Qt::NoBrush);
curve->setPen(pen);
curve->setSamples(x.data()+1, y.data()+1, x.count()-2); // ignore he first 0,0 point
// attach and register
curve->attach(cpPlot);
intervalCurves[index] = curve;
}
void
CriticalPowerWindow::intervalsChanged()
{
if (rangemode) return; // do nothing for ranges!
// wipe away what we got, even if not visible
// clear any interval curves -- even if we are not visible
foreach(QwtPlotCurve *p, intervalCurves) {
if (p) {
p->detach();
delete p;
}
}
// clear, resize to interval count and set to null
intervalCurves.clear();
if (!amVisible()) return;
// replot if needed
forceReplot();
}
void
CriticalPowerWindow::rideSelected()
{
if (!rangemode) { // we only highlight intervals in normal mode
// clear any interval curves -- even if we are not visible
foreach(QwtPlotCurve *p, intervalCurves) {
if (p) {
p->detach();
delete p;
}
}
// clear, resize to interval count and set to null
intervalCurves.clear();
// clear the hover curve
if (hoverCurve) {
hoverCurve->detach();
delete hoverCurve;
hoverCurve = NULL;
}
Season season = seasons->seasons.at(cComboSeason->currentIndex());
if (myRideItem) {
// if the range selected is relative (e.g. "This Year" or
// "Last 28 days", compute it with respect to the ride's
// date
cpPlot->setDateRange(season.getStart(myRideItem->dateTime.date()), season.getEnd(myRideItem->dateTime.date()));
} else {
cpPlot->setDateRange(season.getStart(), season.getEnd());
}
}
if (!amVisible()) return;
currentRide = myRideItem;
if (currentRide) {
if (context->athlete->zones(currentRide->sport)) {
int zoneRange = context->athlete->zones(currentRide->sport)->whichRange(currentRide->dateTime.date());
int CP = zoneRange >= 0 ? context->athlete->zones(currentRide->sport)->getCP(zoneRange) : 0;
CPEdit->setText(QString("%1").arg(CP));
cpPlot->setDateCP(CP);
} else {
cpPlot->setDateCP(0);
}
if ((currentRide->isRun || currentRide->isSwim) && context->athlete->paceZones(currentRide->isSwim)) {
int paceZoneRange = context->athlete->paceZones(currentRide->isSwim)->whichRange(currentRide->dateTime.date());
double CV = paceZoneRange >= 0.0 ? context->athlete->paceZones(currentRide->isSwim)->getCV(paceZoneRange) : 0.0;
cpPlot->setDateCV(CV);
} else {
cpPlot->setDateCV(0.0);
}
cpPlot->setRide(currentRide);
if (!rangemode && currentRide->ride() && currentRide->ride()->dataPoints().count() == 0)
setIsBlank(true);
else
setIsBlank(false);
} else if (!rangemode) {
setIsBlank(true);
}
// refresh intervals
intervalSelected();
}
bool
CriticalPowerWindow::event(QEvent *event)
{
// nasty nasty nasty hack to move widgets as soon as the widget geometry
// is set properly by the layout system, by default the width is 100 and
// we wait for it to be set properly then put our helper widget on the RHS
if (event->type() == QEvent::Resize && geometry().width() != 100) {
// put somewhere nice on first show
if (firstShow) {
firstShow = false;
helperWidget()->move(mainWidget()->geometry().width()-(275*dpiXFactor), 50*dpiYFactor);
}
// if off the screen move on screen
if (helperWidget()->geometry().x() > geometry().width() || helperWidget()->geometry().x() < geometry().x()) {
helperWidget()->move(mainWidget()->geometry().width()-(275*dpiXFactor), 50*dpiYFactor);
}
}
return QWidget::event(event);
}
void
CriticalPowerWindow::setSeries(int index)
{
// update reveal control
rSeriesSelector->setValue(index);
if (index >= 0) {
// need a helper any more ?
CriticalSeriesType series = static_cast<CriticalSeriesType>(seriesCombo->itemData(index).toInt());
// hide velo cp editing
if (series == veloclinicplot) {
// || ((series == watts || series == wattsKg ) && modelCombo->currentIndex() >= 1)) {
CPEdit->show();
CPSlider->show();
CPLabel->show();
cpPlot->setVeloCP(CPEdit->text().toInt());
} else {
CPEdit->hide();
CPSlider->hide();
CPLabel->hide();
}
updateOptions(series);
if (rangemode) {
cpPlot->setSeries(series);
cpPlot->setRide(currentRide);
} else {
// clear any interval curves -- even if we are not visible
foreach(QwtPlotCurve *p, intervalCurves) {
if (p) {
p->detach();
delete p;
}
}
// clear, resize to interval count and set to null
intervalCurves.clear();
if (myRideItem) for (int i=0; i<= myRideItem->intervals().count(); i++) intervalCurves << NULL;
cpPlot->setSeries(series);
cpPlot->setRide(currentRide);
// refresh intervals
intervalSelected();
}
}
}
double
CriticalPowerWindow::curve_to_point(double x, const QwtPlotCurve *curve, CriticalSeriesType serie)
{
double result = 0;
if (curve) {
const QwtSeriesData<QPointF> *data = curve->data();
if (data->size() > 0) {
if (x < data->sample(0).x() || x > data->sample(data->size() - 1).x())
return 0;
size_t min = 0, mid = 0, max = data->size();
while (min < max - 1) {
mid = (max - min) / 2 + min;
if (x < data->sample(mid).x()) {
double a = pow(10,RideFileCache::decimalsFor(getRideSeries(serie)));
result = ((int)((0.5/a + data->sample(mid).y()) * a))/a;
//result = (unsigned) round(data->sample(mid).y());
max = mid;
}
else {
min = mid;
}
}
}
}
return result;
}
void
CriticalPowerWindow::updateCpint(double minutes)
{
QString units;
switch (series()) {
case veloclinicplot:
case work:
units = "kJ";
break;
case cad:
units = "rpm";
break;
case kphd:
units = "metres/s/s";
break;
case wattsd:
units = "watts/s";
break;
case cadd:
units = "rpm/s";
break;
case hrd:
units = "bpm/s";
break;
case nmd:
units = "nm/s";
break;
case kph:
units = "kph";
break;
case hr:
units = "bpm";
break;
case nm:
units = "nm";
break;
case vam:
units = "metres/hour";
break;
case aPowerKg:
case wattsKg:
units = "Watts/kg";
break;
default:
case aPower:
case watts:
units = "Watts";
break;
}
// current ride
{
double value = curve_to_point(minutes, cpPlot->getThisCurve(), series());
QString label;
if (value > 0)
label = QString("%1 %2").arg(value).arg(units);
else
label = tr("no data");
//XXXcpintTodayValue->setText(label);
}
// cp line
if (cpPlot->getModelCurve()) {
double value = curve_to_point(minutes, cpPlot->getModelCurve(), series());
QString label;
if (value > 0)
label = QString("%1 %2").arg(value).arg(units);
else
label = tr("no data");
cpintCPValue->setText(label);
}
// global ride
{
QString label;
int index = (int) ceil(minutes * 60);
if (index >= 0 && cpPlot->getBests().count() > index) {
QDate date = cpPlot->getBestDates()[index];
double value = cpPlot->getBests()[index];
double a = pow(10,RideFileCache::decimalsFor(getRideSeries(series())));
value = ((int)((0.5/a + value) * a))/a;
label = QString("%1 %2 (%3)").arg(value).arg(units)
.arg(date.isValid() ? date.toString(tr("MM/dd/yyyy")) : tr("no date"));
}
else {
label = tr("no data");
}
cpintAllValue->setText(label);
}
}
QString
CriticalPowerWindow::seriesName(CriticalSeriesType series)
{
switch (series) {
case watts: return QString(tr("Power"));
case wattsKg: return QString(tr("Watts per Kilogram"));
case xPower: return QString(tr("xPower"));
case IsoPower: return QString(tr("Iso Power"));
case hr: return QString(tr("Heartrate"));
case kph: return QString(tr("Speed"));
case kphd: return QString(tr("Acceleration"));
case wattsd: return QString(tr("Power %1").arg(deltaChar));
case cadd: return QString(tr("Cadence %1").arg(deltaChar));
case nmd: return QString(tr("Torque %1").arg(deltaChar));
case hrd: return QString(tr("Heartrate %1").arg(deltaChar));
case cad: return QString(tr("Cadence"));
case nm: return QString(tr("Torque"));
case vam: return QString(tr("VAM"));
case aPower: return QString(tr("aPower"));
case aPowerKg: return QString(tr("aPower per Kilogram"));
case work: return QString(tr("Work"));
case veloclinicplot: return QString(tr("Veloclinic Plot"));
default: return QString(tr("Unknown"));
}
}
RideFile::SeriesType
CriticalPowerWindow::getRideSeries(CriticalSeriesType series)
{
switch (series) {
case watts: return RideFile::watts;
case wattsKg: return RideFile::wattsKg;
case xPower: return RideFile::xPower;
case IsoPower: return RideFile::IsoPower;
case hr: return RideFile::hr;
case kph: return RideFile::kph;
case kphd: return RideFile::kphd;
case wattsd: return RideFile::wattsd;
case cadd: return RideFile::cadd;
case nmd: return RideFile::nmd;
case hrd: return RideFile::hrd;
case cad: return RideFile::cad;
case nm: return RideFile::nm;
case vam: return RideFile::vam;
case aPower: return RideFile::aPower;
case aPowerKg: return RideFile::aPowerKg;
case veloclinicplot: return RideFile::watts;
// non RideFile series
case work: return RideFile::none;
default: return RideFile::none;
}
}
void
CriticalPowerWindow::addSeries()
{
// setup series list
seriesList << watts
<< wattsKg
<< xPower
<< IsoPower
<< hr
<< kph
<< cad
<< nm
<< vam
<< aPower
<< aPowerKg
<< kphd
<< wattsd
<< nmd
<< cadd
<< hrd
<< work
<< veloclinicplot;
QStringList seriesNames;
foreach (CriticalSeriesType x, seriesList) {
seriesCombo->addItem(seriesName(x), static_cast<int>(x));
seriesNames << seriesName(x);
}
rSeriesSelector->setStrings(seriesNames);
}
void
CriticalPowerWindow::updateOptions(CriticalSeriesType series)
{
if ((series == work || series == watts || series == wattsKg || series == kph || series == aPower || series == aPowerKg) && modelCombo->currentIndex() >= 1) {
helperWidget()->show();
} else {
helperWidget()->hide();
}
if (series == kph) {
// speed series have option to display x-axis linear or log, others are defined fix in CPPlot::setSeries()
showCSLinearCheck->show();
showCSLinearLabel->show();
} else {
showCSLinearCheck->hide();
showCSLinearLabel->hide();
}
}
/*----------------------------------------------------------------------
* Seasons stuff
*--------------------------------------------------------------------*/
void
CriticalPowerWindow::resetSeasons()
{
if (rangemode) return;
QString prev = cComboSeason->itemText(cComboSeason->currentIndex());
// remove seasons
cComboSeason->clear();
//Store current selection
QString previousDateRange = _dateRange;
// insert seasons
for (int i=0; i <seasons->seasons.count(); i++) {
Season season = seasons->seasons.at(i);
// we add the id as user data since there are some 'fixed' ids
// that represent last 28 days etc that we can then use to offset
// from the date of the ride
cComboSeason->addItem(season.getName(), QVariant(season.id()));
}
// restore previous selection
int index = cComboSeason->findText(prev);
if (index != -1) {
cComboSeason->setCurrentIndex(index);
}
}
void
CriticalPowerWindow::useCustomRange(DateRange range)
{
// plot using the supplied range
useCustom = true;
useToToday = false;
custom = range;
dateRangeChanged(custom);
}
void
CriticalPowerWindow::useStandardRange()
{
useToToday = useCustom = false;
dateRangeChanged(myDateRange);
}
void
CriticalPowerWindow::useThruToday()
{
// plot using the supplied range
useCustom = false;
useToToday = true;
custom = myDateRange;
if (custom.to > QDate::currentDate()) custom.to = QDate::currentDate();
dateRangeChanged(custom);
}
void
CriticalPowerWindow::dateRangeChanged(DateRange dateRange)
{
if (!amVisible()) return;
// it will either be sidebar or custom...
if (useCustom) dateRange = custom;
else if (useToToday) {
dateRange = myDateRange;
QDate today = QDate::currentDate();
if (dateRange.to > today) dateRange.to = today;
} else dateRange = myDateRange;
// only change date range if its actually changed!
if (series() == veloclinicplot || dateRange.from != cfrom || dateRange.to != cto || stale) {
cfrom = dateRange.from;
cto = dateRange.to;
FilterSet fs;
fs.addFilter(searchBox->isFiltered(), SearchFilterBox::matches(context, filter()));
fs.addFilter(context->isfiltered, context->filters);
fs.addFilter(context->ishomefiltered, context->homeFilters);
if (myPerspective) fs.addFilter(myPerspective->isFiltered(), myPerspective->filterlist(dateRange));
int nActivities, nRides, nRuns, nSwims;
QString sport;
context->athlete->rideCache->getRideTypeCounts(
Specification(dateRange, fs),
nActivities, nRides, nRuns, nSwims, sport);
// lets work out the average CP configure value
if (series() != veloclinicplot && context->athlete->zones(sport)) {
int fromZoneRange = context->athlete->zones(sport)->whichRange(cfrom);
int toZoneRange = context->athlete->zones(sport)->whichRange(cto);
int CPfrom = fromZoneRange >= 0 ? context->athlete->zones(sport)->getCP(fromZoneRange) : 0;
int CPto = toZoneRange >= 0 ? context->athlete->zones(sport)->getCP(toZoneRange) : CPfrom;
if (CPfrom == 0) CPfrom = CPto;
int dateCP = (CPfrom + CPto) / 2;
cpPlot->setDateCP(dateCP);
} else {
cpPlot->setDateCP(CPEdit->text().toInt());
}
// lets work out the average CV configure value
if (((nActivities == nRuns) || (nActivities == nSwims)) &&
context->athlete->paceZones(nActivities == nSwims)) {
int fromZoneRange = context->athlete->paceZones(nActivities == nSwims)->whichRange(cfrom);
int toZoneRange = context->athlete->paceZones(nActivities == nSwims)->whichRange(cto);
double CVfrom = fromZoneRange >= 0 ? context->athlete->paceZones(nActivities == nSwims)->getCV(fromZoneRange) : 0.0;
double CVto = toZoneRange >= 0 ? context->athlete->paceZones(nActivities == nSwims)->getCV(toZoneRange) : CVfrom;
if (CVfrom == 0.0) CVfrom = CVto;
double dateCV = (CVfrom + CVto) / 2.0;
cpPlot->setDateCV(dateCV);
cpPlot->setSport(sport);
} else {
cpPlot->setDateCV(0.0);
cpPlot->setSport(sport);
}
cpPlot->setDateRange(dateRange.from, dateRange.to, stale);
}
// always refresh though
cpPlot->setRide(currentRide);
stale = false;
}
void CriticalPowerWindow::seasonSelected(int iSeason)
{
if (iSeason >= seasons->seasons.count() || iSeason < 0) return;
Season season = seasons->seasons.at(iSeason);
//XXX BROKEM CODE IN 5.1 PORT // _dateRange = season.id();
cpPlot->setDateRange(season.getStart(), season.getEnd());
cpPlot->setRide(currentRide);
}
void CriticalPowerWindow::perspectiveFilterChanged()
{
if (rangemode) {
cpPlot->perspectiveFilterChanged();
forceReplot();
}
}
void CriticalPowerWindow::filterChanged()
{
cpPlot->setRide(currentRide);
// Pace Zones Shading and Pace Units needs updating if sport(s) changed
forceReplot();
}
void
CriticalPowerWindow::shadingSelected(int shading)
{
cpPlot->setShadeMode(shading);
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showGridChanged(int state)
{
// redraw
if (state) grid->setVisible(true);
else grid->setVisible(false);
cpPlot->replot();
}
void
CriticalPowerWindow::filterBestChanged(int state)
{
cpPlot->setFilterBest(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showTestChanged(int state)
{
cpPlot->setShowTest(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showBestChanged(int state)
{
cpPlot->setShowBest(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showPercentChanged(int state)
{
if (state) showPowerIndexCheck->setChecked(false);
cpPlot->setShowPercent(state);
rPercent->setChecked(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showPowerIndexChanged(int state)
{
if (state) showPercentCheck->setChecked(false);
cpPlot->setShowPowerIndex(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::rSeriesSelectorChanged(int value)
{
seriesCombo->setCurrentIndex(value);
setSeries(value);
}
void
CriticalPowerWindow::rPercentChanged(int check)
{
showPercentCheck->setChecked(check);
}
void
CriticalPowerWindow::showEffortChanged(int state)
{
cpPlot->setShowEffort(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showPPChanged(int state)
{
cpPlot->setShowPP(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showHeatChanged(int state)
{
cpPlot->setShowHeat(state);
rHeat->setChecked(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showCSLinearChanged(int state)
{
cpPlot->showXAxisLinear(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
}
void
CriticalPowerWindow::rHeatChanged(int check)
{
showHeatCheck->setChecked(check);
}
void
CriticalPowerWindow::rDeltaChanged()
{
cpPlot->setShowDelta(rDelta->isChecked(), rDeltaPercent->isChecked());
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::showHeatByDateChanged(int state)
{
cpPlot->setShowHeatByDate(state);
// redraw
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::shadeIntervalsChanged(int state)
{
cpPlot->setShadeIntervals(state);
// any existing interval curves need brush or no brush
foreach(QwtPlotCurve *p, intervalCurves) {
if (p) {
if (state) {
QColor curveColor = p->pen().color();
curveColor.setAlpha(64);
QBrush brush(curveColor);
p->setBrush(brush);
}
else p->setBrush(Qt::NoBrush);
}
}
if (rangemode) dateRangeChanged(DateRange());
else cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::setPlotType(int index)
{
cpPlot->setPlotType(index);
cpPlot->setRide(currentRide);
}
void
CriticalPowerWindow::exportData()
{
QString fileName = title()+".csv";
fileName = QFileDialog::getSaveFileName(this, tr("Save Best Data as CSV"), QString(), title()+".csv (*.csv)");
if (!fileName.isEmpty()) {
// open and write bests data to the csv file
cpPlot->exportBests(fileName);
}
}