diff --git a/src/AllPlot.cpp b/src/AllPlot.cpp index 4c44cf174..2183da11b 100644 --- a/src/AllPlot.cpp +++ b/src/AllPlot.cpp @@ -16,9 +16,12 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include "MainWindow.h" +#include "AllPlotWindow.h" #include "AllPlot.h" #include "RideFile.h" #include "RideItem.h" +#include "IntervalItem.h" #include "Settings.h" #include "Units.h" #include "Zones.h" @@ -185,6 +188,8 @@ AllPlot::AllPlot(QWidget *parent): bg = new AllPlotBackground(this); bg->attach(this); + intervalPlotData = new IntervalPlotData(this); + insertLegend(new QwtLegend(), QwtPlot::BottomLegend); setCanvasBackground(Qt::white); @@ -223,6 +228,18 @@ AllPlot::AllPlot(QWidget *parent): altCurve->setBrush(brush_color); // fill below the line altCurve->setYAxis(yRight2); + intervalHighlighterCurve = new QwtPlotCurve(); + QPen ihlPen = QPen(Qt::blue); + ihlPen.setWidth(2); + intervalHighlighterCurve->setPen(ihlPen); + intervalHighlighterCurve->setYAxis(yLeft); + QColor ihlbrush = QColor(Qt::blue); + ihlbrush.setAlpha(64); + intervalHighlighterCurve->setBrush(ihlbrush); // fill below the line + intervalHighlighterCurve->setData(*intervalPlotData); + intervalHighlighterCurve->attach(this); + this->legend()->remove(intervalHighlighterCurve); // don't show in legend + grid = new QwtPlotGrid(); grid->enableX(false); QPen gridPen; @@ -386,17 +403,25 @@ AllPlot::recalc() setAxisScale(xBottom, 0.0, bydist ? totalDist : smoothTime[rideTimeSecs]); setYMax(); - + refreshIntervalMarkers(); refreshZoneLabels(); + replot(); +} + +void +AllPlot::refreshIntervalMarkers() +{ foreach(QwtPlotMarker *mrk, d_mrk) { mrk->detach(); delete mrk; } d_mrk.clear(); + QRegExp wkoAuto("^(Peak *[0-9]*(s|min)|Entire workout|Find #[0-9]*) *\\([^)]*\\)$"); if (rideItem->ride) { foreach(const RideFileInterval &interval, rideItem->ride->intervals()) { - if (interval.start < xaxis[startingIndex]) + // skip WKO autogenerated peak intervals + if (wkoAuto.exactMatch(interval.name)) continue; QwtPlotMarker *mrk = new QwtPlotMarker; d_mrk.append(mrk); @@ -410,12 +435,13 @@ AllPlot::recalc() if (!bydist) mrk->setValue(interval.start / 60.0, 0.0); else - mrk->setValue(smoothDistance[int(ceil(interval.start))], 0.0); + mrk->setValue((useMetricUnits ? 1 : MILES_PER_KM) * + rideItem->ride->timeToDistance(interval.start), 0.0); mrk->setLabel(text); } } - replot(); + } void @@ -614,3 +640,110 @@ AllPlot::setByDistance(int id) setXTitle(); recalc(); } + +/*---------------------------------------------------------------------- + * Interval plotting + *--------------------------------------------------------------------*/ + + +/* + * HELPER FUNCTIONS: + * intervalNum - returns a pointer to the nth selected interval + * intervalCount - returns the number of highlighted intervals + */ + +// note this is operating on the children of allIntervals and not the +// intervalWidget (QTreeWidget) -- this is why we do not use the +// selectedItems() member. N starts a one not zero. +static IntervalItem *intervalNum(int n) +{ + int highlighted=0; + const QTreeWidgetItem *allIntervals = mainwindow->allIntervalItems(); + for (int i=0; ichildCount(); i++) { + IntervalItem *current = (IntervalItem *)allIntervals->child(i); + + if (current != NULL) { + if (current->isSelected() == true) ++highlighted; + } else { + return NULL; + } + if (highlighted == n) return current; + } + return NULL; +} + +// how many intervals selected? +static int intervalCount() +{ + int highlighted; + highlighted = 0; + if (mainwindow == NULL || mainwindow->allIntervalItems() == NULL) return 0; // not inited yet! + + const QTreeWidgetItem *allIntervals = mainwindow->allIntervalItems(); + for (int i=0; ichildCount(); i++) { + IntervalItem *current = (IntervalItem *)allIntervals->child(i); + if (current != NULL) { + if (current->isSelected() == true) { + ++highlighted; + } + } + } + return highlighted; +} + +/* + * INTERVAL HIGHLIGHTING CURVE + * IntervalPlotData - implements the qwtdata interface where + * x,y return point co-ordinates and + * size returns the number of points + */ + +// The interval curve data is derived from the intervals that have +// been selected in the mainwindow leftlayout for each selected +// interval we return 4 data points; bottomleft, topleft, topright +// and bottom right. +// +// the points correspond to: +// bottom left = interval start, 0 watts +// top left = interval start, maxwatts +// top right = interval stop, maxwatts +// bottom right = interval stop, 0 watts +// +double IntervalPlotData::x(size_t i) const +{ + // for each interval there are four points, which interval is this for? + int interval = i ? i/4 : 0; + interval += 1; // interval numbers start at 1 not ZERO in the utility functions + + double multiplier = allPlot->useMetricUnits ? 1 : MILES_PER_KM; + + // get the interval + IntervalItem *current = intervalNum(interval); + if (current == NULL) return 0; // out of bounds !? + + // which point are we returning? + switch (i%4) { + case 0 : return allPlot->byDistance() ? multiplier * current->startKM : current->start/60; // bottom left + case 1 : return allPlot->byDistance() ? multiplier * current->startKM : current->start/60; // top left + case 2 : return allPlot->byDistance() ? multiplier * current->stopKM : current->stop/60; // bottom right + case 3 : return allPlot->byDistance() ? multiplier * current->stopKM : current->stop/60; // bottom right + } + return 0; // shouldn't get here, but keeps compiler happy +} + + +double IntervalPlotData::y(size_t i) const +{ + // which point are we returning? + switch (i%4) { + case 0 : return -100; // bottom left + case 1 : return 5000; // top left - set to out of bound value + case 2 : return 5000; // top right - set to out of bound value + case 3 : return -100; // bottom right + } + return 0; +} + + +size_t IntervalPlotData::size() const { return intervalCount()*4; } +QwtData *IntervalPlotData::copy() const { return new IntervalPlotData(allPlot); } diff --git a/src/AllPlot.h b/src/AllPlot.h index 35c6100e5..490ee5c8a 100644 --- a/src/AllPlot.h +++ b/src/AllPlot.h @@ -20,6 +20,7 @@ #define _GC_AllPlot_h 1 #include +#include #include class QwtPlotCurve; @@ -28,6 +29,20 @@ class QwtPlotMarker; class RideItem; class AllPlotBackground; class AllPlotZoneLabel; +class AllPlotWindow; +class AllPlot; + +class IntervalPlotData : public QwtData +{ + public: + IntervalPlotData(AllPlot *p) { allPlot=p; } + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; + AllPlot *allPlot; +}; class AllPlot : public QwtPlot { @@ -41,8 +56,11 @@ class AllPlot : public QwtPlot bool byDistance() const { return bydist; } + bool useMetricUnits; // whether metric units are used (or imperial) + bool shadeZones() const; void refreshZoneLabels(); + void refreshIntervalMarkers(); void setData(RideItem *_rideItem); @@ -61,6 +79,7 @@ class AllPlot : public QwtPlot friend class ::AllPlotBackground; friend class ::AllPlotZoneLabel; + friend class ::AllPlotWindow; AllPlotBackground *bg; QSettings *settings; @@ -71,6 +90,7 @@ class AllPlot : public QwtPlot QwtPlotCurve *speedCurve; QwtPlotCurve *cadCurve; QwtPlotCurve *altCurve; + QwtPlotCurve *intervalHighlighterCurve; // highlight selected intervals on the Plot QVector d_mrk; QList zoneLabels; @@ -78,6 +98,7 @@ class AllPlot : public QwtPlot QwtPlotGrid *grid; + IntervalPlotData *intervalPlotData; QVector hrArray; QVector wattsArray; QVector speedArray; @@ -96,7 +117,6 @@ class AllPlot : public QwtPlot void setXTitle(); bool shade_zones; // whether power should be shaded - bool useMetricUnits; // whether metric units are used (or imperial) }; #endif // _GC_AllPlot_h diff --git a/src/AllPlotWindow.cpp b/src/AllPlotWindow.cpp index c1d042740..8bd64b743 100644 --- a/src/AllPlotWindow.cpp +++ b/src/AllPlotWindow.cpp @@ -17,13 +17,20 @@ */ +#include "MainWindow.h" #include "AllPlotWindow.h" #include "AllPlot.h" #include "MainWindow.h" #include "RideFile.h" #include "RideItem.h" +#include "IntervalItem.h" +#include "TimeUtils.h" +#include "Settings.h" +#include "Units.h" // for MILES_PER_KM #include #include +#include +#include AllPlotWindow::AllPlotWindow(MainWindow *mainWindow) : QWidget(mainWindow), mainWindow(mainWindow) @@ -108,6 +115,39 @@ AllPlotWindow::AllPlotWindow(MainWindow *mainWindow) : // TODO: zoomer doesn't interact well with automatic axis resizing + + // Interval selection + allPicker = new QwtPlotPicker(QwtPlot::xBottom, QwtPlot::yLeft, + QwtPicker::RectSelection | QwtPicker::CornerToCorner|QwtPicker::DragSelection, + QwtPicker::VLineRubberBand, + QwtPicker::ActiveOnly, allPlot->canvas()); + allPicker->setRubberBandPen(QColor(Qt::blue)); + allPicker->setRubberBand(QwtPicker::CrossRubberBand); + allPicker->setTrackerPen(QColor(Qt::blue)); + // now select rectangles + allPicker->setSelectionFlags(QwtPicker::PointSelection | QwtPicker::RectSelection | QwtPicker::DragSelection); + allPicker->setRubberBand(QwtPicker::VLineRubberBand); + allPicker->setMousePattern(QwtEventPattern::MouseSelect1, + Qt::LeftButton, Qt::ShiftModifier); + + connect(allPicker, SIGNAL(moved(const QPoint &)), + SLOT(plotPickerMoved(const QPoint &))); + connect(allPicker, SIGNAL(appended(const QPoint &)), + SLOT(plotPickerSelected(const QPoint &))); + + allMarker1 = new QwtPlotMarker(); + allMarker1->setLineStyle(QwtPlotMarker::VLine); + allMarker1->attach(allPlot); + allMarker1->setLabelAlignment(Qt::AlignTop|Qt::AlignRight); + + allMarker2 = new QwtPlotMarker(); + allMarker2->setLineStyle(QwtPlotMarker::VLine); + allMarker2->attach(allPlot); + + allMarker3 = new QwtPlotMarker(); + allMarker3->setLineStyle(QwtPlotMarker::VLine); + allMarker3->attach(allPlot); + vlayout->addWidget(allPlot); vlayout->addLayout(showLayout); vlayout->addLayout(smoothLayout); @@ -133,6 +173,8 @@ AllPlotWindow::AllPlotWindow(MainWindow *mainWindow) : this, SLOT(setSmoothingFromLineEdit())); connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged())); + connect(mainWindow, SIGNAL(intervalsChanged()), this, SLOT(intervalsChanged())); + connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); } void @@ -141,6 +183,7 @@ AllPlotWindow::rideSelected() RideItem *ride = mainWindow->rideItem(); if (!ride) return; + clearSelection(); // clear any ride interval selection data setAllPlotWidgets(ride); allPlot->setData(ride); allZoomer->setZoomBase(); @@ -153,6 +196,20 @@ AllPlotWindow::zonesChanged() allPlot->replot(); } +void +AllPlotWindow::intervalsChanged() +{ + allPlot->refreshIntervalMarkers(); + allPlot->replot(); +} + +void +AllPlotWindow::intervalSelected() +{ + hideSelection(); + allPlot->replot(); +} + void AllPlotWindow::setSmoothingFromSlider() { @@ -192,3 +249,181 @@ AllPlotWindow::setAllPlotWidgets(RideItem *ride) } } +void +AllPlotWindow::zoomInterval(IntervalItem *which) +{ + QwtDoubleRect rect; + + if (!allPlot->byDistance()) { + rect.setLeft(which->start/60); + rect.setRight(which->stop/60); + } else { + rect.setLeft(which->startKM); + rect.setRight(which->stopKM); + } + rect.setTop(allPlot->wattsCurve->maxYValue()*1.1); + rect.setBottom(0); + allZoomer->zoom(rect); +} + +void +AllPlotWindow::plotPickerSelected(const QPoint &pos) +{ + // set start of selection in xunits (minutes assumed for now) + setStartSelection(allPlot->invTransform(QwtPlot::xBottom, pos.x())); +} + +void +AllPlotWindow::plotPickerMoved(const QPoint &pos) +{ + QString name = QString("Selection #%1").arg(selection); + // set end of selection in xunits (minutes assumed for now) + setEndSelection(allPlot->invTransform(QwtPlot::xBottom, pos.x()), true, name); +} + +void +AllPlotWindow::setStartSelection(double xValue) +{ + selection++; + + if (!allMarker1->isVisible() || allMarker1->xValue() != xValue) { + + allMarker1->hide(); + allMarker2->hide(); + allMarker3->hide(); + allMarker1->setValue(xValue, allPlot->byDistance() ? 0 : 100); + allMarker1->show(); + } +} + +void +AllPlotWindow::setEndSelection(double xValue, bool newInterval, QString name) +{ + bool useMetricUnits = allPlot->useMetricUnits; + if (!allMarker2->isVisible() || allMarker2->xValue() != xValue) { + allMarker2->setValue(xValue, allPlot->byDistance() ? 0 : 100); + allMarker2->show(); + double x1, x2; // time or distance depending upon mode selected + + // swap to low-high if neccessary + if (allMarker1->xValue()>allMarker2->xValue()){ + x2 = allMarker1->xValue(); + x1 = allMarker2->xValue(); + } else { + x1 = allMarker1->xValue(); + x2 = allMarker2->xValue(); + } + double lwidth=allPlot->transform(QwtPlot::xBottom, x2)-allPlot->transform(QwtPlot::xBottom, x1); + + allMarker3->setValue((x2-x1)/2+x1, 100); + QColor marker_color = QColor(Qt::blue); + marker_color.setAlpha(64); + allMarker3->setLinePen(QPen(QBrush(marker_color), lwidth, Qt::SolidLine)) ; + //allMarker3->setZ(-1000.0); + allMarker3->show(); + + RideFile tmpRide = RideFile(); + + QTreeWidgetItem *which = mainwindow->allRideItems()->treeWidget()->selectedItems().first(); + RideItem *ride = (RideItem*)which; + + double distance1 = -1; + double distance2 = -1; + double duration1 = -1; + double duration2 = -1; + double secsMoving = 0; + double wattsTotal = 0; + double bpmTotal = 0; + int arrayLength = 0; + + + // if we are in distance mode then x1 and x2 are distances + // we need to make sure they are in KM for the rest of this + // code. + if (allPlot->byDistance() && useMetricUnits == false) { + x1 *= KM_PER_MILE; + x2 *= KM_PER_MILE; + } + + foreach (const RideFilePoint *point, ride->ride->dataPoints()) { + if ((allPlot->byDistance()==true && point->km>=x1 && point->kmbyDistance()==false && point->secs/60>=x1 && point->secs/60km; + distance2 = point->km; + + if (duration1 == -1) duration1 = point->secs; + duration2 = point->secs; + + if (point->kph > 0.0) + secsMoving += ride->ride->recIntSecs(); + wattsTotal += point->watts; + bpmTotal += point->hr; + ++arrayLength; + } + } + QString s("\n%1%2 %3 %4\n%5%6 %7%8 %9%10"); + s = s.arg(useMetricUnits ? distance2-distance1 : (distance2-distance1)*MILES_PER_KM, 0, 'f', 1); + s = s.arg((useMetricUnits? "km":"m")); + s = s.arg(time_to_string(duration2-duration1)); + if (duration2-duration1-secsMoving>1) + s = s.arg("("+time_to_string(secsMoving)+")"); + else + s = s.arg(""); + s = s.arg((useMetricUnits ? 1 : MILES_PER_KM) * (distance2-distance1)/secsMoving*3600, 0, 'f', 1); + s = s.arg((useMetricUnits? "km/h":"m/h")); + if (wattsTotal>0) { + s = s.arg(wattsTotal/arrayLength, 0, 'f', 1); + s = s.arg("W"); + } + else{ + s = s.arg(""); + s = s.arg(""); + } + if (bpmTotal>0) { + s = s.arg(bpmTotal/arrayLength, 0, 'f', 0); + s = s.arg("bpm"); + } + else { + s = s.arg(""); + s = s.arg(""); + } + + allMarker1->setLabel(s); + + if (newInterval) { + + QTreeWidgetItem *allIntervals = (QTreeWidgetItem *)mainwindow->allIntervalItems(); + int count = allIntervals->childCount(); + + // are we adjusting an existing interval? - if so delete it and readd it + if (count > 0) { + IntervalItem *bottom = (IntervalItem *) allIntervals->child(count-1); + if (bottom->name == name) delete allIntervals->takeChild(count-1); + } + + QTreeWidgetItem *last = new IntervalItem(ride->ride, name, duration1, duration2, distance1, distance2); + allIntervals->addChild(last); + + // now update the RideFileIntervals and all the plots etc + mainwindow->updateRideFileIntervals(); + } + } +} + +void +AllPlotWindow::clearSelection() +{ + selection = 0; + hideSelection(); +} + +void +AllPlotWindow::hideSelection() +{ + allMarker1->setVisible(false); + allMarker2->setVisible(false); + allMarker3->setVisible(false); + allPlot->replot(); +} + diff --git a/src/AllPlotWindow.h b/src/AllPlotWindow.h index 3e69164f0..72d56d8fd 100644 --- a/src/AllPlotWindow.h +++ b/src/AllPlotWindow.h @@ -25,7 +25,10 @@ class AllPlot; class MainWindow; class QwtPlotPanner; class QwtPlotZoomer; +class QwtPlotPicker; +class QwtPlotMarker; class RideItem; +class IntervalItem; class AllPlotWindow : public QWidget { @@ -34,22 +37,39 @@ class AllPlotWindow : public QWidget public: AllPlotWindow(MainWindow *mainWindow); + void setData(RideItem *ride); + void setStartSelection(double seconds); + void setEndSelection(double seconds, bool newInterval, QString name); + void clearSelection(); + void hideSelection(); + void zoomInterval(IntervalItem *); // zoom into a specified interval public slots: void setSmoothingFromSlider(); void setSmoothingFromLineEdit(); void rideSelected(); + void intervalSelected(); void zonesChanged(); + void intervalsChanged(); protected: + // whilst we refactor, lets make friend + friend class IntervalPlotData; + friend class MainWindow; + void setAllPlotWidgets(RideItem *rideItem); MainWindow *mainWindow; AllPlot *allPlot; QwtPlotPanner *allPanner; QwtPlotZoomer *allZoomer; + QwtPlotPicker *allPicker; + int selection; + QwtPlotMarker *allMarker1; + QwtPlotMarker *allMarker2; + QwtPlotMarker *allMarker3; QCheckBox *showHr; QCheckBox *showSpeed; QCheckBox *showCad; @@ -57,6 +77,13 @@ class AllPlotWindow : public QWidget QComboBox *showPower; QSlider *smoothSlider; QLineEdit *smoothLineEdit; + + private: + void showInfo(QString); + + private slots: + void plotPickerMoved(const QPoint &); + void plotPickerSelected(const QPoint &); }; #endif // _GC_AllPlotWindow_h diff --git a/src/BestIntervalDialog.cpp b/src/BestIntervalDialog.cpp index eec34ee6d..503b83734 100644 --- a/src/BestIntervalDialog.cpp +++ b/src/BestIntervalDialog.cpp @@ -65,6 +65,7 @@ BestIntervalDialog::BestIntervalDialog(MainWindow *mainWindow) : 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); @@ -73,19 +74,42 @@ BestIntervalDialog::BestIntervalDialog(MainWindow *mainWindow) : QLabel *resultsLabel = new QLabel(tr("Results:"), this); mainLayout->addWidget(resultsLabel); - resultsText = new QTextEdit(this); - resultsText->setReadOnly(true); - mainLayout->addWidget(resultsText); + // 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); + } + return; } void @@ -128,13 +152,17 @@ BestIntervalDialog::findClicked() bests.insertMulti(avg, point->secs); } - QMap results; + // clean up the results - results ordered by power not + // offset in workout + results.clear(); while (!bests.empty() && maxIntervals--) { QMutableMapIterator j(bests); j.toBack(); j.previous(); double secs = j.value(); - results.insert(j.value() - windowSizeSecs, j.key()); + + if (j.value() >= windowSizeSecs) + results.insert(j.key(), j.value() - windowSizeSecs); j.remove(); while (j.hasPrevious()) { j.previous(); @@ -143,35 +171,129 @@ BestIntervalDialog::findClicked() } } - QString resultsHtml = - "
" - "" - "" - " "; + // clear the table + clearResultsTable(resultsTable); + + // populate the table + resultsTable->setRowCount(results.count()); // only those we didn't skip + int i=0; // count QMapIterator j(results); - while (j.hasNext()) { - j.next(); - double secs = j.key(); + j.toBack(); + while (j.hasPrevious()) { + + j.previous(); + + double secs = j.value(); double mins = ((int) secs) / 60; secs = secs - mins * 60.0; double hrs = ((int) mins) / 60; mins = mins - hrs * 60.0; - QString row = - "" - " "; - row = row.arg(hrs, 0, 'f', 0); - row = row.arg(mins, 2, 'f', 0, QLatin1Char('0')); - row = row.arg(secs, 2, 'f', 0, QLatin1Char('0')); - row = row.arg(j.value(), 0, 'f', 0, QLatin1Char('0')); - resultsHtml += row; + + // check box + QCheckBox *c = new QCheckBox; + c->setCheckState(Qt::Checked); + resultsTable->setCellWidget(i,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(secs, 2, 'f', 0, QLatin1Char('0')); + + QTableWidgetItem *t = new QTableWidgetItem; + t->setText(start); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + resultsTable->setItem(i,1,t); + + // name + int x = windowSizeSecs; // int is more help here + QString name = "Best %2%3 #%1 (%4w)"; + name = name.arg(i+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(j.key())); + + QTableWidgetItem *n = new QTableWidgetItem; + n->setText(name); + n->setFlags(n->flags() | (Qt::ItemIsEditable)); + resultsTable->setItem(i,2,n); + + // hidden columns - start, stop + QString strt = QString("%1").arg((int)j.value()); // can't use secs as it gets modified + QTableWidgetItem *st = new QTableWidgetItem; + st->setText(strt); + resultsTable->setItem(i,3,st); + + QString stp = QString("%1").arg((int)(j.value()+x)); + QTableWidgetItem *sp = new QTableWidgetItem; + sp->setText(stp); + resultsTable->setItem(i,4,sp); + + // increment counter + i++; } - resultsHtml += "
Start TimeAverage Power
%1:%2:%3%4
"; - resultsText->setHtml(resultsHtml); + resultsTable->resizeColumnToContents(0); + resultsTable->resizeColumnToContents(1); + resultsTable->setColumnWidth(2,200); } void BestIntervalDialog::doneClicked() { + clearResultsTable(resultsTable); // clear out that table! done(0); } +void +BestIntervalDialog::addClicked() +{ + // 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(); + RideFile *ride = (RideFile*)mainWindow->currentRide(); + + QTreeWidgetItem *last = + new IntervalItem(ride, name, start, stop, + ride->timeToDistance(start), + ride->timeToDistance(stop)); + + // add + QTreeWidgetItem *allIntervals = (QTreeWidgetItem *)mainwindow->allIntervalItems(); + allIntervals->addChild(last); + } + } + mainWindow->updateRideFileIntervals(); + return; +} diff --git a/src/BestIntervalDialog.h b/src/BestIntervalDialog.h index 6376d416b..89ed85fba 100644 --- a/src/BestIntervalDialog.h +++ b/src/BestIntervalDialog.h @@ -33,13 +33,16 @@ class BestIntervalDialog : public QDialog private slots: void findClicked(); void doneClicked(); + void addClicked(); // add to inverval selections private: + QMap results; + MainWindow *mainWindow; - QPushButton *findButton, *doneButton; + QPushButton *findButton, *doneButton, *addButton; QDoubleSpinBox *hrsSpinBox, *minsSpinBox, *secsSpinBox, *countSpinBox; - QTextEdit *resultsText; + QTableWidget *resultsTable; }; #endif // _GC_BestIntervalDialog_h diff --git a/src/GcRideFile.cpp b/src/GcRideFile.cpp new file mode 100644 index 000000000..2b4a046c0 --- /dev/null +++ b/src/GcRideFile.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2009 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 "GcRideFile.h" +#include // for std::sort +#include +#include +#include + +static int gcFileReaderRegistered = + RideFileFactory::instance().registerReader( + "gc", "GoldenCheetah Native Format", new GcFileReader()); + +RideFile * +GcFileReader::openRideFile(QFile &file, QStringList &errors) const +{ + QDomDocument doc("GoldenCheetah"); + if (!file.open(QIODevice::ReadOnly)) { + errors << "Could not open file."; + return NULL; + } + + bool parsed = doc.setContent(&file); + file.close(); + if (!parsed) { + errors << "Could not parse file."; + return NULL; + } + + RideFile *rideFile = new RideFile(); + QDomElement root = doc.documentElement(); + QDomNode attributes = root.firstChildElement("attributes"); + + for (QDomElement attr = attributes.firstChildElement("attribute"); + !attr.isNull(); attr = attr.nextSiblingElement("attribute")) { + QString key = attr.attribute("key"); + QString value = attr.attribute("value"); + if (key == "Device type") + rideFile->setDeviceType(value); + if (key == "Start time") + rideFile->setStartTime(QDateTime::fromString(value)); // TODO format + } + + QVector intervalStops; // used to set the interval number for each point + RideFileInterval add; // used to add each named interval to RideFile + QDomNode intervals = root.firstChildElement("intervals"); + if (!intervals.isNull()) { + for (QDomElement interval = intervals.firstChildElement("interval"); + !interval.isNull(); interval = interval.nextSiblingElement("interval")) { + + // record the stops for old-style datapoint interval numbering + double stop = interval.attribute("stop").toDouble(); + intervalStops.append(stop); + + // add a new interval to the new-style interval ranges + add.stop = stop; + add.start = interval.attribute("start").toDouble(); + add.name = interval.attribute("name"); + rideFile->addInterval(add.start, add.stop, add.name); + } + } + std::sort(intervalStops.begin(), intervalStops.end()); // just in case + int interval = 0; + + QDomElement samples = root.firstChildElement("samples"); + if (samples.isNull()) { + errors << "no sample section in ride file"; + return NULL; + } + + bool recIntSet = false; + for (QDomElement sample = samples.firstChildElement("sample"); + !sample.isNull(); sample = sample.nextSiblingElement("sample")) { + double secs, cad, hr, km, kph, nm, watts, alt; + secs = sample.attribute("secs", "0.0").toDouble(); + cad = sample.attribute("cad", "0.0").toDouble(); + hr = sample.attribute("hr", "0.0").toDouble(); + km = sample.attribute("km", "0.0").toDouble(); + kph = sample.attribute("kph", "0.0").toDouble(); + nm = sample.attribute("nm", "0.0").toDouble(); + watts = sample.attribute("watts", "0.0").toDouble(); + alt = sample.attribute("alt", "0.0").toDouble(); + while ((interval < intervalStops.size()) && (secs >= intervalStops[interval])) + ++interval; + rideFile->appendPoint(secs, cad, hr, km, kph, nm, watts, alt, interval); + if (!recIntSet) { + rideFile->setRecIntSecs(sample.attribute("len").toDouble()); + recIntSet = true; + } + } + + if (!recIntSet) { + errors << "no samples in ride file"; + return NULL; + } + + return rideFile; +} + +#define add_sample(name) \ + if (present->name) \ + sample.setAttribute(#name, QString("%1").arg(point->name)); + +void +GcFileReader::writeRideFile(const RideFile *ride, QFile &file) const +{ + QDomDocument doc("GoldenCheetah"); + QDomElement root = doc.createElement("ride"); + doc.appendChild(root); + + QDomElement attributes = doc.createElement("attributes"); + root.appendChild(attributes); + + QDomElement attribute = doc.createElement("attribute"); + attributes.appendChild(attribute); + attribute.setAttribute("key", "Start time"); + attribute.setAttribute( + "value", ride->startTime().toUTC().toString("yyyy/MM/dd hh:mm:ss' UTC'")); + attribute = doc.createElement("attribute"); + attributes.appendChild(attribute); + attribute.setAttribute("key", "Device type"); + attribute.setAttribute("value", ride->deviceType()); + + if (!ride->intervals().empty()) { + QDomElement intervals = doc.createElement("intervals"); + root.appendChild(intervals); + foreach (RideFileInterval i, ride->intervals()) { + QDomElement interval = doc.createElement("interval"); + intervals.appendChild(interval); + interval.setAttribute("name", i.name); + interval.setAttribute("start", QString("%1").arg(i.start)); + interval.setAttribute("stop", QString("%1").arg(i.stop)); + } + } + + if (!ride->dataPoints().empty()) { + QDomElement samples = doc.createElement("samples"); + root.appendChild(samples); + const RideFileDataPresent *present = ride->areDataPresent(); + assert(present->secs); + foreach (const RideFilePoint *point, ride->dataPoints()) { + QDomElement sample = doc.createElement("sample"); + samples.appendChild(sample); + assert(present->secs); + add_sample(secs); + add_sample(cad); + add_sample(hr); + add_sample(km); + add_sample(kph); + add_sample(nm); + add_sample(watts); + add_sample(alt); + sample.setAttribute("len", QString("%1").arg(ride->recIntSecs())); + } + } + + QByteArray xml = doc.toByteArray(4); + if (!file.open(QIODevice::WriteOnly)) + assert(false); + if (file.write(xml) != xml.size()) + assert(false); + file.close(); +} + diff --git a/src/GcRideFile.h b/src/GcRideFile.h new file mode 100644 index 000000000..4649665e7 --- /dev/null +++ b/src/GcRideFile.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2009 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 + */ + +#ifndef _GcRideFile_h +#define _GcRideFile_h + +#include "RideFile.h" + +struct GcFileReader : public RideFileReader { + virtual RideFile *openRideFile(QFile &file, QStringList &errors) const; + virtual void writeRideFile(const RideFile *ride, QFile &file) const; +}; + +#endif // _GcRideFile_h + diff --git a/src/HistogramWindow.cpp b/src/HistogramWindow.cpp index ab3988ca5..6fc8ea90e 100644 --- a/src/HistogramWindow.cpp +++ b/src/HistogramWindow.cpp @@ -74,6 +74,7 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) : connect(histParameterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setHistSelection(int))); connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected())); + connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged())); } @@ -91,6 +92,16 @@ HistogramWindow::rideSelected() setHistWidgets(ride); } +void +HistogramWindow::intervalSelected() +{ + RideItem *ride = mainWindow->rideItem(); + if (!ride) return; + + // set the histogram data + powerHist->setData(ride); +} + void HistogramWindow::zonesChanged() { diff --git a/src/HistogramWindow.h b/src/HistogramWindow.h index 6f64d6234..295e98806 100644 --- a/src/HistogramWindow.h +++ b/src/HistogramWindow.h @@ -40,6 +40,7 @@ class HistogramWindow : public QWidget public slots: void rideSelected(); + void intervalSelected(); void zonesChanged(); protected slots: diff --git a/src/IntervalItem.cpp b/src/IntervalItem.cpp new file mode 100644 index 000000000..eb332a367 --- /dev/null +++ b/src/IntervalItem.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2006 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 "IntervalItem.h" +#include "RideFile.h" + +IntervalItem::IntervalItem(RideFile *ride, QString name, double start, double stop, double startKM, double stopKM) : ride(ride), name(name), start(start), stop(stop), startKM(startKM), stopKM(stopKM) +{ + setText(0, name); +} + +void +IntervalItem::takeText() +{ + name = text(0); +} diff --git a/src/IntervalItem.h b/src/IntervalItem.h new file mode 100644 index 000000000..98dca5d76 --- /dev/null +++ b/src/IntervalItem.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2006 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 + */ + +#ifndef _GC_IntervalItem_h +#define _GC_IntervalItem_h 1 + +#include + +class RideFile; + +class IntervalItem : public QTreeWidgetItem +{ + public: + RideFile *ride; + QString name; + double start, stop; // by Time + double startKM, stopKM; // by Distance + + IntervalItem(RideFile *, QString, double, double, double, double); + void takeText(); +}; +#endif // _GC_IntervalItem_h + diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 594b8a5e1..91bed092d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -18,17 +18,20 @@ #include "MainWindow.h" #include "AllPlotWindow.h" +#include "AllPlot.h" #include "BestIntervalDialog.h" #include "ChooseCyclistDialog.h" #include "Computrainer.h" #include "ConfigDialog.h" #include "CriticalPowerWindow.h" +#include "GcRideFile.h" #include "PfPvWindow.h" #include "DownloadRideDialog.h" #include "ManualRideDialog.h" #include "HistogramWindow.h" #include "RealtimeWindow.h" #include "RideItem.h" +#include "IntervalItem.h" #include "RideFile.h" #include "RideSummaryWindow.h" #include "RideImportWizard.h" @@ -146,11 +149,33 @@ MainWindow::MainWindow(const QDir &home) : allRides->setText(0, tr("All Rides")); treeWidget->expandItem(allRides); + intervalWidget = new QTreeWidget; + intervalWidget->setColumnCount(1); + intervalWidget->setIndentation(5); + intervalWidget->setSortingEnabled(false); + intervalWidget->header()->hide(); + intervalWidget->setAlternatingRowColors (true); + intervalWidget->setSelectionBehavior(QAbstractItemView::SelectRows); + intervalWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); + intervalWidget->setSelectionMode(QAbstractItemView::MultiSelection); + intervalWidget->setContextMenuPolicy(Qt::CustomContextMenu); + + allIntervals = new QTreeWidgetItem(intervalWidget, FOLDER_TYPE); + allIntervals->setText(0, tr("Intervals")); + intervalWidget->expandItem(allIntervals); + + intervalsplitter = new QSplitter(this); + intervalsplitter->setOrientation(Qt::Vertical); + intervalsplitter->addWidget(treeWidget); + intervalsplitter->setCollapsible(0, true); + intervalsplitter->addWidget(intervalWidget); + intervalsplitter->setCollapsible(1, true); + leftLayout = new QSplitter; leftLayout->setOrientation(Qt::Vertical); leftLayout->addWidget(calendar); - leftLayout->setCollapsible(0, false); - leftLayout->addWidget(treeWidget); + leftLayout->setCollapsible(0, true); + leftLayout->addWidget(intervalsplitter); leftLayout->setCollapsible(1, false); splitter->addWidget(leftLayout); splitter->setCollapsible(0, true); @@ -256,13 +281,18 @@ MainWindow::MainWindow(const QDir &home) : connect(leftLayout, SIGNAL(splitterMoved(int,int)), this, SLOT(leftLayoutMoved())); connect(treeWidget, SIGNAL(itemSelectionChanged()), - this, SLOT(treeWidgetSelectionChanged())); + this, SLOT(rideTreeWidgetSelectionChanged())); connect(splitter, SIGNAL(splitterMoved(int,int)), this, SLOT(splitterMoved())); connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int))); connect(rideNotes, SIGNAL(textChanged()), this, SLOT(notesChanged())); + connect(intervalWidget,SIGNAL(customContextMenuRequested(const QPoint &)), + this, SLOT(showContextMenuPopup(const QPoint &))); + connect(intervalWidget,SIGNAL(itemSelectionChanged()), + this, SLOT(intervalTreeWidgetSelectionChanged())); + /////////////////////////////// Menus /////////////////////////////// @@ -275,14 +305,20 @@ MainWindow::MainWindow(const QDir &home) : SLOT(close()), tr("Ctrl+Q")); QMenu *rideMenu = menuBar()->addMenu(tr("&Ride")); + rideMenu->addAction(tr("&Save Ride"), this, + SLOT(saveRide()), tr("Ctrl+S")); rideMenu->addAction(tr("&Download from device..."), this, SLOT(downloadRide()), tr("Ctrl+D")); rideMenu->addAction(tr("&Export to CSV..."), this, SLOT(exportCSV()), tr("Ctrl+E")); + rideMenu->addAction(tr("&Export to GC..."), this, + SLOT(exportGC())); rideMenu->addAction(tr("&Import from File..."), this, SLOT (importFile()), tr ("Ctrl+I")); rideMenu->addAction(tr("Find &best intervals..."), this, SLOT(findBestIntervals()), tr ("Ctrl+B")); + rideMenu->addAction(tr("Find power &peaks..."), this, + SLOT(findPowerPeaks()), tr ("Ctrl+P")); rideMenu->addAction(tr("Split &ride..."), this, SLOT(splitRide())); rideMenu->addAction(tr("D&elete ride..."), this, @@ -427,7 +463,7 @@ MainWindow::removeCurrentRide() criticalPowerWindow->deleteCpiFile(strOldFileName); treeWidget->setCurrentItem(itemToSelect); - treeWidgetSelectionChanged(); + rideTreeWidgetSelectionChanged(); } void @@ -484,6 +520,26 @@ MainWindow::currentRide() return ((RideItem*) treeWidget->selectedItems().first())->ride; } +void +MainWindow::exportGC() +{ + if ((treeWidget->selectedItems().size() != 1) + || (treeWidget->selectedItems().first()->type() != RIDE_TYPE)) { + QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!")); + return; + } + + QString fileName = QFileDialog::getSaveFileName( + this, tr("Export GC"), QDir::homePath(), tr("GC (*.gc)")); + if (fileName.length() == 0) + return; + + QString err; + QFile file(fileName); + GcFileReader reader; + reader.writeRideFile(currentRide(), file); +} + void MainWindow::exportCSV() { @@ -552,11 +608,90 @@ MainWindow::importFile() void MainWindow::findBestIntervals() { - (new BestIntervalDialog(this))->show(); + BestIntervalDialog *p = new BestIntervalDialog(this); + p->setWindowModality(Qt::ApplicationModal); // don't allow select other ride or it all goes wrong! + p->exec(); } +void +MainWindow::addIntervalForPowerPeaksForSecs(RideFile *ride, int windowSizeSecs, QString name) +{ + + QList window; + QMap bests; + + double secsDelta = ride->recIntSecs(); + int expectedSamples = (int) floor(windowSizeSecs / secsDelta); + double totalWatts = 0.0; + + foreach (const RideFilePoint *point, ride->dataPoints()) { + while (!window.empty() + && (point->secs >= window.first()->secs + windowSizeSecs)) { + totalWatts -= window.first()->watts; + window.takeFirst(); + } + totalWatts += point->watts; + window.append(point); + int divisor = std::max(window.size(), expectedSamples); + double avg = totalWatts / divisor; + bests.insertMulti(avg, point->secs); + } + + QMap results; + if (!bests.empty()) { + QMutableMapIterator j(bests); + j.toBack(); + j.previous(); + double secs = j.value(); + results.insert(j.value() - windowSizeSecs, j.key()); + j.remove(); + while (j.hasPrevious()) { + j.previous(); + if (abs(secs - j.value()) < windowSizeSecs) + j.remove(); + } + } + QMapIterator j(results); + if (j.hasNext()) { + j.next(); + double secs = j.key(); + double watts = j.value(); + + QTreeWidgetItem *peak = new IntervalItem(ride, name+tr(" (%1 watts)").arg((int) round(watts)), secs, secs+windowSizeSecs, 0, 0); + allIntervals->addChild(peak); + } +} + +void +MainWindow::findPowerPeaks() +{ + QTreeWidgetItem *which = treeWidget->selectedItems().first(); + if (which->type() != RIDE_TYPE) { + return; + } + + addIntervalForPowerPeaksForSecs(ride->ride, 5, "Peak 5s"); + addIntervalForPowerPeaksForSecs(ride->ride, 10, "Peak 10s"); + addIntervalForPowerPeaksForSecs(ride->ride, 20, "Peak 20s"); + addIntervalForPowerPeaksForSecs(ride->ride, 30, "Peak 30s"); + addIntervalForPowerPeaksForSecs(ride->ride, 60, "Peak 1min"); + addIntervalForPowerPeaksForSecs(ride->ride, 120, "Peak 2min"); + addIntervalForPowerPeaksForSecs(ride->ride, 300, "Peak 5min"); + addIntervalForPowerPeaksForSecs(ride->ride, 600, "Peak 10min"); + addIntervalForPowerPeaksForSecs(ride->ride, 1200, "Peak 20min"); + addIntervalForPowerPeaksForSecs(ride->ride, 1800, "Peak 30min"); + addIntervalForPowerPeaksForSecs(ride->ride, 3600, "Peak 60min"); + + // now update the RideFileIntervals + updateRideFileIntervals(); +} + +//---------------------------------------------------------------------- +// User-define Intervals and Interval manipulation on left layout +//---------------------------------------------------------------------- + void -MainWindow::treeWidgetSelectionChanged() +MainWindow::rideTreeWidgetSelectionChanged() { assert(treeWidget->selectedItems().size() <= 1); if (treeWidget->selectedItems().isEmpty()) @@ -574,19 +709,144 @@ MainWindow::treeWidgetSelectionChanged() return; calendar->setSelectedDate(ride->dateTime.date()); - // turn off tabs that don't make sense for manual file entry - if (ride->ride && ride->ride->deviceType() == QString("Manual CSV")) { - tabWidget->setTabEnabled(3,false); // Power Histogram - tabWidget->setTabEnabled(4,false); // PF/PV Plot - } - else { - tabWidget->setTabEnabled(3,true); // Power Histogram - tabWidget->setTabEnabled(4,true); // PF/PV Plot + + // refresh interval list for bottom left + // first lets wipe away the existing intervals + // we need to disconnect this signal since it gets called as the + // widget is destroyed causing us to reference data that has just been + // deleted (takeText will SEGV under certain conditions) + disconnect(intervalWidget, SIGNAL(itemChanged(QTreeWidgetItem *, int)), + this, SLOT(itemChanged(QTreeWidgetItem *, int))); + intervalWidget->clear(); + allIntervals = new QTreeWidgetItem(intervalWidget, FOLDER_TYPE); + allIntervals->setText(0, tr("Intervals")); + intervalWidget->expandItem(allIntervals); + // now add the intervals for the current ride + if (ride) { // only if we have a ride pointer + RideFile *selected = ride->ride; + if (selected) { + // get all the intervals in the currently selected RideFile + QList intervals = selected->intervals(); + for (int i=0; i < intervals.count(); i++) { + // add as a child to allIntervals + IntervalItem *add = new IntervalItem(selected, + intervals.at(i).name, + intervals.at(i).start, + intervals.at(i).stop, + selected->timeToDistance(intervals.at(i).start), + selected->timeToDistance(intervals.at(i).stop)); + allIntervals->addChild(add); + } + } } + // turn off tabs that don't make sense for manual file entry + if (ride->ride && ride->ride->deviceType() == QString("Manual CSV")) { + tabWidget->setTabEnabled(3,false); // Power Histogram + tabWidget->setTabEnabled(4,false); // PF/PV Plot + } + else { + tabWidget->setTabEnabled(3,true); // Power Histogram + tabWidget->setTabEnabled(4,true); // PF/PV Plot + } saveAndOpenNotes(); } +void +MainWindow::showContextMenuPopup(const QPoint &pos) +{ + QTreeWidgetItem *trItem = intervalWidget->itemAt( pos ); + if (trItem != NULL && trItem->text(0) != tr("Intervals")) { + QMenu menu(intervalWidget); + + activeInterval = (IntervalItem *)trItem; + + QAction *actRenameInt = new QAction(tr("Rename interval"), intervalWidget); + QAction *actDeleteInt = new QAction(tr("Delete interval"), intervalWidget); + QAction *actZoomInt = new QAction(tr("Zoom to interval"), intervalWidget); + connect(actRenameInt, SIGNAL(triggered(void)), this, SLOT(renameInterval(void))); + connect(actDeleteInt, SIGNAL(triggered(void)), this, SLOT(deleteInterval(void))); + connect(actZoomInt, SIGNAL(triggered(void)), this, SLOT(zoomInterval(void))); + + if (tabWidget->currentIndex() == 1) // on ride plot + menu.addAction(actZoomInt); + menu.addAction(actRenameInt); + menu.addAction(actDeleteInt); + menu.exec(intervalWidget->mapToGlobal( pos )); + } +} +void +MainWindow::updateRideFileIntervals() +{ + // iterate over allIntervals as they are now defined + // and update the RideFile->intervals + RideItem *which = (RideItem *)treeWidget->selectedItems().first(); + RideFile *current = which->ride; + current->clearIntervals(); + for (int i=0; i < allIntervals->childCount(); i++) { + // add the intervals as updated + IntervalItem *it = (IntervalItem *)allIntervals->child(i); + current->addInterval(it->start, it->stop, it->name); + } + + // emit signal for interval data changed + intervalsChanged(); + + // regenerate the summary details + //rideSummary->clear(); + //which->clearSummary(); // clear cached value + //rideSummary->setHtml(which->htmlSummary()); + //rideSummary->setAlignment(Qt::AlignCenter); + + // set dirty + which->setDirty(true); +} + +void +MainWindow::deleteInterval() { + int index = allIntervals->indexOfChild(activeInterval); + delete allIntervals->takeChild(index); + + // now update the ride file to reflect this + updateRideFileIntervals(); +} + +void +MainWindow::renameInterval() { + activeInterval->setFlags(activeInterval->flags() | Qt::ItemIsEditable); + intervalWidget->editItem(activeInterval, 0); + connect(intervalWidget, SIGNAL(itemChanged(QTreeWidgetItem *, int)), + this, SLOT(itemChanged(QTreeWidgetItem *, int))); +} + +void +MainWindow::zoomInterval() { + // zoom into this interval on allPlot + allPlotWindow->zoomInterval(activeInterval); +} + +void +MainWindow::itemChanged(QTreeWidgetItem *item, int) // int col not used since only 1 column! +{ + // only for the rename action! + disconnect(intervalWidget, SIGNAL(itemChanged(QTreeWidgetItem *, int)), + this, SLOT(itemChanged(QTreeWidgetItem *, int))); + + if (item != NULL) { + + IntervalItem *citem = (IntervalItem*)item; + citem->takeText(); // take the text just set in the gui + + // now update the ride file to reflect this + updateRideFileIntervals(); + } +} + +void +MainWindow::intervalTreeWidgetSelectionChanged() +{ + intervalSelected(); +} void MainWindow::getBSFactors(float &timeBS, float &distanceBS) { @@ -751,8 +1011,9 @@ MainWindow::moveEvent(QMoveEvent*) } void -MainWindow::closeEvent(QCloseEvent*) +MainWindow::closeEvent(QCloseEvent* event) { + if (saveRideExitDialog() == false) event->ignore(); saveNotes(); } @@ -862,6 +1123,12 @@ void MainWindow::showTools() td->show(); } +void +MainWindow::saveRide() +{ + saveRideSingleDialog(ride); // will update Dirty flag if saved +} + void MainWindow::splitRide() { diff --git a/src/MainWindow.h b/src/MainWindow.h index 1ed4dfad7..0065eceb1 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -24,6 +24,7 @@ #include #include #include "RideItem.h" +#include "IntervalItem.h" #include "QuarqdClient.h" #include @@ -53,6 +54,7 @@ class MainWindow : public QMainWindow const RideFile *currentRide(); const RideItem *currentRideItem() { return ride; } const QTreeWidgetItem *allRideItems() { return allRides; } + const QTreeWidgetItem *allIntervalItems() { return allIntervals; } void getBSFactors(float &timeBS, float &distanceBS); QDir home; void setCriticalPower(int cp); @@ -60,6 +62,9 @@ class MainWindow : public QMainWindow RealtimeWindow *realtimeWindow; // public so config dialog can notify it of changes config const Zones *zones() const { return zones_; } + void updateRideFileIntervals(); + void saveSilent(RideItem *); + bool saveRideSingleDialog(RideItem *); RideItem *rideItem() const { return ride; } protected: @@ -75,10 +80,13 @@ class MainWindow : public QMainWindow signals: void rideSelected(); + void intervalSelected(); + void intervalsChanged(); void zonesChanged(); private slots: - void treeWidgetSelectionChanged(); + void rideTreeWidgetSelectionChanged(); + void intervalTreeWidgetSelectionChanged(); void leftLayoutMoved(); void splitterMoved(); void newCyclist(); @@ -86,13 +94,18 @@ class MainWindow : public QMainWindow void downloadRide(); void manualRide(); void exportCSV(); + void exportGC(); void importFile(); void findBestIntervals(); + void addIntervalForPowerPeaksForSecs(RideFile *ride, int windowSizeSecs, QString name); + void findPowerPeaks(); void splitRide(); void deleteRide(); void tabChanged(int index); void aboutDialog(); void notesChanged(); + void saveRide(); // save current ride menu item + bool saveRideExitDialog(); // save dirty rides on exit dialog void saveNotes(); void showOptions(); void showTools(); @@ -100,6 +113,11 @@ class MainWindow : public QMainWindow void scanForMissing(); void saveAndOpenNotes(); void dateChanged(const QDate &); + void showContextMenuPopup(const QPoint &); + void deleteInterval(); + void renameInterval(); + void zoomInterval(); + void itemChanged(QTreeWidgetItem *, int); protected: @@ -109,10 +127,13 @@ class MainWindow : public QMainWindow bool parseRideFileName(const QString &name, QString *notesFileName, QDateTime *dt); boost::shared_ptr settings; + IntervalItem *activeInterval; // currently active for context menu popup RideCalendar *calendar; QSplitter *splitter; QTreeWidget *treeWidget; + QSplitter *intervalsplitter; + QTreeWidget *intervalWidget; QTabWidget *tabWidget; RideSummaryWindow *rideSummaryWindow; AllPlotWindow *allPlotWindow; @@ -120,6 +141,7 @@ class MainWindow : public QMainWindow WeeklySummaryWindow *weeklySummaryWindow; CriticalPowerWindow *criticalPowerWindow; QTreeWidgetItem *allRides; + QTreeWidgetItem *allIntervals; QSplitter *leftLayout; QWidget *notesWidget; QVBoxLayout *notesLayout; diff --git a/src/PfPvPlot.cpp b/src/PfPvPlot.cpp index 95db4b4a0..8d42c000f 100644 --- a/src/PfPvPlot.cpp +++ b/src/PfPvPlot.cpp @@ -18,8 +18,10 @@ */ #include "PfPvPlot.h" +#include "MainWindow.h" #include "RideFile.h" #include "RideItem.h" +#include "IntervalItem.h" #include "Settings.h" #include "Zones.h" @@ -154,7 +156,7 @@ PfPvPlot::PfPvPlot() QwtSymbol sym; sym.setStyle(QwtSymbol::Ellipse); sym.setSize(6); - sym.setPen(QPen(Qt::red)); + sym.setPen(QPen(Qt::black)); sym.setBrush(QBrush(Qt::NoBrush)); curve->setSymbol(sym); @@ -269,15 +271,45 @@ PfPvPlot::refreshZoneItems() zoneLabels.append(label); } // get the zones visible, even if data may take awhile - replot(); + //replot(); } } } + +// how many intervals selected? +static int intervalCount() +{ + int highlighted; + highlighted = 0; + if (mainwindow == NULL || mainwindow->allIntervalItems() == NULL) return 0; // not inited yet! + + for (int i=0; iallIntervalItems()->childCount(); i++) { + IntervalItem *current = (IntervalItem *)mainwindow->allIntervalItems()->child(i); + if (current != NULL) { + if (current->isSelected() == true) { + ++highlighted; + } + } + } + return highlighted; +} + void PfPvPlot::setData(RideItem *_rideItem) { + // clear out any interval curves which are presently defined + if (intervalCurves.size()) { + QListIterator i(intervalCurves); + while (i.hasNext()) { + QwtPlotCurve *curve = i.next(); + curve->detach(); + delete curve; + } + } + intervalCurves.clear(); + rideItem = _rideItem; RideFile *ride = rideItem->ride; @@ -296,6 +328,13 @@ PfPvPlot::setData(RideItem *_rideItem) // Rather than pass them all to the curve, use a set to strip // out duplicates. std::set > dataSet; + std::set > dataSetSelected; + + int num_intervals=intervalCount(); + if (mergeIntervals()) + num_intervals = 1; + QVector > > dataSetInterval(num_intervals); + long tot_cad = 0; long tot_cad_points = 0; @@ -307,7 +346,11 @@ PfPvPlot::setData(RideItem *_rideItem) double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI); double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0; - dataSet.insert(std::make_pair(aepf, cpv)); + int selection = isSelected(p1); + if (selection > -1) { + dataSetInterval[selection].insert(std::make_pair(aepf, cpv)); + } else + dataSet.insert(std::make_pair(aepf, cpv)); tot_cad += p1->cad; tot_cad_points++; @@ -327,6 +370,10 @@ PfPvPlot::setData(RideItem *_rideItem) // QwtArrays needed to set the curve's data. QwtArray aepfArray; QwtArray cpvArray; + + QVector > aepfArrayInterval(num_intervals); + QVector > cpvArrayInterval(num_intervals); + std::set >::const_iterator j(dataSet.begin()); while (j != dataSet.end()) { const std::pair& dataPoint = *j; @@ -336,11 +383,62 @@ PfPvPlot::setData(RideItem *_rideItem) ++j; } + + for (int i=0;i >::const_iterator l(dataSetInterval[i].begin()); + while (l != dataSetInterval[i].end()) { + const std::pair& dataPoint = *l; + + aepfArrayInterval[i].push_back(dataPoint.first); + cpvArrayInterval[i].push_back(dataPoint.second); + + ++l; + } + } setCAD(tot_cad / tot_cad_points); curve->setData(cpvArray, aepfArray); + QwtSymbol sym; + sym.setStyle(QwtSymbol::Ellipse); + sym.setSize(6); + sym.setBrush(QBrush(Qt::NoBrush)); + + // ensure same colors are used for each interval selected + int num_intervals_defined=0; + QVector intervalmap; + if (mainwindow != NULL && mainwindow->allIntervalItems() != NULL) { + num_intervals_defined = mainwindow->allIntervalItems()->childCount(); + for (int g=0; gallIntervalItems()->childCount(); g++) { + IntervalItem *curr = (IntervalItem *)mainwindow->allIntervalItems()->child(g); + if (curr->isSelected()) intervalmap.append(g); + } + } + + for (int z = 0; z < num_intervals; z ++) { + QwtPlotCurve *curve; + curve = new QwtPlotCurve(); + + QColor intervalColor; + if (mergeIntervals()) + intervalColor = Qt::red; + else + intervalColor.setHsv((intervalmap.count() > 0 ? intervalmap.at(z) : 1) * 255/num_intervals_defined, 255,255); + + QPen pen; + pen.setColor(intervalColor); + sym.setPen(pen); + + curve->setSymbol(sym); + curve->setStyle(QwtPlotCurve::Dots); + curve->setRenderHint(QwtPlotItem::RenderAntialiased); + curve->setData(cpvArrayInterval[z],aepfArrayInterval[z]); + curve->attach(this); + + intervalCurves.append(curve); + } + // now show the data (zone shading would already be visible) curve->setVisible(true); } @@ -388,7 +486,7 @@ PfPvPlot::recalc() // an empty curve if no power (or zero power) is specified cpCurve->setData(QwtArray ::QwtArray(), QwtArray ::QwtArray()); - replot(); + //replot(); } int @@ -446,3 +544,34 @@ PfPvPlot::setShadeZones(bool value) replot(); } + +void +PfPvPlot::setMergeIntervals(bool value) +{ + merge_intervals = value; + + setData(rideItem); +} + +int +PfPvPlot::isSelected(const RideFilePoint *p) { + int highlighted=-1; // Return -1 for point not in interval + if (mainwindow!= NULL && mainwindow->allIntervalItems() != NULL) { + for (int i=0; iallIntervalItems()->childCount(); i++) { + IntervalItem *current = (IntervalItem *)mainwindow->allIntervalItems()->child(i); + if (current != NULL) { + if (current->isSelected()) { + ++highlighted; + if (p->secs>=current->start && p->secs<=current->stop) { + if (mergeIntervals()) + return 0; + return highlighted; + } + } + } + } + } + return -1; +} + + diff --git a/src/PfPvPlot.h b/src/PfPvPlot.h index ed334e7e9..1c20fd822 100644 --- a/src/PfPvPlot.h +++ b/src/PfPvPlot.h @@ -25,6 +25,7 @@ // forward references class RideFile; class RideItem; +class RideFilePoint; class QwtPlotCurve; class QwtPlotMarker; class PfPvPlotZoneLabel; @@ -52,6 +53,9 @@ class PfPvPlot : public QwtPlot bool shadeZones() const { return shade_zones; } void setShadeZones(bool value); + bool mergeIntervals() const { return merge_intervals; } + void setMergeIntervals(bool value); + public slots: signals: @@ -61,6 +65,7 @@ signals: protected: QwtPlotCurve *curve; + QList intervalCurves; QwtPlotCurve *cpCurve; QList zoneCurves; QList zoneLabels; @@ -68,11 +73,13 @@ signals: QwtPlotMarker *mY; static QwtArray contour_xvalues; // values used in CP and contour plots: djconnel + int isSelected(const RideFilePoint *p); int cp_; int cad_; double cl_; bool shade_zones; // whether to shade zones, added 27Apr2009 djconnel + bool merge_intervals; }; #endif // _GC_QaPlot_h diff --git a/src/PfPvWindow.cpp b/src/PfPvWindow.cpp index fba4b57f3..ef5baefb6 100644 --- a/src/PfPvWindow.cpp +++ b/src/PfPvWindow.cpp @@ -40,6 +40,9 @@ PfPvWindow::PfPvWindow(MainWindow *mainWindow) : shadeZonesPfPvCheckBox = new QCheckBox; shadeZonesPfPvCheckBox->setText("Shade zones"); shadeZonesPfPvCheckBox->setCheckState(Qt::Checked); + mergeIntervalPfPvCheckBox = new QCheckBox; + mergeIntervalPfPvCheckBox->setText("Merge intervals"); + mergeIntervalPfPvCheckBox->setCheckState(Qt::Unchecked); qaLayout->addWidget(qaCPLabel); qaLayout->addWidget(qaCPValue); @@ -48,6 +51,7 @@ PfPvWindow::PfPvWindow(MainWindow *mainWindow) : qaLayout->addWidget(qaClLabel); qaLayout->addWidget(qaClValue); qaLayout->addWidget(shadeZonesPfPvCheckBox); + qaLayout->addWidget(mergeIntervalPfPvCheckBox); vlayout->addWidget(pfPvPlot); vlayout->addLayout(qaLayout); @@ -67,7 +71,10 @@ PfPvWindow::PfPvWindow(MainWindow *mainWindow) : this, SLOT(setQaCLFromLineEdit())); connect(shadeZonesPfPvCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setShadeZonesPfPvFromCheckBox())); + connect(mergeIntervalPfPvCheckBox, SIGNAL(stateChanged(int)), + this, SLOT(setMergeIntervalsPfPvFromCheckBox())); connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected())); + connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged())); } @@ -82,6 +89,15 @@ PfPvWindow::rideSelected() qaCPValue->setText(QString("%1").arg(pfPvPlot->getCP())); } +void +PfPvWindow::intervalSelected() +{ + RideItem *ride = mainWindow->rideItem(); + if (!ride) return; + pfPvPlot->setData(ride); + +} + void PfPvWindow::zonesChanged() { @@ -98,6 +114,14 @@ PfPvWindow::setShadeZonesPfPvFromCheckBox() } } +void +PfPvWindow::setMergeIntervalsPfPvFromCheckBox() +{ + if (pfPvPlot->mergeIntervals() != mergeIntervalPfPvCheckBox->isChecked()) { + pfPvPlot->setMergeIntervals(mergeIntervalPfPvCheckBox->isChecked()); + } +} + void PfPvWindow::setQaCPFromLineEdit() { diff --git a/src/PfPvWindow.h b/src/PfPvWindow.h index 0bd22df9b..eda2bde7c 100644 --- a/src/PfPvWindow.h +++ b/src/PfPvWindow.h @@ -38,6 +38,7 @@ class PfPvWindow : public QWidget public slots: void rideSelected(); + void intervalSelected(); void zonesChanged(); protected slots: @@ -46,12 +47,14 @@ class PfPvWindow : public QWidget void setQaCADFromLineEdit(); void setQaCLFromLineEdit(); void setShadeZonesPfPvFromCheckBox(); + void setMergeIntervalsPfPvFromCheckBox(); protected: MainWindow *mainWindow; PfPvPlot *pfPvPlot; QCheckBox *shadeZonesPfPvCheckBox; + QCheckBox *mergeIntervalPfPvCheckBox; QLineEdit *qaCPValue; QLineEdit *qaCadValue; QLineEdit *qaClValue; diff --git a/src/PowerHist.cpp b/src/PowerHist.cpp index 5dc881347..9be74abca 100644 --- a/src/PowerHist.cpp +++ b/src/PowerHist.cpp @@ -1,23 +1,25 @@ -/* +/* * Copyright (c) 2006 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 "PowerHist.h" +#include "MainWindow.h" #include "RideItem.h" +#include "IntervalItem.h" #include "RideFile.h" #include "Settings.h" #include "Zones.h" @@ -43,14 +45,14 @@ class penTooltip: public QwtPlotZoomer //setTrackerMode(AlwaysOn); setTrackerMode(AlwaysOff); } - + virtual QwtText trackerText(const QwtDoublePoint &pos) const { QColor bg(Qt::white); #if QT_VERSION >= 0x040300 bg.setAlpha(200); #endif - + QwtText text = QString("%1").arg((int)pos.x()); text.setBackgroundBrush( QBrush( bg )); return text; @@ -204,11 +206,11 @@ PowerHist::PowerHist(): unit(0), lny(false) { - + boost::shared_ptr settings = GetApplicationSettings(); - + unit = settings->value(GC_UNIT); - + useMetricUnits = (unit.toString() == "Metric"); // create a background object for shading @@ -219,7 +221,7 @@ PowerHist::PowerHist(): setParameterAxisTitle(); setAxisTitle(yLeft, "Cumulative Time (minutes)"); - + curve = new QwtPlotCurve(""); curve->setStyle(QwtPlotCurve::Steps); curve->setRenderHint(QwtPlotItem::RenderAntialiased); @@ -232,6 +234,18 @@ PowerHist::PowerHist(): delete pen; curve->attach(this); + curveSelected = new QwtPlotCurve(""); + curveSelected->setStyle(QwtPlotCurve::Steps); + curveSelected->setRenderHint(QwtPlotItem::RenderAntialiased); + pen = new QPen(Qt::blue); + pen->setWidth(2.0); + curveSelected->setPen(*pen); + brush_color = Qt::blue; + brush_color.setAlpha(64); + curveSelected->setBrush(brush_color); // fill below the line + delete pen; + curveSelected->attach(this); + grid = new QwtPlotGrid(); grid->enableX(false); QPen gridPen; @@ -247,6 +261,7 @@ PowerHist::PowerHist(): PowerHist::~PowerHist() { delete bg; delete curve; + delete curveSelected; delete grid; } @@ -270,7 +285,7 @@ PowerHist::refreshZoneLabels() { // delete any existing power zone labels if (zoneLabels.size()) { - QListIterator i(zoneLabels); + QListIterator i(zoneLabels); while (i.hasNext()) { PowerHistZoneLabel *label = i.next(); label->detach(); @@ -302,6 +317,7 @@ void PowerHist::recalc() { QVector *array; + QVector *selectedArray; int arrayLength = 0; double delta; @@ -315,26 +331,31 @@ PowerHist::recalc() array = &wattsArray; delta = wattsDelta; arrayLength = wattsArray.size(); + selectedArray = &wattsSelectedArray; } else if (selected == nm) { array = &nmArray; delta = nmDelta; arrayLength = nmArray.size(); + selectedArray = &nmSelectedArray; } else if (selected == hr) { array = &hrArray; delta = hrDelta; arrayLength = hrArray.size(); + selectedArray = &hrSelectedArray; } else if (selected == kph) { array = &kphArray; delta = kphDelta; arrayLength = kphArray.size(); + selectedArray = &kphSelectedArray; } else if (selected == cad) { array = &cadArray; delta = cadDelta; arrayLength = cadArray.size(); + selectedArray = &cadSelectedArray; } if (!array) @@ -345,6 +366,7 @@ PowerHist::recalc() // allocate space for data, plus beginning and ending point QVector parameterValue(count+2); QVector totalTime(count+2); + QVector totalTimeSelected(count+2); int i; for (i = 1; i <= count; ++i) { int high = i * binw; @@ -353,14 +375,19 @@ PowerHist::recalc() low++; parameterValue[i] = high * delta; totalTime[i] = 1e-9; // nonzero to accomodate log plot - while (low < high) + totalTimeSelected[i] = 1e-9; // nonzero to accomodate log plot + while (low < high) { + if (selectedArray && (*selectedArray).size()>low) + totalTimeSelected[i] += dt * (*selectedArray)[low]; totalTime[i] += dt * (*array)[low++]; + } } totalTime[i] = 1e-9; // nonzero to accomodate log plot parameterValue[i] = i * delta * binw; totalTime[0] = 1e-9; parameterValue[0] = 0; curve->setData(parameterValue.data(), totalTime.data(), count + 2); + curveSelected->setData(parameterValue.data(), totalTimeSelected.data(), count + 2); setAxisScale(xBottom, 0.0, parameterValue[count + 1]); refreshZoneLabels(); @@ -370,7 +397,7 @@ PowerHist::recalc() } void -PowerHist::setYMax() +PowerHist::setYMax() { static const double tmin = 1.0/60; setAxisScale(yLeft, (lny ? tmin : 0.0), curve->maxYValue() * 1.1); @@ -384,12 +411,12 @@ PowerHist::setData(RideItem *_rideItem) RideFile *ride = rideItem->ride; if (ride) { - setTitle(ride->startTime().toString(GC_DATETIME_FORMAT)); + setTitle(ride->startTime().toString(GC_DATETIME_FORMAT)); - static const int maxSize = 4096; + static const int maxSize = 4096; - // recording interval in minutes - dt = ride->recIntSecs() / 60.0; + // recording interval in minutes + dt = ride->recIntSecs() / 60.0; wattsArray.resize(0); nmArray.resize(0); @@ -397,46 +424,85 @@ PowerHist::setData(RideItem *_rideItem) kphArray.resize(0); cadArray.resize(0); - // unit conversion factor for imperial units for selected parameters - double torque_factor = (useMetricUnits ? 1.0 : 0.73756215); - double speed_factor = (useMetricUnits ? 1.0 : 0.62137119); + wattsSelectedArray.resize(0); + nmSelectedArray.resize(0); + hrSelectedArray.resize(0); + kphSelectedArray.resize(0); + cadSelectedArray.resize(0); + + // unit conversion factor for imperial units for selected parameters + double torque_factor = (useMetricUnits ? 1.0 : 0.73756215); + double speed_factor = (useMetricUnits ? 1.0 : 0.62137119); foreach(const RideFilePoint *p1, ride->dataPoints()) { + bool selected = isSelected(p1); - int wattsIndex = int(floor(p1->watts / wattsDelta)); - if (wattsIndex >= 0 && wattsIndex < maxSize) { - if (wattsIndex >= wattsArray.size()) - wattsArray.resize(wattsIndex + 1); - wattsArray[wattsIndex]++; - } + int wattsIndex = int(floor(p1->watts / wattsDelta)); + if (wattsIndex >= 0 && wattsIndex < maxSize) { + if (wattsIndex >= wattsArray.size()) + wattsArray.resize(wattsIndex + 1); + wattsArray[wattsIndex]++; + + if (selected) { + if (wattsIndex >= wattsSelectedArray.size()) + wattsSelectedArray.resize(wattsIndex + 1); + wattsSelectedArray[wattsIndex]++; + } + } int nmIndex = int(floor(p1->nm * torque_factor / nmDelta)); if (nmIndex >= 0 && nmIndex < maxSize) { if (nmIndex >= nmArray.size()) nmArray.resize(nmIndex + 1); nmArray[nmIndex]++; + + if (selected) { + if (nmIndex >= nmSelectedArray.size()) + nmSelectedArray.resize(nmIndex + 1); + nmSelectedArray[nmIndex]++; + } } int hrIndex = int(floor(p1->hr / hrDelta)); if (hrIndex >= 0 && hrIndex < maxSize) { - if (hrIndex >= hrArray.size()) - hrArray.resize(hrIndex + 1); - hrArray[hrIndex]++; + if (hrIndex >= hrArray.size()) + hrArray.resize(hrIndex + 1); + hrArray[hrIndex]++; + + if (selected) { + if (hrIndex >= hrSelectedArray.size()) + hrSelectedArray.resize(hrIndex + 1); + hrSelectedArray[hrIndex]++; + } } int kphIndex = int(floor(p1->kph * speed_factor / kphDelta)); if (kphIndex >= 0 && kphIndex < maxSize) { - if (kphIndex >= kphArray.size()) - kphArray.resize(kphIndex + 1); - kphArray[kphIndex]++; + if (kphIndex >= kphArray.size()) + kphArray.resize(kphIndex + 1); + kphArray[kphIndex]++; + + if (selected) { + if (kphIndex >= kphSelectedArray.size()) + kphSelectedArray.resize(kphIndex + 1); + kphSelectedArray[kphIndex]++; + } } int cadIndex = int(floor(p1->cad / cadDelta)); if (cadIndex >= 0 && cadIndex < maxSize) { - if (cadIndex >= cadArray.size()) - cadArray.resize(cadIndex + 1); - cadArray[cadIndex]++; + if (cadIndex >= cadArray.size()) + cadArray.resize(cadIndex + 1); + cadArray[cadIndex]++; + + if (selected) { + if (cadIndex >= cadSelectedArray.size()) + cadSelectedArray.resize(cadIndex + 1); + cadSelectedArray[cadIndex]++; + } } + + } recalc(); @@ -602,7 +668,7 @@ PowerHist::fixSelection() { else s = hr; } - + else if (s == hr) { if (ride->areDataPresent()->hr) @@ -610,7 +676,7 @@ PowerHist::fixSelection() { else s = kph; } - + else if (s == kph) { if (ride->areDataPresent()->kph) @@ -618,7 +684,7 @@ PowerHist::fixSelection() { else s = cad; } - + else if (s == cad) { if (ride->areDataPresent()->cad) @@ -636,5 +702,19 @@ bool PowerHist::shadeZones() const rideItem && rideItem->ride && selected == wattsShaded - ); -} + ); +} + +bool PowerHist::isSelected(const RideFilePoint *p) { + if (mainwindow!= NULL && mainwindow->allIntervalItems() != NULL) { + for (int i=0; iallIntervalItems()->childCount(); i++) { + IntervalItem *current = (IntervalItem *)mainwindow->allIntervalItems()->child(i); + if (current != NULL) { + if (current->isSelected() && p->secs>=current->start && p->secs<=current->stop) { + return true; + } + } + } + } + return false; +} diff --git a/src/PowerHist.h b/src/PowerHist.h index 4115a4161..3db5dbb1d 100644 --- a/src/PowerHist.h +++ b/src/PowerHist.h @@ -26,6 +26,7 @@ class QwtPlotCurve; class QwtPlotGrid; class RideItem; +class RideFilePoint; class PowerHistBackground; class PowerHistZoneLabel; class QwtPlotZoomer; @@ -36,7 +37,7 @@ class PowerHist : public QwtPlot public: - QwtPlotCurve *curve; + QwtPlotCurve *curve, *curveSelected; QList zoneLabels; PowerHist(); @@ -81,13 +82,21 @@ class PowerHist : public QwtPlot QwtPlotGrid *grid; - // storage for data counts + // storage for data counts QVector - wattsArray, - nmArray, - hrArray, - kphArray, - cadArray; + wattsArray, + nmArray, + hrArray, + kphArray, + cadArray; + + // storage for data counts in interval selected + QVector + wattsSelectedArray, + nmSelectedArray, + hrSelectedArray, + kphSelectedArray, + cadSelectedArray; int binw; @@ -120,7 +129,7 @@ class PowerHist : public QwtPlot static const int cadDigits = 0; void setParameterAxisTitle(); - + bool isSelected(const RideFilePoint *p); bool useMetricUnits; // whether metric units are used (or imperial) }; diff --git a/src/RideFile.cpp b/src/RideFile.cpp index 4c9275fe2..7310ec4eb 100644 --- a/src/RideFile.cpp +++ b/src/RideFile.cpp @@ -31,6 +31,11 @@ interval = point->interval; \ start = point->secs; \ } +void +RideFile::clearIntervals() +{ + intervals_.clear(); +} void RideFile::fillInIntervals() @@ -194,3 +199,49 @@ void RideFile::appendPoint(double secs, double cad, double hr, double km, dataPresent.alt |= (alt != 0); dataPresent.interval |= (interval != 0); } + +double +RideFile::distanceToTime(double km) +{ + // inefficient but robust - iterate over points until + // you have gone past the km desired. + // rounded to the nearest data point - no smoothing + for (int i=0; ikm >= km) return dataPoints_.at(i)->secs; + } + return 0; +} + +double +RideFile::timeToDistance(double secs) +{ + // inefficient but robust - iterate over points until + // you have gone past the km desired. + // rounded to the nearest data point - no smoothing + RideFilePoint *midp, *leftp, *rightp; + + if (dataPoints_.count() == 0) return 0; + + int left =0; + int right=dataPoints_.count()-1; + int middle; + while (right-left > 1) { + middle = left + ((right - left ) / 2); + + midp = dataPoints_.at(middle); + leftp = dataPoints_.at(left); + rightp = dataPoints_.at(right); + + if (leftp->secs >= secs) return leftp->km; + if (rightp->secs <= secs) return rightp->km; + if (midp->secs == secs) return midp->km; + + if (midp->secs > secs) right = middle; + else if (midp->secs < secs) left = middle; + } + + // just in case it is between points + double leftdelta = secs - leftp->secs; + double rightdelta = rightp->secs - secs; + return (leftdelta > rightdelta) ? rightp->km : leftp->km; +} diff --git a/src/RideFile.h b/src/RideFile.h index 312f9c135..4f39c6ac7 100644 --- a/src/RideFile.h +++ b/src/RideFile.h @@ -113,12 +113,16 @@ class RideFile void addInterval(double start, double stop, const QString &name) { intervals_.append(RideFileInterval(start, stop, name)); } + void clearIntervals(); void fillInIntervals(); int intervalBegin(const RideFileInterval &interval) const; void writeAsCsv(QFile &file, bool bIsMetric) const; void resetDataPresent(); + + double distanceToTime(double); // get distance km at time secs + double timeToDistance(double); // get time secs at distance km }; struct RideFileReader { diff --git a/src/RideItem.cpp b/src/RideItem.cpp index 30ad33c03..84196d8e4 100644 --- a/src/RideItem.cpp +++ b/src/RideItem.cpp @@ -29,6 +29,7 @@ RideItem::RideItem(int type, QTreeWidgetItem(type), path(path), fileName(fileName), dateTime(dateTime), ride(NULL), zones(zones), notesFileName(notesFileName) { + isdirty = false; setText(0, dateTime.toString("ddd")); setText(1, dateTime.toString("MMM d, yyyy")); setText(2, dateTime.toString("h:mm AP")); @@ -45,6 +46,39 @@ RideItem::~RideItem() } } +void +RideItem::setDirty(bool val) +{ + isdirty = val; + + if (isdirty == true) { + + // show ride in bold on the list view + for (int i=0; i<3; i++) { + QFont current = font(i); + current.setWeight(QFont::Black); + setFont(i, current); + } + + } else { + + // show ride in normal on the list view + for (int i=0; i<3; i++) { + QFont current = font(i); + current.setWeight(QFont::Normal); + setFont(i, current); + } + } +} + +// name gets changed when file is converted in save +void +RideItem::setFileName(QString path, QString fileName) +{ + this->path = path; + this->fileName = fileName; +} + int RideItem::zoneRange() { return zones->whichRange(dateTime.date()); @@ -144,4 +178,3 @@ RideItem::computeMetrics() } } - diff --git a/src/RideItem.h b/src/RideItem.h index ed2327859..54c061b79 100644 --- a/src/RideItem.h +++ b/src/RideItem.h @@ -30,6 +30,7 @@ class RideItem : public QTreeWidgetItem { protected: QVector time_in_zone; + bool isdirty; public: @@ -52,6 +53,9 @@ class RideItem : public QTreeWidgetItem { ~RideItem(); + void setDirty(bool); + bool isDirty() { return isdirty; } + void setFileName(QString, QString); void computeMetrics(); void freeMemory(); @@ -59,6 +63,5 @@ class RideItem : public QTreeWidgetItem { int numZones(); double timeInZone(int zone); }; - #endif // _GC_RideItem_h diff --git a/src/RideSummaryWindow.cpp b/src/RideSummaryWindow.cpp index 854863bc3..140cd2a8b 100644 --- a/src/RideSummaryWindow.cpp +++ b/src/RideSummaryWindow.cpp @@ -37,8 +37,10 @@ RideSummaryWindow::RideSummaryWindow(MainWindow *mainWindow) : rideSummary = new QTextEdit(this); rideSummary->setReadOnly(true); vlayout->addWidget(rideSummary); + connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(refresh())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(refresh())); + connect(mainWindow, SIGNAL(intervalsChanged()), this, SLOT(refresh())); setLayout(vlayout); } @@ -161,7 +163,9 @@ RideSummaryWindow::htmlSummary() const RideItem *rideItem = mainWindow->rideItem(); QFile file(rideItem->path + "/" + rideItem->fileName); QStringList errors; - RideFile *ride = RideFileFactory::instance().openRideFile(file, errors); + RideFile *ride = rideItem->ride; + if (!ride) + ride = RideFileFactory::instance().openRideFile(file, errors); if (!ride) { summary = "

Couldn't read file \"" + file.fileName() + "\":"; QListIterator i(errors); @@ -314,7 +318,7 @@ RideSummaryWindow::htmlSummary() const // and an integer < 30 when in an interval. // We'll need to create a counter for the intervals // rather than relying on the final data point's interval number. - if (ride->intervals().size() > 1) { + if (ride->intervals().size() > 0) { summary += "

Intervals

\n

\n"; summary += " settings = GetApplicationSettings(); + QVariant warnsetting = settings->value(GC_WARNCONVERT); + if (warnsetting.isNull()) setting = true; + else setting = warnsetting.toBool(); + return setting; +} + +void +setWarnOnConvert(bool setting) +{ + boost::shared_ptr settings = GetApplicationSettings(); + settings->setValue(GC_WARNCONVERT, setting); +} + +static bool +warnExit() +{ + bool setting; + + boost::shared_ptr settings = GetApplicationSettings(); + QVariant warnsetting = settings->value(GC_WARNEXIT); + if (warnsetting.isNull()) setting = true; + else setting = warnsetting.toBool(); + return setting; +} + +void +setWarnExit(bool setting) +{ + boost::shared_ptr settings = GetApplicationSettings(); + settings->setValue(GC_WARNEXIT, setting); +} + +//---------------------------------------------------------------------- +// User selected Save... menu option, prompt if conversion is needed +//---------------------------------------------------------------------- +bool +MainWindow::saveRideSingleDialog(RideItem *rideItem) +{ + if (rideItem->isDirty() == false) return false; // nothing to save you must be a ^S addict. + + // get file type + QFile currentFile(rideItem->path + QDir::separator() + rideItem->fileName); + QFileInfo currentFI(currentFile); + QString currentType = currentFI.completeSuffix().toUpper(); + + // either prompt etc, or just save that file away! + if (currentType != "GC" && warnOnConvert() == true) { + SaveSingleDialogWidget *dialog = new SaveSingleDialogWidget(this, rideItem); + dialog->exec(); + return true; + } else { + // go for it, the user doesn't want warnings! + saveSilent(rideItem); + return true; + } +} + +//---------------------------------------------------------------------- +// Check if data needs saving on exit and prompt user for action +//---------------------------------------------------------------------- +bool +MainWindow::saveRideExitDialog() +{ + QList dirtyList; + + // have we been told to not warn on exit? + if (warnExit() == false) return true; // just close regardless! + + for (int i=0; ichildCount(); i++) { + RideItem *curr = (RideItem *)allRides->child(i); + if (curr->isDirty() == true) dirtyList.append(curr); + } + + // we have some files to save... + if (dirtyList.count() > 0) { + SaveOnExitDialogWidget *dialog = new SaveOnExitDialogWidget(this, dirtyList); + int result = dialog->exec(); + if (result == QDialog::Rejected) return false; // cancel that closeEvent! + } + + // You can exit and close now + return true; +} + +//---------------------------------------------------------------------- +// Silently save ride and convert to GC format without warning user +//---------------------------------------------------------------------- +void +MainWindow::saveSilent(RideItem *rideItem) +{ + QFile currentFile(rideItem->path + QDir::separator() + rideItem->fileName); + QFileInfo currentFI(currentFile); + QString currentType = currentFI.completeSuffix().toUpper(); + QFile savedFile; + bool convert; + + // Do we need to convert the file type? + if (currentType != "GC") convert = true; + else convert = false; + + // set target filename + if (convert) { + // rename the source + savedFile.setFileName(currentFI.path() + QDir::separator() + currentFI.baseName() + ".gc"); + } else { + savedFile.setFileName(currentFile.fileName()); + } + + // save in GC format + GcFileReader reader; + reader.writeRideFile(rideItem->ride, savedFile); + + // rename the file and update the rideItem list to reflect the change + if (convert) { + + // rename on disk + currentFile.rename(currentFile.fileName(), currentFile.fileName() + ".sav"); + + // rename in memory + rideItem->setFileName(QFileInfo(savedFile).path(), QFileInfo(savedFile).fileName()); + + // refresh summary to show new file type + //rideSummary->clear(); + //rideItem->clearSummary(); // clear cached value + //rideSummary->setHtml(rideItem->htmlSummary()); + //rideSummary->setAlignment(Qt::AlignCenter); + } + + // mark clean as we have now saved the data + rideItem->setDirty(false); +} + +//---------------------------------------------------------------------- +// Save Single File Dialog Widget +//---------------------------------------------------------------------- +SaveSingleDialogWidget::SaveSingleDialogWidget(QWidget *parent, RideItem *rideItem) : QDialog(parent, Qt::Dialog) +{ + this->rideItem = rideItem; + setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle("Save and Conversion"); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Warning text + warnText = new QLabel(tr("WARNING\n\nYou have made changes to ") + rideItem->fileName + tr(" If you want to save\nthem, we need to convert the ride to GoldenCheetah\'s\nnative format. Should we do so?\n")); + mainLayout->addWidget(warnText); + + // Buttons + QHBoxLayout *buttonLayout = new QHBoxLayout; + saveButton = new QPushButton(tr("&Save and Convert"), this); + buttonLayout->addWidget(saveButton); + abandonButton = new QPushButton(tr("&Discard Changes"), this); + buttonLayout->addWidget(abandonButton); + cancelButton = new QPushButton(tr("&Cancel Save"), this); + buttonLayout->addWidget(cancelButton); + mainLayout->addLayout(buttonLayout); + + // Don't warn me! + warnCheckBox = new QCheckBox(tr("Always warn me about file conversions"), this); + warnCheckBox->setChecked(true); + mainLayout->addWidget(warnCheckBox); + + // connect up slots + connect(saveButton, SIGNAL(clicked()), this, SLOT(saveClicked())); + connect(abandonButton, SIGNAL(clicked()), this, SLOT(abandonClicked())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); + connect(warnCheckBox, SIGNAL(clicked()), this, SLOT(warnSettingClicked())); +} + +void +SaveSingleDialogWidget::saveClicked() +{ + mainwindow->saveSilent(rideItem); + accept(); +} + +void +SaveSingleDialogWidget::abandonClicked() +{ + rideItem->setDirty(false); // lose changes + reject(); +} + +void +SaveSingleDialogWidget::cancelClicked() +{ + reject(); +} + +void +SaveSingleDialogWidget::warnSettingClicked() +{ + setWarnOnConvert(warnCheckBox->isChecked()); +} + +//---------------------------------------------------------------------- +// Save on Exit File Dialog Widget +//---------------------------------------------------------------------- + +SaveOnExitDialogWidget::SaveOnExitDialogWidget(QWidget *parent, QListdirtyList) : QDialog(parent, Qt::Dialog) +{ + this->dirtyList = dirtyList; + setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle("Save Changes"); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Warning text + warnText = new QLabel(tr("WARNING\n\nYou have made changes to some rides which\nhave not been saved. They are listed below.")); + mainLayout->addWidget(warnText); + + // File List + dirtyFiles = new QTableWidget(dirtyList.count(), 0, this); + dirtyFiles->setColumnCount(2); + dirtyFiles->horizontalHeader()->hide(); + dirtyFiles->verticalHeader()->hide(); + + // Populate with dirty List + for (int i=0; isetCheckState(Qt::Checked); + dirtyFiles->setCellWidget(i,0,c); + + // filename + QTableWidgetItem *t = new QTableWidgetItem; + t->setText(dirtyList.at(i)->fileName); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + dirtyFiles->setItem(i,1,t); + } + + // prettify the list + dirtyFiles->setShowGrid(false); + dirtyFiles->resizeColumnToContents(0); + dirtyFiles->resizeColumnToContents(1); + mainLayout->addWidget(dirtyFiles); + + // Buttons + QHBoxLayout *buttonLayout = new QHBoxLayout; + saveButton = new QPushButton(tr("&Save and Exit"), this); + buttonLayout->addWidget(saveButton); + abandonButton = new QPushButton(tr("&Discard and Exit"), this); + buttonLayout->addWidget(abandonButton); + cancelButton = new QPushButton(tr("&Cancel Exit"), this); + buttonLayout->addWidget(cancelButton); + mainLayout->addLayout(buttonLayout); + + // Don't warn me! + exitWarnCheckBox = new QCheckBox(tr("Always check for unsaved chages on exit"), this); + exitWarnCheckBox->setChecked(true); + mainLayout->addWidget(exitWarnCheckBox); + + // connect up slots + connect(saveButton, SIGNAL(clicked()), this, SLOT(saveClicked())); + connect(abandonButton, SIGNAL(clicked()), this, SLOT(abandonClicked())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); + connect(exitWarnCheckBox, SIGNAL(clicked()), this, SLOT(warnSettingClicked())); +} + +void +SaveOnExitDialogWidget::saveClicked() +{ + // whizz through the list and save one by one using + // singleSave to ensure warnings are given if neccessary + for (int i=0; icellWidget(i,0); + if (c->isChecked()) { + mainwindow->saveRideSingleDialog(dirtyList.at(i)); + } + } + accept(); +} + +void +SaveOnExitDialogWidget::abandonClicked() +{ + accept(); +} + +void +SaveOnExitDialogWidget::cancelClicked() +{ + reject(); +} + +void +SaveOnExitDialogWidget::warnSettingClicked() +{ + setWarnExit(exitWarnCheckBox->isChecked()); +} diff --git a/src/SaveDialogs.h b/src/SaveDialogs.h new file mode 100644 index 000000000..72e826976 --- /dev/null +++ b/src/SaveDialogs.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2009 Mark Liversedge (liversedge@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_SaveDialogs_h +#define _GC_SaveDialogs_h 1 + +#include +#include +#include "RideItem.h" + +class MainWindow; + +class SaveSingleDialogWidget : public QDialog +{ + Q_OBJECT + + public: + SaveSingleDialogWidget(QWidget *, RideItem *); +// QWidget *parent; + + public slots: + void saveClicked(); + void abandonClicked(); + void cancelClicked(); + void warnSettingClicked(); + + private: + + RideItem *rideItem; + QPushButton *saveButton, *abandonButton, *cancelButton; + QCheckBox *warnCheckBox; + QLabel *warnText; +}; + +class SaveOnExitDialogWidget : public QDialog +{ + Q_OBJECT + + public: + SaveOnExitDialogWidget(QWidget *, QList); + + public slots: + void saveClicked(); + void abandonClicked(); + void cancelClicked(); + void warnSettingClicked(); + + private: + QListdirtyList; + QPushButton *saveButton, *abandonButton, *cancelButton; + QCheckBox *exitWarnCheckBox; + QLabel *warnText; + + QTableWidget *dirtyFiles; +}; + +#endif // _GC_SaveDialogs_h diff --git a/src/Settings.h b/src/Settings.h index 80b7fe6ca..b983a0a59 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -46,6 +46,8 @@ #define GC_LTS_ACRONYM "LTS" #define GC_SB_NAME "SBname" #define GC_SB_ACRONYM "SB" +#define GC_WARNCONVERT "warnconvert" +#define GC_WARNEXIT "warnexit" // device Configurations NAME/SPEC/TYPE/DEFI/DEFR all get a number appended // to them to specify which configured device i.e. devices1 ... devicesn where diff --git a/src/WkoRideFile.cpp b/src/WkoRideFile.cpp index 75d596f0b..29e859af4 100644 --- a/src/WkoRideFile.cpp +++ b/src/WkoRideFile.cpp @@ -67,6 +67,7 @@ // local holding varables shared between WkoParseHeaderData() and WkoParseRawData() static WKO_ULONG WKO_device; // Device ID used for this workout static char WKO_GRAPHS[32]; // GRAPHS available in this workout +static QList references; // saved with data point references static int wkoFileReaderRegistered = RideFileFactory::instance().registerReader( @@ -119,6 +120,29 @@ RideFile *WkoFileReader::openRideFile(QFile &file, QStringList &errors) const if (rawdata) footerdata = WkoParseRawData(rawdata, rideFile, errors); else return NULL; + // Post process the ride intervals to convert from point + // offsets to time in seconds + QVector datapoints = rideFile->dataPoints(); + for (int i=0; iname; + + if (references.at(i)->start < datapoints.count()) + add.start = datapoints.at(references.at(i)->start)->secs; + else + continue; // out of bounds + + if (references.at(i)->stop < datapoints.count()) + add.stop = datapoints.at(references.at(i)->stop)->secs; + else + continue; // out of bounds + + rideFile->addInterval(add.start, add.stop, add.name); + } + references.clear(); + if (footerdata) return (RideFile *)rideFile; else return NULL; } @@ -620,8 +644,17 @@ WKO_UCHAR *WkoParseHeaderData(QString fname, WKO_UCHAR *fb, RideFile *rideFile, /* Ranges */ + + // The ranges are stored as references to data points + // whilst the RideFileInterval structure uses start and stop + // in seconds. We cannot translate from one to the other at this + // point because the raw data has not been parsed yet. + // So intervals are created here with point references + // and they are post-processed after the call to + // WkoParseRawData in openRideFile above. p += doshort(p, &us); /* 237: Number of ranges XXVARIABLEXX */ for (i=0; iname = reinterpret_cast(&txtbuf[0]); p += dotext(p, &txtbuf[0]); - p += 24; - //p += donumber(p, &ul); - //p += donumber(p, &ul); - //p += donumber(p, &ul); + p += donumber(p, &ul); + p += donumber(p, &ul); + add->start = ul; + + p += donumber(p, &ul); + add->stop = ul; + + p += 12; //p += donumber(p, &ul); //p += donumber(p, &ul); //p += donumber(p, &ul); + + // add to intervals - referencing data point for interval + references.append(add); } /*************************************************** diff --git a/src/src.pro b/src/src.pro index 129a7124b..eb97c6103 100644 --- a/src/src.pro +++ b/src/src.pro @@ -64,7 +64,9 @@ HEADERS += \ DownloadRideDialog.h \ ErgFile.h \ ErgFilePlot.h \ + GcRideFile.h \ HistogramWindow.h \ + IntervalItem.h \ LogTimeScaleDraw.h \ LogTimeScaleEngine.h \ MainWindow.h \ @@ -94,6 +96,7 @@ HEADERS += \ RideImportWizard.h \ RideItem.h \ RideMetric.h \ + SaveDialogs.h \ RideSummaryWindow.h \ Season.h \ SeasonParser.h \ @@ -138,7 +141,9 @@ SOURCES += \ DownloadRideDialog.cpp \ ErgFile.cpp \ ErgFilePlot.cpp \ + GcRideFile.cpp \ HistogramWindow.cpp \ + IntervalItem.cpp \ LogTimeScaleDraw.cpp \ LogTimeScaleEngine.cpp \ MainWindow.cpp \ @@ -168,6 +173,7 @@ SOURCES += \ RideImportWizard.cpp \ RideItem.cpp \ RideMetric.cpp \ + SaveDialogs.cpp \ RideSummaryWindow.cpp \ Season.cpp \ SeasonParser.cpp \