diff --git a/src/HistogramWindow.cpp b/src/HistogramWindow.cpp index 70f45fd92..9aaa3a9d5 100644 --- a/src/HistogramWindow.cpp +++ b/src/HistogramWindow.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net) + * 2011 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 @@ -29,8 +30,7 @@ #include "Zones.h" #include "HrZones.h" -HistogramWindow::HistogramWindow(MainWindow *mainWindow) : - GcWindow(mainWindow), mainWindow(mainWindow) +HistogramWindow::HistogramWindow(MainWindow *mainWindow) : GcWindow(mainWindow), mainWindow(mainWindow), source(NULL) { setInstanceName("Histogram Window"); @@ -60,119 +60,141 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) : binWidthLayout->addWidget(binWidthSlider); cl->addLayout(binWidthLayout); - lnYHistCheckBox = new QCheckBox; - lnYHistCheckBox->setText(tr("Log Y")); - cl->addWidget(lnYHistCheckBox); + showLnY = new QCheckBox; + showLnY->setText(tr("Log Y")); + cl->addWidget(showLnY); - withZerosCheckBox = new QCheckBox; - withZerosCheckBox->setText(tr("With zeros")); - cl->addWidget(withZerosCheckBox); + showZeroes = new QCheckBox; + showZeroes->setText(tr("With zeros")); + cl->addWidget(showZeroes); - histShadeZones = new QCheckBox; - histShadeZones->setText(tr("Shade zones")); - cl->addWidget(histShadeZones); + shadeZones = new QCheckBox; + shadeZones->setText(tr("Shade zones")); + shadeZones->setChecked(powerHist->shade); + cl->addWidget(shadeZones); - histParameterCombo = new QComboBox(); - histParameterCombo->addItem(tr("Watts")); - histParameterCombo->addItem(tr("Watts (by Zone)")); - histParameterCombo->addItem(tr("Torque")); - histParameterCombo->addItem(tr("Heartrate")); - histParameterCombo->addItem(tr("Heartrate (by Zone)")); - histParameterCombo->addItem(tr("Speed")); - histParameterCombo->addItem(tr("Cadence")); - histParameterCombo->setCurrentIndex(0); - cl->addWidget(histParameterCombo); + showInZones = new QCheckBox; + showInZones->setText(tr("Show in zones")); + cl->addWidget(showInZones); - histSumY = new QComboBox(); - histSumY->addItem(tr("Absolute Time")); - histSumY->addItem(tr("Percentage Time")); - cComboSeason = new QComboBox(this); + seriesCombo = new QComboBox(); + addSeries(); + cl->addWidget(seriesCombo); + + showSumY = new QComboBox(); + showSumY->addItem(tr("Absolute Time")); + showSumY->addItem(tr("Percentage Time")); + seasonCombo = new QComboBox(this); addSeasons(); - cl->addWidget(histSumY); - cl->addWidget(cComboSeason); + seasonCombo->setCurrentIndex(0); // default to current ride selected + cl->addWidget(showSumY); + cl->addWidget(seasonCombo); cl->addStretch(); // sort out default values setHistTextValidator(); - lnYHistCheckBox->setChecked(powerHist->islnY()); - withZerosCheckBox->setChecked(powerHist->withZeros()); + showLnY->setChecked(powerHist->islnY()); + showZeroes->setChecked(powerHist->withZeros()); binWidthSlider->setValue(powerHist->binWidth()); setHistBinWidthText(); - connect(binWidthSlider, SIGNAL(valueChanged(int)), - this, SLOT(setBinWidthFromSlider())); - connect(binWidthLineEdit, SIGNAL(editingFinished()), - this, SLOT(setBinWidthFromLineEdit())); - connect(lnYHistCheckBox, SIGNAL(stateChanged(int)), - this, SLOT(setlnYHistFromCheckBox())); - connect(withZerosCheckBox, SIGNAL(stateChanged(int)), - this, SLOT(setWithZerosFromCheckBox())); - connect(histParameterCombo, SIGNAL(currentIndexChanged(int)), - this, SLOT(setHistSelection(int))); - connect(histShadeZones, SIGNAL(stateChanged(int)), - this, SLOT(setHistSelection(int))); - connect(histSumY, SIGNAL(currentIndexChanged(int)), - this, SLOT(setSumY(int))); - //connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected())); + // set the defaults etc + updateChart(); + + // the bin slider/input update each other + // only the input box triggers an update to the chart + connect(binWidthSlider, SIGNAL(valueChanged(int)), this, SLOT(setBinWidthFromSlider())); + connect(binWidthLineEdit, SIGNAL(editingFinished()), this, SLOT(setBinWidthFromLineEdit())); + + // when season changes we need to retrieve data from the cache then update the chart + connect(seasonCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int))); + + // if any of the controls change we pass the chart everything + connect(showLnY, SIGNAL(stateChanged(int)), this, SLOT(updateChart())); + connect(showZeroes, SIGNAL(stateChanged(int)), this, SLOT(updateChart())); + connect(seriesCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateChart())); + connect(showInZones, SIGNAL(stateChanged(int)), this, SLOT(updateChart())); + connect(shadeZones, SIGNAL(stateChanged(int)), this, SLOT(updateChart())); + connect(showSumY, SIGNAL(currentIndexChanged(int)), this, SLOT(updateChart())); + connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected())); connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged())); connect(mainWindow, SIGNAL(configChanged()), powerHist, SLOT(configChanged())); - connect(cComboSeason, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int))); } void HistogramWindow::rideSelected() { - if (!amVisible()) - return; + if (!amVisible()) return; + RideItem *ride = myRideItem; - if (!ride) - return; + if (!ride || seasonCombo->currentIndex() != 0) return; // get range that applies to this ride powerRange = mainWindow->zones()->whichRange(ride->dateTime.date()); hrRange = mainWindow->hrZones()->whichRange(ride->dateTime.date()); - // set the histogram data - powerHist->setData(ride); + // update + updateChart(); } void HistogramWindow::intervalSelected() { - if (!amVisible()) - return; - RideItem *ride = myRideItem; - if (!ride) return; + if (!amVisible()) return; - // set the histogram data - powerHist->setData(ride); + RideItem *ride = myRideItem; + + // null? or not plotting current ride, ignore signal + if (!ride || seasonCombo->currentIndex() != 0) return; + + // update + interval = true; + updateChart(); } void HistogramWindow::zonesChanged() { - if (!amVisible()) - return; + if (!amVisible()) return; + powerHist->refreshZoneLabels(); powerHist->replot(); } void HistogramWindow::seasonSelected(int index) { + RideFileCache *old = source; + if (index > 0) { index--; // it is now an index into the season array // Set data from BESTS Season season = seasons.at(index); + QDate start = season.getStart(); + QDate end = season.getEnd(); + if (end == QDate()) end = QDate(3000,12,31); + if (start == QDate()) start = QDate(1900,1,1); + source = new RideFileCache(mainWindow, start, end); - } else if (!index) { - // Set data from RIDE - + if (old) delete old; // guarantee source pointer changes } + updateChart(); } +void HistogramWindow::addSeries() +{ + // setup series list + seriesList << RideFile::watts + << RideFile::hr + << RideFile::kph + << RideFile::cad + << RideFile::nm; + + foreach (RideFile::SeriesType x, seriesList) + seriesCombo->addItem(RideFile::seriesName(x), static_cast(x)); +} void HistogramWindow::addSeasons() { QFile seasonFile(mainWindow->home.absolutePath() + "/seasons.xml"); @@ -182,52 +204,26 @@ void HistogramWindow::addSeasons() xmlReader.setContentHandler(&handler); xmlReader.setErrorHandler(&handler); bool ok = xmlReader.parse( source ); - if(!ok) - qWarning("Failed to parse seasons.xml"); + if(!ok) qWarning("Failed to parse seasons.xml"); seasons = handler.getSeasons(); Season season; season.setName(tr("All Seasons")); seasons.insert(0,season); - cComboSeason->addItem("Selected Ride"); + seasonCombo->addItem("Selected Ride"); foreach (Season season, seasons) - cComboSeason->addItem(season.getName()); - if (!seasons.empty()) { - cComboSeason->setCurrentIndex(cComboSeason->count() - 1); - Season season = seasons.last(); - // set default parameters here - // XXX todo - } + seasonCombo->addItem(season.getName()); } + void HistogramWindow::setBinWidthFromSlider() { if (powerHist->binWidth() != binWidthSlider->value()) { powerHist->setBinWidth(binWidthSlider->value()); - setHistBinWidthText(); - } -} + setHistBinWidthText(); -void -HistogramWindow::setlnYHistFromCheckBox() -{ - if (powerHist->islnY() != lnYHistCheckBox->isChecked()) - powerHist->setlnY(! powerHist->islnY()); -} - -void -HistogramWindow::setSumY(int index) -{ - if (index < 0) return; // being destroyed - else powerHist->setSumY(index == 0); -} - -void -HistogramWindow::setWithZerosFromCheckBox() -{ - if (powerHist->withZeros() != withZerosCheckBox->isChecked()) { - powerHist->setWithZeros(withZerosCheckBox->isChecked()); + updateChart(); } } @@ -242,67 +238,21 @@ HistogramWindow::setHistTextValidator() { double delta = powerHist->getDelta(); int digits = powerHist->getDigits(); - binWidthLineEdit->setValidator( - (digits == 0) ? - (QValidator *) ( - new QIntValidator(binWidthSlider->minimum() * delta, - binWidthSlider->maximum() * delta, - binWidthLineEdit - ) - ) : - (QValidator *) ( - new QDoubleValidator(binWidthSlider->minimum() * delta, - binWidthSlider->maximum() * delta, - digits, - binWidthLineEdit - ) - ) - ); -} + QValidator *validator; + if (digits == 0) { -void -HistogramWindow::setHistSelection(int /*id*/) -{ - // Set shading first, since the dataseries selection - // below will trigger a redraw, and we need to have - // set the shading beforehand. OK, so we could make - // either change trigger it, but this makes for simpler - // code here and in powerhist.cpp - if (histShadeZones->isChecked()) powerHist->setShading(true); - else powerHist->setShading(false); + validator = new QIntValidator(binWidthSlider->minimum() * delta, + binWidthSlider->maximum() * delta, + binWidthLineEdit); + } else { - // Which data series are we plotting? - switch (histParameterCombo->currentIndex()) { - default: - case 0 : - powerHist->setSelection(PowerHist::watts); - break; - - case 1 : - powerHist->setSelection(PowerHist::wattsZone); - break; - - case 2 : - powerHist->setSelection(PowerHist::nm); - break; - - case 3 : - powerHist->setSelection(PowerHist::hr); - break; - - case 4 : - powerHist->setSelection(PowerHist::hrZone); - break; - case 5 : - powerHist->setSelection(PowerHist::kph); - break; - - case 6 : - powerHist->setSelection(PowerHist::cad); + validator = new QDoubleValidator(binWidthSlider->minimum() * delta, + binWidthSlider->maximum() * delta, + digits, + binWidthLineEdit); } - setHistBinWidthText(); - setHistTextValidator(); + binWidthLineEdit->setValidator(validator); } void @@ -310,7 +260,43 @@ HistogramWindow::setBinWidthFromLineEdit() { double value = binWidthLineEdit->text().toDouble(); if (value != powerHist->binWidth()) { - binWidthSlider->setValue(powerHist->setBinWidthRealUnits(value)); - setHistBinWidthText(); + binWidthSlider->setValue(powerHist->setBinWidthRealUnits(value)); + setHistBinWidthText(); + + updateChart(); } } + +void +HistogramWindow::updateChart() +{ + // set data + if (seasonCombo->currentIndex() != 0 && source) + powerHist->setData(source); + else if (seasonCombo->currentIndex() == 0 && myRideItem) + powerHist->setData(myRideItem, interval); // intervals selected forces data to + // be recomputed since interval selection + // has changed. + + // and now the controls + powerHist->setShading(shadeZones->isChecked() ? true : false); + powerHist->setZoned(showInZones->isChecked() ? true : false); + powerHist->setlnY(showLnY->isChecked() ? true : false); + powerHist->setWithZeros(showZeroes->isChecked() ? true : false); + powerHist->setSumY(showSumY->currentIndex()== 0 ? true : false); + powerHist->setBinWidth(binWidthLineEdit->text().toDouble()); + + // and which series to plot + powerHist->setSeries(static_cast(seriesCombo->itemData(seriesCombo->currentIndex()).toInt())); + + // now go plot yourself + //powerHist->setAxisTitle(int axis, QString label); + powerHist->recalc(interval); // interval changed? force recalc + powerHist->replot(); + + interval = false;// we force a recalc whem called coz intervals + // have been selected. The recalc routine in + // powerhist optimises out, but doesn't keep track + // of interval selection -- simplifies the setters + // and getters, so worth this 'hack'. +} diff --git a/src/HistogramWindow.h b/src/HistogramWindow.h index b4db0388c..8ed078494 100644 --- a/src/HistogramWindow.h +++ b/src/HistogramWindow.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net) + * 2011 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 @@ -28,6 +29,7 @@ class MainWindow; class PowerHist; class RideItem; +class RideFileCache; class HistogramWindow : public GcWindow { @@ -40,24 +42,30 @@ class HistogramWindow : public GcWindow Q_PROPERTY(bool logY READ logY WRITE setLogY USER true) Q_PROPERTY(bool zeroes READ zeroes WRITE setZeroes USER true) Q_PROPERTY(bool shade READ shade WRITE setShade USER true) + Q_PROPERTY(bool zoned READ zoned WRITE setZoned USER true) + Q_PROPERTY(bool scope READ scope WRITE setScope USER true) public: HistogramWindow(MainWindow *mainWindow); // get/set properties - int series() const { return histParameterCombo->currentIndex(); } - void setSeries(int x) { histParameterCombo->setCurrentIndex(x); } - int percent() const { return histSumY->currentIndex(); } - void setPercent(int x) { histSumY->setCurrentIndex(x); } + int series() const { return seriesCombo->currentIndex(); } + void setSeries(int x) { seriesCombo->setCurrentIndex(x); } + int percent() const { return showSumY->currentIndex(); } + void setPercent(int x) { showSumY->setCurrentIndex(x); } double bin() const { return binWidthSlider->value(); } void setBin(double x) { binWidthSlider->setValue(x); } - bool logY() const { return lnYHistCheckBox->isChecked(); } - void setLogY(bool x) { lnYHistCheckBox->setChecked(x); } - bool zeroes() const { return withZerosCheckBox->isChecked(); } - void setZeroes(bool x) { withZerosCheckBox->setChecked(x); } - bool shade() const { return histShadeZones->isChecked(); } - void setShade(bool x) { histShadeZones->setChecked(x); } + bool logY() const { return showLnY->isChecked(); } + void setLogY(bool x) { showLnY->setChecked(x); } + bool zeroes() const { return showZeroes->isChecked(); } + void setZeroes(bool x) { showZeroes->setChecked(x); } + bool shade() const { return shadeZones->isChecked(); } + void setShade(bool x) { shadeZones->setChecked(x); } + bool zoned() const { return showInZones->isChecked(); } + void setZoned(bool x) { return showInZones->setChecked(x); } + int scope() const { return seasonCombo->currentIndex(); } + void setScope(int x) { seasonCombo->setCurrentIndex(x); } public slots: @@ -69,13 +77,10 @@ class HistogramWindow : public GcWindow void setBinWidthFromSlider(); void setBinWidthFromLineEdit(); - void setlnYHistFromCheckBox(); - void setWithZerosFromCheckBox(); - void setHistSelection(int id); - void setSumY(int); void seasonSelected(int season); + void updateChart(); - protected: + private: QList seasons; void setHistTextValidator(); @@ -83,17 +88,25 @@ class HistogramWindow : public GcWindow MainWindow *mainWindow; PowerHist *powerHist; - QSlider *binWidthSlider; - QLineEdit *binWidthLineEdit; - QCheckBox *lnYHistCheckBox; - QCheckBox *withZerosCheckBox; - QCheckBox *histShadeZones; - QComboBox *histParameterCombo; - QComboBox *histSumY; - QComboBox *cComboSeason; + + QSlider *binWidthSlider; // seet Bin Width from a slider + QLineEdit *binWidthLineEdit; // set Bin Width from the line edit + QCheckBox *showLnY; // set show as Log(y) + QCheckBox *showZeroes; // Include zeroes + QComboBox *showSumY; // ?? + QCheckBox *shadeZones; // Shade zone background + QCheckBox *showInZones; // Plot by Zone + QComboBox *seriesCombo; // Which data series to plot + QComboBox *seasonCombo; // Plot for Date range or current ride + + QList seriesList; void addSeasons(); + void addSeries(); int powerRange, hrRange; + + RideFileCache *source; + bool interval; }; #endif // _GC_HistogramWindow_h diff --git a/src/PowerHist.cpp b/src/PowerHist.cpp index 480cda5d4..3fde6f0f2 100644 --- a/src/PowerHist.cpp +++ b/src/PowerHist.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * 2011 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 @@ -41,284 +42,21 @@ #include "LTMCanvasPicker.h" // for tooltip - -// define a background class to handle shading of power zones -// draws power zone bands IF zones are defined and the option -// to draw bonds has been selected -class PowerHistBackground: public QwtPlotItem -{ -private: - PowerHist *parent; - -public: - PowerHistBackground(PowerHist *_parent) - { - setZ(0.0); - parent = _parent; - } - - virtual int rtti() const - { - return QwtPlotItem::Rtti_PlotUserItem; - } - - virtual void draw(QPainter *painter, - const QwtScaleMap &xMap, const QwtScaleMap &, - const QRect &rect) const - { - RideItem *rideItem = parent->rideItem; - - if (! rideItem) - return; - - const Zones *zones = rideItem->zones; - int zone_range = rideItem->zoneRange(); - - if (parent->shadeZones() && (zone_range >= 0)) { - QList zone_lows = zones->getZoneLows(zone_range); - int num_zones = zone_lows.size(); - if (num_zones > 0) { - for (int z = 0; z < num_zones; z ++) { - QRect r = rect; - - QColor shading_color = - zoneColor(z, num_zones); - shading_color.setHsv( - shading_color.hue(), - shading_color.saturation() / 4, - shading_color.value() - ); - r.setLeft(xMap.transform(zone_lows[z])); - if (z + 1 < num_zones) - r.setRight(xMap.transform(zone_lows[z + 1])); - if (r.right() >= r.left()) - painter->fillRect(r, shading_color); - } - } - } - } -}; - - -// Zone labels are drawn if power zone bands are enabled, automatically -// at the center of the plot -class PowerHistZoneLabel: public QwtPlotItem -{ -private: - PowerHist *parent; - int zone_number; - double watts; - QwtText text; - -public: - PowerHistZoneLabel(PowerHist *_parent, int _zone_number) - { - parent = _parent; - zone_number = _zone_number; - - RideItem *rideItem = parent->rideItem; - - if (! rideItem) - return; - - const Zones *zones = rideItem->zones; - int zone_range = rideItem->zoneRange(); - - setZ(1.0 + zone_number / 100.0); - - // create new zone labels if we're shading - if (parent->shadeZones() && (zone_range >= 0)) { - QList zone_lows = zones->getZoneLows(zone_range); - QList zone_names = zones->getZoneNames(zone_range); - int num_zones = zone_lows.size(); - assert(zone_names.size() == num_zones); - if (zone_number < num_zones) { - watts = - ( - (zone_number + 1 < num_zones) ? - 0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) : - ( - (zone_number > 0) ? - (1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) : - 2.0 * zone_lows[zone_number] - ) - ); - - text = QwtText(zone_names[zone_number]); - text.setFont(QFont("Helvetica",24, QFont::Bold)); - QColor text_color = zoneColor(zone_number, num_zones); - text_color.setAlpha(64); - text.setColor(text_color); - } - } - - } - - virtual int rtti() const - { - return QwtPlotItem::Rtti_PlotUserItem; - } - - void draw(QPainter *painter, - const QwtScaleMap &xMap, const QwtScaleMap &, - const QRect &rect) const - { - if (parent->shadeZones()) { - int x = xMap.transform(watts); - int y = (rect.bottom() + rect.top()) / 2; - - // the following code based on source for QwtPlotMarker::draw() - QRect tr(QPoint(0, 0), text.textSize(painter->font())); - tr.moveCenter(QPoint(y, -x)); - painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed - text.draw(painter, tr); - } - } -}; - -// define a background class to handle shading of HR zones -// draws power zone bands IF zones are defined and the option -// to draw bonds has been selected -class HrHistBackground: public QwtPlotItem -{ -private: - PowerHist *parent; - -public: - HrHistBackground(PowerHist *_parent) - { - setZ(0.0); - parent = _parent; - } - - virtual int rtti() const - { - return QwtPlotItem::Rtti_PlotUserItem; - } - - virtual void draw(QPainter *painter, - const QwtScaleMap &xMap, const QwtScaleMap &, - const QRect &rect) const - { - RideItem *rideItem = parent->rideItem; - - if (! rideItem) - return; - - const HrZones *zones = parent->mainWindow->hrZones(); - int zone_range = rideItem->hrZoneRange(); - - if (parent->shadeHRZones() && (zone_range >= 0)) { - QList zone_lows = zones->getZoneLows(zone_range); - int num_zones = zone_lows.size(); - if (num_zones > 0) { - for (int z = 0; z < num_zones; z ++) { - QRect r = rect; - - QColor shading_color = - hrZoneColor(z, num_zones); - shading_color.setHsv( - shading_color.hue(), - shading_color.saturation() / 4, - shading_color.value() - ); - r.setLeft(xMap.transform(zone_lows[z])); - if (z + 1 < num_zones) - r.setRight(xMap.transform(zone_lows[z + 1])); - if (r.right() >= r.left()) - painter->fillRect(r, shading_color); - } - } - } - } -}; - - -// Zone labels are drawn if power zone bands are enabled, automatically -// at the center of the plot -class HrHistZoneLabel: public QwtPlotItem -{ -private: - PowerHist *parent; - int zone_number; - double watts; - QwtText text; - -public: - HrHistZoneLabel(PowerHist *_parent, int _zone_number) - { - parent = _parent; - zone_number = _zone_number; - - RideItem *rideItem = parent->rideItem; - - if (! rideItem) - return; - - const HrZones *zones = parent->mainWindow->hrZones(); - int zone_range = rideItem->hrZoneRange(); - - setZ(1.0 + zone_number / 100.0); - - // create new zone labels if we're shading - if (parent->shadeHRZones() && (zone_range >= 0)) { - QList zone_lows = zones->getZoneLows(zone_range); - QList zone_names = zones->getZoneNames(zone_range); - int num_zones = zone_lows.size(); - assert(zone_names.size() == num_zones); - if (zone_number < num_zones) { - watts = - ( - (zone_number + 1 < num_zones) ? - 0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) : - ( - (zone_number > 0) ? - (1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) : - 2.0 * zone_lows[zone_number] - ) - ); - - text = QwtText(zone_names[zone_number]); - text.setFont(QFont("Helvetica",24, QFont::Bold)); - QColor text_color = hrZoneColor(zone_number, num_zones); - text_color.setAlpha(64); - text.setColor(text_color); - } - } - - } - - virtual int rtti() const - { - return QwtPlotItem::Rtti_PlotUserItem; - } - - void draw(QPainter *painter, - const QwtScaleMap &xMap, const QwtScaleMap &, - const QRect &rect) const - { - if (parent->shadeHRZones()) { - int x = xMap.transform(watts); - int y = (rect.bottom() + rect.top()) / 2; - - // the following code based on source for QwtPlotMarker::draw() - QRect tr(QPoint(0, 0), text.textSize(painter->font())); - tr.moveCenter(QPoint(y, -x)); - painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed - text.draw(painter, tr); - } - } -}; - PowerHist::PowerHist(MainWindow *mainWindow): - selected(watts), - shade(false), rideItem(NULL), mainWindow(mainWindow), - withz(true), + series(RideFile::watts), + useMetricUnits(true), unit(0), lny(false), - absolutetime(true) + shade(false), + zoned(false), + binw(3), + withz(true), + dt(1), + absolutetime(true), + cache(NULL), + source(Ride) { unit = appsettings->value(this, GC_UNIT); @@ -377,29 +115,28 @@ PowerHist::configChanged() QPen pen; QColor brush_color; - switch (selected) { - case watts: - case wattsZone: + switch (series) { + case RideFile::watts: pen.setColor(GColor(CPOWER).darker(200)); brush_color = GColor(CPOWER); break; - case nm: + case RideFile::nm: pen.setColor(GColor(CTORQUE).darker(200)); brush_color = GColor(CTORQUE); break; - case hr: - case hrZone: - pen.setColor(GColor(CHEARTRATE).darker(200)); - brush_color = GColor(CHEARTRATE); - break; - case kph: + case RideFile::kph: pen.setColor(GColor(CSPEED).darker(200)); brush_color = GColor(CSPEED); break; - case cad: + case RideFile::cad: pen.setColor(GColor(CCADENCE).darker(200)); brush_color = GColor(CCADENCE); break; + default: + case RideFile::hr: + pen.setColor(GColor(CHEARTRATE).darker(200)); + brush_color = GColor(CHEARTRATE); + break; } double width = appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble(); @@ -454,31 +191,30 @@ PowerHist::refreshZoneLabels() { // delete any existing power zone labels if (zoneLabels.size()) { - QListIterator i(zoneLabels); - while (i.hasNext()) { - PowerHistZoneLabel *label = i.next(); - label->detach(); - delete label; - } + QListIterator i(zoneLabels); + while (i.hasNext()) { + PowerHistZoneLabel *label = i.next(); + label->detach(); + delete label; + } } zoneLabels.clear(); - if (! rideItem) - return; + if (!rideItem) return; - if (selected == watts) { - const Zones *zones = rideItem->zones; - int zone_range = rideItem->zoneRange(); + if (series == RideFile::watts) { + const Zones *zones = rideItem->zones; + int zone_range = rideItem->zoneRange(); // generate labels for existing zones - if (zone_range >= 0) { - int num_zones = zones->numZones(zone_range); - for (int z = 0; z < num_zones; z ++) { - PowerHistZoneLabel *label = new PowerHistZoneLabel(this, z); - label->attach(this); - zoneLabels.append(label); - } - } + if (zone_range >= 0) { + int num_zones = zones->numZones(zone_range); + for (int z = 0; z < num_zones; z ++) { + PowerHistZoneLabel *label = new PowerHistZoneLabel(this, z); + label->attach(this); + zoneLabels.append(label); + } + } } } @@ -487,56 +223,132 @@ PowerHist::refreshHRZoneLabels() { // delete any existing power zone labels if (hrzoneLabels.size()) { - QListIterator i(hrzoneLabels); - while (i.hasNext()) { - HrHistZoneLabel *label = i.next(); - label->detach(); - delete label; - } + QListIterator i(hrzoneLabels); + while (i.hasNext()) { + HrHistZoneLabel *label = i.next(); + label->detach(); + delete label; + } } hrzoneLabels.clear(); - if (! rideItem) - return; + if (!rideItem) return; - if (selected == hr) { - const HrZones *zones = mainWindow->hrZones(); - int zone_range = rideItem->hrZoneRange(); + if (series == RideFile::hr) { + const HrZones *zones = mainWindow->hrZones(); + int zone_range = rideItem->hrZoneRange(); // generate labels for existing zones - if (zone_range >= 0) { - int num_zones = zones->numZones(zone_range); - for (int z = 0; z < num_zones; z ++) { - HrHistZoneLabel *label = new HrHistZoneLabel(this, z); - label->attach(this); - hrzoneLabels.append(label); - } - } + if (zone_range >= 0) { + int num_zones = zones->numZones(zone_range); + for (int z = 0; z < num_zones; z ++) { + HrHistZoneLabel *label = new HrHistZoneLabel(this, z); + label->attach(this); + hrzoneLabels.append(label); + } + } } } void -PowerHist::recalc() +PowerHist::recalc(bool force) { QVector *array; QVector *selectedArray; int arrayLength = 0; double delta; - if (!rideItem) return; + // lets make sure we need to recalculate + if (force == false && + LASTsource == source && + LASTcache == cache && + LASTrideItem == rideItem && + LASTseries == series && + LASTshade == shade && + LASTuseMetricUnits == useMetricUnits && + LASTlny == lny && + LASTzoned == zoned && + LASTbinw == binw && + LASTwithz == withz && + LASTdt == dt && + LASTabsolutetime == absolutetime) { + return; // nothing has changed + + } else { + + // remember for next time + LASTsource = source; + LASTcache = cache; + LASTrideItem = rideItem; + LASTseries = series; + LASTshade = shade; + LASTuseMetricUnits = useMetricUnits; + LASTlny = lny; + LASTzoned = zoned; + LASTbinw = binw; + LASTwithz = withz; + LASTdt = dt; + LASTabsolutetime = absolutetime; + } + + + if (source == Ride && !rideItem) return; // make sure the interval length is set if (dt <= 0) return; - // if there is no data empty curve - if (((selected == watts && rideItem->ride()->areDataPresent()->watts) || - (selected == nm && rideItem->ride()->areDataPresent()->nm) || - (selected == kph && rideItem->ride()->areDataPresent()->kph) || - (selected == cad && rideItem->ride()->areDataPresent()->cad) || - (selected == hr && rideItem->ride()->areDataPresent()->hr) || - (selected == wattsZone && rideItem->ride()->areDataPresent()->watts) || - (selected == hrZone && rideItem->ride()->areDataPresent()->hr)) == false) { + if (series == RideFile::watts && zoned == false) { + array = &wattsArray; + delta = wattsDelta; + arrayLength = wattsArray.size(); + selectedArray = &wattsSelectedArray; + + } else if (series == RideFile::watts && zoned == true) { + + array = &wattsZoneArray; + delta = 1; + arrayLength = wattsZoneArray.size(); + selectedArray = &wattsZoneSelectedArray; + + } else if (series == RideFile::nm) { + + array = &nmArray; + delta = nmDelta; + arrayLength = nmArray.size(); + selectedArray = &nmSelectedArray; + + } else if (series == RideFile::hr && zoned == false) { + + array = &hrArray; + delta = hrDelta; + arrayLength = hrArray.size(); + selectedArray = &hrSelectedArray; + + } else if (series == RideFile::hr && zoned == true) { + + array = &hrZoneArray; + delta = 1; + arrayLength = hrZoneArray.size(); + selectedArray = &hrZoneSelectedArray; + + } else if (series == RideFile::kph) { + + array = &kphArray; + delta = kphDelta; + arrayLength = kphArray.size(); + selectedArray = &kphSelectedArray; + + } else if (series == RideFile::cad) { + array = &cadArray; + delta = cadDelta; + arrayLength = cadArray.size(); + selectedArray = &cadSelectedArray; + } + + // null curve please -- we have no data! + if (!array || arrayLength == 0 || (source == Ride && !rideItem->ride()->isDataPresent(series))) { + // create empty curves when no data const double zero = 0; curve->setData(&zero, &zero, 0); curveSelected->setData(&zero, &zero, 0); @@ -544,54 +356,9 @@ PowerHist::recalc() return; } - if (selected == watts) { - array = &wattsArray; - delta = wattsDelta; - arrayLength = wattsArray.size(); - selectedArray = &wattsSelectedArray; - } - else if (selected == wattsZone) { - array = &wattsZoneArray; - delta = 1; - arrayLength = wattsZoneArray.size(); - selectedArray = &wattsZoneSelectedArray; - } - 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 == hrZone) { - array = &hrZoneArray; - delta = 1; - arrayLength = hrZoneArray.size(); - selectedArray = &hrZoneSelectedArray; - } - 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) - return; - - // binning of data when not zoned - if (selected != wattsZone && selected != hrZone) { + // binning of data when not zoned - we can't zone for series besides + // watts and hr so ignore zoning for those data series + if (zoned == false || (zoned == true && (series != RideFile::watts && series != RideFile::hr))) { // we add a bin on the end since the last "incomplete" bin // will be dropped otherwise @@ -605,8 +372,7 @@ PowerHist::recalc() for (i = 1; i <= count; ++i) { int high = i * binw; int low = high - binw; - if (low==0 && !withz) - low++; + if (low==0 && !withz) low++; parameterValue[i] = high * delta; totalTime[i] = 1e-9; // nonzero to accomodate log plot totalTimeSelected[i] = 1e-9; // nonzero to accomodate log plot @@ -635,17 +401,17 @@ PowerHist::recalc() QBrush brush = curve->brush(); QColor bcol = brush.color(); - bool zoning = (selected == watts && shadeZones()) || (selected == hr && shadeHRZones()); + bool zoning = (zoned && (series == RideFile::watts || series == RideFile::hr)); bcol.setAlpha(zoning ? 165 : 200); brush.setColor(bcol); - curve->setBrush(brush); + //curve->setBrush(brush); //XXX weird artefact on first run only? setAxisScaleDraw(QwtPlot::xBottom, new QwtScaleDraw); // HR typically starts at 80 or so, rather than zero // lets crop the chart so we can focus on the data // if we're working with HR data... - if (selected == hr) { + if (series == RideFile::hr) { double MinX=0; for (int i=0; i 0) { @@ -727,7 +493,7 @@ PowerHist::recalc() curveSelected->setData(selectedxaxis.data(), selectedyaxis.data(), selectedxaxis.size()); // zone scale draw - if (selected == wattsZone && rideItem && rideItem->zones) { + if (series == RideFile::watts && zoned && rideItem && rideItem->zones) { setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(rideItem->zones, rideItem->zoneRange())); if (rideItem->zoneRange() >= 0) setAxisScale(QwtPlot::xBottom, -0.99, rideItem->zones->numZones(rideItem->zoneRange()), 1); @@ -737,7 +503,7 @@ PowerHist::recalc() // hr scale draw int hrRange; - if (selected == hrZone && rideItem && mainWindow->hrZones() && + if (series == RideFile::hr && zoned && rideItem && mainWindow->hrZones() && (hrRange=mainWindow->hrZones()->whichRange(rideItem->dateTime.date())) != -1) { setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(mainWindow->hrZones(), hrRange)); @@ -763,24 +529,82 @@ PowerHist::setYMax() setAxisScale(yLeft, (lny ? tmin : 0.0), MaxY * 1.1); } -void -PowerHist::setData(RideItem *_rideItem) +static void +longFromDouble(QVector&here, QVector&there) { - rideItem = _rideItem; + int highest = 0; + here.resize(there.size()); + for (int i=0; icache = cache; + dt = 1.0f / 60.0f; // rideFileCache is normalised to 1secs + + // we set with this data already? + if (cache == LASTcache && source == LASTsource) return; + + // Now go set all those tedious arrays from + // the ride cache + wattsArray.resize(0); + wattsZoneArray.resize(0); + nmArray.resize(0); + hrArray.resize(0); + hrZoneArray.resize(0); + kphArray.resize(0); + cadArray.resize(0); + + // we do not use the selected array since it is + // not meaningful to overlay interval selection + // with long term data + wattsSelectedArray.resize(0); + wattsZoneSelectedArray.resize(0); + nmSelectedArray.resize(0); + hrSelectedArray.resize(0); + hrZoneSelectedArray.resize(0); + kphSelectedArray.resize(0); + cadSelectedArray.resize(0); + + longFromDouble(wattsArray, cache->distributionArray(RideFile::watts)); + longFromDouble(hrArray, cache->distributionArray(RideFile::hr)); + longFromDouble(nmArray, cache->distributionArray(RideFile::nm)); + longFromDouble(cadArray, cache->distributionArray(RideFile::cad)); + longFromDouble(kphArray, cache->distributionArray(RideFile::kph)); + + if (!useMetricUnits) { + double torque_factor = (useMetricUnits ? 1.0 : 0.73756215); + double speed_factor = (useMetricUnits ? 1.0 : 0.62137119); + + for(int i=0; iride(); - RideFileCache updater(mainWindow, mainWindow->home.absolutePath() + "/" + rideItem->fileName, ride); - - bool hasData = (selected == watts && ride->areDataPresent()->watts) || - (selected == nm && ride->areDataPresent()->nm) || - (selected == kph && ride->areDataPresent()->kph) || - (selected == cad && ride->areDataPresent()->cad) || - (selected == hr && ride->areDataPresent()->hr) || - (selected == wattsZone && ride->areDataPresent()->watts) || - (selected == hrZone && ride->areDataPresent()->hr); + bool hasData = (series == RideFile::watts && ride->areDataPresent()->watts) || + (series == RideFile::nm && ride->areDataPresent()->nm) || + (series == RideFile::kph && ride->areDataPresent()->kph) || + (series == RideFile::cad && ride->areDataPresent()->cad) || + (series == RideFile::hr && ride->areDataPresent()->hr); if (ride && hasData) { //setTitle(ride->startTime().toString(GC_DATETIME_FORMAT)); @@ -861,70 +685,66 @@ PowerHist::setData(RideItem *_rideItem) } } - int hrIndex = int(floor(p1->hr / hrDelta)); - if (hrIndex >= 0 && hrIndex < maxSize) { - if (hrIndex >= hrArray.size()) - hrArray.resize(hrIndex + 1); - hrArray[hrIndex]++; - - if (selected) { - if (hrIndex >= hrSelectedArray.size()) - hrSelectedArray.resize(hrIndex + 1); - hrSelectedArray[hrIndex]++; - } - } - - // hr zoned array - int hrZoneRange = mainWindow->hrZones() ? mainWindow->hrZones()->whichRange(ride->startTime().date()) : -1; - - // Only calculate zones if we have a valid range - if (hrZoneRange > -1 && (withz || (!withz && p1-hr))) { - hrIndex = mainWindow->hrZones()->whichZone(hrZoneRange, p1->hr); - + int hrIndex = int(floor(p1->hr / hrDelta)); if (hrIndex >= 0 && hrIndex < maxSize) { - if (hrIndex >= hrZoneArray.size()) - hrZoneArray.resize(hrIndex + 1); - hrZoneArray[hrIndex]++; + if (hrIndex >= hrArray.size()) + hrArray.resize(hrIndex + 1); + hrArray[hrIndex]++; if (selected) { - if (hrIndex >= hrZoneSelectedArray.size()) - hrZoneSelectedArray.resize(hrIndex + 1); - hrZoneSelectedArray[hrIndex]++; + if (hrIndex >= hrSelectedArray.size()) + hrSelectedArray.resize(hrIndex + 1); + hrSelectedArray[hrIndex]++; + } + } + + // hr zoned array + int hrZoneRange = mainWindow->hrZones() ? mainWindow->hrZones()->whichRange(ride->startTime().date()) : -1; + + // Only calculate zones if we have a valid range + if (hrZoneRange > -1 && (withz || (!withz && p1->hr))) { + hrIndex = mainWindow->hrZones()->whichZone(hrZoneRange, p1->hr); + + if (hrIndex >= 0 && hrIndex < maxSize) { + if (hrIndex >= hrZoneArray.size()) + hrZoneArray.resize(hrIndex + 1); + hrZoneArray[hrIndex]++; + + if (selected) { + if (hrIndex >= hrZoneSelectedArray.size()) + hrZoneSelectedArray.resize(hrIndex + 1); + hrZoneSelectedArray[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 (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 (selected) { + if (cadIndex >= cadSelectedArray.size()) + cadSelectedArray.resize(cadIndex + 1); + cadSelectedArray[cadIndex]++; } } } - 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 (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 (selected) { - if (cadIndex >= cadSelectedArray.size()) - cadSelectedArray.resize(cadIndex + 1); - cadSelectedArray[cadIndex]++; - } - } - - - } - - recalc(); - } else { // create empty curves when no data @@ -932,7 +752,6 @@ PowerHist::setData(RideItem *_rideItem) curve->setData(&zero, &zero, 0); curveSelected->setData(&zero, &zero, 0); replot(); - replot(); } zoomer->setZoomBase(); } @@ -940,55 +759,48 @@ PowerHist::setData(RideItem *_rideItem) void PowerHist::setBinWidth(int value) { + if (!value) value = 1; // binwidth must be nonzero binw = value; appsettings->setValue(GC_HIST_BIN_WIDTH, value); - recalc(); +} + +void +PowerHist::setZoned(bool value) +{ + zoned = value; } double PowerHist::getDelta() { - switch (selected) { - case watts: - case wattsZone: - return wattsDelta; - case nm: - return nmDelta; - case hr: - case hrZone: - return hrDelta; - case kph: - return kphDelta; - case cad: - return cadDelta; + switch (series) { + case RideFile::watts: return wattsDelta; + case RideFile::nm: return nmDelta; + case RideFile::hr: return hrDelta; + case RideFile::kph: return kphDelta; + case RideFile::cad: return cadDelta; + default: return 1; } - return 1; } int PowerHist::getDigits() { - switch (selected) { - case watts: - case wattsZone: - return wattsDigits; - case nm: - return nmDigits; - case hr: - case hrZone: - return hrDigits; - case kph: - return kphDigits; - case cad: - return cadDigits; + switch (series) { + case RideFile::watts: return wattsDigits; + case RideFile::nm: return nmDigits; + case RideFile::hr: return hrDigits; + case RideFile::kph: return kphDigits; + case RideFile::cad: return cadDigits; + default: return 1; } - return 1; } int PowerHist::setBinWidthRealUnits(double value) { setBinWidth(round(value / getDelta())); + if (!binw) binw = 1; // must be nonzero return binw; } @@ -1002,8 +814,6 @@ void PowerHist::setWithZeros(bool value) { withz = value; - setData(rideItem); // for zone recalculating with/without zero - recalc(); } void @@ -1013,15 +823,16 @@ PowerHist::setlnY(bool value) // "new" in the argument list is not a leak lny=value; - if (lny && selected != wattsZone && selected != hrZone) - { + if (lny && !zoned) { + setAxisScaleEngine(yLeft, new QwtLog10ScaleEngine); - curve->setBaseline(1e-6); - } - else - { + curve->setBaseline(1e-6); + + } else { + setAxisScaleEngine(yLeft, new QwtLinearScaleEngine); - curve->setBaseline(0); + curve->setBaseline(0); + } setYMax(); replot(); @@ -1032,36 +843,39 @@ PowerHist::setSumY(bool value) { absolutetime = value; setParameterAxisTitle(); - setData(rideItem); // for zone recalculating with/without zero - recalc(); } void PowerHist::setParameterAxisTitle() { QString axislabel; - switch (selected) { - case watts: - axislabel = tr("Power (watts)"); + switch (series) { + + case RideFile::watts: + if (zoned) axislabel = tr("Power zone"); + else axislabel = tr("Power (watts)"); break; - case wattsZone: - axislabel = tr("Power zone"); + + case RideFile::hr: + if (zoned) axislabel = tr("Heartrate zone"); + else axislabel = tr("Heartrate (bpm)"); break; - case hr: - axislabel = tr("Heartrate (bpm)"); - break; - case hrZone: - axislabel = tr("Heartrate zone"); - break; - case cad: + + case RideFile::cad: axislabel = tr("Cadence (rpm)"); break; - case kph: + + case RideFile::kph: axislabel = QString(tr("Speed (%1)")).arg(useMetricUnits ? tr("kph") : tr("mph")); break; - case nm: + + case RideFile::nm: axislabel = QString(tr("Torque (%1)")).arg(useMetricUnits ? tr("N-m") : tr("ft-lbf")); break; + + default: + axislabel = QString(tr("Unknown data series")); + break; } setAxisTitle(xBottom, axislabel); setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)")); @@ -1082,77 +896,21 @@ PowerHist::setAxisTitle(int axis, QString label) } void -PowerHist::setSelection(Selection selection) { - selected = selection; - if (selected == wattsZone || selected == hrZone) setlnY(false); +PowerHist::setSeries(RideFile::SeriesType x) { + // user selected a different series to plot + series = x; configChanged(); // set colors setParameterAxisTitle(); - recalc(); } - -void -PowerHist::fixSelection() { - - Selection s = selected; - RideFile *ride = rideItem->ride(); - - int powerRange = mainWindow->zones()->whichRange(rideItem->dateTime.date()); - int hrRange = mainWindow->hrZones()->whichRange(rideItem->dateTime.date()); - - if (ride) do { - - if (s == watts) { - if (ride->areDataPresent()->watts) setSelection(s); - else s = nm; - - } else if (s == wattsZone) { - if (ride->areDataPresent()->watts && powerRange != -1) setSelection(s); - else s = nm; - - } else if (s == nm) { - if (ride->areDataPresent()->nm) setSelection(s); - else s = hr; - - } else if (s == hr) { - if (ride->areDataPresent()->hr) setSelection(s); - else s = kph; - - } else if (s == hrZone) { - if (ride->areDataPresent()->hr && hrRange != -1) setSelection(s); - else s = kph; - - } else if (s == kph) { - if (ride->areDataPresent()->kph) setSelection(s); - else s = cad; - - } else if (s == cad) { - if (ride->areDataPresent()->cad) setSelection(s); - else s = watts; - } - - } while (s != selected); -} - - bool PowerHist::shadeZones() const { - return ( - rideItem && - rideItem->ride() && - selected == watts && - shaded() == true - ); + return (rideItem && rideItem->ride() && series == RideFile::watts && !zoned && shade == true); } bool PowerHist::shadeHRZones() const { - return ( - rideItem && - rideItem->ride() && - selected == hr && - shaded() == true - ); + return (rideItem && rideItem->ride() && series == RideFile::hr && !zoned && shade == true); } bool PowerHist::isSelected(const RideFilePoint *p, double sample) { @@ -1178,7 +936,7 @@ PowerHist::pointHover(QwtPlotCurve *curve, int index) double yvalue = curve->y(index); QString text; - if ((selected == hrZone || selected == wattsZone) && yvalue > 0) { + if (zoned && yvalue > 0) { // output the tooltip text = QString("%1 %2").arg(yvalue, 0, 'f', 1).arg(absolutetime ? tr("minutes") : tr("%")); diff --git a/src/PowerHist.h b/src/PowerHist.h index f55654e1d..d7cba1c79 100644 --- a/src/PowerHist.h +++ b/src/PowerHist.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * 2011 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 @@ -19,6 +20,10 @@ #ifndef _GC_PowerHist_h #define _GC_PowerHist_h 1 #include "GoldenCheetah.h" +#include "RideFile.h" +#include "MainWindow.h" +#include "Zones.h" +#include "HrZones.h" #include #include @@ -31,6 +36,7 @@ class QwtPlotGrid; class MainWindow; class RideItem; class RideFilePoint; +class RideFileCache; class PowerHistBackground; class PowerHistZoneLabel; class HrHistBackground; @@ -41,167 +47,437 @@ class ZoneScaleDraw; class penTooltip: public QwtPlotZoomer { public: - penTooltip(QwtPlotCanvas *canvas): - QwtPlotZoomer(canvas), tip("") - { - // With some versions of Qt/Qwt, setting this to AlwaysOn - // causes an infinite recursion. - //setTrackerMode(AlwaysOn); - setTrackerMode(AlwaysOn); + penTooltip(QwtPlotCanvas *canvas): QwtPlotZoomer(canvas), tip("") { + // With some versions of Qt/Qwt, setting this to AlwaysOn + // causes an infinite recursion. + //setTrackerMode(AlwaysOn); + setTrackerMode(AlwaysOn); } - virtual QwtText trackerText(const QwtDoublePoint &/*pos*/) const - { - QColor bg = QColor(255,255, 170); // toolyip yellow + virtual QwtText trackerText(const QwtDoublePoint &/*pos*/) const { + QColor bg = QColor(255,255, 170); // toolyip yellow #if QT_VERSION >= 0x040300 - bg.setAlpha(200); + bg.setAlpha(200); #endif - QwtText text; - QFont def; - //def.setPointSize(8); // too small on low res displays (Mac) - //double val = ceil(pos.y()*100) / 100; // round to 2 decimal place - //text.setText(QString("%1 %2").arg(val).arg(format), QwtText::PlainText); - text.setText(tip); - text.setFont(def); - text.setBackgroundBrush( QBrush( bg )); - text.setRenderFlags(Qt::AlignLeft | Qt::AlignTop); - return text; - } - void setFormat(QString fmt) { format = fmt; } - void setText(QString txt) { tip = txt; } + QwtText text; + QFont def; + //def.setPointSize(8); // too small on low res displays (Mac) + //double val = ceil(pos.y()*100) / 100; // round to 2 decimal place + //text.setText(QString("%1 %2").arg(val).arg(format), QwtText::PlainText); + text.setText(tip); + text.setFont(def); + text.setBackgroundBrush( QBrush( bg )); + text.setRenderFlags(Qt::AlignLeft | Qt::AlignTop); + return text; + } + + void setFormat(QString fmt) { format = fmt; } + void setText(QString txt) { tip = txt; } + private: - QString format; - QString tip; - }; + QString format; + QString tip; +}; class PowerHist : public QwtPlot { Q_OBJECT G_OBJECT + friend class ::HrHistBackground; + friend class ::HrHistZoneLabel; + friend class ::PowerHistBackground; + friend class ::PowerHistZoneLabel; + friend class ::HistogramWindow; public: - QwtPlotCurve *curve, *curveSelected; - QList zoneLabels; - QList hrzoneLabels; - PowerHist(MainWindow *mainWindow); - ~PowerHist(); + ~PowerHist(); - int binWidth() const { return binw; } - inline bool islnY() const { return lny; } - inline bool withZeros() const { return withz; } - bool shadeZones() const; - bool shadeHRZones() const; - - enum Selection { - watts, - wattsZone, - nm, - hr, - hrZone, - kph, - cad - } selected; - inline Selection selection() { return selected; } - - bool shade; - inline bool shaded() const { return shade; } - - - void setData(RideItem *_rideItem); - - void setSelection(Selection selection); - void fixSelection(); - - void setShading(bool x) { shade=x; } - - void setBinWidth(int value); - double getDelta(); - int getDigits(); - double getBinWidthRealUnits(); - int setBinWidthRealUnits(double value); - - void refreshZoneLabels(); - void refreshHRZoneLabels(); - - RideItem *rideItem; - MainWindow *mainWindow; public slots: + // public setters + void setShading(bool x) { shade=x; } + void setSeries(RideFile::SeriesType series); + void setData(RideItem *_rideItem, bool force=false); + void setData(RideFileCache *source); void setlnY(bool value); void setWithZeros(bool value); + void setZoned(bool value); void setSumY(bool value); - void pointHover(QwtPlotCurve *curve, int index); void configChanged(); void setAxisTitle(int axis, QString label); + void setYMax(); + void setBinWidth(int value); + int setBinWidthRealUnits(double value); + + // public getters + double getDelta(); + double getBinWidthRealUnits(); + int getDigits(); + inline bool islnY() const { return lny; } + inline bool withZeros() const { return withz; } + inline int binWidth() const { return binw; } + + // react to plot signals + void pointHover(QwtPlotCurve *curve, int index); + + // get told to refresh + void recalc(bool force=false); + void refreshZoneLabels(); protected: - QwtPlotGrid *grid; + void refreshHRZoneLabels(); + void setParameterAxisTitle(); + bool isSelected(const RideFilePoint *p, double); + void percentify(QVector &, double factor); // and a function to convert - // storage for data counts - QVector - wattsArray, - wattsZoneArray, - nmArray, - hrArray, - hrZoneArray, - kphArray, - cadArray; - - // storage for data counts in interval selected - QVector - wattsSelectedArray, - wattsZoneSelectedArray, - nmSelectedArray, - hrSelectedArray, - hrZoneSelectedArray, - kphSelectedArray, - cadSelectedArray; + bool shadeZones() const; // check if zone shading is both wanted and possible + bool shadeHRZones() const; // check if zone shading is both wanted and possible + // plot settings + RideItem *rideItem; + MainWindow *mainWindow; + RideFile::SeriesType series; + bool useMetricUnits; // whether metric units are used (or imperial) + QVariant unit; + bool lny; + bool shade; + bool zoned; // show in zones int binw; - bool withz; // whether zeros are included in histogram - double dt; // length of sample + double dt; // length of sample + bool absolutetime; // do we sum absolute or percentage? - void recalc(); - void setYMax(); - penTooltip *zoomer; private: - QVariant unit; - PowerHistBackground *bg; - HrHistBackground *hrbg; + // plot objects + QwtPlotGrid *grid; + PowerHistBackground *bg; + HrHistBackground *hrbg; + penTooltip *zoomer; + LTMCanvasPicker *canvasPicker; + QwtPlotCurve *curve, *curveSelected; + QList zoneLabels; + QList hrzoneLabels; - bool lny; + // source cache + RideFileCache *cache; - // discritized unit for smoothing + // discritized unit for smoothing static const double wattsDelta = 1.0; - static const double nmDelta = 0.1; - static const double hrDelta = 1.0; - static const double kphDelta = 0.1; - static const double cadDelta = 1.0; + static const double nmDelta = 0.1; + static const double hrDelta = 1.0; + static const double kphDelta = 0.1; + static const double cadDelta = 1.0; - // digits for text entry validator + // digits for text entry validator static const int wattsDigits = 0; - static const int nmDigits = 1; - static const int hrDigits = 0; - static const int kphDigits = 1; - static const int cadDigits = 0; + static const int nmDigits = 1; + static const int hrDigits = 0; + static const int kphDigits = 1; + static const int cadDigits = 0; - void setParameterAxisTitle(); - bool isSelected(const RideFilePoint *p, double); + // storage for data counts + QVector wattsArray, wattsZoneArray, nmArray, hrArray, + hrZoneArray, kphArray, cadArray; - bool useMetricUnits; // whether metric units are used (or imperial) + // storage for data counts in interval selected + QVector wattsSelectedArray, wattsZoneSelectedArray, + nmSelectedArray, hrSelectedArray, + hrZoneSelectedArray, kphSelectedArray, + cadSelectedArray; - bool absolutetime; // do we sum absolute or percentage? - void percentify(QVector &, double factor); // and a function to convert + enum Source { Ride, Cache } source, LASTsource; - LTMCanvasPicker *canvasPicker; + // last plot settings - to avoid lots of uneeded recalcs + RideItem *LASTrideItem; + RideFileCache *LASTcache; + RideFile::SeriesType LASTseries; + bool LASTshade; + bool LASTuseMetricUnits; // whether metric units are used (or imperial) + bool LASTlny; + bool LASTzoned; // show in zones + int LASTbinw; + bool LASTwithz; // whether zeros are included in histogram + double LASTdt; // length of sample + bool LASTabsolutetime; // do we sum absolute or percentage? +}; + +/*---------------------------------------------------------------------- + * From here to the end of source file the routines for zone shading + *--------------------------------------------------------------------*/ + +// define a background class to handle shading of power zones +// draws power zone bands IF zones are defined and the option +// to draw bonds has been selected +class PowerHistBackground: public QwtPlotItem +{ +private: + PowerHist *parent; + +public: + PowerHistBackground(PowerHist *_parent) + { + setZ(0.0); + parent = _parent; + } + + virtual int rtti() const + { + return QwtPlotItem::Rtti_PlotUserItem; + } + + virtual void draw(QPainter *painter, + const QwtScaleMap &xMap, const QwtScaleMap &, + const QRect &rect) const + { + RideItem *rideItem = parent->rideItem; + + if (! rideItem) + return; + + const Zones *zones = rideItem->zones; + int zone_range = rideItem->zoneRange(); + + if (parent->shadeZones() && (zone_range >= 0)) { + QList zone_lows = zones->getZoneLows(zone_range); + int num_zones = zone_lows.size(); + if (num_zones > 0) { + for (int z = 0; z < num_zones; z ++) { + QRect r = rect; + + QColor shading_color = + zoneColor(z, num_zones); + shading_color.setHsv( + shading_color.hue(), + shading_color.saturation() / 4, + shading_color.value() + ); + r.setLeft(xMap.transform(zone_lows[z])); + if (z + 1 < num_zones) + r.setRight(xMap.transform(zone_lows[z + 1])); + if (r.right() >= r.left()) + painter->fillRect(r, shading_color); + } + } + } + } +}; + + +// Zone labels are drawn if power zone bands are enabled, automatically +// at the center of the plot +class PowerHistZoneLabel: public QwtPlotItem +{ +private: + PowerHist *parent; + int zone_number; + double watts; + QwtText text; + +public: + PowerHistZoneLabel(PowerHist *_parent, int _zone_number) + { + parent = _parent; + zone_number = _zone_number; + + RideItem *rideItem = parent->rideItem; + + if (! rideItem) + return; + + const Zones *zones = rideItem->zones; + int zone_range = rideItem->zoneRange(); + + setZ(1.0 + zone_number / 100.0); + + // create new zone labels if we're shading + if (parent->shadeZones() && (zone_range >= 0)) { + QList zone_lows = zones->getZoneLows(zone_range); + QList zone_names = zones->getZoneNames(zone_range); + int num_zones = zone_lows.size(); + assert(zone_names.size() == num_zones); + if (zone_number < num_zones) { + watts = + ( + (zone_number + 1 < num_zones) ? + 0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) : + ( + (zone_number > 0) ? + (1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) : + 2.0 * zone_lows[zone_number] + ) + ); + + text = QwtText(zone_names[zone_number]); + text.setFont(QFont("Helvetica",24, QFont::Bold)); + QColor text_color = zoneColor(zone_number, num_zones); + text_color.setAlpha(64); + text.setColor(text_color); + } + } + + } + + virtual int rtti() const + { + return QwtPlotItem::Rtti_PlotUserItem; + } + + void draw(QPainter *painter, + const QwtScaleMap &xMap, const QwtScaleMap &, + const QRect &rect) const + { + if (parent->shadeZones()) { + int x = xMap.transform(watts); + int y = (rect.bottom() + rect.top()) / 2; + + // the following code based on source for QwtPlotMarker::draw() + QRect tr(QPoint(0, 0), text.textSize(painter->font())); + tr.moveCenter(QPoint(y, -x)); + painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed + text.draw(painter, tr); + } + } +}; + +// define a background class to handle shading of HR zones +// draws power zone bands IF zones are defined and the option +// to draw bonds has been selected +class HrHistBackground: public QwtPlotItem +{ +private: + PowerHist *parent; + +public: + HrHistBackground(PowerHist *_parent) + { + setZ(0.0); + parent = _parent; + } + + virtual int rtti() const + { + return QwtPlotItem::Rtti_PlotUserItem; + } + + virtual void draw(QPainter *painter, + const QwtScaleMap &xMap, const QwtScaleMap &, + const QRect &rect) const + { + RideItem *rideItem = parent->rideItem; + + if (! rideItem) + return; + + const HrZones *zones = parent->mainWindow->hrZones(); + int zone_range = rideItem->hrZoneRange(); + + if (parent->shadeHRZones() && (zone_range >= 0)) { + QList zone_lows = zones->getZoneLows(zone_range); + int num_zones = zone_lows.size(); + if (num_zones > 0) { + for (int z = 0; z < num_zones; z ++) { + QRect r = rect; + + QColor shading_color = + hrZoneColor(z, num_zones); + shading_color.setHsv( + shading_color.hue(), + shading_color.saturation() / 4, + shading_color.value() + ); + r.setLeft(xMap.transform(zone_lows[z])); + if (z + 1 < num_zones) + r.setRight(xMap.transform(zone_lows[z + 1])); + if (r.right() >= r.left()) + painter->fillRect(r, shading_color); + } + } + } + } +}; + + +// Zone labels are drawn if power zone bands are enabled, automatically +// at the center of the plot +class HrHistZoneLabel: public QwtPlotItem +{ +private: + PowerHist *parent; + int zone_number; + double watts; + QwtText text; + +public: + HrHistZoneLabel(PowerHist *_parent, int _zone_number) + { + parent = _parent; + zone_number = _zone_number; + + RideItem *rideItem = parent->rideItem; + + if (! rideItem) + return; + + const HrZones *zones = parent->mainWindow->hrZones(); + int zone_range = rideItem->hrZoneRange(); + + setZ(1.0 + zone_number / 100.0); + + // create new zone labels if we're shading + if (parent->shadeHRZones() && (zone_range >= 0)) { + QList zone_lows = zones->getZoneLows(zone_range); + QList zone_names = zones->getZoneNames(zone_range); + int num_zones = zone_lows.size(); + assert(zone_names.size() == num_zones); + if (zone_number < num_zones) { + watts = + ( + (zone_number + 1 < num_zones) ? + 0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) : + ( + (zone_number > 0) ? + (1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) : + 2.0 * zone_lows[zone_number] + ) + ); + + text = QwtText(zone_names[zone_number]); + text.setFont(QFont("Helvetica",24, QFont::Bold)); + QColor text_color = hrZoneColor(zone_number, num_zones); + text_color.setAlpha(64); + text.setColor(text_color); + } + } + + } + + virtual int rtti() const + { + return QwtPlotItem::Rtti_PlotUserItem; + } + + void draw(QPainter *painter, + const QwtScaleMap &xMap, const QwtScaleMap &, + const QRect &rect) const + { + if (parent->shadeHRZones()) { + int x = xMap.transform(watts); + int y = (rect.bottom() + rect.top()) / 2; + + // the following code based on source for QwtPlotMarker::draw() + QRect tr(QPoint(0, 0), text.textSize(painter->font())); + tr.moveCenter(QPoint(y, -x)); + painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed + text.draw(painter, tr); + } + } }; #endif // _GC_PowerHist_h diff --git a/src/RideFileCache.cpp b/src/RideFileCache.cpp index 3faa840d2..56f2a4ce1 100644 --- a/src/RideFileCache.cpp +++ b/src/RideFileCache.cpp @@ -161,6 +161,37 @@ RideFileCache::meanMaxArray(RideFile::SeriesType series) } } +QVector & +RideFileCache::distributionArray(RideFile::SeriesType series) +{ + switch (series) { + + case RideFile::watts: + return wattsDistributionDouble; + break; + + case RideFile::cad: + return cadDistributionDouble; + break; + + case RideFile::hr: + return hrDistributionDouble; + break; + + case RideFile::nm: + return nmDistributionDouble; + break; + + case RideFile::kph: + return kphDistributionDouble; + break; + + default: + //? dunno give em power anyway + return wattsMeanMaxDouble; + break; + } +} // // COMPUTATION @@ -550,7 +581,7 @@ RideFileCache::computeDistribution(QVector &array, RideFile::Seri unsigned long lvalue = value * pow(10, decimals); int offset = lvalue - min; - if (offset >= 0 && offset < array.size()) array[offset]++; // XXX recintsecs != 1 + if (offset >= 0 && offset < array.size()) array[offset] += ride->recIntSecs(); } } @@ -584,7 +615,8 @@ static void meanMaxAggregate(QVector &into, QVector &other, QVec // resize into and then sum the arrays static void distAggregate(QVector &into, QVector &other) { - for (int i=0; i