Files
GoldenCheetah/src/Gui/SolveCPDialog.cpp
Alejandro Martinez 3a07cc52d7 Generalize Power Zones for any sport
All sports defined as values for Sport metadata field can have
specifics Power Zones and default to Bike zones otherwise.
Similar to current power zones for Run.
Part 3 of #3280
2021-05-26 16:15:27 -03:00

507 lines
16 KiB
C++

/*
* Copyright (c) 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 "SolveCPDialog.h"
#include "HelpWhatsThis.h"
#include "Settings.h"
#include "Colors.h"
#include "Context.h"
#include "Athlete.h"
#include "RideCache.h"
#include "RideItem.h"
#include "CPSolver.h"
#include "SolverDisplay.h"
#include <QSplitter>
#include <QFont>
#include <QFontMetrics>
SolveCPDialog::SolveCPDialog(QWidget *parent, Context *context) : QDialog(parent), context(context)
{
setWindowTitle(tr("Critical Power Solver"));
setAttribute(Qt::WA_DeleteOnClose);
setMinimumSize(QSize(800 *dpiXFactor, 400 *dpiYFactor));
// are we integral or differential ?
integral = (appsettings->value(NULL, GC_WBALFORM, "int").toString() == "int");
//
// Widget creation
//
solver = new CPSolver(context);
QFont bolden;
bolden.setWeight(QFont::Bold);
inputsLabel = new QLabel(tr("Solver Constraints"), this);
inputsLabel->setAlignment(Qt::AlignHCenter);
inputsLabel->setFont(bolden);
selectCheckBox = new QCheckBox(tr("Select/Deselect All"), this);
// constraints
parmLabelCP = new QLabel(tr("CP"), this);
parmLabelW = new QLabel(tr("W'"), this);
parmLabelTAU = new QLabel(integral ? tr("Tau") : tr("R"), this);
dashCP = new QLabel("-");
dashW = new QLabel("-");
dashTAU = new QLabel("-");
fromCP = new QDoubleSpinBox(this); fromCP->setDecimals(0);
fromCP->setMinimum(0);
fromCP->setMaximum(1000);
fromCP->setValue(100);
toCP = new QDoubleSpinBox(this); toCP->setDecimals(0);
toCP->setMinimum(0);
toCP->setMaximum(1000);
toCP->setValue(500);
fromW = new QDoubleSpinBox(this); fromW->setDecimals(0);
fromW->setMinimum(0);
fromW->setMaximum(80000);
fromW->setValue(5000);
toW = new QDoubleSpinBox(this); toW->setDecimals(0);
toW->setMinimum(0);
toW->setMaximum(80000);
toW->setValue(50000);
if (integral) {
fromTAU = new QDoubleSpinBox(this); fromTAU->setDecimals(0);
fromTAU->setMinimum(0);
fromTAU->setMaximum(1000);
fromTAU->setValue(300);
toTAU = new QDoubleSpinBox(this); toTAU->setDecimals(0);
toTAU->setMinimum(0);
toTAU->setMaximum(1000);
toTAU->setValue(700);
} else {
fromTAU = new QDoubleSpinBox(this); fromTAU->setDecimals(2);
fromTAU->setMinimum(0);
fromTAU->setMaximum(5);
fromTAU->setValue(0.2);
toTAU = new QDoubleSpinBox(this); toTAU->setDecimals(2);
toTAU->setMinimum(0);
toTAU->setMaximum(5);
toTAU->setValue(1.0);
}
// list all the activities that contain exhaustion points
dataTable = new QTreeWidget(this);
dataTable->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding));
progressLabel = new QLabel(tr("Progress"), this);
progressLabel->setAlignment(Qt::AlignHCenter);
progressLabel->setFont(bolden);
// headings
itLabel = new QLabel(tr("Iteration"));
cpLabel = new QLabel(tr("CP"));
wLabel = new QLabel(tr("W'"));
tLabel = new QLabel(integral ? tr("tau") : tr("R"));
sumLabel = new QLabel("ΣW'bal²");
currentLabel = new QLabel(tr("Current"));
bestLabel = new QLabel(tr("Best"));
// current value
citLabel = new QLabel(tr(""));
ccpLabel = new QLabel(tr(""));
cwLabel = new QLabel(tr(""));
ctLabel = new QLabel(tr(""));
csumLabel = new QLabel("");
// best so far
bitLabel = new QLabel(tr(""));
bcpLabel = new QLabel(tr(""));
bwLabel = new QLabel(tr(""));
btLabel = new QLabel(tr(""));
bsumLabel = new QLabel("");
// fix the label heights and alignment
QFont def;
QFontMetrics fm(def);
progressLabel->setFixedHeight(fm.height());
parmLabelCP->setFixedHeight(fm.height());
parmLabelW->setFixedHeight(fm.height());
parmLabelTAU->setFixedHeight(fm.height());
parmLabelCP->setFixedWidth(fm.boundingRect("XXXX").width());
parmLabelW->setFixedWidth(fm.boundingRect("XXXX").width());
parmLabelTAU->setFixedWidth(fm.boundingRect("XXXX").width());
dashCP->setFixedHeight(fm.height());
dashTAU->setFixedHeight(fm.height());
dashW->setFixedHeight(fm.height());
dashCP->setFixedWidth(fm.boundingRect(" - ").width());
dashW->setFixedWidth(fm.boundingRect(" - ").width());
dashTAU->setFixedWidth(fm.boundingRect(" - ").width());
currentLabel->setFixedHeight(fm.height());
bestLabel->setFixedHeight(fm.height());
itLabel->setFixedHeight(fm.height());
cpLabel->setFixedHeight(fm.height());
wLabel->setFixedHeight(fm.height());
tLabel->setFixedHeight(fm.height());
sumLabel->setFixedHeight(fm.height());
citLabel->setFixedHeight(fm.height());
ccpLabel->setFixedHeight(fm.height());
cwLabel->setFixedHeight(fm.height());
ctLabel->setFixedHeight(fm.height());
csumLabel->setFixedHeight(fm.height());
bitLabel->setFixedHeight(fm.height());
bcpLabel->setFixedHeight(fm.height());
bwLabel->setFixedHeight(fm.height());
btLabel->setFixedHeight(fm.height());
bsumLabel->setFixedHeight(fm.height());
itLabel->setAlignment(Qt::AlignHCenter);
cpLabel->setAlignment(Qt::AlignHCenter);
wLabel->setAlignment(Qt::AlignHCenter);
tLabel->setAlignment(Qt::AlignHCenter);
sumLabel->setAlignment(Qt::AlignHCenter);
citLabel->setAlignment(Qt::AlignHCenter);
ccpLabel->setAlignment(Qt::AlignHCenter);
cwLabel->setAlignment(Qt::AlignHCenter);
ctLabel->setAlignment(Qt::AlignHCenter);
csumLabel->setAlignment(Qt::AlignHCenter);
bitLabel->setAlignment(Qt::AlignHCenter);
bcpLabel->setAlignment(Qt::AlignHCenter);
bwLabel->setAlignment(Qt::AlignHCenter);
btLabel->setAlignment(Qt::AlignHCenter);
bsumLabel->setAlignment(Qt::AlignHCenter);
citLabel->setText("-");
ccpLabel->setText("-");
cwLabel->setText("-");
ctLabel->setText("-");
csumLabel->setText("-");
bitLabel->setText("-");
bcpLabel->setText("-");
bwLabel->setText("-");
btLabel->setText("-");
bsumLabel->setText("-");
// visualise
solverDisplay = new SolverDisplay(this, context);
solverDisplay->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
solverDisplay->setBackgroundRole(QPalette::Light);
solverDisplay->setMouseTracking(true);
solverDisplay->installEventFilter(this);
solve = new QPushButton(tr("Solve"));
close = new QPushButton(tr("Close"));
clear = new QPushButton(tr("Clear"));
//
// Layout the widget
//
// main widget with buttons at the bottom
QVBoxLayout *fullLayout = new QVBoxLayout;
QSplitter *mainSplitter = new QSplitter(Qt::Horizontal, this);
QHBoxLayout *buttonLayout = new QHBoxLayout;
fullLayout->addWidget(mainSplitter);
fullLayout->addLayout(buttonLayout);
QWidget *data = new QWidget(this);
QWidget *progress = new QWidget(this);
// data on left, progress on the right
QVBoxLayout *dataLayout = new QVBoxLayout(data);
QGridLayout *constraintsLayout = new QGridLayout;
QVBoxLayout *progressLayout = new QVBoxLayout(progress);
mainSplitter->addWidget(data);
mainSplitter->addWidget(progress);
mainSplitter->setStretchFactor(0, 30);
mainSplitter->setStretchFactor(1, 70);
// conatraints
constraintsLayout->addWidget(parmLabelCP, 0,0);
constraintsLayout->addWidget(fromCP, 0,1);
constraintsLayout->addWidget(dashCP, 0,2);
constraintsLayout->addWidget(toCP, 0,3);
constraintsLayout->addWidget(parmLabelW, 1,0);
constraintsLayout->addWidget(fromW, 1,1);
constraintsLayout->addWidget(dashW, 1,2);
constraintsLayout->addWidget(toW, 1,3);
constraintsLayout->addWidget(parmLabelTAU, 2,0);
constraintsLayout->addWidget(fromTAU, 2,1);
constraintsLayout->addWidget(dashTAU, 2,2);
constraintsLayout->addWidget(toTAU, 2,3);
// data layout on left
dataLayout->addWidget(inputsLabel);
dataLayout->addLayout(constraintsLayout);
dataLayout->addWidget(selectCheckBox);
dataLayout->addWidget(dataTable);
// progress layout, labels then progress viz
QGridLayout *gridLayout = new QGridLayout;
progressLayout->addWidget(progressLabel);
progressLayout->addLayout(gridLayout);
progressLayout->addWidget(solverDisplay);
progressLayout->setStretchFactor(gridLayout, 0);
progressLayout->setStretchFactor(progressLabel, 0);
progressLayout->setStretchFactor(solverDisplay, 1);
// all the labels...
gridLayout->addWidget(itLabel, 0, 1);
gridLayout->addWidget(cpLabel, 0, 2);
gridLayout->addWidget(wLabel, 0, 3);
gridLayout->addWidget(tLabel, 0, 4);
gridLayout->addWidget(sumLabel, 0, 5);
gridLayout->addWidget(currentLabel, 1, 0);
gridLayout->addWidget(citLabel, 1, 1);
gridLayout->addWidget(ccpLabel, 1, 2);
gridLayout->addWidget(cwLabel, 1, 3);
gridLayout->addWidget(ctLabel, 1, 4);
gridLayout->addWidget(csumLabel, 1, 5);
gridLayout->addWidget(bestLabel, 2, 0);
gridLayout->addWidget(bitLabel, 2, 1);
gridLayout->addWidget(bcpLabel, 2, 2);
gridLayout->addWidget(bwLabel, 2, 3);
gridLayout->addWidget(btLabel, 2, 4);
gridLayout->addWidget(bsumLabel, 2, 5);
// buttons
buttonLayout->addStretch();
buttonLayout->addWidget(solve);
buttonLayout->addWidget(clear);
buttonLayout->addWidget(close);
//
// Connect the dots
//
connect(selectCheckBox, SIGNAL(stateChanged(int)), this, SLOT(selectAll()));
connect(solve, SIGNAL(clicked()), this, SLOT(solveClicked()));
connect(close, SIGNAL(clicked()), this, SLOT(closeClicked()));
connect(clear, SIGNAL(clicked()), this, SLOT(clearClicked()));
connect(solver, SIGNAL(current(int,WBParms,double)), this, SLOT(current(int,WBParms,double)));
connect(solver, SIGNAL(newBest(int,WBParms,double)), this, SLOT(newBest(int,WBParms,double)));
//
// Prepare
//
QStringList headers;
headers << " " << "Date" << "Min W'bal";
dataTable->setColumnCount(3);
dataTable->setHeaderLabels(headers);
dataTable->headerItem()->setTextAlignment(0, Qt::AlignLeft);
dataTable->headerItem()->setTextAlignment(1, Qt::AlignHCenter);
dataTable->headerItem()->setTextAlignment(2, Qt::AlignHCenter);
// get a list
foreach(RideItem *item, context->athlete->rideCache->rides()) {
int te = item->getForSymbol("ride_te");
if (te) items<<item;
}
// most recent first !
for (int k=items.count()-1; k>=0; k--) {
RideItem *item = items[k];
// we have one
QTreeWidgetItem *t = new QTreeWidgetItem(dataTable);
t->setText(1, item->dateTime.date().toString("dd MMM yy"));
t->setText(2, item->getStringForSymbol("skiba_wprime_low"));
t->setTextAlignment(0, Qt::AlignLeft);
t->setTextAlignment(1, Qt::AlignHCenter);
t->setTextAlignment(2, Qt::AlignHCenter);
// remember which rideitem this is for
t->setData(0, Qt::UserRole, qVariantFromValue(static_cast<void*>(item)));
// checkbox
QCheckBox *check = new QCheckBox(this);
dataTable->setItemWidget(t, 0, check);
}
dataTable->setColumnWidth(0,50*dpiXFactor);
dataTable->resizeColumnToContents(1);
dataTable->resizeColumnToContents(2);
#ifdef Q_OS_MAC
dataTable->setAttribute(Qt::WA_MacShowFocusRect, 0);
#endif
setLayout(fullLayout);
}
SolveCPDialog::~SolveCPDialog()
{
delete solverDisplay;
delete solver;
}
bool
SolveCPDialog::eventFilter(QObject *o, QEvent *e)
{
// this is a hack related to mouse event compression
// and is not required after QT5.6, but does no harm
// and reduces processing overhead anyway.
static QTime t;
static int gc__last = 0;
if (e->type()==QEvent::Paint) return false;
// if not solving then update cursor as we move around
if ((gc__last != QMouseEvent::MouseMove || t.elapsed() > 100) && o == solverDisplay && solve->text() == tr("Solve")
&& solverDisplay->underMouse() && e->type() == QMouseEvent::MouseMove) {
t.start();
solverDisplay->repaint();
}
gc__last = e->type();
return false;
}
void
SolveCPDialog::selectAll()
{
for (int i=0; i<dataTable->invisibleRootItem()->childCount(); i++) {
QTreeWidgetItem *it = const_cast<QTreeWidgetItem*>(dataTable->invisibleRootItem()->child(i));
QCheckBox *check = static_cast<QCheckBox*>(dataTable->itemWidget(it,0));
check->setChecked(selectCheckBox->isChecked());
}
}
void
SolveCPDialog::newBest(int k,WBParms p,double sum)
{
bitLabel->setText(QString("%1").arg(k));
bcpLabel->setText(QString("%1").arg(p.CP));
bwLabel->setText(QString("%1").arg(p.W));
if (integral) btLabel->setText(QString("%1").arg(p.TAU));
else btLabel->setText(QString("%1").arg(double(p.TAU)/100.0f, 0, 'f', 2));
if (sum > 100) bsumLabel->setText("> 100kJ");
else bsumLabel->setText(QString("%1").arg(sum, 0, 'f', 3));
// is this the end?
if (k == 0) {
end();
}
QApplication::processEvents();
}
void
SolveCPDialog::current(int k,WBParms p,double sum)
{
citLabel->setText(QString("%1").arg(k));
ccpLabel->setText(QString("%1").arg(p.CP));
cwLabel->setText(QString("%1").arg(p.W));
if (integral) ctLabel->setText(QString("%1").arg(p.TAU));
else ctLabel->setText(QString("%1").arg(double(p.TAU)/100.0f, 0, 'f', 2));
if (sum > 100) csumLabel->setText("> 100kJ");
else csumLabel->setText(QString("%1").arg(sum, 0, 'f', 3));
// visualise new point
solverDisplay->addPoint(SolverPoint(p.CP, p.W, sum, p.TAU));
if (!(k++%200) || k == 99999) QApplication::processEvents();
}
void
SolveCPDialog::end()
{
solve->setText(tr("Solve"));
solver->stop();
}
void
SolveCPDialog::solveClicked()
{
if (solve->text() == tr("Stop")) {
end();
return;
}
// loop through the table and collect the rides to solve
QList<RideItem*> solveme;
int mincp=0, maxcp=0, minw=0, maxw=0;
// get the rides selected
for(int i=0; i<dataTable->invisibleRootItem()->childCount(); i++) {
QTreeWidgetItem *it = dataTable->invisibleRootItem()->child(i);
RideItem *item = NULL;
// is it selected?
if (static_cast<QCheckBox*>(dataTable->itemWidget(it, 0))->isChecked())
item = static_cast<RideItem*>(it->data(0, Qt::UserRole).value<void *>());
if (item && item->context->athlete->zones(item->sport)) {
// get CP etc
int zoneRange = item->context->athlete->zones(item->sport)->whichRange(item->dateTime.date());
int CP = zoneRange >= 0 ? item->context->athlete->zones(item->sport)->getCP(zoneRange) : 0;
int WPRIME = zoneRange >= 0 ? item->context->athlete->zones(item->sport)->getWprime(zoneRange) : 0;
if (!mincp || CP < mincp) mincp = CP;
if (!maxcp || CP > maxcp) maxcp = CP;
if (!minw || WPRIME < minw) minw = WPRIME;
if (!minw || WPRIME > maxw) maxw = WPRIME;
// get the cp and w' as configured for the ride for the
// config constraints (red box on visualisation)
solveme << item;
}
}
// if we got something to solve then kick it off
if (solveme.count()) {
// reset and reinitialise before kicking off
solver->reset();
double factor = integral ? 1 : 100;
CPSolverConstraints constraints (fromCP->value(), toCP->value(), fromW->value(), toW->value(),
fromTAU->value() * factor, toTAU->value() * factor);
// configured values
constraints.setConfig(mincp,maxcp,minw, maxw);
solverDisplay->setConstraints(constraints);
solver->setData(constraints, solveme);
solve->setText(tr("Stop"));
solver->start();
}
return;
}
void
SolveCPDialog::clearClicked()
{
solver->reset();
solverDisplay->reset();
solverDisplay->repaint();
}
void
SolveCPDialog::closeClicked()
{
end();
QApplication::processEvents();
accept();
}