mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
1600 lines
50 KiB
C++
1600 lines
50 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 "MetricAggregator.h"
|
|
#include "CPPlot.h"
|
|
#include "Context.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "RideItem.h"
|
|
#include "TimeUtils.h"
|
|
#include "IntervalItem.h"
|
|
#include "GcOverlayWidget.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_compat.h>
|
|
#include <QFile>
|
|
#include "Season.h"
|
|
#include "SeasonParser.h"
|
|
#include "Colors.h"
|
|
#include "Zones.h"
|
|
#include <QXmlInputSource>
|
|
#include <QXmlSimpleReader>
|
|
#include <QFileDialog>
|
|
|
|
CriticalPowerWindow::CriticalPowerWindow(const QDir &home, Context *context, bool rangemode) :
|
|
GcChartWindow(context), _dateRange("{00000000-0000-0000-0000-000000000001}"), home(home), 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);
|
|
|
|
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();
|
|
|
|
QVBoxLayout *checks = new QVBoxLayout;
|
|
checks->addStretch();
|
|
checks->addWidget(rPercent);
|
|
checks->addWidget(rHeat);
|
|
checks->addWidget(rDelta);
|
|
checks->addStretch();
|
|
|
|
revealLayout->addStretch();
|
|
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);
|
|
|
|
//
|
|
// Chart settings
|
|
//
|
|
|
|
// controls widget and layout
|
|
QTabWidget *settingsTabs = new QTabWidget(this);
|
|
|
|
QWidget *settingsWidget = new QWidget(this);
|
|
settingsWidget->setContentsMargins(0,0,0,0);
|
|
settingsTabs->addTab(settingsWidget, tr("Basic"));
|
|
|
|
QFormLayout *cl = new QFormLayout(settingsWidget);;
|
|
cl->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
|
|
|
QWidget *modelWidget = new QWidget(this);
|
|
modelWidget->setContentsMargins(0,0,0,0);
|
|
settingsTabs->addTab(modelWidget, tr("CP 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 *exportData = new QAction(tr("Export Chart Data..."), this);
|
|
addAction(exportData);
|
|
|
|
setControls(settingsTabs);
|
|
|
|
#ifdef GC_HAVE_LUCENE
|
|
// 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
|
|
#endif
|
|
|
|
// 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);
|
|
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
|
|
shadeCombo = new QComboBox(this);
|
|
shadeCombo->addItem(tr("None"));
|
|
shadeCombo->addItem(tr("Using CP"));
|
|
shadeCombo->addItem(tr("Using derived CP"));
|
|
QLabel *shading = new QLabel(tr("Power Shading"));
|
|
shadeCombo->setCurrentIndex(2);
|
|
cl->addRow(shading, shadeCombo);
|
|
|
|
showGridCheck = new QCheckBox(this);
|
|
showGridCheck->setChecked(true); // default on
|
|
QLabel *gridify = new QLabel(tr("Show grid"));
|
|
cl->addRow(gridify, showGridCheck);
|
|
|
|
showBestCheck = new QCheckBox(this);
|
|
showBestCheck->setChecked(true); // default off
|
|
QLabel *bestify = new QLabel(tr("Show Bests"));
|
|
cl->addRow(bestify, showBestCheck);
|
|
|
|
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 *heaties = new QLabel(tr("Show curve heat"));
|
|
cl->addRow(heaties, 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);
|
|
|
|
ridePlotStyleCombo = new QComboBox(this);
|
|
ridePlotStyleCombo->addItem(tr("Ride Mean Max"));
|
|
ridePlotStyleCombo->addItem(tr("Ride Centile"));
|
|
ridePlotStyleCombo->addItem(tr("No Ride"));
|
|
|
|
cl->addWidget(new QLabel("")); //spacing
|
|
cl->addRow(new QLabel(tr("Current Ride")), ridePlotStyleCombo);
|
|
|
|
// model config
|
|
// 2 or 3 point model ?
|
|
modelCombo= new QComboBox(this);
|
|
modelCombo->addItem("None");
|
|
modelCombo->addItem("2 parameter");
|
|
modelCombo->addItem("3 parameter");
|
|
modelCombo->addItem("Extended CP");
|
|
modelCombo->addItem("Multicomponent");
|
|
modelCombo->setCurrentIndex(1);
|
|
|
|
mcl->addRow(new QLabel(tr("CP Model")), modelCombo);
|
|
|
|
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(1200); // 30 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); // 60 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(true); // not needed
|
|
grid->enableY(true);
|
|
grid->setZ(-20);
|
|
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);
|
|
ftpTitle = new QLabel(tr("FTP"), this);
|
|
ftpValue = new QLabel(tr("0 w"), this);
|
|
ftpRank = new QLabel(tr("n/a"), 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);
|
|
ftpTitle->setAutoFillBackground(true);
|
|
ftpValue->setAutoFillBackground(true);
|
|
ftpRank->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);
|
|
ftpTitle->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
ftpValue->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
|
|
ftpRank->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(wprimeTitle, 1, 0);
|
|
gridLayout->addWidget(wprimeValue, 1, 1);
|
|
gridLayout->addWidget(wprimeRank, 1, 2);
|
|
gridLayout->addWidget(cpTitle, 2, 0);
|
|
gridLayout->addWidget(cpValue, 2, 1);
|
|
gridLayout->addWidget(cpRank, 2, 2);
|
|
gridLayout->addWidget(pmaxTitle, 3, 0);
|
|
gridLayout->addWidget(pmaxValue, 3, 1);
|
|
gridLayout->addWidget(pmaxRank, 3, 2);
|
|
gridLayout->addWidget(ftpTitle, 4, 0);
|
|
gridLayout->addWidget(ftpValue, 4, 1);
|
|
gridLayout->addWidget(ftpRank, 4, 2);
|
|
|
|
addHelper(QString("CP Model"), helper);
|
|
|
|
if (rangemode) {
|
|
connect(this, SIGNAL(dateRangeChanged(DateRange)), SLOT(dateRangeChanged(DateRange)));
|
|
|
|
// Compare
|
|
connect(context, SIGNAL(compareDateRangesStateChanged(bool)), SLOT(forceReplot()));
|
|
connect(context, SIGNAL(compareDateRangesChanged()), SLOT(forceReplot()));
|
|
} else {
|
|
// when working on a ride we can selecct 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(RideFileInterval)), this, SLOT(intervalHover(RideFileInterval)));
|
|
|
|
// 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()), cpPlot, SLOT(configChanged()));
|
|
connect(exportData, SIGNAL(triggered()), this, SLOT(exportData()));
|
|
|
|
// model updated?
|
|
connect(modelCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(modelChanged()));
|
|
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(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()), this, SLOT(configChanged()));
|
|
connect(context->athlete->metricDB, SIGNAL(dataChanged()), this, SLOT(refreshRideSaved()));
|
|
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(shadeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(shadingSelected(int)));
|
|
connect(shadeIntervalsCheck, SIGNAL(stateChanged(int)), this, SLOT(shadeIntervalsChanged(int)));
|
|
connect(showHeatCheck, SIGNAL(stateChanged(int)), this, SLOT(showHeatChanged(int)));
|
|
connect(rHeat, SIGNAL(stateChanged(int)), this, SLOT(rHeatChanged(int)));
|
|
connect(rDelta, SIGNAL(stateChanged(int)), this, SLOT(rDeltaChanged(int)));
|
|
connect(showHeatByDateCheck, SIGNAL(stateChanged(int)), this, SLOT(showHeatByDateChanged(int)));
|
|
connect(showPercentCheck, SIGNAL(stateChanged(int)), this, SLOT(showPercentChanged(int)));
|
|
connect(showBestCheck, SIGNAL(stateChanged(int)), this, SLOT(showBestChanged(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()));
|
|
|
|
// set widgets and model parameters
|
|
modelChanged();
|
|
|
|
configChanged(); // get colors set
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::configChanged()
|
|
{
|
|
setProperty("color", GColor(CPLOTBACKGROUND));
|
|
|
|
// tinted palette for headings etc
|
|
QPalette palette;
|
|
palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
|
|
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
|
|
setPalette(palette);
|
|
|
|
// inverted palette for data etc
|
|
QPalette whitepalette;
|
|
whitepalette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
|
|
whitepalette.setColor(QPalette::WindowText, GCColor::invertColor(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);
|
|
ftpTitle->setFont(font);
|
|
ftpValue->setFont(font);
|
|
ftpRank->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);
|
|
ftpTitle->setPalette(palette);
|
|
ftpValue->setPalette(whitepalette);
|
|
ftpRank->setPalette(whitepalette);
|
|
|
|
QPen gridPen(GColor(CPLOTGRID));
|
|
grid->setPen(gridPen);
|
|
|
|
rideSelected();
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::modelChanged()
|
|
{
|
|
// we changed from/to a 2 or 3 parameter model
|
|
// so lets set some semsible 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();
|
|
|
|
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();
|
|
|
|
// 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();
|
|
|
|
// 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();
|
|
|
|
// Default values: class 2-20 model
|
|
anI1SpinBox->setValue(100);
|
|
anI2SpinBox->setValue(120);
|
|
aeI1SpinBox->setValue(1000);
|
|
aeI2SpinBox->setValue(1200);
|
|
|
|
break;
|
|
|
|
case 2 : // 3 param model: 3-30 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();
|
|
|
|
// Default values
|
|
anI1SpinBox->setValue(1800);
|
|
anI2SpinBox->setValue(2400);
|
|
aeI1SpinBox->setValue(2400);
|
|
aeI2SpinBox->setValue(3600);
|
|
|
|
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();
|
|
|
|
// Default values
|
|
sanI1SpinBox->setValue(20);
|
|
sanI2SpinBox->setValue(90);
|
|
anI1SpinBox->setValue(120);
|
|
anI2SpinBox->setValue(300);
|
|
aeI1SpinBox->setValue(600);
|
|
aeI2SpinBox->setValue(3000);
|
|
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());
|
|
if ((series == watts || series == wattsKg) && modelCombo->currentIndex() >= 1) helperWidget()->show();
|
|
else helperWidget()->hide();
|
|
}
|
|
|
|
// tell the plot
|
|
cpPlot->setModel(sanI1SpinBox->value(),
|
|
sanI2SpinBox->value(),
|
|
anI1SpinBox->value(),
|
|
anI2SpinBox->value(),
|
|
aeI1SpinBox->value(),
|
|
aeI2SpinBox->value(),
|
|
laeI1SpinBox->value(),
|
|
laeI2SpinBox->value(),
|
|
modelCombo->currentIndex(),
|
|
variant());
|
|
|
|
// and apply
|
|
if (amVisible() && myRideItem != NULL) {
|
|
cpPlot->setRide(myRideItem);
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::refreshRideSaved()
|
|
{
|
|
const RideItem *current = context->rideItem();
|
|
if (!current) return;
|
|
|
|
// if the saved ride is in the aggregated time period
|
|
QDate date = current->dateTime.date();
|
|
if (date >= cpPlot->startDate &&
|
|
date <= cpPlot->endDate) {
|
|
|
|
// force a redraw next time visible
|
|
cpPlot->setDateRange(cpPlot->startDate, cpPlot->endDate);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
} else {
|
|
|
|
// show helper if we're showing power
|
|
CriticalSeriesType series = static_cast<CriticalSeriesType>(seriesCombo->itemData(seriesCombo->currentIndex()).toInt());
|
|
if ((series == watts || series == wattsKg) && modelCombo->currentIndex() >= 1) helperWidget()->show();
|
|
else helperWidget()->hide();
|
|
|
|
// these are allowed outside of compare mode
|
|
rPercent->show();
|
|
rHeat->show();
|
|
rDelta->hide();
|
|
}
|
|
|
|
if (rangemode) {
|
|
|
|
// force replot...
|
|
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
|
|
// 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 (context->athlete->allIntervalItems() == NULL) return; // not inited yet!
|
|
|
|
// if the array hasn't been initialised properly then clean it up
|
|
// this is because intervalsChanged gets called when selecting rides
|
|
if (intervalCurves.count() != context->athlete->allIntervalItems()->childCount()) {
|
|
// 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->ride()->intervals().count())
|
|
for (int i=0; i< myRideItem->ride()->intervals().count(); i++)
|
|
intervalCurves << NULL;
|
|
}
|
|
|
|
// which itervals are selected?
|
|
IntervalItem *current=NULL;
|
|
for (int i=0; i<context->athlete->allIntervalItems()->childCount(); i++) {
|
|
current = static_cast<IntervalItem *>(context->athlete->allIntervalItems()->child(i));
|
|
if (current != NULL) {
|
|
if (current->isSelected() == true) {
|
|
showIntervalCurve(current, i); // set it all up
|
|
} else {
|
|
hideIntervalCurve(i); // in case its shown at present
|
|
}
|
|
}
|
|
}
|
|
cpPlot->replot();
|
|
}
|
|
|
|
// user hovered over an interval
|
|
void
|
|
CriticalPowerWindow::intervalHover(RideFileInterval x)
|
|
{
|
|
// ignore in compare mode
|
|
if (!amVisible() || context->isCompareIntervals) return;
|
|
|
|
// do we need to fill with nulls ?
|
|
if (intervalCurves.count() == 0 && myRideItem && myRideItem->ride() && myRideItem->ride()->intervals().count())
|
|
for (int i=0; i< myRideItem->ride()->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?
|
|
IntervalItem *current=NULL;
|
|
int index = -1;
|
|
|
|
for (int i=0; i<context->athlete->allIntervalItems()->childCount(); i++) {
|
|
current = dynamic_cast<IntervalItem *>(context->athlete->allIntervalItems()->child(i));
|
|
if (current != NULL) {
|
|
// is this the one ?
|
|
if (x.start == current->start && x.stop == current->stop) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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(current, 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, 1.0).toDouble();
|
|
pen.setWidth(width);
|
|
|
|
// create the hover curve
|
|
hoverCurve = new QwtPlotCurve("Interval");
|
|
hoverCurve->setPen(pen);
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).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!
|
|
|
|
// 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->smo2, p->thb, 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, false).toBool() == true)
|
|
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
|
|
// set its color - based upon index in intervals!
|
|
QColor intervalColor;
|
|
int count=context->athlete->allIntervalItems()->childCount();
|
|
intervalColor.setHsv(index * (255/count), 255,255);
|
|
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 (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
|
|
intervalSelected(); // will refresh
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (!amVisible()) return;
|
|
currentRide = myRideItem;
|
|
|
|
if (currentRide) {
|
|
|
|
if (context->athlete->zones()) {
|
|
int zoneRange = context->athlete->zones()->whichRange(currentRide->dateTime.date());
|
|
int CP = zoneRange >= 0 ? context->athlete->zones()->getCP(zoneRange) : 0;
|
|
cpPlot->setDateCP(CP);
|
|
} else {
|
|
cpPlot->setDateCP(0);
|
|
}
|
|
cpPlot->setRide(currentRide);
|
|
|
|
if (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, 50);
|
|
}
|
|
|
|
// if off the screen move on screen
|
|
if (helperWidget()->geometry().x() > geometry().width()) {
|
|
helperWidget()->move(mainWidget()->geometry().width()-275, 50);
|
|
}
|
|
}
|
|
return QWidget::event(event);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::setSeries(int index)
|
|
{
|
|
if (index >= 0) {
|
|
|
|
// need a helper any more ?
|
|
CriticalSeriesType series = static_cast<CriticalSeriesType>(seriesCombo->itemData(index).toInt());
|
|
if ((series == watts || series == wattsKg) && modelCombo->currentIndex() >= 1) helperWidget()->show();
|
|
else helperWidget()->hide();
|
|
|
|
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();
|
|
for (int i=0; i<= context->athlete->allIntervalItems()->childCount(); 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;
|
|
unsigned 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 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 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 NP: return QString(tr("Normalized 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 work: return QString(tr("Work"));
|
|
case watts_inv_time: return QString(tr("Power by inv of time"));
|
|
|
|
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 NP: return RideFile::NP;
|
|
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;
|
|
|
|
|
|
// non RideFile series
|
|
case work: return RideFile::none;
|
|
case watts_inv_time: return RideFile::watts;
|
|
|
|
default: return RideFile::none;
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::addSeries()
|
|
{
|
|
// setup series list
|
|
seriesList << watts
|
|
<< wattsKg
|
|
<< xPower
|
|
<< NP
|
|
<< hr
|
|
<< kph
|
|
<< cad
|
|
<< nm
|
|
<< vam
|
|
<< aPower
|
|
<< kphd
|
|
<< wattsd
|
|
<< nmd
|
|
<< cadd
|
|
<< hrd
|
|
<< work
|
|
<< watts_inv_time;
|
|
|
|
foreach (CriticalSeriesType x, seriesList) {
|
|
seriesCombo->addItem(seriesName(x), static_cast<int>(x));
|
|
}
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* 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);
|
|
cComboSeason->addItem(season.getName());
|
|
}
|
|
// 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 (dateRange.from != cfrom || dateRange.to != cto || stale) {
|
|
|
|
cfrom = dateRange.from;
|
|
cto = dateRange.to;
|
|
|
|
// lets work out the average CP configure value
|
|
if (context->athlete->zones()) {
|
|
int fromZoneRange = context->athlete->zones()->whichRange(cfrom);
|
|
int toZoneRange = context->athlete->zones()->whichRange(cto);
|
|
|
|
int CPfrom = fromZoneRange >= 0 ? context->athlete->zones()->getCP(fromZoneRange) : 0;
|
|
int CPto = toZoneRange >= 0 ? context->athlete->zones()->getCP(toZoneRange) : CPfrom;
|
|
if (CPfrom == 0) CPfrom = CPto;
|
|
int dateCP = (CPfrom + CPto) / 2;
|
|
|
|
cpPlot->setDateCP(dateCP);
|
|
}
|
|
|
|
cpPlot->setDateRange(dateRange.from, dateRange.to);
|
|
}
|
|
|
|
// 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::filterChanged()
|
|
{
|
|
cpPlot->setRide(currentRide);
|
|
}
|
|
|
|
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::showBestChanged(int state)
|
|
{
|
|
cpPlot->setShowBest(state);
|
|
|
|
// redraw
|
|
if (rangemode) dateRangeChanged(DateRange());
|
|
else cpPlot->setRide(currentRide);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::showPercentChanged(int state)
|
|
{
|
|
cpPlot->setShowPercent(state);
|
|
rPercent->setChecked(state);
|
|
|
|
// redraw
|
|
if (rangemode) dateRangeChanged(DateRange());
|
|
else cpPlot->setRide(currentRide);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::rPercentChanged(int check)
|
|
{
|
|
showPercentCheck->setChecked(check);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::showHeatChanged(int state)
|
|
{
|
|
cpPlot->setShowHeat(state);
|
|
rHeat->setChecked(state);
|
|
|
|
// redraw
|
|
if (rangemode) dateRangeChanged(DateRange());
|
|
else cpPlot->setRide(currentRide);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::rHeatChanged(int check)
|
|
{
|
|
showHeatCheck->setChecked(check);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::rDeltaChanged(int check)
|
|
{
|
|
cpPlot->setShowDelta(check);
|
|
|
|
// 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);
|
|
}
|
|
}
|