Files
GoldenCheetah/src/AddIntervalDialog.cpp
Mark Liversedge 6b2a2dbb65 Auto Interval Discovery (Part 3a of 3)
In this part we have now reinstated the sidebar interval tree
but it is refactored out of athlete and into AnalysisSidebar
where it belongs.

* you can hover and select/deselect intervals in the sidebar

Left to do;

* All the interval operations like sort, drag and drop, find
  delete etc do not work at present and will require some work

* Add a color button to the tree to let the user change the color
  for USER defined intervals

* Add more types of intervals to the discovery; esp routes and
  sustained intervals
2015-05-10 08:30:13 +01:00

887 lines
31 KiB
C++

/*
* Copyright (c) 2012 Damien Grauser (Damien.Grauser@pev-geneve.ch)
*
* 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 "AddIntervalDialog.h"
#include "Settings.h"
#include "Athlete.h"
#include "Context.h"
#include "IntervalItem.h"
#include "RideFile.h"
#include "RideItem.h"
#include "WPrime.h"
#include "HelpWhatsThis.h"
#include <QMap>
#include <cmath>
// helper function
static void clearResultsTable(QTableWidget *);
AddIntervalDialog::AddIntervalDialog(Context *context) :
context(context)
{
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Find Intervals"));
HelpWhatsThis *help = new HelpWhatsThis(this);
this->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::FindIntervals));
QVBoxLayout *mainLayout = new QVBoxLayout(this);
intervalMethodWidget = new QWidget();
mainLayout->addWidget(intervalMethodWidget);
QHBoxLayout *intervalMethodLayout = new QHBoxLayout(intervalMethodWidget);;
intervalMethodLayout->addStretch();
QLabel *intervalMethodLabel = new QLabel(tr("Method: "), this);
intervalMethodLayout->addWidget(intervalMethodLabel);
QButtonGroup *methodButtonGroup = new QButtonGroup(this);
QVBoxLayout *methodRadios = new QVBoxLayout;
intervalMethodLayout->addLayout(methodRadios);
intervalMethodLayout->addStretch();
methodBestPower = new QRadioButton(tr("Peak Power"));
methodBestPower->setChecked(true);
methodButtonGroup->addButton(methodBestPower);
methodRadios->addWidget(methodBestPower);
methodClimb = new QRadioButton(tr("Ascent (elevation)"));
methodClimb->setChecked(false);
methodButtonGroup->addButton(methodClimb);
methodRadios->addWidget(methodClimb);
methodWPrime = new QRadioButton(tr("W' (Energy)"));
methodWPrime->setChecked(false);
methodButtonGroup->addButton(methodWPrime);
methodRadios->addWidget(methodWPrime);
methodFirst = new QRadioButton(tr("Time / Distance"));
methodFirst->setChecked(false);
methodButtonGroup->addButton(methodFirst);
methodRadios->addWidget(methodFirst);
intervalPeakPowerWidget = new QWidget();
intervalPeakPowerTypeLayout = new QHBoxLayout;
intervalPeakPowerTypeLayout->addStretch();
QLabel *intervalPeakPowerTypeLabel = new QLabel(tr("Type: "), this);
intervalPeakPowerTypeLayout->addWidget(intervalPeakPowerTypeLabel);
QButtonGroup *peakPowerTypeButtonGroup = new QButtonGroup(this);
peakPowerStandard = new QRadioButton(tr("Standard"));
peakPowerStandard->setChecked(true);
peakPowerTypeButtonGroup->addButton(peakPowerStandard);
intervalPeakPowerTypeLayout->addWidget(peakPowerStandard);
peakPowerCustom = new QRadioButton(tr("Custom"));
peakPowerTypeButtonGroup->addButton(peakPowerCustom);
intervalPeakPowerTypeLayout->addWidget(peakPowerCustom);
intervalPeakPowerTypeLayout->addStretch();
//mainLayout->addLayout(intervalPeakPowerTypeLayout);
intervalPeakPowerWidget->setLayout(intervalPeakPowerTypeLayout);
intervalPeakPowerWidget->hide();
mainLayout->addWidget(intervalPeakPowerWidget);
intervalTypeWidget = new QWidget();
QHBoxLayout *intervalTypeLayout = new QHBoxLayout;
intervalTypeLayout->addStretch();
QLabel *intervalTypeLabel = new QLabel(tr("Length: "), this);
intervalTypeLayout->addWidget(intervalTypeLabel);
QButtonGroup *typeButtonGroup = new QButtonGroup(this);
typeTime = new QRadioButton(tr("By time"));
typeTime->setChecked(true);
typeButtonGroup->addButton(typeTime);
intervalTypeLayout->addWidget(typeTime);
typeDistance = new QRadioButton(tr("By distance"));
typeButtonGroup->addButton(typeDistance);
intervalTypeLayout->addWidget(typeDistance);
intervalTypeLayout->addStretch();
//mainLayout->addLayout(intervalTypeLayout);
intervalTypeWidget->setLayout(intervalTypeLayout);
mainLayout->addWidget(intervalTypeWidget);
intervalTimeWidget = new QWidget();
QHBoxLayout *intervalTimeLayout = new QHBoxLayout;
QLabel *intervalTimeLabel = new QLabel(tr("Time: "), this);
intervalTimeLayout->addWidget(intervalTimeLabel);
hrsSpinBox = new QDoubleSpinBox(this);
hrsSpinBox->setDecimals(0);
hrsSpinBox->setMinimum(0.0);
hrsSpinBox->setSuffix(tr(" hrs"));
hrsSpinBox->setSingleStep(1.0);
hrsSpinBox->setAlignment(Qt::AlignRight);
intervalTimeLayout->addWidget(hrsSpinBox);
minsSpinBox = new QDoubleSpinBox(this);
minsSpinBox->setDecimals(0);
minsSpinBox->setRange(0.0, 59.0);
minsSpinBox->setSuffix(tr(" mins"));
minsSpinBox->setSingleStep(1.0);
minsSpinBox->setWrapping(true);
minsSpinBox->setAlignment(Qt::AlignRight);
minsSpinBox->setValue(1.0);
intervalTimeLayout->addWidget(minsSpinBox);
secsSpinBox = new QDoubleSpinBox(this);
secsSpinBox->setDecimals(0);
secsSpinBox->setRange(0.0, 59.0);
secsSpinBox->setSuffix(tr(" secs"));
secsSpinBox->setSingleStep(1.0);
secsSpinBox->setWrapping(true);
secsSpinBox->setAlignment(Qt::AlignRight);
intervalTimeLayout->addWidget(secsSpinBox);
//mainLayout->addLayout(intervalLengthLayout);
intervalTimeWidget->setLayout(intervalTimeLayout);
mainLayout->addWidget(intervalTimeWidget);
intervalDistanceWidget = new QWidget();
QHBoxLayout *intervalDistanceLayout = new QHBoxLayout;
QLabel *intervalDistanceLabel = new QLabel(tr("Distance: "), this);
intervalDistanceLayout->addWidget(intervalDistanceLabel);
kmsSpinBox = new QDoubleSpinBox(this);
kmsSpinBox->setDecimals(0);
kmsSpinBox->setRange(0.0, 999);
kmsSpinBox->setValue(5.0);
kmsSpinBox->setSuffix(" km");
kmsSpinBox->setSingleStep(1.0);
kmsSpinBox->setAlignment(Qt::AlignRight);
intervalDistanceLayout->addWidget(kmsSpinBox);
msSpinBox = new QDoubleSpinBox(this);
msSpinBox->setDecimals(0);
msSpinBox->setRange(0.0, 999);
msSpinBox->setSuffix(" m");
msSpinBox->setSingleStep(1.0);
msSpinBox->setAlignment(Qt::AlignRight);
intervalDistanceLayout->addWidget(msSpinBox);
//mainLayout->addLayout(intervalDistanceLayout);
intervalDistanceWidget->setLayout(intervalDistanceLayout);
intervalDistanceWidget->hide();
mainLayout->addWidget(intervalDistanceWidget);
intervalCountWidget = new QWidget();
QHBoxLayout *intervalCountLayout = new QHBoxLayout;
QLabel *intervalCountLabel = new QLabel(tr("How many to find: "), this);
intervalCountLayout->addWidget(intervalCountLabel);
countSpinBox = new QDoubleSpinBox(this);
countSpinBox->setDecimals(0);
countSpinBox->setMinimum(1.0);
countSpinBox->setValue(5.0); // lets default to the top 5 powers
countSpinBox->setSingleStep(1.0);
countSpinBox->setAlignment(Qt::AlignRight);
intervalCountLayout->addWidget(countSpinBox);
//mainLayout->addLayout(intervalCountLayout);
intervalCountWidget->setLayout(intervalCountLayout);
mainLayout->addWidget(intervalCountWidget);
intervalWPrimeWidget = new QWidget();
QHBoxLayout *intervalWPrimeLayout = new QHBoxLayout;
QLabel *intervalWPrimeLabel = new QLabel(tr("Minimum W' Cost: "), this);
intervalWPrimeLayout->addStretch();
intervalWPrimeLayout->addWidget(intervalWPrimeLabel);
kjSpinBox = new QDoubleSpinBox(this);
kjSpinBox->setDecimals(1);
kjSpinBox->setRange(0.1, 50);
kjSpinBox->setValue(2.0);
kjSpinBox->setSuffix(" kj");
kjSpinBox->setSingleStep(0.1);
kjSpinBox->setAlignment(Qt::AlignRight);
intervalWPrimeLayout->addWidget(kjSpinBox);
intervalWPrimeLayout->addStretch();
intervalWPrimeWidget->setLayout(intervalWPrimeLayout);
intervalWPrimeWidget->hide();
mainLayout->addWidget(intervalWPrimeWidget);
intervalClimbWidget = new QWidget();
QHBoxLayout *intervalClimbLayout = new QHBoxLayout;
QLabel *intervalClimbLabel = new QLabel(tr("Minimum Ascent: "), this);
intervalClimbLayout->addStretch();
intervalClimbLayout->addWidget(intervalClimbLabel);
altSpinBox = new QDoubleSpinBox(this);
altSpinBox->setDecimals(1);
altSpinBox->setRange(10, 5000);
altSpinBox->setValue(100);
altSpinBox->setSuffix(" metres");
altSpinBox->setSingleStep(10);
altSpinBox->setAlignment(Qt::AlignRight);
intervalClimbLayout->addWidget(altSpinBox);
intervalClimbLayout->addStretch();
intervalClimbWidget->setLayout(intervalClimbLayout);
intervalClimbWidget->hide();
mainLayout->addWidget(intervalClimbWidget);
QHBoxLayout *findbuttonLayout = new QHBoxLayout;
findbuttonLayout->addStretch();
createButton = new QPushButton(tr("&Find"), this);
findbuttonLayout->addWidget(createButton);
findbuttonLayout->addStretch();
mainLayout->addLayout(findbuttonLayout);
QLabel *resultsLabel = new QLabel(tr("Results:"), this);
mainLayout->addWidget(resultsLabel);
// user can select from the results to add
// to the ride intervals
resultsTable = new QTableWidget;
mainLayout->addWidget(resultsTable);
resultsTable->setColumnCount(5);
resultsTable->setColumnHidden(3, true); // has start time in secs
resultsTable->setColumnHidden(4, true); // has stop time in secs
resultsTable->horizontalHeader()->hide();
resultsTable->horizontalHeader()->setStretchLastSection(true);
// resultsTable->verticalHeader()->hide();
resultsTable->setShowGrid(false);
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
addButton = new QPushButton(tr("&Add to Activity"));
buttonLayout->addWidget(addButton);
buttonLayout->addStretch();
mainLayout->addLayout(buttonLayout);
connect(methodFirst, SIGNAL(clicked()), this, SLOT(methodFirstClicked()));
connect(methodBestPower, SIGNAL(clicked()), this, SLOT(methodBestPowerClicked()));
connect(methodWPrime, SIGNAL(clicked()), this, SLOT(methodWPrimeClicked()));
connect(methodClimb, SIGNAL(clicked()), this, SLOT(methodClimbClicked()));
connect(peakPowerStandard, SIGNAL(clicked()), this, SLOT(peakPowerStandardClicked()));
connect(peakPowerCustom, SIGNAL(clicked()), this, SLOT(peakPowerCustomClicked()));
connect(typeTime, SIGNAL(clicked()), this, SLOT(typeTimeClicked()));
connect(typeDistance, SIGNAL(clicked()), this, SLOT(typeDistanceClicked()));
connect(createButton, SIGNAL(clicked()), this, SLOT(createClicked()));
connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked()));
// get set to default to best powers (peaks)
methodBestPowerClicked();
}
void
AddIntervalDialog::methodFirstClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalPeakPowerWidget->hide();
intervalClimbWidget->hide();
intervalWPrimeWidget->hide();
intervalTypeWidget->show();
if (typeDistance->isChecked())
typeDistanceClicked();
else
typeTimeClicked();
intervalCountWidget->show();
}
void
AddIntervalDialog::methodBestPowerClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalWPrimeWidget->hide();
intervalPeakPowerWidget->show();
intervalClimbWidget->hide();
if (peakPowerCustom->isChecked())
peakPowerCustomClicked();
else
peakPowerStandardClicked();
}
void
AddIntervalDialog::methodWPrimeClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalPeakPowerWidget->hide();
intervalClimbWidget->hide();
intervalTypeWidget->hide();
intervalDistanceWidget->hide();
intervalTimeWidget->hide();
intervalCountWidget->hide();
intervalWPrimeWidget->show();
}
void
AddIntervalDialog::methodClimbClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalClimbWidget->show();
intervalPeakPowerWidget->hide();
intervalTypeWidget->hide();
intervalDistanceWidget->hide();
intervalTimeWidget->hide();
intervalCountWidget->hide();
intervalWPrimeWidget->hide();
}
void
AddIntervalDialog::peakPowerStandardClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalTypeWidget->hide();
intervalTimeWidget->hide();
intervalDistanceWidget->hide();
intervalCountWidget->hide();
}
void
AddIntervalDialog::peakPowerCustomClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalTypeWidget->show();
if (typeDistance->isChecked())
typeDistanceClicked();
else
typeTimeClicked();
intervalCountWidget->show();
}
void
AddIntervalDialog::typeTimeClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalDistanceWidget->hide();
intervalTimeWidget->show();
}
void
AddIntervalDialog::typeDistanceClicked()
{
// clear the table
clearResultsTable(resultsTable);
intervalDistanceWidget->show();
intervalTimeWidget->hide();
}
// little helper function
static void
clearResultsTable(QTableWidget *resultsTable)
{
// zap the 3 main cols and two hidden ones
for (int i=0; i<resultsTable->rowCount(); i++) {
for (int j=0; j<resultsTable->columnCount(); j++) {
delete resultsTable->takeItem(i,j);
}
}
resultsTable->setRowCount(0);
}
static double
intervalDuration(const RideFilePoint *start, const RideFilePoint *stop, const RideFile *ride)
{
return stop->secs - start->secs + ride->recIntSecs();
}
static double
intervalDistance(const RideFilePoint *start, const RideFilePoint *stop, const RideFile *)
{
return 1000*(stop->km - start->km);// + (ride->recIntSecs()*stop->kph/3600));
}
static bool
intervalsOverlap(const AddIntervalDialog::AddedInterval &a,
const AddIntervalDialog::AddedInterval &b)
{
if ((a.start <= b.start) && (a.stop > b.start))
return true;
if ((b.start <= a.start) && (b.stop > a.start))
return true;
return false;
}
struct CompareBests {
// Sort by decreasing power and increasing start time.
bool operator()(const AddIntervalDialog::AddedInterval &a,
const AddIntervalDialog::AddedInterval &b) const {
if (a.avg > b.avg)
return true;
if (b.avg > a.avg)
return false;
return a.start < b.start;
}
};
void
AddIntervalDialog::createClicked()
{
const RideFile *ride = context->ride ? context->ride->ride() : NULL;
if (!ride) {
QMessageBox::critical(this, tr("Select Activity"), tr("No activity selected!"));
return;
}
int maxIntervals = (int) countSpinBox->value();
double windowSizeMeters = (kmsSpinBox->value() * 1000.0
+ msSpinBox->value());
double windowSizeSecs = (hrsSpinBox->value() * 3600.0
+ minsSpinBox->value() * 60.0
+ secsSpinBox->value());
bool byTime = typeTime->isChecked();
double minWPrime = kjSpinBox->value() * 1000;
QList<AddedInterval> results;
// FIND PEAKS
if (methodBestPower->isChecked()) {
if (peakPowerStandard->isChecked())
findPeakPowerStandard(ride, results);
else {
// bad window size?
if (windowSizeSecs == 0.0) {
QMessageBox::critical(this, tr("Bad Interval Length"), tr("Interval length must be greater than zero!"));
return;
}
findBests(byTime, ride, (byTime?windowSizeSecs:windowSizeMeters), maxIntervals, results, "");
}
}
// FIND BY TIME OR DISTANCE
if (methodFirst->isChecked()) {
findFirsts(byTime, ride, (byTime?windowSizeSecs:windowSizeMeters), maxIntervals, results);
}
// FIND ASCENTS
if (methodClimb->isChecked()) {
// we need altitude and more than 3 data points
if (ride->areDataPresent()->alt == false || ride->dataPoints().count() < 3) return;
double hysteresis = appsettings->value(NULL, GC_ELEVATION_HYSTERESIS).toDouble();
if (hysteresis <= 0.1) hysteresis = 3.00;
// first apply hysteresis
QVector<QPoint> points;
int index=0;
int runningAlt = ride->dataPoints().first()->alt;
foreach(RideFilePoint *p, ride->dataPoints()) {
// up
if (p->alt > (runningAlt + hysteresis)) {
runningAlt = p->alt;
points << QPoint(index, runningAlt);
}
// down
if (p->alt < (runningAlt - hysteresis)) {
runningAlt = p->alt;
points << QPoint(index, runningAlt);
}
index++;
}
// now find peaks and troughs in the point data
// there will only be ups and downs, no flats
QVector<QPoint> peaks;
for(int i=1; i<(points.count()-1); i++) {
// peak
if (points[i].y() > points[i-1].y() &&
points[i].y() > points[i+1].y()) peaks << points[i];
// trough
if (points[i].y() < points[i-1].y() &&
points[i].y() < points[i+1].y()) peaks << points[i];
}
// now run through looking for diffs > requested
int counter=0;
for (int i=0; i<(peaks.count()-1); i++) {
int ascent = 0; // ascent found in meters
if ((ascent=peaks[i+1].y() - peaks[i].y()) >= altSpinBox->value()) {
// found one so increment from zero
counter++;
// we have a winner...
struct AddedInterval add;
add.start = ride->dataPoints()[peaks[i].x()]->secs;
add.stop = ride->dataPoints()[peaks[i+1].x()]->secs;
add.name = QString(tr("Climb #%1 (%2m)")).arg(counter)
.arg(ascent);
results << add;
}
}
}
// FIND W' BAL DROPS
if (methodWPrime->isChecked()) {
WPrime wp;
wp.setRide((RideFile*)ride);
foreach(struct Match match, wp.matches) {
if (match.cost > minWPrime) {
struct AddedInterval add;
add.start = match.start;
add.stop = match.stop;
int lenSecs = match.stop-match.start+1;
QString duration;
// format the duration nicely!
if (lenSecs < 120) duration = QString("%1s").arg(lenSecs);
else {
int mins = lenSecs / 60;
int secs = lenSecs - (mins * 60);
if (secs) {
QChar zero = QLatin1Char ( '0' );
duration = QString("%1:%2").arg(mins,2,10,zero)
.arg(secs,2,10,zero);
} else duration = QString("%1min").arg(mins);
}
add.name = QString(tr("Match %1 (%2kJ)")).arg(duration)
.arg(match.cost/1000.00, 0, 'f', 1);
results << add;
}
}
}
// clear the table
clearResultsTable(resultsTable);
// populate the table
resultsTable->setRowCount(results.size());
int row = 0;
foreach (const AddedInterval &interval, results) {
double secs = interval.start;
double mins = floor(secs / 60);
secs = secs - mins * 60.0;
double hrs = floor(mins / 60);
mins = mins - hrs * 60.0;
// check box
QCheckBox *c = new QCheckBox;
c->setCheckState(Qt::Checked);
resultsTable->setCellWidget(row, 0, c);
// start time
QString start = "%1:%2:%3";
start = start.arg(hrs, 0, 'f', 0);
start = start.arg(mins, 2, 'f', 0, QLatin1Char('0'));
start = start.arg(round(secs), 2, 'f', 0, QLatin1Char('0'));
QTableWidgetItem *t = new QTableWidgetItem;
t->setText(start);
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
resultsTable->setItem(row, 1, t);
QTableWidgetItem *n = new QTableWidgetItem;
n->setText(interval.name);
n->setFlags(n->flags() | (Qt::ItemIsEditable));
resultsTable->setItem(row, 2, n);
// hidden columns - start, stop
QString strt = QString("%1").arg(interval.start); // can't use secs as it gets modified
QTableWidgetItem *st = new QTableWidgetItem;
st->setText(strt);
resultsTable->setItem(row, 3, st);
QString stp = QString("%1").arg(interval.stop); // was interval.start+x
QTableWidgetItem *sp = new QTableWidgetItem;
sp->setText(stp);
resultsTable->setItem(row, 4, sp);
row++;
}
resultsTable->resizeColumnToContents(0);
resultsTable->resizeColumnToContents(1);
}
void
AddIntervalDialog::findFirsts(bool typeTime, const RideFile *ride, double windowSize,
int maxIntervals, QList<AddedInterval> &results)
{
QList<AddedInterval> bests;
double secsDelta = ride->recIntSecs();
double totalWatts = 0.0;
QList<const RideFilePoint*> window;
// ride is shorter than the window size!
if (typeTime && windowSize > ride->dataPoints().last()->secs + secsDelta) return;
else if (windowSize > ride->dataPoints().last()->km*1000) return;
double rest = 0;
// We're looking for intervals with durations in [windowSizeSecs, windowSizeSecs + secsDelta).
foreach (const RideFilePoint *point, ride->dataPoints()) {
// Add points until interval duration is >= windowSizeSecs.
totalWatts += point->watts;
window.append(point);
double duration = intervalDuration(window.first(), window.last(), ride);
double distance = intervalDistance(window.first(), window.last(), ride);
if ((typeTime && duration >= (windowSize - rest)) ||
(!typeTime && distance >= (windowSize - rest))) {
double start = window.first()->secs;
double stop = start + duration - (typeTime?0:ride->recIntSecs()); // correction for distance
double avg = totalWatts * secsDelta / duration;
bests.append(AddedInterval(start, stop, avg));
rest = (typeTime?duration-windowSize+rest:distance-windowSize+rest);
// Discard points
window.clear();
totalWatts = 0;
// For distance the last point of an interval is also the first point of the next
if (!typeTime) {
totalWatts += point->watts;
window.append(point);
}
}
}
while (!bests.empty() && (results.size() < maxIntervals)) {
AddedInterval candidate = bests.takeFirst();
QString name = "#%1 %2%3 (%4w)";
name = name.arg(results.count()+1);
if (typeTime) {
// best n mins
if (windowSize < 60) {
// whole seconds
name = name.arg(windowSize);
name = name.arg("sec");
} else if (windowSize >= 60 && !(((int)windowSize)%60)) {
// whole minutes
name = name.arg(windowSize/60);
name = name.arg("min");
} else {
double secs = windowSize;
double mins = ((int) secs) / 60;
secs = secs - mins * 60.0;
double hrs = ((int) mins) / 60;
mins = mins - hrs * 60.0;
QString tm = "%1:%2:%3";
tm = tm.arg(hrs, 0, 'f', 0);
tm = tm.arg(mins, 2, 'f', 0, QLatin1Char('0'));
tm = tm.arg(secs, 2, 'f', 0, QLatin1Char('0'));
// mins and secs
name = name.arg(tm);
name = name.arg("");
}
} else {
// best n mins
if (windowSize < 1000) {
// whole seconds
name = name.arg(windowSize);
name = name.arg("m");
} else {
double dist = windowSize;
double kms = ((int) dist) / 1000;
dist = dist - kms * 1000.0;
double ms = dist;
QString tm = "%1,%2";
tm = tm.arg(kms);
tm = tm.arg(ms);
// km and m
name = name.arg(tm);
name = name.arg("km");
}
}
name = name.arg(round(candidate.avg));
candidate.name = name;
results.append(candidate);
}
}
void
AddIntervalDialog::findPeakPowerStandard(const RideFile *ride, QList<AddedInterval> &results)
{
findBests(true, ride, 5, 1, results, tr("Peak 5s"));
findBests(true, ride, 10, 1, results, tr("Peak 10s"));
findBests(true, ride, 20, 1, results, tr("Peak 20s"));
findBests(true, ride, 30, 1, results, tr("Peak 30s"));
findBests(true, ride, 60, 1, results, tr("Peak 1min"));
findBests(true, ride, 120, 1, results, tr("Peak 2min"));
findBests(true, ride, 300, 1, results, tr("Peak 5min"));
findBests(true, ride, 600, 1, results, tr("Peak 10min"));
findBests(true, ride, 1200, 1, results, tr("Peak 20min"));
findBests(true, ride, 1800, 1, results, tr("Peak 30min"));
findBests(true, ride, 3600, 1, results, tr("Peak 60min"));
}
void
AddIntervalDialog::findBests(bool typeTime, const RideFile *ride, double windowSize,
int maxIntervals, QList<AddedInterval> &results, QString prefix)
{
QList<AddedInterval> bests;
QList<AddedInterval> _results;
double secsDelta = ride->recIntSecs();
double totalWatts = 0.0;
QList<const RideFilePoint*> window;
// ride is shorter than the window size!
if (typeTime && windowSize > ride->dataPoints().last()->secs + secsDelta) return;
if (!typeTime && windowSize > ride->dataPoints().last()->km*1000) return;
// We're looking for intervals with durations in [windowSizeSecs, windowSizeSecs + secsDelta).
foreach (const RideFilePoint *point, ride->dataPoints()) {
// Discard points until interval duration is < windowSizeSecs + secsDelta.
while ((typeTime && !window.empty() && intervalDuration(window.first(), point, ride) >= windowSize + secsDelta) ||
(!typeTime && window.length()>1 && intervalDistance(window.at(1), point, ride) >= windowSize)) {
totalWatts -= window.first()->watts;
window.takeFirst();
}
// Add points until interval duration or distance is >= windowSize.
totalWatts += point->watts;
window.append(point);
double duration = intervalDuration(window.first(), window.last(), ride);
double distance = intervalDistance(window.first(), window.last(), ride);
if ((typeTime && duration >= windowSize) ||
(!typeTime && distance >= windowSize)) {
double start = window.first()->secs;
double stop = window.last()->secs; //start + duration;
double avg = totalWatts * secsDelta / duration;
bests.append(AddedInterval(start, stop, avg));
}
}
std::sort(bests.begin(), bests.end(), CompareBests());
while (!bests.empty() && (_results.size() < maxIntervals)) {
AddedInterval candidate = bests.takeFirst();
bool overlaps = false;
foreach (const AddedInterval &existing, _results) {
if (intervalsOverlap(candidate, existing)) {
overlaps = true;
break;
}
}
if (!overlaps) {
QString name = prefix;
if (prefix == "") {
name = tr("Best %2%3 #%1");
name = name.arg(_results.count()+1);
if (typeTime) {
// best n mins
if (windowSize < 60) {
// whole seconds
name = name.arg(windowSize);
name = name.arg("sec");
} else if (windowSize >= 60 && !(((int)windowSize)%60)) {
// whole minutes
name = name.arg(windowSize/60);
name = name.arg("min");
} else {
double secs = windowSize;
double mins = ((int) secs) / 60;
secs = secs - mins * 60.0;
double hrs = ((int) mins) / 60;
mins = mins - hrs * 60.0;
QString tm = "%1:%2:%3";
tm = tm.arg(hrs, 0, 'f', 0);
tm = tm.arg(mins, 2, 'f', 0, QLatin1Char('0'));
tm = tm.arg(secs, 2, 'f', 0, QLatin1Char('0'));
// mins and secs
name = name.arg(tm);
name = name.arg("");
}
} else {
// best n mins
if (windowSize < 1000) {
// whole seconds
name = name.arg(windowSize);
name = name.arg("m");
} else {
double dist = windowSize;
double kms = ((int) dist) / 1000;
dist = dist - kms * 1000.0;
double ms = dist;
QString tm = "%1,%2";
tm = tm.arg(kms);
tm = tm.arg(ms);
// km and m
name = name.arg(tm);
name = name.arg("km");
}
}
}
name += " (%4w)";
name = name.arg(round(candidate.avg));
candidate.name = name;
name = "";
_results.append(candidate);
}
}
results.append(_results);
}
void
AddIntervalDialog::addClicked()
{
//XXX REFACTOR NEED TO DECIDE HOW TO DO THIS!!
#if 0
// run through the table row by row
// and when the checkbox is shown
// get name from column 2
// get start in secs as a string from column 3
// get stop in secs as a string from column 4
for (int i=0; i<resultsTable->rowCount(); i++) {
// is it checked?
QCheckBox *c = (QCheckBox *)resultsTable->cellWidget(i,0);
if (c->isChecked()) {
double start = resultsTable->item(i,3)->text().toDouble();
double stop = resultsTable->item(i,4)->text().toDouble();
QString name = resultsTable->item(i,2)->text();
const RideFile *ride = context->ride ? context->ride->ride() : NULL;
QTreeWidgetItem *allIntervals = context->athlete->mutableIntervalItems();
QTreeWidgetItem *last =
new IntervalItem(ride, name, start, stop,
ride->timeToDistance(start),
ride->timeToDistance(stop),
allIntervals->childCount()+1,
RideFileInterval::USER);
last->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
// add
allIntervals->addChild(last);
}
}
context->athlete->updateRideFileIntervals();
#endif
done(0);
}