From 42508c7f887c37aaec4eb0e297bf64943ccb24f6 Mon Sep 17 00:00:00 2001 From: Greg Lonnon Date: Tue, 4 Jan 2011 17:15:56 -0700 Subject: [PATCH 1/4] Computrainer Workout Wizard A wizard to create workouts based on absolute wattage and time relative wattage and time (to CP60) slope and distance and import an existing ride, and provide some smoothing to the ride data. Also fixes NP calculation SEGV when recIntSecs is negative. Fixes #249 --- src/Coggan.cpp | 2 + src/ErgFile.cpp | 55 ++- src/MainWindow.cpp | 11 + src/MainWindow.h | 1 + src/RideFile.cpp | 6 + src/RideFile.h | 1 + src/WorkoutWizard.cpp | 758 ++++++++++++++++++++++++++++++++++++++++++ src/WorkoutWizard.h | 335 +++++++++++++++++++ src/src.pro | 2 + 9 files changed, 1143 insertions(+), 28 deletions(-) create mode 100644 src/WorkoutWizard.cpp create mode 100644 src/WorkoutWizard.h diff --git a/src/Coggan.cpp b/src/Coggan.cpp index f2e35f3a3..c38910050 100644 --- a/src/Coggan.cpp +++ b/src/Coggan.cpp @@ -44,6 +44,8 @@ class NP : public RideMetric { QVector last30secs; double secsDelta = ride->recIntSecs(); + if(secsDelta == 0) + return; double sampsPerWindow = ceil(30.0 / secsDelta); last30secs.resize(sampsPerWindow + 1); // add 1 just in case diff --git a/src/ErgFile.cpp b/src/ErgFile.cpp index 9967a5ac7..2a849f91a 100644 --- a/src/ErgFile.cpp +++ b/src/ErgFile.cpp @@ -37,6 +37,32 @@ ErgFile::ErgFile(QString filename, int &mode, double Cp) valid = false; return; } + // Section markers + QRegExp startHeader("^.*\\[COURSE HEADER\\].*$", Qt::CaseInsensitive); + QRegExp endHeader("^.*\\[END COURSE HEADER\\].*$", Qt::CaseInsensitive); + QRegExp startData("^.*\\[COURSE DATA\\].*$", Qt::CaseInsensitive); + QRegExp endData("^.*\\[END COURSE DATA\\].*$", Qt::CaseInsensitive); + // ignore whitespace and support for ';' comments (a GC extension) + QRegExp ignore("^(;.*|[ \\t\\n]*)$", Qt::CaseInsensitive); + // workout settings + QRegExp settings("^([^=]*)=[ \\t]*([^=\\n\\r\\t]*).*$", Qt::CaseInsensitive); + + // format setting for ergformat + QRegExp ergformat("^[;]*(MINUTES[ \\t]+WATTS).*$", Qt::CaseInsensitive); + QRegExp mrcformat("^[;]*(MINUTES[ \\t]+PERCENT).*$", Qt::CaseInsensitive); + QRegExp crsformat("^[;]*(DISTANCE[ \\t]+GRADE[ \\t]+WIND).*$", Qt::CaseInsensitive); + + // time watts records + QRegExp absoluteWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)[ \\t\\n]*$", Qt::CaseInsensitive); + QRegExp relativeWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)%[ \\t\\n]*$", Qt::CaseInsensitive); + + // distance slope wind records + QRegExp absoluteSlope("^[ \\t]*([0-9\\.]+)[ \\t]*([-0-9\\.]+)[ \\t\\n]([-0-9\\.]+)[ \\t\\n]*$", + Qt::CaseInsensitive); + + // Lap marker in an ERG/MRC file + QRegExp lapmarker("^[ \\t]*([0-9\\.]+)[ \\t]*LAP[ \\t\\n]*$", Qt::CaseInsensitive); + QRegExp crslapmarker("^[ \\t]*LAP[ \\t\\n]*$", Qt::CaseInsensitive); // ok. opened ok lets parse. QTextStream inputStream(&ergFile); @@ -58,33 +84,6 @@ ErgFile::ErgFile(QString filename, int &mode, double Cp) for (int li = 0; li < lines.size(); ++li) { QString line = lines[li]; - // Section markers - QRegExp startHeader("^.*\\[COURSE HEADER\\].*$", Qt::CaseInsensitive); - QRegExp endHeader("^.*\\[END COURSE HEADER\\].*$", Qt::CaseInsensitive); - QRegExp startData("^.*\\[COURSE DATA\\].*$", Qt::CaseInsensitive); - QRegExp endData("^.*\\[END COURSE DATA\\].*$", Qt::CaseInsensitive); - // ignore whitespace and support for ';' comments (a GC extension) - QRegExp ignore("^(;.*|[ \\t\\n]*)$", Qt::CaseInsensitive); - // workout settings - QRegExp settings("^([^=]*)=[ \\t]*([^=\\n\\r\\t]*).*$", Qt::CaseInsensitive); - - // format setting for ergformat - QRegExp ergformat("^[;]*(MINUTES[ \\t]+WATTS).*$", Qt::CaseInsensitive); - QRegExp mrcformat("^[;]*(MINUTES[ \\t]+PERCENT).*$", Qt::CaseInsensitive); - QRegExp crsformat("^[;]*(DISTANCE[ \\t]+GRADE[ \\t]+WIND).*$", Qt::CaseInsensitive); - - // time watts records - QRegExp absoluteWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)[ \\t\\n]*$", Qt::CaseInsensitive); - QRegExp relativeWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)%[ \\t\\n]*$", Qt::CaseInsensitive); - - // distance slope wind records - QRegExp absoluteSlope("^[ \\t]*([0-9\\.]+)[ \\t]*([-0-9\\.]+)[ \\t\\n]([-0-9\\.]+)[ \\t\\n]*$", - Qt::CaseInsensitive); - - // Lap marker in an ERG/MRC file - QRegExp lapmarker("^[ \\t]*([0-9\\.]+)[ \\t]*LAP[ \\t\\n]*$", Qt::CaseInsensitive); - QRegExp crslapmarker("^[ \\t]*LAP[ \\t\\n]*$", Qt::CaseInsensitive); - // so what we go then? if (startHeader.exactMatch(line)) { section = SETTINGS; @@ -145,7 +144,7 @@ ErgFile::ErgFile(QString filename, int &mode, double Cp) ErgFilePoint add; add.x = absoluteWatts.cap(1).toDouble() * 60000; // from mins to 1000ths of a second - add.val = add.y = absoluteWatts.cap(2).toInt(); // plain watts + add.val = add.y = round(absoluteWatts.cap(2).toDouble()); // plain watts switch (format) { diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 568f86e98..bdd5afece 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -83,6 +83,8 @@ #include "TwitterDialog.h" #include "WithingsDownload.h" #include "CalendarDownload.h" +#include "WorkoutWizard.h" + #include "GcWindowTool.h" #ifdef GC_HAVE_SOAP #include "TPUploadDialog.h" @@ -599,6 +601,9 @@ MainWindow::MainWindow(const QDir &home) : SLOT(showOptions()), tr("Ctrl+O")); optionsMenu->addAction(tr("Critical Power Calculator..."), this, SLOT(showTools())); + optionsMenu->addAction(tr("Workout Wizard"), this, + SLOT(showWorkoutWizard())); + #ifdef GC_HAVE_ICAL optionsMenu->addSeparator(); optionsMenu->addAction(tr("Upload Ride to Calendar"), this, @@ -1622,6 +1627,12 @@ void MainWindow::showTools() td->show(); } +void MainWindow::showWorkoutWizard() +{ + WorkoutWizard *ww = new WorkoutWizard(this); + ww->show(); +} + void MainWindow::saveRide() { diff --git a/src/MainWindow.h b/src/MainWindow.h index db8e08a04..8c6d311d8 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -186,6 +186,7 @@ class MainWindow : public QMainWindow bool saveRideExitDialog(); // save dirty rides on exit dialog void showOptions(); void showTools(); + void showWorkoutWizard(); void importRideToDB(); void scanForMissing(); void dateChanged(const QDate &); diff --git a/src/RideFile.cpp b/src/RideFile.cpp index 20f77fb80..a6408f6bd 100644 --- a/src/RideFile.cpp +++ b/src/RideFile.cpp @@ -335,6 +335,12 @@ void RideFile::appendPoint(double secs, double cad, double hr, double km, dataPresent.interval |= (interval != 0); } +void RideFile::appendPoint(const RideFilePoint &point) +{ + dataPoints_.append(new RideFilePoint(point.secs,point.cad,point.hr,point.km,point.kph,point.nm,point.watts,point.alt,point.lon,point.lat, + point.headwind,point.interval)); +} + void RideFile::setDataPresent(SeriesType series, bool value) { diff --git a/src/RideFile.h b/src/RideFile.h index 939a21b7f..f5f35a422 100644 --- a/src/RideFile.h +++ b/src/RideFile.h @@ -108,6 +108,7 @@ class RideFile : public QObject // QObject to emit signals void appendPoint(double secs, double cad, double hr, double km, double kph, double nm, double watts, double alt, double lon, double lat, double headwind, int interval); + void appendPoint(const RideFilePoint &); const QVector &dataPoints() const { return dataPoints_; } // Working with DATAPRESENT flags diff --git a/src/WorkoutWizard.cpp b/src/WorkoutWizard.cpp new file mode 100644 index 000000000..83e7dd7f8 --- /dev/null +++ b/src/WorkoutWizard.cpp @@ -0,0 +1,758 @@ +/* + * 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 "qwt_plot.h" +#include "qwt_plot_curve.h" +#include + + +// hack... Need to get the CP and zones for metrics +MainWindow *hackMW; + +/// 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(QwtData &data) + { + workoutCurve->setData(data); + } +}; + +//// Workout Editor + +WorkoutEditorBase::WorkoutEditorBase(QStringList &colms, QWidget *parent =0 ) :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 +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 &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(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 &map) +{ + QMap::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(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 =0) : 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")); + QString s = hackMW->rideItem()->ride()->startTime().toLocalTime().toString(); + QString importStr = "Import Selected Ride (" + s + ")"; + importRadioButton = new QRadioButton((importStr)); + 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 = NULL) : 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 > data; + QwtArray x; + QwtArray y; + + we->rawData(data); + + int curSecs = 0; + // create rideFile + boost::shared_ptr workout(new RideFile()); + 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 + QwtArrayData d(x,y); + plot->setAxisAutoScale(QwtPlot::yLeft); + plot->setAxisAutoScale(QwtPlot::xBottom); + plot->setData(d); + 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 results = rm->computeMetrics(NULL,&*workout,hackMW->zones(),hackMW->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 > rawData; + we->rawData(rawData); + double currentX = 0; + stream << "[COURSE DATA]" << endl; + QPair p; + foreach (p,rawData) + { + if(p.first == "LAP") + { + stream << "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 = NULL) : WorkoutPage(parent) +{} + +void RelWattagePage::initializePage() +{ + int zoneRange = hackMW->zones()->whichRange(QDate::currentDate()); + ftp = hackMW->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 > data; + QwtArray x; + QwtArray y; + + we->rawData(data); + + int curSecs = 0; + // create rideFile + boost::shared_ptr workout(new RideFile()); + 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 + QwtArrayData d(x,y); + plot->setAxisAutoScale(QwtPlot::yLeft); + plot->setAxisAutoScale(QwtPlot::xBottom); + plot->setData(d); + 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 results = rm->computeMetrics(NULL,&*workout,hackMW->zones(),hackMW->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 > rawData; + we->rawData(rawData); + double currentX = 0; + stream << "[COURSE DATA]" << endl; + QPair p; + foreach (p,rawData) + { + if(p.first == "LAP") + { + stream << "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 = NULL) : WorkoutPage(parent) +{} + +void GradientPage::initializePage() +{ + int zoneRange = hackMW->zones()->whichRange(QDate::currentDate()); + ftp = hackMW->zones()->getCP(zoneRange); + metricUnits = (appsettings->value(NULL, GC_UNIT).toString() == "Metric"); + 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 > data; + we->rawData(data); + + int totalDistance = 0; + double gain = 0; + int elevation = 0; + // create rideFile + boost::shared_ptr 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 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 > rawData; + we->rawData(rawData); + double currentX = 0; + stream << "[COURSE DATA]" << endl; + QPair p; + foreach (p,rawData) + { + currentX += p.first.toDouble(); + stream << currentX << " " << p.second << " 0" << endl; + } + stream << "[END COURSE DATA]" << endl; +} + + + +ImportPage::ImportPage(QWidget *parent = 0) : 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 = (appsettings->value(NULL, GC_UNIT).toString() == "Metric"); + 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 = hackMW->rideItem(); + + if(rideItem == NULL) + return; + + foreach(RideFilePoint *rfp,rideItem->ride()->dataPoints()) + { + rideData.append(QPair(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 x; + QwtArray y; + QPair 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(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 metrics; + metrics["Elevation Climbed"] = QString::number((int)gain); + metrics["Distance"] = QString::number(totalDistance,'f',1); + metricsSummary->updateMetrics(metrics); + + QwtArrayData profileData(x,y); + plot->setData(profileData); + 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 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(QWidget *parent) :QWizard(parent) +{ + hackMW = (MainWindow *)parent; + 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); +} diff --git a/src/WorkoutWizard.h b/src/WorkoutWizard.h new file mode 100644 index 000000000..59442eaee --- /dev/null +++ b/src/WorkoutWizard.h @@ -0,0 +1,335 @@ +/* + * 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 + */ + +#ifndef _GC_WORKOUTEDITOR_H +#define _GC_WORKOUTEDITOR_H +#include + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "RideFile.h" +#include "MainWindow.h" +#include "GoldenCheetah.h" +#include "Settings.h" +class QwtPlotCurve; +class RideItem; + +#include +#include +#include +#include +#include +#include "Zones.h" + +#include "Settings.h" +class WorkoutPlot; + +class WorkoutItem : public QTableWidgetItem +{ +public: + WorkoutItem(const QString &text) :QTableWidgetItem(QTableWidgetItem::UserType) + { + this->setText(text); + } + + virtual QString validateData(const QString &text) = 0; + + void setData(int role, const QVariant &value) + { + if(role == Qt::EditRole) + { + QVariant v(validateData(value.toString())); + QTableWidgetItem::setData(role,v); + } + } +}; + +class WorkoutItemDouble : public WorkoutItem +{ +public: + WorkoutItemDouble() : WorkoutItem("0") {} + virtual QString validateData(const QString &text) + { + double d = text.toDouble(); + return QString::number(d); + } +}; + +class WorkoutItemInt : public WorkoutItem +{ +public: + WorkoutItemInt() : WorkoutItem("0") {} + QString validateData(const QString &text) + { + bool ok; + double d = text.toDouble(&ok); + return QString::number((int)d); + } + +}; + +class WorkoutItemLap : public QTableWidgetItem +{ +public: + WorkoutItemLap() : QTableWidgetItem(QTableWidgetItem::UserType) { + setText("LAP"); + setFlags(flags() & (~Qt::ItemIsEditable)); + } +}; + +class WorkoutEditorBase : public QFrame +{ + Q_OBJECT +protected: + QTableWidget *table; + +public slots: + void addButtonClicked() { insertDataRow(table->rowCount()); } + void delButtonClicked() { table->removeRow(table->currentRow()); } + void lapButtonClicked() + { + int row = table->currentRow(); + table->insertRow(row); + table->setItem(row,0,new WorkoutItemLap()); + table->setItem(row,1,new QTableWidgetItem()); + } + void insertButtonClicked() { insertDataRow(table->currentRow()); } + void cellChanged(int, int) { dataChanged(); } + +signals: + void dataChanged(); + + +public: + WorkoutEditorBase(QStringList &colms, QWidget *parent); + virtual void insertDataRow(int row) =0; + virtual void rawData(QVector > &rawData) + { + int maxRow = table->rowCount(); + for(int i = 0; i < maxRow; i++) + { + QTableWidgetItem *item1 = table->item(i,0); + QTableWidgetItem *item2 = table->item(i,1); + if(item1 && item2) + { + rawData.append(QPair(item1->text(),item2->text())); + } + } + + } + + +}; + +class WorkoutEditorAbs : public WorkoutEditorBase +{ +public: + WorkoutEditorAbs(QStringList &colms, QWidget *parent = 0) : WorkoutEditorBase(colms,parent) + {} + void insertDataRow(int row); +}; + +class WorkoutEditorRel : public WorkoutEditorBase +{ + Q_OBJECT + + int ftp; +public slots: + + void updateWattage(int row, int colm) + { + if(colm == 1) + { + QTableWidgetItem *minItem = table->item(row,0); + QTableWidgetItem *percentageItem = table->item(row,1); + if(minItem->text() == "" || minItem->text() == "LAP" || percentageItem->text() == "") return; + double percentage = percentageItem->text() .toDouble(); + WorkoutItemInt *item = new WorkoutItemInt(); + int wattage = (int) (percentage * ftp / 100); + item->setData(Qt::EditRole,QVariant(QString::number(wattage))); + item->setFlags(item->flags() & (~Qt::ItemIsEditable)); + table->setItem(row,2,item); + } + } + +public: + WorkoutEditorRel(QStringList &colms, int _ftp, QWidget *parent = 0) : WorkoutEditorBase(colms,parent), ftp(_ftp) + { + connect(table,SIGNAL(cellChanged(int,int)),this,SLOT(updateWattage(int,int))); + } + void insertDataRow(int row); +}; + +class WorkoutEditorGradient :public WorkoutEditorBase +{ +public: + WorkoutEditorGradient(QStringList &colms, QWidget *parent = 0) : WorkoutEditorBase(colms,parent) + {} + void insertDataRow(int row); +}; + +class WorkoutMetricsSummary :public QGroupBox +{ + Q_OBJECT + + QGridLayout *layout; + QMap > metricMap; +public: + WorkoutMetricsSummary(QWidget *parent = 0) : QGroupBox(parent) + { + setTitle(tr("Workout Metrics")); + layout = new QGridLayout(); + setLayout(layout); + } + void updateMetrics(QStringList &order, QHash &metrics); + void updateMetrics(QMap &map); + +}; + +class WorkoutPage : public QWizardPage +{ +public: + WorkoutPage(QWidget *parent) : QWizardPage(parent) {} + virtual void SaveWorkout() = 0; + void SaveWorkoutHeader(QTextStream &stream, QString fileName, QString description, QString units) + { + stream << "[COURSE HEADER]" << endl; + stream << "VERSION = 2" << endl; + stream << "UNITS = METRIC" << endl; + stream << "DESCRIPTION = " << description << endl; + stream << "FILE NAME = " << fileName << endl; + stream << units << endl; + stream << "[END COURSE HEADER]" << endl; + } + +}; + +class WorkoutTypePage : public QWizardPage +{ + Q_OBJECT + QButtonGroup *buttonGroupBox; + QRadioButton *absWattageRadioButton, *relWattageRadioButton, *gradientRadioButton, *importRadioButton; +public: + WorkoutTypePage(QWidget *parent); + bool isComplete() const { return true; } + void initializePage(); + int nextId() const; +}; + +class AbsWattagePage : public WorkoutPage +{ + Q_OBJECT + WorkoutEditorAbs *we; + WorkoutMetricsSummary *metricsSummary; + WorkoutPlot *plot; +private slots: + void updateMetrics(); +public: + AbsWattagePage(QWidget *parent); + void initializePage(); + void SaveWorkout(); + bool isFinalPage() const { return true; } + int nextId() const { return -1; } +}; + +class RelWattagePage : public WorkoutPage +{ + Q_OBJECT + WorkoutEditorRel *we; + WorkoutMetricsSummary *metricsSummary; + WorkoutPlot *plot; + int ftp; +private slots: + void updateMetrics(); +public: + RelWattagePage(QWidget *parent); + void initializePage(); + bool isFinalPage() const { return true; } + int nextId() const { return -1; } + + void SaveWorkout(); +}; + +class GradientPage : public WorkoutPage +{ + Q_OBJECT + + WorkoutEditorGradient *we; + WorkoutMetricsSummary *metricsSummary; + int ftp; + bool metricUnits; + +private slots: + + void updateMetrics(); +public: + GradientPage(QWidget *parent); + void initializePage(); + void SaveWorkout(); + bool isFinalPage() const { return true; } + int nextId() const { return -1; } +}; + +class ImportPage : public WorkoutPage +{ + Q_OBJECT + WorkoutPlot *plot; + QVector > rideData; // orignal distance/alt in metric + bool metricUnits; + QSpinBox *gradeBox; + QSpinBox *segmentBox; + WorkoutMetricsSummary *metricsSummary; + QVector > rideProfile; // distance and slope +public slots: + void updatePlot(); +public: + ImportPage(QWidget * parent); + void initializePage(); + void SaveWorkout(); + bool isFinalPage() const { return true; } +}; + + + +class WorkoutWizard : public QWizard +{ + Q_OBJECT +public: + enum { WW_WorkoutTypePage, WW_AbsWattagePage, WW_RelWattagePage, WW_GradientPage, WW_ImportPage }; + + WorkoutWizard(QWidget *parent = NULL); + int nextId(); + // called at the end of the wizard... + void accept(); +}; + + +#endif + + diff --git a/src/src.pro b/src/src.pro index f017c8167..a7886560e 100644 --- a/src/src.pro +++ b/src/src.pro @@ -252,6 +252,7 @@ HEADERS += \ WeeklyViewItemDelegate.h \ WithingsDownload.h \ WkoRideFile.h \ + WorkoutWizard.h \ Zones.h \ ZoneScaleDraw.h @@ -403,6 +404,7 @@ SOURCES += \ WithingsDownload.cpp \ WeeklySummaryWindow.cpp \ WkoRideFile.cpp \ + WorkoutWizard.cpp \ Zones.cpp \ main.cpp \ From 073079a6e78ca2de04f2bc3ff9e24b0684653573 Mon Sep 17 00:00:00 2001 From: Darren Hague Date: Wed, 26 Jan 2011 23:50:03 +0000 Subject: [PATCH 2/4] Add virtual power support for BT-ATS trainer Add "BT Advanced Training System" to dropdown. Implement 3rd-order polynomial to get power from speed. Fixes #246. --- src/Pages.cpp | 1 + src/QuarqdClient.cpp | 1 + src/RealtimeController.cpp | 15 +++++++++++++++ src/RealtimeData.cpp | 10 +++++++++- src/RealtimeData.h | 5 +++-- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Pages.cpp b/src/Pages.cpp index 195df667e..3b128ab20 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -368,6 +368,7 @@ DevicePage::DevicePage(QWidget *parent) : QWidget(parent) virtualPower->addItem("Power - Kurt Kinetic Cyclone"); virtualPower->addItem("Power - Kurt Kinetic Road Machine"); virtualPower->addItem("Power - Cyclops Fluid 2"); + virtualPower->addItem("Power - BT Advanced Training System"); virtualPower->setCurrentIndex(0); // THIS CODE IS DISABLED FOR THIS RELEASE XXX diff --git a/src/QuarqdClient.cpp b/src/QuarqdClient.cpp index ab353b253..8062434bd 100644 --- a/src/QuarqdClient.cpp +++ b/src/QuarqdClient.cpp @@ -160,6 +160,7 @@ QuarqdClient::parseElement(QString &strBuf) // updates QuarqdClient::telemetry if (value > 0) { // TODO: let wheel size be a configurable, default now to 2101 mm telemetry.setSpeed((value*2101/1000*60)/1000); // meter/minute -> meter/hour -> km/hour + telemetry.setWheelRpm(value); lastReadSpeed = elapsedTime.elapsed(); } telemetry.setTime(getTimeStamp(str)); diff --git a/src/RealtimeController.cpp b/src/RealtimeController.cpp index 4ba47299a..ca0796eff 100644 --- a/src/RealtimeController.cpp +++ b/src/RealtimeController.cpp @@ -80,6 +80,19 @@ RealtimeController::processRealtimeData(RealtimeData &rtData) // http://thebikegeek.blogspot.com/2009/12/while-we-wait-for-better-and-better.html rtData.setWatts((0.0115*(mph*mph*mph)) - ((0.0137)*(mph*mph)) + ((8.9788)*(mph))); } + case 4 : // BT-ATS - BT Advanced Training System + { + // v is expressed in revs/second + double v = rtData.getWheelRpm()/60.0; + // using the algorithm from Steven Sansonetti of BT: + // This is a 3rd order polynomial, where P = av3 + bv2 + cv + d + // where: + double a = 2.90390167E-01; // ( 0.290390167) + double b = - 4.61311774E-02; // ( -0.0461311774) + double c = 5.92125507E-01; // (0.592125507) + double d = 0.0; + rtData.setWatts(a*v*v*v + b*v*v +c*v + d); + } default : // unknown - do nothing break; } @@ -101,6 +114,8 @@ RealtimeController::processSetup() break; case 3 : // TODO Cyclops Fluid 2 - use an algorithm break; + case 4 : // TODO BT-ATS - BT Advanced Training System - use an algorithm + break; default : // unknown - do nothing break; } diff --git a/src/RealtimeData.cpp b/src/RealtimeData.cpp index f4a344f0c..db48ed6d7 100644 --- a/src/RealtimeData.cpp +++ b/src/RealtimeData.cpp @@ -21,7 +21,7 @@ RealtimeData::RealtimeData() { - watts = hr = speed = cadence = load = 0; + watts = hr = speed = wheelRpm = cadence = load = 0; time = 0; } @@ -41,6 +41,10 @@ void RealtimeData::setSpeed(double speed) { this->speed = speed; } +void RealtimeData::setWheelRpm(double wheelRpm) +{ + this->wheelRpm = wheelRpm; +} void RealtimeData::setCadence(double aCadence) { cadence = aCadence; @@ -66,6 +70,10 @@ double RealtimeData::getSpeed() { return speed; } +double RealtimeData::getWheelRpm() +{ + return wheelRpm; +} double RealtimeData::getCadence() { return cadence; diff --git a/src/RealtimeData.h b/src/RealtimeData.h index e90b97e6e..f064a0013 100644 --- a/src/RealtimeData.h +++ b/src/RealtimeData.h @@ -32,19 +32,20 @@ public: void setHr(double hr); void setTime(long time); void setSpeed(double speed); + void setWheelRpm(double wheelRpm); void setCadence(double aCadence); void setLoad(double load); double getWatts(); double getHr(); long getTime(); double getSpeed(); + double getWheelRpm(); double getCadence(); double getLoad(); private: - double hr, watts, speed, load; unsigned long time; - + double hr, watts, speed, wheelRpm, load; double cadence; // in rpm }; From 152239eea4ce755619223d66057ecf3659577150 Mon Sep 17 00:00:00 2001 From: Darren Hague Date: Mon, 24 Jan 2011 23:04:05 +0000 Subject: [PATCH 3/4] Remove toMSecsSinceEpoch() and work around toMSecsSinceEpoch() is from Qt 4.7. Replaced with an implementation based on QTime:start() and QTime.elapsed() from Qt 4.6. There is now a theoretical upper limit on turbo sessions of 24 hrs :-) Fixes #247. --- src/RealtimeWindow.cpp | 38 +++++++++++++++++++++++--------------- src/RealtimeWindow.h | 3 ++- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/RealtimeWindow.cpp b/src/RealtimeWindow.cpp index 57c4cd91f..f7246625b 100644 --- a/src/RealtimeWindow.cpp +++ b/src/RealtimeWindow.cpp @@ -288,6 +288,11 @@ RealtimeWindow::RealtimeWindow(MainWindow *parent, TrainTool *trainTool, const Q stream_timer = new QTimer(this); load_timer = new QTimer(this); + session_time = QTime(); + session_elapsed_msec = 0; + lap_time = QTime(); + lap_elapsed_msec = 0; + recordFile = NULL; status = 0; status |= RT_RECORDING; // recording is on by default! - add others here @@ -376,14 +381,16 @@ void RealtimeWindow::Start() // when start button is pressed streamSelector->setEnabled(false); deviceSelector->setEnabled(false); - QDateTime now = QDateTime::currentDateTime(); - start_msec = lap_start_msec = now.toMSecsSinceEpoch(); - elapsed_msec = 0; + session_time.start(); + session_elapsed_msec = 0; + lap_time.start(); + lap_elapsed_msec = 0; if (status & RT_WORKOUT) { load_timer->start(LOADRATE); // start recording } if (status & RT_RECORDING) { + QDateTime now = QDateTime::currentDateTime(); // setup file QString filename = now.toString(QString("yyyy_MM_dd_hh_mm_ss")) + QString(".csv"); @@ -420,7 +427,8 @@ void RealtimeWindow::Pause() // pause capture to recalibrate if ((status&RT_RUNNING) == 0) return; if (status&RT_PAUSED) { - start_msec = QDateTime::currentDateTime().toMSecsSinceEpoch(); + session_time.start(); + lap_time.start(); status &=~RT_PAUSED; deviceController->restart(); pauseButton->setText(tr("Pause")); @@ -429,7 +437,8 @@ void RealtimeWindow::Pause() // pause capture to recalibrate if (status & RT_RECORDING) disk_timer->start(SAMPLERATE); if (status & RT_WORKOUT) load_timer->start(LOADRATE); } else { - elapsed_msec = total_msecs; + session_elapsed_msec += session_time.elapsed(); + lap_elapsed_msec += lap_time.elapsed(); deviceController->pause(); pauseButton->setText(tr("Un-Pause")); status |=RT_PAUSED; @@ -493,11 +502,10 @@ void RealtimeWindow::Stop(int deviceStatus) // when stop button is presse spdcount = 0; lodcount = 0; displayWorkoutLap = displayLap =0; - lap_msecs = 0; - total_msecs = 0; - elapsed_msec = 0; - start_msec = now.toMSecsSinceEpoch(); - lap_start_msec = now.toMSecsSinceEpoch(); + session_elapsed_msec = 0; + session_time.restart(); + lap_elapsed_msec = 0; + lap_time.restart(); avgPower= avgHeartRate= avgSpeed= avgCadence= avgLoad= 0; displayWorkoutDistance = displayDistance = 0; guiUpdate(); @@ -540,9 +548,8 @@ void RealtimeWindow::guiUpdate() // refreshes the telemetry displayDistance += displaySpeed / (5 * 3600); // XXX assumes 200ms refreshrate displayWorkoutDistance += displaySpeed / (5 * 3600); // XXX assumes 200ms refreshrate - qint64 now = QDateTime::currentDateTime().toMSecsSinceEpoch(); - total_msecs = (long)(elapsed_msec + now - start_msec); - lap_msecs = (long)(now - lap_start_msec); + total_msecs = session_elapsed_msec + session_time.elapsed(); + lap_msecs = lap_elapsed_msec + lap_time.elapsed(); // update those LCDs! timeLCD->display(QString("%1:%2:%3.%4").arg(total_msecs/3600000) @@ -620,13 +627,13 @@ void RealtimeWindow::newLap() { displayLap++; - lap_msecs = 0; pwrcount = 0; cadcount = 0; hrcount = 0; spdcount = 0; - lap_start_msec = QDateTime::currentDateTime().toMSecsSinceEpoch(); + lap_time.restart(); + lap_elapsed_msec = 0; // set avg to current values to ensure averages represent from now onwards // and not from beginning of workout @@ -703,6 +710,7 @@ void RealtimeWindow::diskUpdate() QTextStream recordFileStream(recordFile); // convert from milliseconds to minutes + total_msecs = session_elapsed_msec + session_time.elapsed(); Minutes = total_msecs; Minutes /= 1000.00; Minutes *= (1.0/60); diff --git a/src/RealtimeWindow.h b/src/RealtimeWindow.h index 4a5341a27..4e3a88520 100644 --- a/src/RealtimeWindow.h +++ b/src/RealtimeWindow.h @@ -127,7 +127,8 @@ class RealtimeWindow : public QWidget lap_msecs, load_msecs; - qint64 start_msec, elapsed_msec, lap_start_msec; + uint session_elapsed_msec, lap_elapsed_msec; + QTime session_time, lap_time; // GUI WIDGETS // layout From 470885df5031ef8bd58a293049bc1a2b024b6e7f Mon Sep 17 00:00:00 2001 From: Damien Date: Sun, 23 Jan 2011 14:23:56 +0100 Subject: [PATCH 4/4] Modify csv import for ergomo file with comma or semicolon separator Fixes #244. --- src/CsvRideFile.cpp | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/CsvRideFile.cpp b/src/CsvRideFile.cpp index f3531ee1b..afcaa684c 100644 --- a/src/CsvRideFile.cpp +++ b/src/CsvRideFile.cpp @@ -46,6 +46,8 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const */ QRegExp ergomoCSV("(ZEIT|STRECKE)", Qt::CaseInsensitive); bool ergomo = false; + + QChar ergomo_separator; int unitsHeader = 1; int total_pause = 0; int currentInterval = 0; @@ -97,6 +99,14 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const ergomo = true; rideFile->setDeviceType("Ergomo CSV"); unitsHeader = 2; + + QStringList headers = line.split(';'); + + if (headers.size()>1) + ergomo_separator = ';'; + else + ergomo_separator = ','; + ++lineno; continue; } @@ -201,20 +211,24 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const } else { // for ergomo formatted CSV files - minutes = line.section(',', 0, 0).toDouble() + total_pause; - km = line.section(',', 1, 1).toDouble(); - watts = line.section(',', 2, 2).toDouble(); - cad = line.section(',', 3, 3).toDouble(); - kph = line.section(',', 4, 4).toDouble(); - hr = line.section(',', 5, 5).toDouble(); - alt = line.section(',', 6, 6).toDouble(); + minutes = line.section(ergomo_separator, 0, 0).toDouble() + total_pause; + QString km_string = line.section(ergomo_separator, 1, 1); + km_string.replace(",","."); + km = km_string.toDouble(); + watts = line.section(ergomo_separator, 2, 2).toDouble(); + cad = line.section(ergomo_separator, 3, 3).toDouble(); + QString kph_string = line.section(ergomo_separator, 4, 4); + kph_string.replace(",","."); + kph = kph_string.toDouble(); + hr = line.section(ergomo_separator, 5, 5).toDouble(); + alt = line.section(ergomo_separator, 6, 6).toDouble(); interval = line.section(',', 8, 8).toInt(); if (interval != prevInterval) { prevInterval = interval; if (interval != 0) currentInterval++; } if (interval != 0) interval = currentInterval; - pause = line.section(',', 9, 9).toInt(); + pause = line.section(ergomo_separator, 9, 9).toInt(); total_pause += pause; nm = NULL; // torque is not provided in the Ergomo file