/* * Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net) * * 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 "BestIntervalDialog.h" #include "Athlete.h" #include "Context.h" #include "IntervalItem.h" #include "RideFile.h" #include "RideItem.h" #include "HelpWhatsThis.h" #include #include BestIntervalDialog::BestIntervalDialog(Context *context) : context(context) { setAttribute(Qt::WA_DeleteOnClose); setWindowTitle("Find Intervals"); HelpWhatsThis *help = new HelpWhatsThis(this); this->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::FindIntervals)); QVBoxLayout *mainLayout = new QVBoxLayout(this); QHBoxLayout *intervalLengthLayout = new QHBoxLayout; QLabel *intervalLengthLabel = new QLabel(tr("Interval length: "), this); intervalLengthLayout->addWidget(intervalLengthLabel); hrsSpinBox = new QDoubleSpinBox(this); hrsSpinBox->setDecimals(0); hrsSpinBox->setMinimum(0.0); hrsSpinBox->setSuffix(" hrs"); hrsSpinBox->setSingleStep(1.0); hrsSpinBox->setAlignment(Qt::AlignRight); intervalLengthLayout->addWidget(hrsSpinBox); minsSpinBox = new QDoubleSpinBox(this); minsSpinBox->setDecimals(0); minsSpinBox->setRange(0.0, 59.0); minsSpinBox->setSuffix(" mins"); minsSpinBox->setSingleStep(1.0); minsSpinBox->setWrapping(true); minsSpinBox->setAlignment(Qt::AlignRight); minsSpinBox->setValue(1.0); intervalLengthLayout->addWidget(minsSpinBox); secsSpinBox = new QDoubleSpinBox(this); secsSpinBox->setDecimals(0); secsSpinBox->setRange(0.0, 59.0); secsSpinBox->setSuffix(" secs"); secsSpinBox->setSingleStep(1.0); secsSpinBox->setWrapping(true); secsSpinBox->setAlignment(Qt::AlignRight); intervalLengthLayout->addWidget(secsSpinBox); mainLayout->addLayout(intervalLengthLayout); 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); 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->verticalHeader()->hide(); resultsTable->setShowGrid(false); QHBoxLayout *buttonLayout = new QHBoxLayout; findButton = new QPushButton(tr("&Find Intervals"), this); buttonLayout->addWidget(findButton); doneButton = new QPushButton(tr("&Done"), this); buttonLayout->addWidget(doneButton); addButton = new QPushButton(tr("&Add to Intervals")); buttonLayout->addWidget(addButton); mainLayout->addLayout(buttonLayout); connect(findButton, SIGNAL(clicked()), this, SLOT(findClicked())); connect(doneButton, SIGNAL(clicked()), this, SLOT(doneClicked())); connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); } // little helper function static void clearResultsTable(QTableWidget *resultsTable) { // zap the 3 main cols and two hidden ones for (int i=0; irowCount(); i++) { for (int j=0; jcolumnCount(); j++) delete resultsTable->takeItem(i,j); } } static double intervalDuration(const RideFilePoint *start, const RideFilePoint *stop, const RideFile *ride) { return stop->secs - start->secs + ride->recIntSecs(); } static bool intervalsOverlap(const BestIntervalDialog::BestInterval &a, const BestIntervalDialog::BestInterval &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 BestIntervalDialog::BestInterval &a, const BestIntervalDialog::BestInterval &b) const { if (a.avg > b.avg) return true; if (b.avg > a.avg) return false; return a.start < b.start; } }; void BestIntervalDialog::findClicked() { 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 windowSizeSecs = (hrsSpinBox->value() * 3600.0 + minsSpinBox->value() * 60.0 + secsSpinBox->value()); if (windowSizeSecs == 0.0) { QMessageBox::critical(this, tr("Bad Interval Length"), tr("Interval length must be greater than zero!")); return; } QList results; findBests(ride, Specification(), windowSizeSecs, maxIntervals, results); // clear the table clearResultsTable(resultsTable); // populate the table resultsTable->setRowCount(results.size()); int row = 0; foreach (const BestInterval &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); // name int x = windowSizeSecs; // int is more help here QString name = "Best %2%3 #%1 (%4w)"; name = name.arg(row + 1); // best n mins if (x < 60) { // whole seconds name = name.arg(x); name = name.arg("sec"); } else if (x >= 60 && !(x%60)) { // whole minutes name = name.arg(x/60); name = name.arg("min"); } else { double secs = x; 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(""); } name = name.arg(round(interval.avg)); QTableWidgetItem *n = new QTableWidgetItem; n->setText(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.start + x); QTableWidgetItem *sp = new QTableWidgetItem; sp->setText(stp); resultsTable->setItem(row, 4, sp); row++; } resultsTable->resizeColumnToContents(0); resultsTable->resizeColumnToContents(1); resultsTable->setColumnWidth(2,200); } void BestIntervalDialog::findBests(const RideFile *ride, Specification spec, double windowSizeSecs, int maxIntervals, QList &results) { QList bests; double secsDelta = ride->recIntSecs(); double totalWatts = 0.0; QList window; // ride is shorter than the window size! if (windowSizeSecs > ride->dataPoints().last()->secs + secsDelta) return; // We're looking for intervals with durations in [windowSizeSecs, windowSizeSecs + secsDelta). RideFileIterator it(const_cast(ride), spec); while (it.hasNext()) { struct RideFilePoint *point = it.next(); // Discard points until interval duration is < windowSizeSecs + secsDelta. while (!window.empty() && (intervalDuration(window.first(), point, ride) >= windowSizeSecs + secsDelta)) { totalWatts -= window.first()->watts; window.takeFirst(); } // Add points until interval duration is >= windowSizeSecs. totalWatts += point->watts; window.append(point); double duration = intervalDuration(window.first(), window.last(), ride); if (duration >= windowSizeSecs) { double start = window.first()->secs; double stop = start + duration; double avg = totalWatts * secsDelta / duration; bests.append(BestInterval(start, stop, avg)); } } std::sort(bests.begin(), bests.end(), CompareBests()); while (!bests.empty() && (results.size() < maxIntervals)) { BestInterval candidate = bests.takeFirst(); bool overlaps = false; foreach (const BestInterval &existing, results) { if (intervalsOverlap(candidate, existing)) { overlaps = true; break; } } if (!overlaps) results.append(candidate); } } void BestIntervalDialog::findBestsKPH(const RideFile *ride, Specification spec, double windowSizeSecs, int maxIntervals, QList &results) { QList bests; double secsDelta = ride->recIntSecs(); double totalKPH = 0.0; QList window; // ride is shorter than the window size! if (windowSizeSecs > ride->dataPoints().last()->secs + secsDelta) return; // We're looking for intervals with durations in [windowSizeSecs, windowSizeSecs + secsDelta). RideFileIterator it(const_cast(ride), spec); while (it.hasNext()) { struct RideFilePoint *point = it.next(); // Discard points until interval duration is < windowSizeSecs + secsDelta. while (!window.empty() && (intervalDuration(window.first(), point, ride) >= windowSizeSecs + secsDelta)) { totalKPH -= window.first()->kph; window.takeFirst(); } // Add points until interval duration is >= windowSizeSecs. totalKPH += point->kph; window.append(point); double duration = intervalDuration(window.first(), window.last(), ride); if (duration >= windowSizeSecs) { double start = window.first()->secs; double stop = start + duration; double avg = totalKPH * secsDelta / duration; bests.append(BestInterval(start, stop, avg)); } } std::sort(bests.begin(), bests.end(), CompareBests()); while (!bests.empty() && (results.size() < maxIntervals)) { BestInterval candidate = bests.takeFirst(); bool overlaps = false; foreach (const BestInterval &existing, results) { if (intervalsOverlap(candidate, existing)) { overlaps = true; break; } } if (!overlaps) results.append(candidate); } } void BestIntervalDialog::doneClicked() { clearResultsTable(resultsTable); // clear out that table! done(0); } void BestIntervalDialog::addClicked() { //XXX REFACTOR UPDATE WHEN DECIDE HOW TO ADD AN INTERVAL #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; irowCount(); 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::PEAKPOWER); // TODO not always PEAK... last->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); // add allIntervals->addChild(last); } } context->athlete->updateRideFileIntervals(); #endif }