mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
762 lines
24 KiB
C++
762 lines
24 KiB
C++
/*
|
|
* Copyright (c) 2010 Greg Lonnon (greg.lonnon@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 "WorkoutWizard.h"
|
|
#include "MainWindow.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
|
|
#include "qwt_plot.h"
|
|
#include "qwt_plot_curve.h"
|
|
#include "qwt_compat.h"
|
|
#include <limits>
|
|
|
|
|
|
// hack... Need to get the CP and zones for metrics
|
|
Context *hackContext;
|
|
|
|
/// workout plot
|
|
class WorkoutPlot: public QwtPlot
|
|
{
|
|
QwtPlotCurve *workoutCurve;
|
|
public:
|
|
WorkoutPlot()
|
|
{
|
|
workoutCurve = new QwtPlotCurve();
|
|
setTitle("Workout Chart");
|
|
QPen pen = QPen(Qt::blue,1.0);
|
|
workoutCurve->setPen(pen);
|
|
QColor brush_color = QColor(124, 91, 31);
|
|
brush_color.setAlpha(64);
|
|
workoutCurve->setBrush(brush_color);
|
|
workoutCurve->attach(this);
|
|
}
|
|
void setYAxisTitle(QString title)
|
|
{
|
|
setAxisTitle(QwtPlot::yLeft, title);
|
|
}
|
|
void setXAxisTitle(QString title)
|
|
{
|
|
setAxisTitle(QwtPlot::xBottom,title);
|
|
}
|
|
void setData(QVector<double> &xData, QVector<double> &yData)
|
|
{
|
|
workoutCurve->setData(xData, yData);
|
|
}
|
|
};
|
|
|
|
//// Workout Editor
|
|
|
|
WorkoutEditorBase::WorkoutEditorBase(QStringList &colms, QWidget *parent) :QFrame(parent)
|
|
{
|
|
QVBoxLayout *layout = new QVBoxLayout();
|
|
QHBoxLayout *row1Layout = new QHBoxLayout();
|
|
table = new QTableWidget();
|
|
|
|
table->setColumnCount(colms.count());
|
|
table->setHorizontalHeaderLabels(colms);
|
|
table->horizontalHeader()->setResizeMode(QHeaderView::ResizeToContents);
|
|
table->setShowGrid(true);
|
|
table->setAlternatingRowColors(true);
|
|
table->resizeColumnsToContents();
|
|
row1Layout->addWidget(table);
|
|
|
|
connect(table,SIGNAL(cellChanged(int,int)),this,SLOT(cellChanged(int,int)));
|
|
|
|
|
|
QHBoxLayout *row2Layout = new QHBoxLayout();
|
|
QPushButton *delButton = new QPushButton();
|
|
delButton->setText(tr("Delete"));
|
|
delButton->setToolTip(tr("Delete the highlighted row"));
|
|
connect(delButton,SIGNAL(clicked()),this,SLOT(delButtonClicked()));
|
|
row2Layout->addWidget(delButton);
|
|
QPushButton *addButton = new QPushButton();
|
|
addButton->setText(tr("Add"));
|
|
addButton->setToolTip(tr("Add row at end"));
|
|
connect(addButton,SIGNAL(clicked()),this,SLOT(addButtonClicked()));
|
|
row2Layout->addWidget(addButton);
|
|
QPushButton *insertButton = new QPushButton();
|
|
insertButton->setText(tr("Insert"));
|
|
insertButton->setToolTip(tr("Add a Lap below the highlighted row"));
|
|
connect(insertButton,SIGNAL(clicked()),this,SLOT(insertButtonClicked()));
|
|
row2Layout->addWidget(insertButton);
|
|
QPushButton *lapButton = new QPushButton();
|
|
lapButton->setText(tr("Lap"));
|
|
lapButton->setToolTip(tr("Add a Lap below the highlighted row"));
|
|
row2Layout->addWidget(lapButton);
|
|
connect(lapButton,SIGNAL(clicked()),this,SLOT(lapButtonClicked()));
|
|
layout->addLayout(row1Layout);
|
|
layout->addLayout(row2Layout);
|
|
setLayout(layout);
|
|
}
|
|
|
|
|
|
|
|
void WorkoutEditorAbs::insertDataRow(int row)
|
|
{
|
|
table->insertRow(row);
|
|
// minutes colm can be doubles
|
|
table->setItem(row,0,new WorkoutItemDouble());
|
|
// wattage must be integer
|
|
table->setItem(row,1,new WorkoutItemInt());
|
|
}
|
|
|
|
void WorkoutEditorRel::insertDataRow(int row)
|
|
{
|
|
table->insertRow(row);
|
|
// minutes colm can be doubles
|
|
table->setItem(row,0,new WorkoutItemDouble());
|
|
// precentage of ftp
|
|
table->setItem(row,1,new WorkoutItemDouble());
|
|
// current ftp
|
|
table->setItem(row,1,new WorkoutItemInt());
|
|
}
|
|
|
|
template<int minGrade, int maxGrade>
|
|
class WorkoutItemDoubleRange : public WorkoutItemDouble
|
|
{
|
|
int min, max;
|
|
public:
|
|
WorkoutItemDoubleRange() : min(minGrade), max(maxGrade) {}
|
|
QString validateData(const QString &text)
|
|
{
|
|
double d = text.toDouble();
|
|
d = d > max ? max : d;
|
|
d = d < min ? min : d;
|
|
return QString::number(d);
|
|
}
|
|
|
|
};
|
|
|
|
void WorkoutEditorGradient::insertDataRow(int row)
|
|
{
|
|
table->insertRow(row);
|
|
// distance
|
|
table->setItem(row,0,new WorkoutItemDouble());
|
|
// grade
|
|
table->setItem(row,1,new WorkoutItemDoubleRange<-5,5>());
|
|
}
|
|
|
|
/// Workout Summary
|
|
|
|
void WorkoutMetricsSummary::updateMetrics(QStringList &order, QHash<QString,RideMetricPtr> &metrics)
|
|
{
|
|
int row = 0;
|
|
|
|
foreach(QString name, order)
|
|
{
|
|
RideMetricPtr rmp = metrics[name];
|
|
if(!metricMap.contains(name))
|
|
{
|
|
QLabel *label = new QLabel((rmp->name()) + ":");
|
|
QLabel *lcd = new QLabel();
|
|
metricMap[name] = QPair<QLabel*,QLabel*>(label,lcd);
|
|
layout->addWidget(label,metricMap.size(),0);
|
|
layout->addWidget(lcd,metricMap.size(),1);
|
|
}
|
|
QLabel *lcd = metricMap[name].second;
|
|
if(name == "time_riding")
|
|
{
|
|
QTime time;
|
|
|
|
time = time.addSecs(rmp->value(true));
|
|
QString s = time.toString("HH:mm:ss");
|
|
//qDebug() << s << " " << time.second();
|
|
lcd->setText(s);
|
|
}
|
|
else
|
|
{
|
|
lcd->setText(QString::number(rmp->value(true),'f',rmp->precision()) + " " + (rmp->units(true)) );
|
|
}
|
|
//qDebug() << name << ":" << (int)rmp->value(true);
|
|
row++;
|
|
}
|
|
}
|
|
|
|
void WorkoutMetricsSummary::updateMetrics(QMap<QString, QString> &map)
|
|
{
|
|
QMap<QString, QString>::iterator i = map.begin();
|
|
int row = 0;
|
|
while(i != map.end())
|
|
{
|
|
if(!metricMap.contains(i.key()))
|
|
{
|
|
QLabel *label = new QLabel((i.key() + ":"));
|
|
QLabel *value = new QLabel();
|
|
metricMap[i.key()] = QPair<QLabel*,QLabel*>(label,value);
|
|
layout->addWidget(label,metricMap.size(),0);
|
|
layout->addWidget(value,metricMap.size(),1);
|
|
}
|
|
QLabel *value = metricMap[i.key()].second;
|
|
value->setText(i.value());
|
|
row++;
|
|
++i;
|
|
}
|
|
}
|
|
|
|
/// WorkoutTypePage
|
|
|
|
WorkoutTypePage::WorkoutTypePage(QWidget *parent) : QWizardPage(parent)
|
|
{
|
|
}
|
|
|
|
void WorkoutTypePage::initializePage()
|
|
{
|
|
setTitle(tr("Workout Creator"));
|
|
setSubTitle(tr("Select the workout type to be created"));
|
|
buttonGroupBox = new QButtonGroup(this);
|
|
absWattageRadioButton = new QRadioButton(tr("Absolute Wattage"));
|
|
absWattageRadioButton->click();
|
|
relWattageRadioButton = new QRadioButton(tr("% FTP Wattage"));
|
|
gradientRadioButton = new QRadioButton(tr("Gradient"));
|
|
|
|
if (hackContext->rideItem()) {
|
|
QString s = hackContext->rideItem()->ride()->startTime().toLocalTime().toString();
|
|
QString importStr = "Import Selected Ride (" + s + ")";
|
|
importRadioButton = new QRadioButton((importStr));
|
|
} else {
|
|
importRadioButton = new QRadioButton("No ride selected");
|
|
}
|
|
QVBoxLayout *groupBoxLayout = new QVBoxLayout();
|
|
|
|
groupBoxLayout->addWidget(absWattageRadioButton);
|
|
groupBoxLayout->addWidget(relWattageRadioButton);
|
|
groupBoxLayout->addWidget(gradientRadioButton);
|
|
groupBoxLayout->addWidget(importRadioButton);
|
|
registerField("absWattage",absWattageRadioButton);
|
|
registerField("relWattage",relWattageRadioButton);
|
|
registerField("gradientWattage",gradientRadioButton);
|
|
registerField("import",importRadioButton);
|
|
setLayout(groupBoxLayout);
|
|
}
|
|
|
|
int WorkoutTypePage::nextId() const
|
|
{
|
|
if(absWattageRadioButton->isChecked())
|
|
return WorkoutWizard::WW_AbsWattagePage;
|
|
else if (relWattageRadioButton->isChecked())
|
|
return WorkoutWizard::WW_RelWattagePage;
|
|
else if (gradientRadioButton->isChecked())
|
|
return WorkoutWizard::WW_GradientPage;
|
|
else if (importRadioButton->isChecked())
|
|
return WorkoutWizard::WW_ImportPage;
|
|
return WorkoutWizard::WW_AbsWattagePage;
|
|
}
|
|
|
|
|
|
//// AbsWattagePage
|
|
|
|
AbsWattagePage::AbsWattagePage(QWidget *parent) : WorkoutPage(parent) {}
|
|
|
|
void AbsWattagePage::initializePage()
|
|
{
|
|
setTitle("Workout Wizard");
|
|
setSubTitle("Absolute Wattage Workout Creator");
|
|
QHBoxLayout *layout = new QHBoxLayout();
|
|
setLayout(layout);
|
|
QStringList colms;
|
|
colms.append(tr("Minutes"));
|
|
colms.append(tr("Wattage"));
|
|
we = new WorkoutEditorAbs(colms);
|
|
layout->addWidget(we);
|
|
QVBoxLayout *summaryLayout = new QVBoxLayout();
|
|
metricsSummary = new WorkoutMetricsSummary();
|
|
summaryLayout->addWidget(metricsSummary);
|
|
plot = new WorkoutPlot();
|
|
plot->setYAxisTitle("Wattage");
|
|
plot->setXAxisTitle("Time (minutes)");
|
|
plot->setAxisScale(QwtPlot::yLeft,0,500,0);
|
|
plot->setAxisScale(QwtPlot::xBottom,0,120,0);
|
|
summaryLayout->addWidget(plot);
|
|
summaryLayout->addStretch(1);
|
|
layout->addLayout(summaryLayout);
|
|
layout->addStretch(1);
|
|
connect(we,SIGNAL(dataChanged()),this,SLOT(updateMetrics()));
|
|
updateMetrics();
|
|
}
|
|
|
|
void AbsWattagePage::updateMetrics()
|
|
{
|
|
QVector<QPair<QString,QString> > data;
|
|
QwtArray<double> x;
|
|
QwtArray<double> y;
|
|
|
|
we->rawData(data);
|
|
|
|
int curSecs = 0;
|
|
// create rideFile
|
|
QSharedPointer<RideFile> workout(new RideFile());
|
|
workout->context = hackContext;
|
|
workout->setRecIntSecs(1);
|
|
double curMin = 0;
|
|
for(int i = 0; i < data.size() ; i++)
|
|
{
|
|
if(data[i].first == "LAP") continue;
|
|
|
|
double min = data[i].first.toDouble();
|
|
double watts = data[i].second.toDouble();
|
|
int secs = min * 60;
|
|
for(int j = 0; j < secs; j++)
|
|
{
|
|
RideFilePoint rfp;
|
|
rfp.secs = curSecs++;
|
|
rfp.watts = watts;
|
|
rfp.cad = 90;
|
|
workout->appendPoint(rfp);
|
|
}
|
|
|
|
x.append(curMin);
|
|
y.append(watts);
|
|
curMin += min;
|
|
x.append(curMin);
|
|
y.append(watts);
|
|
|
|
}
|
|
// replot workoutplot
|
|
plot->setAxisAutoScale(QwtPlot::yLeft);
|
|
plot->setAxisAutoScale(QwtPlot::xBottom);
|
|
plot->setData(x,y);
|
|
plot->replot();
|
|
|
|
// calculate bike score, xpower
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
const RideMetric *rm = factory.rideMetric("skiba_xpower");
|
|
QStringList metrics;
|
|
metrics.append("time_riding");
|
|
metrics.append("total_work");
|
|
metrics.append("average_power");
|
|
metrics.append("skiba_bike_score");
|
|
metrics.append("skiba_xpower");
|
|
QHash<QString,RideMetricPtr> results = rm->computeMetrics(NULL,&*workout,hackContext->athlete->zones(),hackContext->athlete->hrZones(),metrics);
|
|
metricsSummary->updateMetrics(metrics,results);
|
|
}
|
|
|
|
void AbsWattagePage::SaveWorkout()
|
|
{
|
|
QString workoutDir = appsettings->value(this,GC_WORKOUTDIR).toString();
|
|
|
|
QString filename = QFileDialog::getSaveFileName(this,QString("Save Workout"),
|
|
workoutDir,"Computrainer Format *.erg");
|
|
if(!filename.endsWith(".erg"))
|
|
{
|
|
filename.append(".erg");
|
|
}
|
|
// open the file
|
|
QFile f(filename);
|
|
f.open(QIODevice::WriteOnly | QIODevice::Text);
|
|
QTextStream stream(&f);
|
|
// create the header
|
|
SaveWorkoutHeader(stream,f.fileName(),QString("golden cheetah"),QString("MINUTES WATTS"));
|
|
QVector<QPair<QString, QString> > rawData;
|
|
we->rawData(rawData);
|
|
double currentX = 0;
|
|
stream << "[COURSE DATA]" << endl;
|
|
QPair<QString, QString > p;
|
|
foreach (p,rawData)
|
|
{
|
|
if(p.first == "LAP")
|
|
{
|
|
stream << currentX << " LAP" << endl;
|
|
}
|
|
else
|
|
{
|
|
stream << currentX << " " << p.second << endl;
|
|
currentX += p.first.toDouble();
|
|
stream << currentX << " " << p.second << endl;
|
|
}
|
|
}
|
|
stream << "[END COURSE DATA]" << endl;
|
|
}
|
|
|
|
/// RelativeWattagePage
|
|
|
|
RelWattagePage::RelWattagePage(QWidget *parent) : WorkoutPage(parent) {}
|
|
|
|
void RelWattagePage::initializePage()
|
|
{
|
|
int zoneRange = hackContext->athlete->zones()->whichRange(QDate::currentDate());
|
|
ftp = hackContext->athlete->zones()->getCP(zoneRange);
|
|
|
|
setTitle("Workout Wizard");
|
|
QString subTitle = "Relative Wattage Workout Wizard, current CP60 = " + QString::number(ftp);
|
|
setSubTitle(subTitle);
|
|
|
|
plot = new WorkoutPlot();
|
|
plot->setYAxisTitle(tr("% of FTP"));
|
|
plot->setXAxisTitle(tr("Time (minutes)"));
|
|
plot->setAxisScale(QwtPlot::yLeft,0,200,0);
|
|
plot->setAxisScale(QwtPlot::xBottom,0,120,0);
|
|
|
|
QHBoxLayout *layout = new QHBoxLayout();
|
|
setLayout(layout);
|
|
QStringList colms;
|
|
colms.append(tr("Minutes"));
|
|
colms.append(tr("% of FTP"));
|
|
colms.append(tr("Wattage"));
|
|
we = new WorkoutEditorRel(colms,ftp);
|
|
layout->addWidget(we);
|
|
QVBoxLayout *summaryLayout = new QVBoxLayout();
|
|
metricsSummary = new WorkoutMetricsSummary();
|
|
summaryLayout->addWidget(metricsSummary);
|
|
summaryLayout->addWidget(plot,1);
|
|
layout->addLayout(summaryLayout);
|
|
layout->addStretch(1);
|
|
connect(we,SIGNAL(dataChanged()),this,SLOT(updateMetrics()));
|
|
updateMetrics();
|
|
}
|
|
|
|
void RelWattagePage::updateMetrics()
|
|
{
|
|
QVector<QPair<QString,QString> > data;
|
|
QwtArray<double> x;
|
|
QwtArray<double> y;
|
|
|
|
we->rawData(data);
|
|
|
|
int curSecs = 0;
|
|
// create rideFile
|
|
QSharedPointer<RideFile> workout(new RideFile());
|
|
workout->context = hackContext;
|
|
workout->setRecIntSecs(1);
|
|
for(int i = 0; i < data.size() ; i++)
|
|
{
|
|
if(data[i].first == "LAP") continue;
|
|
double min = data[i].first.toDouble();
|
|
double percentFtp = data[i].second.toDouble();
|
|
int secs = min * 60;
|
|
x.append(curSecs/60);
|
|
y.append(percentFtp);
|
|
for(int j = 0; j < secs; j++)
|
|
{
|
|
RideFilePoint rfp;
|
|
rfp.secs = curSecs++;
|
|
rfp.watts = percentFtp * ftp /100;
|
|
rfp.cad = 90;
|
|
workout->appendPoint(rfp);
|
|
}
|
|
x.append(curSecs/60);
|
|
y.append(percentFtp);
|
|
}
|
|
|
|
// replot workoutplot
|
|
plot->setAxisAutoScale(QwtPlot::yLeft);
|
|
plot->setAxisAutoScale(QwtPlot::xBottom);
|
|
plot->setData(x,y);;
|
|
plot->replot();
|
|
|
|
// calculate bike score, xpower
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
const RideMetric *rm = factory.rideMetric("skiba_xpower");
|
|
|
|
QStringList metrics;
|
|
metrics.append("time_riding");
|
|
metrics.append("total_work");
|
|
metrics.append("average_power");
|
|
metrics.append("skiba_bike_score");
|
|
metrics.append("skiba_xpower");
|
|
QHash<QString,RideMetricPtr> results = rm->computeMetrics(NULL,&*workout,hackContext->athlete->zones(),hackContext->athlete->hrZones(),metrics);
|
|
metricsSummary->updateMetrics(metrics,results);
|
|
}
|
|
|
|
void RelWattagePage::SaveWorkout()
|
|
{
|
|
QString workoutDir = appsettings->value(this,GC_WORKOUTDIR).toString();
|
|
|
|
QString filename = QFileDialog::getSaveFileName(this,QString("Save Workout"),
|
|
workoutDir,"Computrainer Format *.mrc");
|
|
if(!filename.endsWith(".mrc"))
|
|
{
|
|
filename.append(".mrc");
|
|
}
|
|
// open the file
|
|
QFile f(filename);
|
|
f.open(QIODevice::WriteOnly | QIODevice::Text);
|
|
QTextStream stream(&f);
|
|
// create the header
|
|
SaveWorkoutHeader(stream,f.fileName(),QString("golden cheetah"),QString("MINUTES PERCENTAGE"));
|
|
QVector<QPair<QString, QString> > rawData;
|
|
we->rawData(rawData);
|
|
double currentX = 0;
|
|
stream << "[COURSE DATA]" << endl;
|
|
QPair<QString, QString > p;
|
|
foreach (p,rawData)
|
|
{
|
|
if(p.first == "LAP")
|
|
{
|
|
stream << currentX << " LAP" << endl;
|
|
}
|
|
else
|
|
{
|
|
stream << currentX << " " << p.second << endl;
|
|
currentX += p.first.toDouble();
|
|
stream << currentX << " " << p.second << endl;
|
|
}
|
|
}
|
|
stream << "[END COURSE DATA]" << endl;
|
|
}
|
|
|
|
/// GradientPage
|
|
GradientPage::GradientPage(QWidget *parent) : WorkoutPage(parent) {}
|
|
|
|
void GradientPage::initializePage()
|
|
{
|
|
int zoneRange = hackContext->athlete->zones()->whichRange(QDate::currentDate());
|
|
ftp = hackContext->athlete->zones()->getCP(zoneRange);
|
|
metricUnits = hackContext->athlete->useMetricUnits;
|
|
setTitle("Workout Wizard");
|
|
|
|
setSubTitle("Manually crate a workout based on gradient (slope) and distance, maxium grade is 5.");
|
|
|
|
QHBoxLayout *layout = new QHBoxLayout();
|
|
setLayout(layout);
|
|
QStringList colms;
|
|
|
|
colms.append(tr(metricUnits ? "KM" : "Miles"));
|
|
colms.append(tr("Grade"));
|
|
we = new WorkoutEditorGradient(colms);
|
|
layout->addWidget(we);
|
|
QVBoxLayout *summaryLayout = new QVBoxLayout();
|
|
metricsSummary = new WorkoutMetricsSummary();
|
|
summaryLayout->addWidget(metricsSummary);
|
|
summaryLayout->addStretch(1);
|
|
layout->addLayout(summaryLayout);
|
|
layout->addStretch(1);
|
|
connect(we,SIGNAL(dataChanged()),this,SLOT(updateMetrics()));
|
|
updateMetrics();
|
|
}
|
|
|
|
void GradientPage::updateMetrics()
|
|
{
|
|
QVector<QPair<QString,QString> > data;
|
|
we->rawData(data);
|
|
|
|
int totalDistance = 0;
|
|
double gain = 0;
|
|
int elevation = 0;
|
|
// create rideFile
|
|
QSharedPointer<RideFile> workout(new RideFile());
|
|
workout->setRecIntSecs(1);
|
|
for(int i = 0; i < data.size() ; i++)
|
|
{
|
|
if(data[i].first == "LAP") continue;
|
|
double distance = data[i].first.toDouble();
|
|
double grade = data[i].second.toDouble();
|
|
double delta = distance * (metricUnits ? 1000 : 5120) * grade /100;
|
|
gain += (delta > 0) ? delta : 0;
|
|
elevation += delta;
|
|
totalDistance += distance;
|
|
}
|
|
QMap<QString,QString> metricSummaryMap;
|
|
QString s = (metricUnits ? "KM" : "Miles");
|
|
metricSummaryMap[s] = QString::number(totalDistance);
|
|
s = (metricUnits ? "Meters Gained" : "Feet Gained");
|
|
metricSummaryMap[s] = QString::number(gain);
|
|
metricsSummary->updateMetrics(metricSummaryMap);
|
|
}
|
|
|
|
void GradientPage::SaveWorkout()
|
|
{
|
|
QString workoutDir = appsettings->value(this,GC_WORKOUTDIR).toString();
|
|
|
|
QString filename = QFileDialog::getSaveFileName(this,QString("Save Workout"),
|
|
workoutDir,"Computrainer Format *.crs");
|
|
if(!filename.endsWith(".crs"))
|
|
{
|
|
filename.append(".crs");
|
|
}
|
|
// open the file
|
|
QFile f(filename);
|
|
f.open(QIODevice::WriteOnly | QIODevice::Text);
|
|
QTextStream stream(&f);
|
|
// create the header
|
|
SaveWorkoutHeader(stream,f.fileName(),QString("golden cheetah"),QString("DISTANCE GRADE WIND"));
|
|
QVector<QPair<QString, QString> > rawData;
|
|
we->rawData(rawData);
|
|
double currentX = 0;
|
|
stream << "[COURSE DATA]" << endl;
|
|
QPair<QString, QString > p;
|
|
foreach (p,rawData)
|
|
{
|
|
currentX += p.first.toDouble();
|
|
stream << currentX << " " << p.second << " 0" << endl;
|
|
}
|
|
stream << "[END COURSE DATA]" << endl;
|
|
}
|
|
|
|
|
|
|
|
ImportPage::ImportPage(QWidget *parent) : WorkoutPage(parent) {}
|
|
|
|
void ImportPage::initializePage()
|
|
{
|
|
|
|
setTitle("Workout Wizard");
|
|
setSubTitle("Import current ride as a Gradient Ride (slope based)");
|
|
setFinalPage(true);
|
|
QVBoxLayout *layout = new QVBoxLayout();
|
|
plot = new WorkoutPlot();
|
|
metricUnits = hackContext->athlete->useMetricUnits;
|
|
QString s = (metricUnits ? "KM" : "Miles");
|
|
QString distance = QString("Distance (") + s + QString(")");
|
|
plot->setXAxisTitle(distance);
|
|
s = (metricUnits ? "Meters" : "Feet");
|
|
QString elevation = QString("elevation (") + s + QString(")");
|
|
plot->setYAxisTitle(elevation);
|
|
RideItem *rideItem = hackContext->rideItem();
|
|
|
|
if(rideItem == NULL)
|
|
return;
|
|
|
|
foreach(RideFilePoint *rfp,rideItem->ride()->dataPoints())
|
|
{
|
|
rideData.append(QPair<double,double>(rfp->km,rfp->alt));
|
|
}
|
|
layout->addWidget(plot,1);
|
|
|
|
QGroupBox *spinGroupBox = new QGroupBox();
|
|
spinGroupBox->setTitle(tr("Ride Smoothing Parameters"));
|
|
QLabel *gradeLabel = new QLabel("Maximum Grade");
|
|
gradeBox = new QSpinBox();
|
|
gradeBox->setValue(5);
|
|
gradeBox->setMaximum(8);
|
|
gradeBox->setMinimum(0);
|
|
gradeBox->setToolTip(tr("Maximum supported grade is 8"));
|
|
connect(gradeBox,SIGNAL(valueChanged(int)),this,SLOT(updatePlot()));
|
|
|
|
QLabel *segmentLabel = new QLabel("Segment Length");
|
|
segmentBox = new QSpinBox();
|
|
segmentBox->setMinimum(0);
|
|
segmentBox->setMaximum((metricUnits ? 1000 : 5120));
|
|
segmentBox->setValue((metricUnits ? 1000 : 5120)/2);
|
|
segmentBox->setSingleStep((metricUnits ? 1000 : 5120)/100);
|
|
s = QString("Segment length is based on ") + QString(metricUnits ? "meters": "feet");
|
|
segmentBox->setToolTip((s));
|
|
connect(segmentBox,SIGNAL(valueChanged(int)),this,SLOT(updatePlot()));
|
|
QHBoxLayout *bottomLayout = new QHBoxLayout();
|
|
QGridLayout *spinBoxLayout = new QGridLayout();
|
|
spinBoxLayout->addWidget(gradeLabel,0,0);
|
|
spinBoxLayout->addWidget(gradeBox,0,1);
|
|
spinBoxLayout->addWidget(segmentLabel,1,0);
|
|
spinBoxLayout->addWidget(segmentBox,1,1);
|
|
spinGroupBox->setLayout(spinBoxLayout);
|
|
bottomLayout->addWidget(spinGroupBox);
|
|
metricsSummary = new WorkoutMetricsSummary();
|
|
bottomLayout->addWidget(metricsSummary);
|
|
|
|
layout->addLayout(bottomLayout);
|
|
setLayout(layout);
|
|
updatePlot();
|
|
}
|
|
|
|
void ImportPage::updatePlot()
|
|
{
|
|
QwtArray<double> x;
|
|
QwtArray<double> y;
|
|
QPair<double,double> p;
|
|
|
|
int segmentLength = segmentBox->value();
|
|
double maxSlope = gradeBox->value();
|
|
|
|
double curAlt;
|
|
rideProfile.clear();
|
|
double startDistance = 0;
|
|
double curDistance = 0;
|
|
double startAlt = rideData.at(0).second * (!metricUnits ? FEET_PER_METER : 1);
|
|
double totalDistance = 0;
|
|
foreach(p, rideData)
|
|
{
|
|
totalDistance = p.first * (!metricUnits ? MILES_PER_KM : 1);
|
|
curAlt = p.second * (!metricUnits ? FEET_PER_METER : 1);
|
|
curDistance = (totalDistance - startDistance) * (metricUnits ? 1000 : 5120);
|
|
if(curDistance > segmentLength)
|
|
{
|
|
double slope = (curAlt - startAlt) / curDistance * 100;
|
|
slope = std::min(slope, maxSlope);
|
|
double alt = startAlt + (slope * curDistance / 100);
|
|
x.append(totalDistance);
|
|
y.append(alt);
|
|
rideProfile.append(QPair<double,double>(totalDistance,slope));
|
|
startDistance = totalDistance;
|
|
startAlt = curAlt;
|
|
}
|
|
}
|
|
|
|
double gain= 0;
|
|
double alt;
|
|
double prevAlt = 0;
|
|
foreach(alt, y)
|
|
{
|
|
gain += (alt > prevAlt) &&(prevAlt != 0) ? (alt - prevAlt) : 0;
|
|
prevAlt = alt;
|
|
}
|
|
QMap<QString, QString> metrics;
|
|
metrics["Elevation Climbed"] = QString::number((int)gain);
|
|
metrics["Distance"] = QString::number(totalDistance,'f',1);
|
|
metricsSummary->updateMetrics(metrics);
|
|
|
|
plot->setData(x,y);
|
|
plot->replot();
|
|
|
|
update();
|
|
}
|
|
|
|
void ImportPage::SaveWorkout()
|
|
{
|
|
QString workoutDir = appsettings->value(this,GC_WORKOUTDIR).toString();
|
|
QString filename = QFileDialog::getSaveFileName(this,QString("Save Workout"),
|
|
workoutDir,"Computrainer Format *.crs");
|
|
if(!filename.endsWith(".crs"))
|
|
{
|
|
filename.append(".crs");
|
|
}
|
|
// open the file
|
|
QFile f(filename);
|
|
f.open(QIODevice::WriteOnly | QIODevice::Text);
|
|
QTextStream stream(&f);
|
|
// create the header
|
|
SaveWorkoutHeader(stream,f.fileName(),QString("golden cheetah"),QString("DISTANCE GRADE WIND"));
|
|
stream << "[COURSE DATA]" << endl;
|
|
QPair<double,double> p;
|
|
double prevDistance = 0;
|
|
foreach (p,rideProfile)
|
|
{
|
|
stream << p.first - prevDistance << " " << p.second <<" 0" << endl;
|
|
prevDistance = p.first;
|
|
}
|
|
stream << "[END COURSE DATA]" << endl;
|
|
}
|
|
|
|
WorkoutWizard::WorkoutWizard(Context *context) :QWizard(context->mainWindow)
|
|
{
|
|
hackContext = context;
|
|
setPage(WW_WorkoutTypePage, new WorkoutTypePage());
|
|
setPage(WW_AbsWattagePage, new AbsWattagePage());
|
|
setPage(WW_RelWattagePage, new RelWattagePage());
|
|
setPage(WW_GradientPage, new GradientPage());
|
|
setPage(WW_ImportPage, new ImportPage());
|
|
this->setStartId(WW_WorkoutTypePage);
|
|
|
|
}
|
|
// called at the end of the wizard...
|
|
void WorkoutWizard::accept()
|
|
{
|
|
WorkoutPage *page = (WorkoutPage *)this->currentPage();
|
|
page->SaveWorkout();
|
|
done(0);
|
|
}
|