diff --git a/src/AerobicDecoupling.cpp b/src/AerobicDecoupling.cpp index 084042512..ec0b366f1 100644 --- a/src/AerobicDecoupling.cpp +++ b/src/AerobicDecoupling.cpp @@ -53,7 +53,7 @@ class AerobicDecoupling : public RideMetric { setImperialUnits(tr("%")); setPrecision(2); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { double firstHalfPower = 0.0, secondHalfPower = 0.0; double firstHalfHR = 0.0, secondHalfHR = 0.0; diff --git a/src/BasicRideMetrics.cpp b/src/BasicRideMetrics.cpp index 72d03c5e0..dc0eab576 100644 --- a/src/BasicRideMetrics.cpp +++ b/src/BasicRideMetrics.cpp @@ -35,7 +35,7 @@ class WorkoutTime : public RideMetric { setImperialUnits(tr("seconds")); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { seconds = ride->dataPoints().back()->secs - ride->dataPoints().front()->secs + ride->recIntSecs(); @@ -61,7 +61,7 @@ class TimeRiding : public RideMetric { setMetricUnits(tr("seconds")); setImperialUnits(tr("seconds")); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { secsMovingOrPedaling = 0; foreach (const RideFilePoint *point, ride->dataPoints()) { @@ -97,7 +97,7 @@ class TotalDistance : public RideMetric { setPrecision(1); setConversion(MILES_PER_KM); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { // Note: The 'km' in each sample is the distance travelled by the // *end* of the sampling period. The last term in this equation @@ -131,7 +131,7 @@ class ElevationGain : public RideMetric { setImperialUnits(tr("feet")); setConversion(FEET_PER_METER); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { const double hysteresis = 3.0; bool first = true; @@ -170,7 +170,7 @@ class TotalWork : public RideMetric { setMetricUnits(tr("kJ")); setImperialUnits(tr("kJ")); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { foreach (const RideFilePoint *point, ride->dataPoints()) { if (point->watts >= 0.0) @@ -203,7 +203,7 @@ class AvgSpeed : public RideMetric { setConversion(MILES_PER_KM); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &deps) { assert(deps.contains("total_distance")); km = deps.value("total_distance")->value(true); @@ -242,7 +242,7 @@ struct AvgPower : public RideMetric { setImperialUnits(tr("watts")); setType(RideMetric::Average); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { total = count = 0; foreach (const RideFilePoint *point, ride->dataPoints()) { @@ -274,7 +274,7 @@ struct AvgHeartRate : public RideMetric { setImperialUnits(tr("bpm")); setType(RideMetric::Average); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { total = count = 0; foreach (const RideFilePoint *point, ride->dataPoints()) { @@ -306,7 +306,7 @@ struct AvgCadence : public RideMetric { setImperialUnits(tr("rpm")); setType(RideMetric::Average); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { total = count = 0; foreach (const RideFilePoint *point, ride->dataPoints()) { @@ -337,7 +337,7 @@ class MaxPower : public RideMetric { setImperialUnits(tr("watts")); setType(RideMetric::Peak); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { foreach (const RideFilePoint *point, ride->dataPoints()) { if (point->watts >= max) @@ -364,7 +364,7 @@ class MaxHr : public RideMetric { setImperialUnits(tr("bpm")); setType(RideMetric::Peak); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { foreach (const RideFilePoint *point, ride->dataPoints()) { if (point->hr >= max) @@ -391,7 +391,7 @@ class NinetyFivePercentHeartRate : public RideMetric { setImperialUnits(tr("bpm")); setType(RideMetric::Average); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { QVector hrs; foreach (const RideFilePoint *point, ride->dataPoints()) { diff --git a/src/BikeScore.cpp b/src/BikeScore.cpp index 197a6f99a..78a5b60f7 100644 --- a/src/BikeScore.cpp +++ b/src/BikeScore.cpp @@ -48,7 +48,7 @@ class XPower : public RideMetric { setImperialUnits(tr("watts")); } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { static const double EPSILON = 0.1; @@ -104,7 +104,7 @@ class VariabilityIndex : public RideMetric { setPrecision(3); } - void compute(const RideFile *, const Zones *, int, + void compute(const RideFile *, const Zones *, int, const HrZones *, int, const QHash &deps) { assert(deps.contains("skiba_xpower")); assert(deps.contains("average_power")); @@ -135,7 +135,7 @@ class RelativeIntensity : public RideMetric { setImperialUnits(tr("")); setPrecision(3); } - void compute(const RideFile *, const Zones *zones, int zoneRange, + void compute(const RideFile *, const Zones *zones, int zoneRange, const HrZones *, int, const QHash &deps) { if (zones && zoneRange >= 0) { assert(deps.contains("skiba_xpower")); @@ -176,7 +176,7 @@ class BikeScore : public RideMetric { setImperialUnits(""); } - void compute(const RideFile *, const Zones *zones, int zoneRange, + void compute(const RideFile *, const Zones *zones, int zoneRange,const HrZones *, int, const QHash &deps) { if (!zones || zoneRange < 0) return; diff --git a/src/DBAccess.cpp b/src/DBAccess.cpp index 554cd5980..590edc838 100644 --- a/src/DBAccess.cpp +++ b/src/DBAccess.cpp @@ -40,7 +40,7 @@ // DB Schema Version - YOU MUST UPDATE THIS IF THE SCHEMA VERSION CHANGES!!! // Schema version will change if a) the default metadata.xml is updated // or b) new metrics are added / old changed -static int DBSchemaVersion = 14; +static int DBSchemaVersion = 15; DBAccess::DBAccess(MainWindow* main, QDir home) : main(main), home(home) { diff --git a/src/DanielsPoints.cpp b/src/DanielsPoints.cpp index c2d4a272a..82fe26e2d 100644 --- a/src/DanielsPoints.cpp +++ b/src/DanielsPoints.cpp @@ -52,7 +52,7 @@ class DanielsPoints : public RideMetric { setType(RideMetric::Total); } void compute(const RideFile *ride, const Zones *zones, - int zoneRange, const QHash &) { + int zoneRange, const HrZones *, int, const QHash &) { if (!zones || zoneRange < 0) { setValue(0); return; @@ -111,7 +111,7 @@ class DanielsEquivalentPower : public RideMetric { setType(RideMetric::Average); } - void compute(const RideFile *, const Zones *zones, int zoneRange, + void compute(const RideFile *, const Zones *zones, int zoneRange, const HrZones *, int, const QHash &deps) { if (!zones || zoneRange < 0) { diff --git a/src/HistogramWindow.cpp b/src/HistogramWindow.cpp index 2a04659e4..28aea2fb4 100644 --- a/src/HistogramWindow.cpp +++ b/src/HistogramWindow.cpp @@ -21,9 +21,13 @@ #include "PowerHist.h" #include "RideFile.h" #include "RideItem.h" +#include "Settings.h" #include #include +#include "Zones.h" +#include "HrZones.h" + HistogramWindow::HistogramWindow(MainWindow *mainWindow) : QWidget(mainWindow), mainWindow(mainWindow) { @@ -49,9 +53,20 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) : withZerosCheckBox = new QCheckBox; withZerosCheckBox->setText(tr("With zeros")); binWidthLayout->addWidget(withZerosCheckBox); + + histShadeZones = new QCheckBox; + histShadeZones->setText(tr("Shade zones")); + histShadeZones->setChecked(true); + binWidthLayout->addWidget(histShadeZones); + histParameterCombo = new QComboBox(); binWidthLayout->addWidget(histParameterCombo); + histSumY = new QComboBox(); + histSumY->addItem(tr("Absolute Time")); + histSumY->addItem(tr("Percentage Time")); + binWidthLayout->addWidget(histSumY); + powerHist = new PowerHist(mainWindow); setHistTextValidator(); @@ -73,6 +88,10 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) : 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())); connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged())); @@ -87,6 +106,11 @@ HistogramWindow::rideSelected() RideItem *ride = mainWindow->rideItem(); if (!ride) return; + + // get range that applies to this ride + powerRange = mainWindow->hrZones()->whichRange(ride->dateTime.date()); + hrRange = mainWindow->zones()->whichRange(ride->dateTime.date()); + // set the histogram data powerHist->setData(ride); // make sure the histogram has a legal selection @@ -132,6 +156,13 @@ HistogramWindow::setlnYHistFromCheckBox() powerHist->setlnY(! powerHist->islnY()); } +void +HistogramWindow::setSumY(int index) +{ + if (index < 0) return; // being destroyed + else powerHist->setSumY(index == 0); +} + void HistogramWindow::setWithZerosFromCheckBox() { @@ -171,22 +202,40 @@ HistogramWindow::setHistTextValidator() } void -HistogramWindow::setHistSelection(int id) +HistogramWindow::setHistSelection(int /*id*/) { - if (id == histWattsShadedID) - powerHist->setSelection(PowerHist::wattsShaded); - else if (id == histWattsUnshadedID) - powerHist->setSelection(PowerHist::wattsUnshaded); + // 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); + + // lets get the selection since we are called from + // the checkbox and combobox signal + int id = histParameterCombo->currentIndex(); + + // Which data series are we plotting? + if (id == histWattsID) + powerHist->setSelection(PowerHist::watts); + else if (id == histWattsZoneID) // we can zone power! + powerHist->setSelection(PowerHist::wattsZone); else if (id == histNmID) - powerHist->setSelection(PowerHist::nm); + powerHist->setSelection(PowerHist::nm); else if (id == histHrID) - powerHist->setSelection(PowerHist::hr); + powerHist->setSelection(PowerHist::hr); + else if (id == histHrZoneID) // we can zone HR! + powerHist->setSelection(PowerHist::hrZone); else if (id == histKphID) - powerHist->setSelection(PowerHist::kph); + powerHist->setSelection(PowerHist::kph); else if (id == histCadID) - powerHist->setSelection(PowerHist::cad); + powerHist->setSelection(PowerHist::cad); else - fprintf(stderr, "Illegal id encountered: %d", id); + powerHist->setSelection(PowerHist::watts); + + // just in case it gets switch off... + if (!powerHist->islnY()) lnYHistCheckBox->setChecked(false); setHistBinWidthText(); setHistTextValidator(); @@ -215,23 +264,22 @@ HistogramWindow::setHistWidgets(RideItem *rideItem) if (ride) { // we want to retain the present selection - PowerHist::Selection s = powerHist->selection(); + PowerHist::Selection s = powerHist->selection(); histParameterCombo->clear(); - histWattsShadedID = - histWattsUnshadedID = - histNmID = - histHrID = - histKphID = - histCadID = - -1; + histWattsID = histWattsZoneID = + histNmID = histHrID = histHrZoneID = + histKphID = histCadID = -1; if (ride->areDataPresent()->watts) { - histWattsShadedID = count ++; - histParameterCombo->addItem(tr("Watts(shaded)")); - histWattsUnshadedID = count ++; - histParameterCombo->addItem(tr("Watts(unshaded)")); + histWattsID = count ++; + histParameterCombo->addItem(tr("Watts")); + + if (powerRange != -1) { + histWattsZoneID = count ++; + histParameterCombo->addItem(tr("Watts (by Zone)")); + } } if (ride->areDataPresent()->nm) { histNmID = count ++; @@ -240,6 +288,10 @@ HistogramWindow::setHistWidgets(RideItem *rideItem) if (ride->areDataPresent()->hr) { histHrID = count ++; histParameterCombo->addItem(tr("Heartrate")); + if (hrRange != -1) { + histHrZoneID = count ++; + histParameterCombo->addItem(tr("Heartrate (by Zone)")); + } } if (ride->areDataPresent()->kph) { histKphID = count ++; @@ -257,14 +309,16 @@ HistogramWindow::setHistWidgets(RideItem *rideItem) lnYHistCheckBox->setEnabled(true); // set widget to proper value - if ((s == PowerHist::wattsShaded) && (histWattsShadedID >= 0)) - histParameterCombo->setCurrentIndex(histWattsShadedID); - else if ((s == PowerHist::wattsUnshaded) && (histWattsUnshadedID >= 0)) - histParameterCombo->setCurrentIndex(histWattsUnshadedID); + if ((s == PowerHist::watts) && (histWattsID >= 0)) + histParameterCombo->setCurrentIndex(histWattsID); + else if ((s == PowerHist::wattsZone) && (histWattsZoneID >= 0)) + histParameterCombo->setCurrentIndex(histWattsZoneID); else if ((s == PowerHist::nm) && (histNmID >= 0)) histParameterCombo->setCurrentIndex(histNmID); else if ((s == PowerHist::hr) && (histHrID >= 0)) histParameterCombo->setCurrentIndex(histHrID); + else if ((s == PowerHist::hrZone) && (histHrZoneID >= 0)) + histParameterCombo->setCurrentIndex(histHrZoneID); else if ((s == PowerHist::kph) && (histKphID >= 0)) histParameterCombo->setCurrentIndex(histKphID); else if ((s == PowerHist::cad) && (histCadID >= 0)) diff --git a/src/HistogramWindow.h b/src/HistogramWindow.h index 295e98806..eb219c5ff 100644 --- a/src/HistogramWindow.h +++ b/src/HistogramWindow.h @@ -51,6 +51,7 @@ class HistogramWindow : public QWidget void setlnYHistFromCheckBox(); void setWithZerosFromCheckBox(); void setHistSelection(int id); + void setSumY(int); protected: @@ -63,15 +64,20 @@ class HistogramWindow : public QWidget QLineEdit *binWidthLineEdit; QCheckBox *lnYHistCheckBox; QCheckBox *withZerosCheckBox; + QCheckBox *histShadeZones; QComboBox *histParameterCombo; + QComboBox *histSumY; - int histWattsShadedID; - int histWattsUnshadedID; + int histWattsID; + int histWattsZoneID; int histNmID; int histHrID; + int histHrZoneID; int histKphID; int histCadID; int histAltID; + + int powerRange, hrRange; }; #endif // _GC_HistogramWindow_h diff --git a/src/HrTimeInZone.cpp b/src/HrTimeInZone.cpp new file mode 100644 index 000000000..6c0d8cae5 --- /dev/null +++ b/src/HrTimeInZone.cpp @@ -0,0 +1,173 @@ + +/* + * Copyright (c) 2010 Damien Grauser (Damien.Grauser@pev-geneve.ch), 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 + */ + +#include "RideMetric.h" +#include "BestIntervalDialog.h" +#include "HrZones.h" +#include + +#define tr(s) QObject::tr(s) + +class HrZoneTime : public RideMetric { + int level; + double seconds; + + QList lo; + QList hi; + +public: + + HrZoneTime() : level(0), seconds(0.0) + { + setType(RideMetric::Total); + setMetricUnits("seconds"); + setImperialUnits("seconds"); + setPrecision(0); + setConversion(1.0); + } + void setLevel(int level) { this->level=level-1; } // zones start from zero not 1 + void compute(const RideFile *ride, const Zones *, int, const HrZones *hrZone, int hrZoneRange, + const QHash &) + { + seconds = 0; + // get zone ranges + if (hrZone && hrZoneRange >= 0) { + // iterate and compute + foreach(const RideFilePoint *point, ride->dataPoints()) { + if (hrZone->whichZone(hrZoneRange, point->hr) == level) + seconds += ride->recIntSecs(); + } + } + setValue(seconds); + } + + bool canAggregate() const { return false; } + void aggregateWith(const RideMetric &) {} + RideMetric *clone() const { return new HrZoneTime(*this); } +}; + +class HrZoneTime1 : public HrZoneTime { + +public: + HrZoneTime1() + { + setLevel(1); + setSymbol("time_in_zone_H1"); + setName(tr("H1 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime1(*this); } +}; + +class HrZoneTime2 : public HrZoneTime { + +public: + HrZoneTime2() + { + setLevel(2); + setSymbol("time_in_zone_H2"); + setName(tr("H2 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime2(*this); } +}; + +class HrZoneTime3 : public HrZoneTime { + +public: + HrZoneTime3() + { + setLevel(3); + setSymbol("time_in_zone_H3"); + setName(tr("H3 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime3(*this); } +}; + +class HrZoneTime4 : public HrZoneTime { + +public: + HrZoneTime4() + { + setLevel(4); + setSymbol("time_in_zone_H4"); + setName(tr("H4 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime4(*this); } +}; + +class HrZoneTime5 : public HrZoneTime { + +public: + HrZoneTime5() + { + setLevel(5); + setSymbol("time_in_zone_H5"); + setName(tr("H5 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime5(*this); } +}; + +class HrZoneTime6 : public HrZoneTime { + +public: + HrZoneTime6() + { + setLevel(6); + setSymbol("time_in_zone_H6"); + setName(tr("H6 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime6(*this); } +}; + +class HrZoneTime7 : public HrZoneTime { + +public: + HrZoneTime7() + { + setLevel(7); + setSymbol("time_in_zone_H7"); + setName(tr("H7 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime7(*this); } +}; + +class HrZoneTime8 : public HrZoneTime { + +public: + HrZoneTime8() + { + setLevel(8); + setSymbol("time_in_zone_H8"); + setName(tr("H8 Time in Zone")); + } + RideMetric *clone() const { return new HrZoneTime8(*this); } +}; + +static bool addAllHrZones() { + RideMetricFactory::instance().addMetric(HrZoneTime1()); + RideMetricFactory::instance().addMetric(HrZoneTime2()); + RideMetricFactory::instance().addMetric(HrZoneTime3()); + RideMetricFactory::instance().addMetric(HrZoneTime4()); + RideMetricFactory::instance().addMetric(HrZoneTime5()); + RideMetricFactory::instance().addMetric(HrZoneTime6()); + RideMetricFactory::instance().addMetric(HrZoneTime7()); + RideMetricFactory::instance().addMetric(HrZoneTime8()); + return true; +} + +static bool allHrZonesAdded = addAllHrZones(); diff --git a/src/HrZones.cpp b/src/HrZones.cpp new file mode 100644 index 000000000..7d9c7f68e --- /dev/null +++ b/src/HrZones.cpp @@ -0,0 +1,870 @@ +/* + * Copyright (c) 2010 Damien Grauser (Damien.Grauser@pev-geneve.ch) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include "HrZones.h" +#include "Colors.h" +#include "TimeUtils.h" +#include +#include +#include +#include +#include +#include + + +// the infinity endpoints are indicated with extreme date ranges +// but not zero dates so we can edit and compare them +static const QDate date_zero(1900, 01, 01); +static const QDate date_infinity(9999,12,31); + +// initialize default static zone parameters +void HrZones::initializeZoneParameters() +{ + static int initial_zone_default[] = { + 0, 68, 83, 94, 105 + }; + static double initial_zone_default_trimp[] = { + 0.9, 1.1, 1.2, 2.0, 5.0 + }; + static const char *initial_zone_default_desc[] = { + "Active Recovery", "Endurance", "Tempo", "Threshold", + "VO2Max" + }; + static const char *initial_zone_default_name[] = { + "Z1", "Z2", "Z3", "Z4", "Z5" + }; + + static int initial_nzones_default = + sizeof(initial_zone_default) / + sizeof(initial_zone_default[0]); + + scheme.zone_default.clear(); + scheme.zone_default_is_pct.clear(); + scheme.zone_default_desc.clear(); + scheme.zone_default_name.clear(); + scheme.zone_default_trimp.clear(); + scheme.nzones_default = 0; + + scheme.nzones_default = initial_nzones_default; + + for (int z = 0; z < scheme.nzones_default; z ++) { + scheme.zone_default.append(initial_zone_default[z]); + scheme.zone_default_is_pct.append(true); + scheme.zone_default_name.append(QString(initial_zone_default_name[z])); + scheme.zone_default_desc.append(QString(initial_zone_default_desc[z])); + scheme.zone_default_trimp.append(initial_zone_default_trimp[z]); + } +} + +// read zone file, allowing for zones with or without end dates +bool HrZones::read(QFile &file) +{ + defaults_from_user = false; + scheme.zone_default.clear(); + scheme.zone_default_is_pct.clear(); + scheme.zone_default_name.clear(); + scheme.zone_default_desc.clear(); + scheme.zone_default_trimp.clear(); + scheme.nzones_default = 0; + ranges.clear(); + + // set up possible warning dialog + warning = QString(); + int warning_lines = 0; + const int max_warning_lines = 100; + + // macro to append lines to the warning + #define append_to_warning(s) \ + if (warning_lines < max_warning_lines) \ + warning.append(s); \ + else if (warning_lines == max_warning_lines) \ + warning.append("...\n"); \ + warning_lines ++; + + // read using text mode takes care of end-lines + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + err = "can't open file"; + return false; + } + QTextStream fileStream(&file); + + QRegExp commentrx("\\s*#.*$"); + QRegExp blankrx("^[ \t]*$"); + QRegExp rangerx[] = { + QRegExp("^\\s*(?:from\\s+)?" // optional "from" + "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date + "\\s*([,:]?\\s*(LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)} + "\\s*([,:]?\\s*(RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)} + "\\s*([,:]?\\s*(MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)} + "\\s*:?\\s*$", // optional : + Qt::CaseInsensitive), + QRegExp("^\\s*(?:from\\s+)?" // optional "from" + "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date + "\\s+(?:until|to|-)\\s+" // until + "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?" // end date + "\\s*:?,?\\s*((LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)} + "\\s*:?,?\\s*((RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)} + "\\s*:?,?\\s*((MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)} + "\\s*:?\\s*$", // optional : + Qt::CaseInsensitive) + }; + QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*" + "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+(\\.\\d+)?)\\s*)?$", + Qt::CaseInsensitive);// + QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$", + Qt::CaseInsensitive); + + int lineno = 0; + + // the current range in the file + // ZoneRange *range = NULL; + bool in_range = false; + QDate begin = date_zero, end = date_infinity; + int lt = 0; + int restHr = 0; + int maxHr = 0; + QList zoneInfos; + + // true if zone defaults are found in the file (then we need to write them) + bool zones_are_defaults = false; + + while (! fileStream.atEnd() ) { + ++lineno; + QString line = fileStream.readLine(); + int pos = commentrx.indexIn(line, 0); + if (pos != -1) + line = line.left(pos); + if (blankrx.indexIn(line, 0) == 0) + goto next_line; + + // check for default zone range definition (may be followed by hr zone definitions) + if (zonedefaultsx.indexIn(line, 0) != -1) { + zones_are_defaults = true; + + // defaults are allowed only at the beginning of the file + if (ranges.size()) { + err = "HR Zone defaults must be specified at head of hr.zones file"; + return false; + } + + // only one set of defaults is allowed + if (scheme.nzones_default) { + err = "Only one set of zone defaults may be specified in hr.zones file"; + return false; + } + + goto next_line; + } + + // check for range specification (may be followed by zone definitions) + for (int r=0; r<2; r++) { + if (rangerx[r].indexIn(line, 0) != -1) { + + if (in_range) { + + // if zones are empty, then generate them + HrZoneRange range(begin, end, lt, restHr, maxHr); + range.zones = zoneInfos; + + if (range.zones.empty()) { + if (range.lt > 0) setHrZonesFromLT(range); + else { + err = tr("line %1: read new range without reading " + "any zones for previous one").arg(lineno); + file.close(); + return false; + } + } else { + qSort(range.zones); + } + ranges.append(range); + } + + in_range = true; + zones_are_defaults = false; + zoneInfos.clear(); + + // process the beginning date + if (rangerx[r].cap(1) == "BEGIN") + begin = date_zero; + else { + begin = QDate(rangerx[r].cap(2).toInt(), + rangerx[r].cap(3).toInt(), + rangerx[r].cap(4).toInt()); + } + + // process an end date, if any, else it is null + if (rangerx[r].cap(5) == "END") end = date_infinity; + else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || + rangerx[r].cap(8).toInt()) { + + end = QDate(rangerx[r].cap(6).toInt(), + rangerx[r].cap(7).toInt(), + rangerx[r].cap(8).toInt()); + + } else { + end = QDate(); + } + + // set up the range, capturing LT if it's specified + // range = new ZoneRange(begin, end); + int nLT = (r ? 11 : 7); + if (rangerx[r].numCaptures() >= (nLT)) lt = rangerx[r].cap(nLT).toInt(); + else lt = 0; + + int nRestHr = (r ? 14 : 10); + if (rangerx[r].numCaptures() >= (nRestHr)) restHr = rangerx[r].cap(nRestHr).toInt(); + else restHr = 0; + + int nMaxHr = (r ? 17 : 13); + if (rangerx[r].numCaptures() >= (nRestHr)) maxHr = rangerx[r].cap(nMaxHr).toInt(); + else maxHr = 0; + + // bleck + goto next_line; + } + } + + // check for zone definition + if (zonerx.indexIn(line, 0) != -1) { + if (! (in_range || zones_are_defaults)) { + err = tr("line %1: read zone without " + "preceeding date range").arg(lineno); + file.close(); + return false; + } + + int lo = zonerx.cap(3).toInt(); + double trimp = zonerx.cap(5).toDouble(); + + // allow for zone specified as % of LT + bool lo_is_pct = false; + if (zonerx.cap(4) == "%") { + if (zones_are_defaults) lo_is_pct = true; + else if (lt > 0) lo = int(lo * lt / 100); + else { + err = tr("attempt to set zone based on % of " + "LT without setting LT in line number %1.\n"). + arg(lineno); + file.close(); + return false; + } + } + + int hi = -1; // signal an undefined number + double tr = zonerx.cap(5).toDouble(); + + if (zones_are_defaults) { + scheme.nzones_default ++; + scheme.zone_default_is_pct.append(lo_is_pct); + scheme.zone_default.append(lo); + scheme.zone_default_name.append(zonerx.cap(1)); + scheme.zone_default_desc.append(zonerx.cap(2)); + scheme.zone_default_trimp.append(trimp); + defaults_from_user = true; + } + else { + HrZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi, tr); + zoneInfos.append(zone); + } + } + next_line: {} + } + + if (in_range) { + HrZoneRange range(begin, end, lt, restHr, maxHr); + range.zones = zoneInfos; + if (range.zones.empty()) { + if (range.lt > 0) + setHrZonesFromLT(range); + else { + err = tr("file ended without reading any zones for last range"); + file.close(); + return false; + } + } + else { + qSort(range.zones); + } + + ranges.append(range); + } + file.close(); + + // sort the ranges + qSort(ranges); + + // set the default zones if not in file + if (!scheme.nzones_default) { + + // do we have a zone which is explicitly set? + for (int i=0; i 4) { + append_to_warning(tr("Range %1: matching top of zone %2 " + "(%3) to bottom of zone %4 (%5).\n"). + arg(nr + 1). + arg(ranges[nr].zones[nz].name). + arg(ranges[nr].zones[nz].hi). + arg(ranges[nr].zones[nz + 1].name). + arg(ranges[nr].zones[nz + 1].lo) + ); + } + ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo; + + } else if ((nz == ranges[nr].zones.size() - 1) && + (ranges[nr].zones[nz].hi < INT_MAX)) { + + append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n"). + arg(nr + 1). + arg(ranges[nr].zones[nz].name). + arg(ranges[nr].zones[nz].hi) + ); + ranges[nr].zones[nz].hi = INT_MAX; + } + } + } + } + + // mark zones as modified so pages which depend on zones can be updated + modificationTime = QDateTime::currentDateTime(); + + return true; +} + +// note empty dates are treated as automatic matches for begin or +// end of range +int HrZones::whichRange(const QDate &date) const +{ + for (int rnum = 0; rnum < ranges.size(); ++rnum) { + const HrZoneRange &range = ranges[rnum]; + if (((date >= range.begin) || (range.begin.isNull())) && + ((date < range.end) || (range.end.isNull()))) + return rnum; + } + return -1; +} + +int HrZones::numZones(int rnum) const +{ + assert(rnum < ranges.size()); + return ranges[rnum].zones.size(); +} + +int HrZones::whichZone(int rnum, double value) const +{ + assert(rnum < ranges.size()); + const HrZoneRange &range = ranges[rnum]; + for (int j = 0; j < range.zones.size(); ++j) { + const HrZoneInfo &info = range.zones[j]; + // note: the "end" of range is actually in the next zone + if ((value >= info.lo) && (value < info.hi)) + return j; + } + return -1; +} + +void HrZones::zoneInfo(int rnum, int znum, + QString &name, QString &description, + int &low, int &high, double &trimp) const +{ + assert(rnum < ranges.size()); + const HrZoneRange &range = ranges[rnum]; + assert(znum < range.zones.size()); + const HrZoneInfo &zone = range.zones[znum]; + name = zone.name; + description = zone.desc; + low = zone.lo; + high = zone.hi; + trimp= zone.trimp; +} + +int HrZones::getLT(int rnum) const +{ + assert(rnum < ranges.size()); + return ranges[rnum].lt; +} + +void HrZones::setLT(int rnum, int lt) +{ + ranges[rnum].lt = lt; + modificationTime = QDateTime::currentDateTime(); +} + +// generate a list of zones from LT +int HrZones::lowsFromLT(QList *lows, int lt) const { + + lows->clear(); + + for (int z = 0; z < scheme.nzones_default; z++) + lows->append(scheme.zone_default_is_pct[z] ? + scheme.zone_default[z] * lt / 100 : scheme.zone_default[z]); + + return scheme.nzones_default; +} + +int HrZones::getRestHr(int rnum) const +{ + assert(rnum < ranges.size()); + return ranges[rnum].restHr; +} + +void HrZones::setRestHr(int rnum, int restHr) +{ + ranges[rnum].restHr = restHr; + modificationTime = QDateTime::currentDateTime(); +} + +int HrZones::getMaxHr(int rnum) const +{ + assert(rnum < ranges.size()); + return ranges[rnum].maxHr; +} + +void HrZones::setMaxHr(int rnum, int maxHr) +{ + ranges[rnum].maxHr = maxHr; + modificationTime = QDateTime::currentDateTime(); +} + +// access the zone name +QString HrZones::getDefaultZoneName(int z) const { + return scheme.zone_default_name[z]; +} + +// access the zone description +QString HrZones::getDefaultZoneDesc(int z) const { + return scheme.zone_default_desc[z]; +} + +// set the zones from the LT value (the cp variable) +void HrZones::setHrZonesFromLT(HrZoneRange &range) { + range.zones.clear(); + + if (scheme.nzones_default == 0) + initializeZoneParameters(); + + for (int i = 0; i < scheme.nzones_default; i++) { + int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.lt / 100 : scheme.zone_default[i]; + int hi = lo; + double trimp = scheme.zone_default_trimp[i]; + + HrZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi, trimp); + range.zones.append(zone); + } + + // sort the zones (some may be pct, others absolute, so zones need to be sorted, + // rather than the defaults + qSort(range.zones); + + // set zone end dates + for (int i = 0; i < range.zones.size(); i ++) + range.zones[i].hi = + (i < scheme.nzones_default - 1) ? + range.zones[i + 1].lo : + INT_MAX; + + // mark that the zones were set from LT, so if zones are subsequently + // written, only LT is saved + range.hrZonesSetFromLT = true; +} + +void HrZones::setHrZonesFromLT(int rnum) { + assert((rnum >= 0) && (rnum < ranges.size())); + setHrZonesFromLT(ranges[rnum]); +} + +// return the list of starting values of zones for a given range +QList HrZones::getZoneLows(int rnum) const { + if (rnum >= ranges.size()) + return QList (); + const HrZoneRange &range = ranges[rnum]; + QList return_values; + for (int i = 0; i < range.zones.size(); i ++) + return_values.append(ranges[rnum].zones[i].lo); + return return_values; +} + +// return the list of ending values of zones for a given range +QList HrZones::getZoneHighs(int rnum) const { + if (rnum >= ranges.size()) + return QList (); + const HrZoneRange &range = ranges[rnum]; + QList return_values; + for (int i = 0; i < range.zones.size(); i ++) + return_values.append(ranges[rnum].zones[i].hi); + return return_values; +} + +// return the list of zone names +QList HrZones::getZoneNames(int rnum) const { + if (rnum >= ranges.size()) + return QList (); + const HrZoneRange &range = ranges[rnum]; + QList return_values; + for (int i = 0; i < range.zones.size(); i ++) + return_values.append(ranges[rnum].zones[i].name); + return return_values; +} + +// return the list of zone trimp coef +QList HrZones::getZoneTrimps(int rnum) const { + if (rnum >= ranges.size()) + return QList (); + const HrZoneRange &range = ranges[rnum]; + QList return_values; + for (int i = 0; i < range.zones.size(); i ++) + return_values.append(ranges[rnum].zones[i].trimp); + return return_values; +} + + + +QString HrZones::summarize(int rnum, QVector &time_in_zone) const +{ + assert(rnum < ranges.size()); + const HrZoneRange &range = ranges[rnum]; + assert(time_in_zone.size() == range.zones.size()); + QString summary; + if(range.lt > 0){ + summary += ""; + summary += "
"; + summary += tr("Threshold: %1").arg(range.lt); + summary += "
"; + } + summary += ""; + summary += ""; + summary += tr(""); + summary += tr(""); + summary += tr(""); + summary += tr(""); + summary += tr(""); + summary += ""; + QColor color = QApplication::palette().alternateBase().color(); + color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); + for (int zone = 0; zone < time_in_zone.size(); ++zone) { + if (time_in_zone[zone] > 0.0) { + QString name, desc; + int lo, hi; + double trimp; + zoneInfo(rnum, zone, name, desc, lo, hi, trimp); + if (zone % 2 == 0) + summary += ""; + else + summary += ""; + summary += QString("").arg(name); + summary += QString("").arg(desc); + summary += QString("").arg(lo); + if (hi == INT_MAX) + summary += ""; + else + summary += QString("").arg(hi); + summary += QString("") + .arg(time_to_string((unsigned) round(time_in_zone[zone]))); + summary += ""; + } + } + summary += "
ZoneDescriptionLowHighTime
%1%1%1MAX%1%1
"; + return summary; +} + +#define USE_SHORT_POWER_ZONES_FORMAT true /* whether a less redundent format should be used */ +void HrZones::write(QDir home) +{ + QString strzones; + + // always write the defaults (config pane can adjust) + strzones += QString("DEFAULTS:\n"); + for (int z = 0 ; z < scheme.nzones_default; z ++) + strzones += QString("%1,%2,%3%4,%5\n"). + arg(scheme.zone_default_name[z]). + arg(scheme.zone_default_desc[z]). + arg(scheme.zone_default[z]). + arg(scheme.zone_default_is_pct[z]?"%":""). + arg(scheme.zone_default_trimp[z]); + strzones += QString("\n"); + + for (int i = 0; i < ranges.size(); i++) { + int lt = getLT(i); + int restHr = getRestHr(i); + int maxHr = getMaxHr(i); + + // print header for range + // note this explicitly sets the first and last ranges such that all time is spanned + + // note: BEGIN is not needed anymore + // since it becomes Jan 01 1900 + strzones += QString("%1: LT=%2, RestHr=%3, MaxHr=%4").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(lt).arg(restHr).arg(maxHr); + strzones += QString("\n"); + + // step through and print the zones if they've been explicitly set + if (! ranges[i].hrZonesSetFromLT) { + for (int j = 0; j < ranges[i].zones.size(); j ++) { + const HrZoneInfo &zi = ranges[i].zones[j]; + strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.trimp); + } + strzones += QString("\n"); + } + } + + QFile file(home.absolutePath() + "/hr.zones"); + if (file.open(QFile::WriteOnly)) + { + QTextStream stream(&file); + stream << strzones; + file.close(); + } +} + +void HrZones::addHrZoneRange(QDate _start, QDate _end, int _lt, int _restHr, int _maxHr) +{ + ranges.append(HrZoneRange(_start, _end, _lt, _restHr, _maxHr)); +} + +// insert a new zone range using the current scheme +// return the range number +int HrZones::addHrZoneRange(QDate _start, int _lt, int _restHr, int _maxHr) +{ + int rnum; + + // where to add this range? + for(rnum=0; rnum < ranges.count(); rnum++) if (ranges[rnum].begin > _start) break; + + // at the end ? + if (rnum == ranges.count()) ranges.append(HrZoneRange(_start, date_infinity, _lt, _restHr, _maxHr)); + else ranges.insert(rnum, HrZoneRange(_start, ranges[rnum].begin, _lt, _restHr, _maxHr)); + + // modify previous end date + if (rnum) ranges[rnum-1].end = _start; + + // set zones from LT + if (_lt > 0) { + setLT(rnum, _lt); + setHrZonesFromLT(rnum); + } + + return rnum; +} + +void HrZones::addHrZoneRange() +{ + ranges.append(HrZoneRange(date_zero, date_infinity)); +} + +void HrZones::setEndDate(int rnum, QDate endDate) +{ + ranges[rnum].end = endDate; + modificationTime = QDateTime::currentDateTime(); +} +void HrZones::setStartDate(int rnum, QDate startDate) +{ + ranges[rnum].begin = startDate; + modificationTime = QDateTime::currentDateTime(); +} + +QDate HrZones::getStartDate(int rnum) const +{ + assert(rnum >= 0); + return ranges[rnum].begin; +} + +QString HrZones::getStartDateString(int rnum) const +{ + assert(rnum >= 0); + QDate d = ranges[rnum].begin; + return (d.isNull() ? "BEGIN" : d.toString()); +} + +QDate HrZones::getEndDate(int rnum) const +{ + assert(rnum >= 0); + return ranges[rnum].end; +} + +QString HrZones::getEndDateString(int rnum) const +{ + assert(rnum >= 0); + QDate d = ranges[rnum].end; + return (d.isNull() ? "END" : d.toString()); +} + +int HrZones::getRangeSize() const +{ + return ranges.size(); +} + +// generate a zone color with a specific number of zones +QColor HrZoneColor(int z, int) { + switch(z) { + + case 0 : return GColor(CZONE1); break; + case 1 : return GColor(CZONE2); break; + case 2 : return GColor(CZONE3); break; + case 3 : return GColor(CZONE4); break; + case 4 : return GColor(CZONE5); break; + case 5 : return GColor(CZONE6); break; + case 6 : return GColor(CZONE7); break; + case 7 : return GColor(CZONE8); break; + case 8 : return GColor(CZONE9); break; + case 9 : return GColor(CZONE10); break; + default: return QColor(128,128,128); break; + } +} + +// delete a range, extend an adjacent (prior if available, otherwise next) +// range to cover the same time period, then return the number of the new range +// covering the date range of the deleted range or -1 if none left +int HrZones::deleteRange(int rnum) { + + // check bounds - silently fail, don't assert + assert (rnum < ranges.count() && rnum >= 0); + + // extend the previous range to the end of this range + // but only if we have a previous range + if (rnum > 0) setEndDate(rnum-1, getEndDate(rnum)); + + // delete this range then + ranges.removeAt(rnum); + + return rnum-1; +} + +// insert a new range starting at the given date extending to the end of the zone currently +// containing that date. If the start date of that zone is prior to the specified start +// date, then that zone range is shorted. +int HrZones::insertRangeAtDate(QDate date, int lt) { + assert(date.isValid()); + int rnum; + + if (ranges.empty()) { + addHrZoneRange(); + rnum = 0; + } + else { + rnum = whichRange(date); + assert(rnum >= 0); + QDate date1 = getStartDate(rnum); + + // if the old range has dates before the specified, then truncate + // the old range and shift up the existing ranges + if (date > date1) { + QDate endDate = getEndDate(rnum); + setEndDate(rnum, date); + ranges.insert(++ rnum, HrZoneRange(date, endDate)); + } + } + + if (lt > 0) { + setLT(rnum, lt); + setHrZonesFromLT(rnum); + } + + return rnum; +} + +unsigned long +HrZones::getFingerprint() const +{ + boost::crc_optimal<16, 0x1021, 0xFFFF, 0, false, false> CRC; + for (int i=0; i + +// A zone "scheme" defines how power zones +// are calculated as a percentage of LT +// The default is to use Coggan percentages +// but this can be overriden in hr.zones +struct HrZoneScheme { + QList zone_default; + QList zone_default_is_pct; + QList zone_default_name; + QList zone_default_desc; + QList zone_default_trimp; + int nzones_default; +}; + +// A zone "info" defines a *single zone* +// in absolute watts terms e.g. +// "L4" "Threshold" +struct HrZoneInfo { + QString name, desc; + int lo, hi; + double trimp; + HrZoneInfo(const QString &n, const QString &d, int l, int h, double t) : + name(n), desc(d), lo(l), hi(h), trimp(t) {} + + // used by qSort() + bool operator< (HrZoneInfo right) const { + return ((lo < right.lo) || ((lo == right.lo) && (hi < right.hi))); + } +}; + +// A zone "range" defines the power zones +// that are active for a *specific date period* +// e.g. between 01/01/2008 and 01/04/2008 +// my LT was 170 and I chose to setup +// 5 zoneinfos from Active Recovery through +// to VO2Max +struct HrZoneRange { + QDate begin, end; + int lt; + int restHr; + int maxHr; + + QList zones; + bool hrZonesSetFromLT; + HrZoneRange(const QDate &b, const QDate &e) : + begin(b), end(e), lt(0), hrZonesSetFromLT(false) {} + HrZoneRange(const QDate &b, const QDate &e, int _lt, int _restHr, int _maxHr) : + begin(b), end(e), lt(_lt), restHr(_restHr), maxHr(_maxHr), hrZonesSetFromLT(false) {} + + // used by qSort() + bool operator< (HrZoneRange right) const { + return (((! right.begin.isNull()) && + (begin.isNull() || begin < right.begin )) || + ((begin == right.begin) && (! end.isNull()) && + ( right.end.isNull() || end < right.end ))); + } +}; + + +class HrZones : public QObject +{ + Q_OBJECT + + private: + + // Scheme + bool defaults_from_user; + HrZoneScheme scheme; + + // LT History + QList ranges; + + // utility + QString err, warning; + void setHrZonesFromLT(HrZoneRange &range); + + public: + + HrZones() : defaults_from_user(false) { + initializeZoneParameters(); + } + + // + // Zone settings - Scheme (& default scheme) + // + HrZoneScheme getScheme() const { return scheme; } + void setScheme(HrZoneScheme x) { scheme = x; } + + // get defaults from the current scheme + QString getDefaultZoneName(int z) const; + QString getDefaultZoneDesc(int z) const; + + // set zone parameters to either user-specified defaults + // or to defaults using Coggan's coefficients + void initializeZoneParameters(); + + // + // Zone history - Ranges + // + // How many ranges in our history + int getRangeSize() const; + + // Add ranges + void addHrZoneRange(QDate _start, QDate _end, int _lt, int _restHr, int _maxHr); + int addHrZoneRange(QDate _start, int _lt, int _restHr, int _maxHr); + void addHrZoneRange(); + + // insert a range from the given date to the end date of the range + // presently including the date + int insertRangeAtDate(QDate date, int lt = 0); + + // Get / Set ZoneRange details + HrZoneRange getHrZoneRange(int rnum) { return ranges[rnum]; } + void setHrZoneRange(int rnum, HrZoneRange x) { ranges[rnum] = x; } + + // get and set LT for a given range + int getLT(int rnum) const; + void setLT(int rnum, int cp); + + // get and set Rest Hr for a given range + int getRestHr(int rnum) const; + void setRestHr(int rnum, int restHr); + + // get and set LT for a given range + int getMaxHr(int rnum) const; + void setMaxHr(int rnum, int maxHr); + + // calculate and then set zoneinfo for a given range + void setHrZonesFromLT(int rnum); + + // delete the range rnum, and adjust dates on adjacent zone; return + // the range number of the range extended to cover the deleted zone + int deleteRange(const int rnum); + + // + // read and write hr.zones + // + bool read(QFile &file); + void write(QDir home); + const QString &errorString() const { return err; } + const QString &warningString() const { return warning; } + + + // + // Typical APIs to get details of ranges and zones + // + + // which range is active for a particular date + int whichRange(const QDate &date) const; + + // which zone is the power value in for a given range + int whichZone(int range, double value) const; + + // how many zones are there for a given range + int numZones(int range) const; + + // get zoneinfo for a given range and zone + void zoneInfo(int range, int zone, + QString &name, QString &description, + int &low, int &high, double &trimp) const; + + QString summarize(int rnum, QVector &time_in_zone) const; + + // get all highs/lows for zones (plot shading uses these) + int lowsFromLT(QList *lows, int LT) const; + QList getZoneLows(int rnum) const; + QList getZoneHighs(int rnum) const; + QList getZoneTrimps(int rnum) const; + QList getZoneNames(int rnum) const; + + // get/set range start and end date + QDate getStartDate(int rnum) const; + QDate getEndDate(int rnum) const; + QString getStartDateString(int rnum) const; + QString getEndDateString(int rnum) const; + void setEndDate(int rnum, QDate date); + void setStartDate(int rnum, QDate date); + + // When was this last updated? + QDateTime modificationTime; + + // calculate a CRC for the zones data - used to see if zones + // data is changed since last referenced in Metric code + // could also be used in Configuration pages (later) + unsigned long getFingerprint() const; +}; + +QColor hrZoneColor(int zone, int num_zones); + +#endif // _HrZones_h diff --git a/src/LTMPlot.cpp b/src/LTMPlot.cpp index b5c1175a0..2c1d598c6 100644 --- a/src/LTMPlot.cpp +++ b/src/LTMPlot.cpp @@ -487,6 +487,8 @@ LTMPlot::createPMCCurveData(LTMSettings *settings, MetricDetail metricDetail, scoreType = "skiba_bike_score"; } else if (metricDetail.name.startsWith("Daniels")) { scoreType = "daniels_points"; + } else if (metricDetail.name.startsWith("TRIMP")) { + scoreType = "trimp_points"; } // create the Stress Calculation List @@ -519,6 +521,12 @@ LTMPlot::createPMCCurveData(LTMSettings *settings, MetricDetail metricDetail, add.setForSymbol("daniels_sb", sc->getSBvalues()[i]); add.setForSymbol("daniels_sr", sc->getSRvalues()[i]); add.setForSymbol("daniels_lr", sc->getLRvalues()[i]); + } else if (scoreType == "trimp_points") { + add.setForSymbol("trimp_lts", sc->getLTSvalues()[i]); + add.setForSymbol("trimp_sts", sc->getSTSvalues()[i]); + add.setForSymbol("trimp_sb", sc->getSBvalues()[i]); + add.setForSymbol("trimp_sr", sc->getSRvalues()[i]); + add.setForSymbol("trimp_lr", sc->getLRvalues()[i]); } add.setForSymbol("workout_time", 1.0); // averaging is per day customData << add; diff --git a/src/LTMTool.cpp b/src/LTMTool.cpp index 3ef080792..363286e60 100644 --- a/src/LTMTool.cpp +++ b/src/LTMTool.cpp @@ -241,6 +241,78 @@ LTMTool::LTMTool(MainWindow *parent, const QDir &home) : QWidget(parent), home(h danielsLTR.uunits = "Ramp"; metrics.append(danielsLTR); + // TRIMP LTS + MetricDetail trimpLTS; + trimpLTS.type = METRIC_PM; + trimpLTS.symbol = "trimp_lts"; + trimpLTS.metric = NULL; // not a factory metric + trimpLTS.penColor = QColor(Qt::blue); + trimpLTS.curveStyle = QwtPlotCurve::Lines; + trimpLTS.symbolStyle = QwtSymbol::NoSymbol; + trimpLTS.smooth = false; + trimpLTS.trend = false; + trimpLTS.topN = 5; + trimpLTS.uname = trimpLTS.name = "TRIMP Long Term Stress"; + trimpLTS.uunits = "Stress"; + metrics.append(trimpLTS); + + MetricDetail trimpSTS; + trimpSTS.type = METRIC_PM; + trimpSTS.symbol = "trimp_sts"; + trimpSTS.metric = NULL; // not a factory metric + trimpSTS.penColor = QColor(Qt::magenta); + trimpSTS.curveStyle = QwtPlotCurve::Lines; + trimpSTS.symbolStyle = QwtSymbol::NoSymbol; + trimpSTS.smooth = false; + trimpSTS.trend = false; + trimpSTS.topN = 5; + trimpSTS.uname = trimpSTS.name = "TRIMP Short Term Stress"; + trimpSTS.uunits = "Stress"; + metrics.append(trimpSTS); + + MetricDetail trimpSB; + trimpSB.type = METRIC_PM; + trimpSB.symbol = "trimp_sb"; + trimpSB.metric = NULL; // not a factory metric + trimpSB.penColor = QColor(Qt::yellow); + trimpSB.curveStyle = QwtPlotCurve::Steps; + trimpSB.symbolStyle = QwtSymbol::NoSymbol; + trimpSB.smooth = false; + trimpSB.trend = false; + trimpSB.topN = 1; + trimpSB.uname = trimpSB.name = "TRIMP Stress Balance"; + trimpSB.uunits = "Stress Balance"; + metrics.append(trimpSB); + + MetricDetail trimpSTR; + trimpSTR.type = METRIC_PM; + trimpSTR.symbol = "trimp_sr"; + trimpSTR.metric = NULL; // not a factory metric + trimpSTR.penColor = QColor(Qt::darkGreen); + trimpSTR.curveStyle = QwtPlotCurve::Steps; + trimpSTR.symbolStyle = QwtSymbol::NoSymbol; + trimpSTR.smooth = false; + trimpSTR.trend = false; + trimpSTR.topN = 1; + trimpSTR.uname = trimpSTR.name = "TRIMP STS Ramp"; + trimpSTR.uunits = "Ramp"; + metrics.append(trimpSTR); + + MetricDetail trimpLTR; + trimpLTR.type = METRIC_PM; + trimpLTR.symbol = "trimp_lr"; + trimpLTR.metric = NULL; // not a factory metric + trimpLTR.penColor = QColor(Qt::darkBlue); + trimpLTR.curveStyle = QwtPlotCurve::Steps; + trimpLTR.symbolStyle = QwtSymbol::NoSymbol; + trimpLTR.smooth = false; + trimpLTR.trend = false; + trimpLTR.topN = 1; + trimpLTR.uname = trimpLTR.name = "TRIMP LTS Ramp"; + trimpLTR.uunits = "Ramp"; + metrics.append(trimpLTR); + + SpecialFields sp; foreach (FieldDefinition field, main->rideMetadata()->getFields()) { if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) { diff --git a/src/LTMWindow.cpp b/src/LTMWindow.cpp index dc9142164..b2291f329 100644 --- a/src/LTMWindow.cpp +++ b/src/LTMWindow.cpp @@ -166,7 +166,7 @@ LTMWindow::rideSelected() active = (main->activeTab() == this); if (active == true && metricDB == NULL) { - metricDB = new MetricAggregator(main, home, main->zones()); + metricDB = new MetricAggregator(main, home, main->zones(), main->hrZones()); // mimic user first selection now that // we are active - choose a chart and diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2f43b5faf..d56b8ad7e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -101,7 +101,7 @@ MainWindow::parseRideFileName(const QString &name, QString *notesFileName, QDate MainWindow::MainWindow(const QDir &home) : home(home), session(0), isclean(false), - zones_(new Zones), currentNotesChanged(false), + zones_(new Zones), hrZones_(new HrZones), currentNotesChanged(false), ride(NULL) { setAttribute(Qt::WA_DeleteOnClose); @@ -130,6 +130,16 @@ MainWindow::MainWindow(const QDir &home) : QMessageBox::warning(this, tr("Reading Zones File"), zones_->warningString()); } + QFile hrZonesFile(home.absolutePath() + "/hr.zones"); + if (hrZonesFile.exists()) { + if (!hrZones_->read(hrZonesFile)) { + QMessageBox::critical(this, tr("Hr Zones File Error"), + hrZones_->errorString()); + } + else if (! hrZones_->warningString().isEmpty()) + QMessageBox::warning(this, tr("Reading Hr Zones File"), hrZones_->warningString()); + } + QVariant geom = settings->value(GC_SETTINGS_MAIN_GEOM); if (geom == QVariant()) resize(640, 480); @@ -212,7 +222,7 @@ MainWindow::MainWindow(const QDir &home) : QDateTime dt; if (parseRideFileName(name, ¬esFileName, &dt)) { last = new RideItem(RIDE_TYPE, home.path(), - name, dt, zones(), notesFileName, this); + name, dt, zones(), hrZones(), notesFileName, this); allRides->addChild(last); calendar->update(); } @@ -284,7 +294,7 @@ MainWindow::MainWindow(const QDir &home) : histogramWindow = new HistogramWindow(this); tabs.append(TabInfo(histogramWindow, tr("Histograms"))); - //////////////////////// Pedal Force/Velocity Plot //////////////////////// + //////////////////////// Pedal Force/zones_Velocity Plot //////////////////////// pfPvWindow = new PfPvWindow(this); tabs.append(TabInfo(pfPvWindow, tr("PF/PV"))); @@ -305,7 +315,7 @@ MainWindow::MainWindow(const QDir &home) : //////////////////////// LTM //////////////////////// // long term metrics window - metricDB = new MetricAggregator(this, home, zones()); // just to catch config updates! + metricDB = new MetricAggregator(this, home, zones(), hrZones()); // just to catch config updates! ltmWindow = new LTMWindow(this, useMetricUnits, home); tabs.append(TabInfo(ltmWindow, tr("Metrics"))); @@ -536,7 +546,7 @@ MainWindow::addRide(QString name, bool bSelect /*=true*/) assert(false); } RideItem *last = new RideItem(RIDE_TYPE, home.path(), - name, dt, zones(), notesFileName, this); + name, dt, zones(), hrZones(), notesFileName, this); QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked); // default is ascending sort int index = 0; @@ -1455,6 +1465,17 @@ MainWindow::notifyConfigChanged() QMessageBox::warning(this, tr("Reading Zones File"), zones_->warningString()); } + // re-read Hr Zones in case it changed + QFile hrZonesFile(home.absolutePath() + "/hr.zones"); + if (hrZonesFile.exists()) { + if (!hrZones_->read(hrZonesFile)) { + QMessageBox::critical(this, tr("Hr Zones File Error"), + hrZones_->errorString()); + } + else if (! hrZones_->warningString().isEmpty()) + QMessageBox::warning(this, tr("Reading Hr Zones File"), hrZones_->warningString()); + } + // now tell everyone else configChanged(); } diff --git a/src/MainWindow.h b/src/MainWindow.h index 8bd6d7c47..e8b1ce569 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -73,6 +73,8 @@ class MainWindow : public QMainWindow void setCriticalPower(int cp); const Zones *zones() const { return zones_; } + const HrZones *hrZones() const { return hrZones_; } + void updateRideFileIntervals(); void saveSilent(RideItem *); bool saveRideSingleDialog(RideItem *); @@ -98,6 +100,7 @@ class MainWindow : public QMainWindow protected: Zones *zones_; + HrZones *hrZones_; virtual void resizeEvent(QResizeEvent*); virtual void moveEvent(QMoveEvent*); diff --git a/src/MetricAggregator.cpp b/src/MetricAggregator.cpp index 15ef00869..ac69cf25e 100644 --- a/src/MetricAggregator.cpp +++ b/src/MetricAggregator.cpp @@ -20,6 +20,7 @@ #include "DBAccess.h" #include "RideFile.h" #include "Zones.h" +#include "HrZones.h" #include "Settings.h" #include "RideItem.h" #include "RideMetric.h" @@ -29,7 +30,7 @@ #include #include -MetricAggregator::MetricAggregator(MainWindow *main, QDir home, const Zones *zones) : QWidget(main), main(main), home(home), zones(zones) +MetricAggregator::MetricAggregator(MainWindow *main, QDir home, const Zones *zones, const HrZones *hrZones) : QWidget(main), main(main), home(home), zones(zones), hrZones(hrZones) { dbaccess = new DBAccess(main, home); connect(main, SIGNAL(configChanged()), this, SLOT(update())); @@ -86,7 +87,7 @@ void MetricAggregator::refreshMetrics() } } - unsigned long zoneFingerPrint = zones->getFingerprint(); // crc of zone data + unsigned long zoneFingerPrint = zones->getFingerprint() + hrZones->getFingerprint(); // crc of *all* zone data (HR and Power) // update statistics for ride files which are out of date // showing a progress bar as we go @@ -148,7 +149,7 @@ bool MetricAggregator::importRide(QDir path, RideFile *ride, QString fileName, u metrics << factory.metricName(i); // compute all the metrics - QHash computed = RideMetric::computeMetrics(ride, zones, metrics); + QHash computed = RideMetric::computeMetrics(ride, zones, hrZones, metrics); // get metrics into summaryMetric QMap for(int i = 0; i < factory.metricCount(); ++i) { diff --git a/src/MetricAggregator.h b/src/MetricAggregator.h index 1f32da55d..f613e12c3 100644 --- a/src/MetricAggregator.h +++ b/src/MetricAggregator.h @@ -24,6 +24,7 @@ #include "RideFile.h" #include #include "Zones.h" +#include "HrZones.h" #include "RideMetric.h" #include "SummaryMetrics.h" #include "MainWindow.h" @@ -34,7 +35,7 @@ class MetricAggregator : public QWidget Q_OBJECT public: - MetricAggregator(MainWindow *, QDir , const Zones *); + MetricAggregator(MainWindow *, QDir , const Zones *, const HrZones *); ~MetricAggregator(); @@ -50,6 +51,7 @@ class MetricAggregator : public QWidget DBAccess *dbaccess; QDir home; const Zones *zones; + const HrZones *hrZones; typedef QHash MetricMap; bool importRide(QDir path, RideFile *ride, QString fileName, unsigned long, bool modify); diff --git a/src/Pages.cpp b/src/Pages.cpp index 1983e1bab..0baf80ae2 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -3,6 +3,7 @@ #include #include "Pages.h" #include "Settings.h" +#include "Units.h" #include "Colors.h" #include "DeviceTypes.h" #include "DeviceConfiguration.h" @@ -217,16 +218,28 @@ CyclistPage::CyclistPage(MainWindow *main) : boost::shared_ptr settings = GetApplicationSettings(); QTabWidget *tabs = new QTabWidget(this); + QWidget *rdTab = new QWidget(this); QWidget *cpTab = new QWidget(this); + QWidget *hrTab = new QWidget(this); QWidget *pmTab = new QWidget(this); + tabs->addTab(rdTab, tr("Rider")); tabs->addTab(cpTab, tr("Power Zones")); + tabs->addTab(hrTab, tr("HR Zones")); tabs->addTab(pmTab, tr("Performance Manager")); + QVBoxLayout *rdLayout = new QVBoxLayout(rdTab); QVBoxLayout *cpLayout = new QVBoxLayout(cpTab); + QVBoxLayout *hrLayout = new QVBoxLayout(hrTab); QVBoxLayout *pmLayout = new QVBoxLayout(pmTab); + riderPage = new RiderPage(this, main); + rdLayout->addWidget(riderPage); + zonePage = new ZonePage(main); cpLayout->addWidget(zonePage); + hrZonePage = new HrZonePage(main); + hrLayout->addWidget(hrZonePage); + perfManLabel = new QLabel(tr("Performance Manager")); showSBToday = new QCheckBox(tr("Show Stress Balance Today"), this); showSBToday->setChecked(settings->value(GC_SB_TODAY).toInt()); @@ -282,6 +295,8 @@ CyclistPage::saveClicked() { // save zone config (other stuff is saved by configdialog) zonePage->saveClicked(); + hrZonePage->saveClicked(); + riderPage->saveClicked(); } @@ -1478,6 +1493,7 @@ SchemePage::deleteClicked() struct schemeitem { QString name, desc; int lo; + double trimp; bool operator<(schemeitem right) const { return lo < right.lo; } }; @@ -1847,6 +1863,700 @@ CPPage::zonesChanged() } } +// +// Zone Config page +// +HrZonePage::HrZonePage(MainWindow *main) : main(main) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + + // get current config by reading it in (leave mainwindow zones alone) + QFile zonesFile(main->home.absolutePath() + "/hr.zones"); + if (zonesFile.exists()) { + zones.read(zonesFile); + zonesFile.close(); + } + + // setup maintenance pages using current config + schemePage = new HrSchemePage(this); + ltPage = new LTPage(this); + + tabs = new QTabWidget(this); + tabs->addTab(ltPage, tr("Lactic Threshold History")); + tabs->addTab(schemePage, tr("Default Zones")); + + layout->addWidget(tabs); +} + +void +HrZonePage::saveClicked() +{ + zones.setScheme(schemePage->getScheme()); + zones.write(main->home); +} + +HrSchemePage::HrSchemePage(HrZonePage* zonePage) : zonePage(zonePage) +{ + QGridLayout *mainLayout = new QGridLayout(this); + + addButton = new QPushButton(tr("Add")); + renameButton = new QPushButton(tr("Rename")); + deleteButton = new QPushButton(tr("Delete")); + + QVBoxLayout *actionButtons = new QVBoxLayout; + actionButtons->addWidget(addButton); + actionButtons->addWidget(renameButton); + actionButtons->addWidget(deleteButton); + actionButtons->addStretch(); + + scheme = new QTreeWidget; + scheme->headerItem()->setText(0, tr("Short")); + scheme->headerItem()->setText(1, tr("Long")); + scheme->headerItem()->setText(2, tr("Percent of LT")); + scheme->headerItem()->setText(3, tr("Trimp k")); + scheme->setColumnCount(4); + scheme->setSelectionMode(QAbstractItemView::SingleSelection); + scheme->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit + scheme->setUniformRowHeights(true); + scheme->setIndentation(0); + scheme->header()->resizeSection(0,60); + scheme->header()->resizeSection(1,180); + scheme->header()->resizeSection(2,65); + scheme->header()->resizeSection(3,65); + + // setup list + for (int i=0; i< zonePage->zones.getScheme().nzones_default; i++) { + + QTreeWidgetItem *add = new QTreeWidgetItem(scheme->invisibleRootItem()); + add->setFlags(add->flags() | Qt::ItemIsEditable); + + // tab name + add->setText(0, zonePage->zones.getScheme().zone_default_name[i]); + // field name + add->setText(1, zonePage->zones.getScheme().zone_default_desc[i]); + + // low + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(zonePage->zones.getScheme().zone_default[i]); + scheme->setItemWidget(add, 2, loedit); + + // trimp + QDoubleSpinBox *trimpedit = new QDoubleSpinBox(this); + trimpedit->setMinimum(0); + trimpedit->setMaximum(10); + trimpedit->setSingleStep(0.1); + trimpedit->setDecimals(2); + trimpedit->setValue(zonePage->zones.getScheme().zone_default_trimp[i]); + scheme->setItemWidget(add, 3, trimpedit); + } + + mainLayout->addWidget(scheme, 0,0); + mainLayout->addLayout(actionButtons, 0,1); + + // button connect + connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); + connect(renameButton, SIGNAL(clicked()), this, SLOT(renameClicked())); + connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); +} + +void +HrSchemePage::addClicked() +{ + // are we at maximum already? + if (scheme->invisibleRootItem()->childCount() == 10) { + QMessageBox err; + err.setText("Maximum of 10 zones reached."); + err.setIcon(QMessageBox::Warning); + err.exec(); + return; + } + + int index = scheme->invisibleRootItem()->childCount(); + + // new item + QTreeWidgetItem *add = new QTreeWidgetItem; + add->setFlags(add->flags() | Qt::ItemIsEditable); + + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(100); + + scheme->invisibleRootItem()->insertChild(index, add); + scheme->setItemWidget(add, 2, loedit); + + // Short + QString text = "New"; + for (int i=0; scheme->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(0, text); + + // long + text = "New"; + for (int i=0; scheme->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(1, text); +} + +void +HrSchemePage::renameClicked() +{ + // which one is selected? + if (scheme->currentItem()) scheme->editItem(scheme->currentItem(), 0); +} + +void +HrSchemePage::deleteClicked() +{ + if (scheme->currentItem()) { + int index = scheme->invisibleRootItem()->indexOfChild(scheme->currentItem()); + delete scheme->invisibleRootItem()->takeChild(index); + } +} + +HrZoneScheme +HrSchemePage::getScheme() +{ + // read the scheme widget and return a scheme object + QList table; + HrZoneScheme results; + + // read back the details from the table + for (int i=0; iinvisibleRootItem()->childCount(); i++) { + + schemeitem add; + add.name = scheme->invisibleRootItem()->child(i)->text(0); + add.desc = scheme->invisibleRootItem()->child(i)->text(1); + add.lo = ((QDoubleSpinBox *)(scheme->itemWidget(scheme->invisibleRootItem()->child(i), 2)))->value(); + add.trimp = ((QDoubleSpinBox *)(scheme->itemWidget(scheme->invisibleRootItem()->child(i), 3)))->value(); + table.append(add); + } + + // sort the list into ascending order + qSort(table); + + // now update the results + results.nzones_default = 0; + foreach(schemeitem zone, table) { + results.nzones_default++; + results.zone_default.append(zone.lo); + results.zone_default_is_pct.append(true); + results.zone_default_name.append(zone.name); + results.zone_default_desc.append(zone.desc); + results.zone_default_trimp.append(zone.trimp); + } + + return results; +} + + +LTPage::LTPage(HrZonePage* zonePage) : zonePage(zonePage) +{ + active = false; + + QGridLayout *mainLayout = new QGridLayout(this); + + addButton = new QPushButton(tr("Add LT")); + deleteButton = new QPushButton(tr("Delete LT")); + defaultButton = new QPushButton(tr("Default")); + defaultButton->hide(); + + addZoneButton = new QPushButton(tr("Add Zone")); + deleteZoneButton = new QPushButton(tr("Delete Zone")); + + QVBoxLayout *actionButtons = new QVBoxLayout; + actionButtons->addWidget(addButton); + actionButtons->addWidget(deleteButton); + actionButtons->addWidget(defaultButton); + actionButtons->addStretch(); + + QVBoxLayout *zoneButtons = new QVBoxLayout; + zoneButtons->addWidget(addZoneButton); + zoneButtons->addWidget(deleteZoneButton); + zoneButtons->addStretch(); + + QHBoxLayout *addLayout = new QHBoxLayout; + QLabel *dateLabel = new QLabel(tr("From Date")); + QLabel *ltLabel = new QLabel(tr("Lactic Threshold")); + dateEdit = new QDateEdit; + dateEdit->setDate(QDate::currentDate()); + + ltEdit = new QDoubleSpinBox; + ltEdit->setMinimum(0); + ltEdit->setMaximum(240); + ltEdit->setSingleStep(1.0); + ltEdit->setDecimals(0); + + addLayout->addWidget(dateLabel); + addLayout->addWidget(dateEdit); + addLayout->addWidget(ltLabel); + addLayout->addWidget(ltEdit); + addLayout->addStretch(); + + QHBoxLayout *addLayout2 = new QHBoxLayout; + QLabel *restHrLabel = new QLabel(tr("Rest HR")); + QLabel *maxHrLabel = new QLabel(tr("Max HR")); + + restHrEdit = new QDoubleSpinBox; + restHrEdit->setMinimum(0); + restHrEdit->setMaximum(240); + restHrEdit->setSingleStep(1.0); + restHrEdit->setDecimals(0); + + maxHrEdit = new QDoubleSpinBox; + maxHrEdit->setMinimum(0); + maxHrEdit->setMaximum(240); + maxHrEdit->setSingleStep(1.0); + maxHrEdit->setDecimals(0); + + addLayout2->addWidget(restHrLabel); + addLayout2->addWidget(restHrEdit); + addLayout2->addWidget(maxHrLabel); + addLayout2->addWidget(maxHrEdit); + addLayout2->addStretch(); + + ranges = new QTreeWidget; + ranges->headerItem()->setText(0, tr("From Date")); + ranges->headerItem()->setText(1, tr("Lactic Threshold")); + ranges->headerItem()->setText(2, tr("Rest HR")); + ranges->headerItem()->setText(3, tr("Max HR")); + ranges->setColumnCount(4); + ranges->setSelectionMode(QAbstractItemView::SingleSelection); + //ranges->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit + ranges->setUniformRowHeights(true); + ranges->setIndentation(0); + ranges->header()->resizeSection(0,180); + + // setup list of ranges + for (int i=0; i< zonePage->zones.getRangeSize(); i++) { + + QTreeWidgetItem *add = new QTreeWidgetItem(ranges->invisibleRootItem()); + add->setFlags(add->flags() & ~Qt::ItemIsEditable); + + // Embolden ranges with manually configured zones + QFont font; + font.setWeight(zonePage->zones.getHrZoneRange(i).hrZonesSetFromLT ? + QFont::Normal : QFont::Black); + + // date + add->setText(0, zonePage->zones.getStartDate(i).toString("MMM d, yyyy")); + add->setFont(0, font); + + // LT + add->setText(1, QString("%1").arg(zonePage->zones.getLT(i))); + add->setFont(1, font); + + // Rest HR + add->setText(2, QString("%1").arg(zonePage->zones.getRestHr(i))); + add->setFont(2, font); + + // Max HR + add->setText(3, QString("%1").arg(zonePage->zones.getMaxHr(i))); + add->setFont(3, font); + } + + zones = new QTreeWidget; + zones->headerItem()->setText(0, tr("Short")); + zones->headerItem()->setText(1, tr("Long")); + zones->headerItem()->setText(2, tr("From BPM")); + zones->headerItem()->setText(3, tr("Trimp k")); + zones->setColumnCount(4); + zones->setSelectionMode(QAbstractItemView::SingleSelection); + zones->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit + zones->setUniformRowHeights(true); + zones->setIndentation(0); + zones->header()->resizeSection(0,50); + zones->header()->resizeSection(1,150); + zones->header()->resizeSection(2,65); + zones->header()->resizeSection(3,65); + + mainLayout->addLayout(addLayout, 0,0); + mainLayout->addLayout(addLayout2, 1,0); + mainLayout->addWidget(ranges, 2,0); + mainLayout->addWidget(zones, 4,0); + mainLayout->addLayout(actionButtons, 0,1,0,3); + mainLayout->addLayout(zoneButtons, 4,1); + + // button connect + connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); + connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); + connect(defaultButton, SIGNAL(clicked()), this, SLOT(defaultClicked())); + connect(addZoneButton, SIGNAL(clicked()), this, SLOT(addZoneClicked())); + connect(deleteZoneButton, SIGNAL(clicked()), this, SLOT(deleteZoneClicked())); + connect(ranges, SIGNAL(itemSelectionChanged()), this, SLOT(rangeSelectionChanged())); + connect(zones, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(zonesChanged())); +} + +void +LTPage::addClicked() +{ + // get current scheme + zonePage->zones.setScheme(zonePage->schemePage->getScheme()); + + //int index = ranges->invisibleRootItem()->childCount(); + int index = zonePage->zones.addHrZoneRange(dateEdit->date(), ltEdit->value(), restHrEdit->value(), maxHrEdit->value()); + + // new item + QTreeWidgetItem *add = new QTreeWidgetItem; + add->setFlags(add->flags() & ~Qt::ItemIsEditable); + ranges->invisibleRootItem()->insertChild(index, add); + + // date + add->setText(0, dateEdit->date().toString("MMM d, yyyy")); + + // LT + add->setText(1, QString("%1").arg(ltEdit->value())); + // Rest HR + add->setText(2, QString("%1").arg(restHrEdit->value())); + // Max HR + add->setText(3, QString("%1").arg(maxHrEdit->value())); +} + +void +LTPage::deleteClicked() +{ + if (ranges->currentItem()) { + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + delete ranges->invisibleRootItem()->takeChild(index); + zonePage->zones.deleteRange(index); + } +} + +void +LTPage::defaultClicked() +{ + if (ranges->currentItem()) { + + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + HrZoneRange current = zonePage->zones.getHrZoneRange(index); + + // unbold + QFont font; + font.setWeight(QFont::Normal); + ranges->currentItem()->setFont(0, font); + ranges->currentItem()->setFont(1, font); + + // set the range to use defaults on the scheme page + zonePage->zones.setScheme(zonePage->schemePage->getScheme()); + zonePage->zones.setHrZonesFromLT(index); + + // hide the default button since we are now using defaults + defaultButton->hide(); + + // update the zones display + rangeSelectionChanged(); + } +} + +void +LTPage::rangeSelectionChanged() +{ + active = true; + + // wipe away current contents of zones + foreach (QTreeWidgetItem *item, zones->invisibleRootItem()->takeChildren()) { + delete zones->itemWidget(item, 2); + delete item; + } + + // fill with current details + if (ranges->currentItem()) { + + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + HrZoneRange current = zonePage->zones.getHrZoneRange(index); + + if (current.hrZonesSetFromLT) { + + // reapply the scheme in case it has been changed + zonePage->zones.setScheme(zonePage->schemePage->getScheme()); + zonePage->zones.setHrZonesFromLT(index); + current = zonePage->zones.getHrZoneRange(index); + + defaultButton->hide(); + + } else defaultButton->show(); + + for (int i=0; i< current.zones.count(); i++) { + + QTreeWidgetItem *add = new QTreeWidgetItem(zones->invisibleRootItem()); + add->setFlags(add->flags() | Qt::ItemIsEditable); + + // tab name + add->setText(0, current.zones[i].name); + // field name + add->setText(1, current.zones[i].desc); + + // low + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(current.zones[i].lo); + zones->setItemWidget(add, 2, loedit); + connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); + + //trimp + QDoubleSpinBox *trimpedit = new QDoubleSpinBox(this); + trimpedit->setMinimum(0); + trimpedit->setMaximum(10); + trimpedit->setSingleStep(0.1); + trimpedit->setDecimals(2); + trimpedit->setValue(current.zones[i].trimp); + zones->setItemWidget(add, 3, trimpedit); + connect(trimpedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); + + } + } + + active = false; +} + +void +LTPage::addZoneClicked() +{ + // no range selected + if (!ranges->currentItem()) return; + + // are we at maximum already? + if (zones->invisibleRootItem()->childCount() == 10) { + QMessageBox err; + err.setText("Maximum of 10 zones reached."); + err.setIcon(QMessageBox::Warning); + err.exec(); + return; + } + + active = true; + int index = zones->invisibleRootItem()->childCount(); + + // new item + QTreeWidgetItem *add = new QTreeWidgetItem; + add->setFlags(add->flags() | Qt::ItemIsEditable); + + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(100); + + zones->invisibleRootItem()->insertChild(index, add); + zones->setItemWidget(add, 2, loedit); + connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); + + // Short + QString text = "New"; + for (int i=0; zones->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(0, text); + + // long + text = "New"; + for (int i=0; zones->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(1, text); + active = false; + + zonesChanged(); +} + +void +LTPage::deleteZoneClicked() +{ + // no range selected + if (ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()) == -1) + return; + + active = true; + if (zones->currentItem()) { + int index = zones->invisibleRootItem()->indexOfChild(zones->currentItem()); + delete zones->invisibleRootItem()->takeChild(index); + } + active = false; + + zonesChanged(); +} + +void +LTPage::zonesChanged() +{ + // only take changes when they are not done programmatically + // the active flag is set when the tree is being modified + // programmatically, but not when users interact with the widgets + if (active == false) { + // get the current zone range + if (ranges->currentItem()) { + + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + HrZoneRange current = zonePage->zones.getHrZoneRange(index); + + // embolden that range on the list to show it has been edited + QFont font; + font.setWeight(QFont::Black); + ranges->currentItem()->setFont(0, font); + ranges->currentItem()->setFont(1, font); + + // show the default button to undo + defaultButton->show(); + + // we manually edited so save in full + current.hrZonesSetFromLT = false; + + // create the new zoneinfos for this range + QList zoneinfos; + for (int i=0; i< zones->invisibleRootItem()->childCount(); i++) { + QTreeWidgetItem *item = zones->invisibleRootItem()->child(i); + zoneinfos << HrZoneInfo(item->text(0), + item->text(1), + ((QDoubleSpinBox*)zones->itemWidget(item, 2))->value(), + 0, ((QDoubleSpinBox*)zones->itemWidget(item, 3))->value()); + } + + // now sort the list + qSort(zoneinfos); + + // now fill the highs + for(int i=0; izones.setHrZoneRange(index, current); + } + } +} + +static QVariant getCValue(QString name, QString parameter) +{ + boost::shared_ptr settings = GetApplicationSettings(); + QString key = QString("%1/%2").arg(name).arg(parameter); + return settings->value(key, ""); +} + +static void setCValue(QString name, QString parameter, QVariant value) +{ + boost::shared_ptr settings = GetApplicationSettings(); + QString key = QString("%1/%2").arg(name).arg(parameter); + return settings->setValue(key, value); +} + +// +// About me +// +RiderPage::RiderPage(QWidget *parent, MainWindow *mainWindow) : QWidget(parent), mainWindow(mainWindow) +{ + boost::shared_ptr settings = GetApplicationSettings(); + useMetricUnits = (settings->value(GC_UNIT).toString() == "Metric"); + + cyclist = mainWindow->home.dirName(); + + QVBoxLayout *all = new QVBoxLayout(this); + QGridLayout *grid = new QGridLayout; + + QLabel *nicklabel = new QLabel(tr("Nickname")); + QLabel *doblabel = new QLabel(tr("Date of Birth")); + QLabel *sexlabel = new QLabel(tr("Gender")); + QLabel *biolabel = new QLabel(tr("Bio")); + + QString weighttext = QString(tr("Weight (%1)")).arg(useMetricUnits ? tr("kg") : tr("lb")); + QLabel *weightlabel = new QLabel(weighttext); + + nickname = new QLineEdit(this); + nickname->setText(getCValue(cyclist, GC_NICKNAME).toString()); + + dob = new QDateEdit(this); + dob->setDate(getCValue(cyclist, GC_DOB).toDate()); + + sex = new QComboBox(this); + sex->addItem(tr("Male")); + sex->addItem(tr("Female")); + + // we set to 0 or 1 for male or female since this + // is language independent (and for once the code is easier!) + sex->setCurrentIndex(getCValue(cyclist, GC_SEX).toInt()); + + weight = new QDoubleSpinBox(this); + weight->setMaximum(999.9); + weight->setMinimum(0.0); + weight->setDecimals(1); + weight->setValue(getCValue(cyclist, GC_WEIGHT).toDouble() * (useMetricUnits ? 1.0 : LB_PER_KG)); + + bio = new QTextEdit(this); + bio->setText(getCValue(cyclist, GC_BIO).toString()); + + if (QFileInfo(mainWindow->home.absolutePath() + "/" + "avatar.png").exists()) + avatar = QPixmap(mainWindow->home.absolutePath() + "/" + "avatar.png"); + else + avatar = QPixmap(":/images/noavatar.png"); + + avatarButton = new QPushButton(this); + avatarButton->setContentsMargins(0,0,0,0); + avatarButton->setFlat(true); + avatarButton->setIcon(avatar.scaled(140,140)); + avatarButton->setIconSize(QSize(140,140)); + avatarButton->setFixedHeight(140); + avatarButton->setFixedWidth(140); + + Qt::Alignment alignment = Qt::AlignLeft|Qt::AlignVCenter; + + grid->addWidget(nicklabel, 0, 0, alignment); + grid->addWidget(doblabel, 1, 0, alignment); + grid->addWidget(sexlabel, 2, 0, alignment); + grid->addWidget(weightlabel, 3, 0, alignment); + grid->addWidget(biolabel, 4, 0, alignment); + + grid->addWidget(nickname, 0, 1, alignment); + grid->addWidget(dob, 1, 1, alignment); + grid->addWidget(sex, 2, 1, alignment); + grid->addWidget(weight, 3, 1, alignment); + grid->addWidget(bio, 5, 0, 1, 4); + + grid->addWidget(avatarButton, 0, 2, 4, 2, Qt::AlignRight|Qt::AlignVCenter); + all->addLayout(grid); + all->addStretch(); + + connect (avatarButton, SIGNAL(clicked()), this, SLOT(chooseAvatar())); +} + +void +RiderPage::chooseAvatar() +{ + QString filename = QFileDialog::getOpenFileName(this, tr("Choose Picture"), + "", tr("Images (*.png *.jpg *.bmp")); + if (filename != "") { + + avatar = QPixmap(filename); + avatarButton->setIcon(avatar.scaled(140,140)); + avatarButton->setIconSize(QSize(140,140)); + } +} + +void +RiderPage::saveClicked() +{ + setCValue(cyclist, GC_NICKNAME, nickname->text()); + setCValue(cyclist, GC_DOB, dob->date()); + setCValue(cyclist, GC_WEIGHT, weight->value() * (useMetricUnits ? 1.0 : KG_PER_LB)); + setCValue(cyclist, GC_SEX, sex->currentIndex()); + setCValue(cyclist, GC_BIO, bio->toPlainText()); + avatar.save(mainWindow->home.absolutePath() + "/" + "avatar.png", "PNG"); +} + #ifdef GC_HAVE_LIBOAUTH // // Twitter Config page diff --git a/src/Pages.h b/src/Pages.h index 5966266dd..9955f9d51 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -12,6 +12,7 @@ #include #include #include "Zones.h" +#include "HrZones.h" #include #include #include @@ -39,6 +40,8 @@ class KeywordsPage; class FieldsPage; class Colors; class ZonePage; +class HrZonePage; +class RiderPage; class ConfigurationPage : public QWidget { @@ -100,6 +103,8 @@ class CyclistPage : public QWidget private: ZonePage *zonePage; + HrZonePage *hrZonePage; + RiderPage *riderPage; MainWindow *main; QGroupBox *cyclistGroup; @@ -404,6 +409,111 @@ class ZonePage : public QWidget // local versions for modification }; +class HrSchemePage : public QWidget +{ + Q_OBJECT + +public: + HrSchemePage(HrZonePage *parent); + HrZoneScheme getScheme(); + void saveClicked(); + + public slots: + void addClicked(); + void deleteClicked(); + void renameClicked(); + +private: + HrZonePage *zonePage; + QTreeWidget *scheme; + QPushButton *addButton, *renameButton, *deleteButton; +}; + + +class LTPage : public QWidget +{ + Q_OBJECT + +public: + LTPage(HrZonePage *parent); + + public slots: + void addClicked(); + void deleteClicked(); + void defaultClicked(); + void rangeSelectionChanged(); + void addZoneClicked(); + void deleteZoneClicked(); + void zonesChanged(); + +private: + bool active; + + QDateEdit *dateEdit; + QDoubleSpinBox *ltEdit; + QDoubleSpinBox *restHrEdit; + QDoubleSpinBox *maxHrEdit; + + HrZonePage *zonePage; + QTreeWidget *ranges; + QTreeWidget *zones; + QPushButton *addButton, *deleteButton, *defaultButton; + QPushButton *addZoneButton, *deleteZoneButton; +}; + +class HrZonePage : public QWidget +{ + Q_OBJECT + +public: + + HrZonePage(MainWindow *); + void saveClicked(); + + //ZoneScheme scheme; + HrZones zones; + + // Children talk to each other + HrSchemePage *schemePage; + LTPage *ltPage; + + public slots: + + +protected: + + MainWindow *main; + bool changed; + + QTabWidget *tabs; + + // local versions for modification +}; + +class RiderPage : public QWidget +{ + Q_OBJECT + + public: + RiderPage(QWidget *parent, MainWindow *mainWindow); + void saveClicked(); + + public slots: + void chooseAvatar(); + + private: + MainWindow *mainWindow; + bool useMetricUnits; + QLineEdit *nickname; + QDateEdit *dob; + QComboBox *sex; + QDoubleSpinBox *weight; + QTextEdit *bio; + QPushButton *avatarButton; + QPixmap avatar; + QString cyclist; +}; + class TwitterPage : public QWidget { Q_OBJECT diff --git a/src/PeakPower.cpp b/src/PeakPower.cpp index 8bd2c5653..d344e0e14 100644 --- a/src/PeakPower.cpp +++ b/src/PeakPower.cpp @@ -36,7 +36,7 @@ class PeakPower : public RideMetric { setImperialUnits(tr("watts")); } void setSecs(double secs) { this->secs=secs; } - void compute(const RideFile *ride, const Zones *, int, + void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash &) { QList results; BestIntervalDialog::findBests(ride, secs, 1, results); diff --git a/src/PerformanceManagerWindow.cpp b/src/PerformanceManagerWindow.cpp index 8cb7ed42a..2ff9e831e 100644 --- a/src/PerformanceManagerWindow.cpp +++ b/src/PerformanceManagerWindow.cpp @@ -51,6 +51,7 @@ PerformanceManagerWindow::PerformanceManagerWindow(MainWindow *mainWindow) : metricCombo = new QComboBox(this); metricCombo->addItem(tr("Use BikeScore"), "skiba_bike_score"); metricCombo->addItem(tr("Use DanielsPoints"), "daniels_points"); + metricCombo->addItem(tr("Use TRIMP"), "trimp_points"); boost::shared_ptr settings = GetApplicationSettings(); QString metricName = settings->value(GC_PERF_MAN_METRIC, "skiba_bike_score").toString(); diff --git a/src/PowerHist.cpp b/src/PowerHist.cpp index fa8a946c5..cc08186de 100644 --- a/src/PowerHist.cpp +++ b/src/PowerHist.cpp @@ -23,8 +23,11 @@ #include "RideFile.h" #include "Settings.h" #include "Zones.h" +#include "HrZones.h" #include "Colors.h" +#include "ZoneScaleDraw.h" + #include #include #include @@ -35,33 +38,7 @@ #include #include -class penTooltip: public QwtPlotZoomer -{ - public: - penTooltip(QwtPlotCanvas *canvas): - QwtPlotZoomer(canvas) - { - // With some versions of Qt/Qwt, setting this to AlwaysOn - // causes an infinite recursion. - //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; - } - }; - - - +#include "LTMCanvasPicker.h" // for tooltip // define a background class to handle shading of power zones @@ -199,22 +176,23 @@ public: }; PowerHist::PowerHist(MainWindow *mainWindow): - selected(wattsShaded), + selected(watts), + shade(false), rideItem(NULL), mainWindow(mainWindow), withz(true), - settings(NULL), unit(0), - lny(false) + lny(false), + absolutetime(true) { boost::shared_ptr settings = GetApplicationSettings(); - unit = settings->value(GC_UNIT); useMetricUnits = (unit.toString() == "Metric"); binw = settings->value(GC_HIST_BIN_WIDTH, 5).toInt(); + shade = true; // create a background object for shading bg = new PowerHistBackground(this); @@ -223,7 +201,7 @@ PowerHist::PowerHist(MainWindow *mainWindow): setCanvasBackground(Qt::white); setParameterAxisTitle(); - setAxisTitle(yLeft, "Cumulative Time (minutes)"); + setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)")); curve = new QwtPlotCurve(""); curve->setStyle(QwtPlotCurve::Steps); @@ -239,9 +217,11 @@ PowerHist::PowerHist(MainWindow *mainWindow): grid->enableX(false); grid->attach(this); - zoneLabels = QList (); + zoneLabels = QList(); zoomer = new penTooltip(this->canvas()); + canvasPicker = new LTMCanvasPicker(this); + connect(canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), this, SLOT(pointHover(QwtPlotCurve*, int))); configChanged(); } @@ -257,37 +237,41 @@ PowerHist::configChanged() QColor brush_color; switch (selected) { - case wattsShaded: - case wattsUnshaded: - pen.setColor(GColor(CPOWER)); + case watts: + case wattsZone: + pen.setColor(GColor(CPOWER).darker(200)); brush_color = GColor(CPOWER); break; case nm: - pen.setColor(GColor(CTORQUE)); + pen.setColor(GColor(CTORQUE).darker(200)); brush_color = GColor(CTORQUE); break; case hr: - pen.setColor(GColor(CHEARTRATE)); + case hrZone: + pen.setColor(GColor(CHEARTRATE).darker(200)); brush_color = GColor(CHEARTRATE); break; case kph: - pen.setColor(GColor(CSPEED)); + pen.setColor(GColor(CSPEED).darker(200)); brush_color = GColor(CSPEED); break; case cad: - pen.setColor(GColor(CCADENCE)); + pen.setColor(GColor(CCADENCE).darker(200)); brush_color = GColor(CCADENCE); break; } - pen.setWidth(2.0); + double width = 2.0; + curve->setRenderHint(QwtPlotItem::RenderAntialiased); + curveSelected->setRenderHint(QwtPlotItem::RenderAntialiased); + pen.setWidth(width); curve->setPen(pen); brush_color.setAlpha(64); curve->setBrush(brush_color); // fill below the line // intervalselection - QPen ivl(GColor(CINTERVALHIGHLIGHTER)); - ivl.setWidth(2.0); + QPen ivl(GColor(CINTERVALHIGHLIGHTER).darker(200)); + ivl.setWidth(width); curveSelected->setPen(ivl); QColor ivlbrush = GColor(CINTERVALHIGHLIGHTER); ivlbrush.setAlpha(64); @@ -338,7 +322,7 @@ PowerHist::refreshZoneLabels() if (! rideItem) return; - if ((selected == wattsShaded) || (selected == wattsUnshaded)) { + if (selected == watts) { const Zones *zones = rideItem->zones; int zone_range = rideItem->zoneRange(); @@ -366,14 +350,18 @@ PowerHist::recalc() if (dt <= 0) return; - if ((selected == wattsShaded) || - (selected == wattsUnshaded) - ) { + 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; @@ -386,6 +374,12 @@ PowerHist::recalc() 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; @@ -402,38 +396,140 @@ PowerHist::recalc() if (!array) return; - // we add a bin on the end since the last "incomplete" bin - // will be dropped otherwise - int count = int(ceil((arrayLength - 1) / binw))+1; + // binning of data when not zoned + if (selected != wattsZone && selected != hrZone) { - // 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; - int low = high - binw; - 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 - while (low < high && lowlow) - totalTimeSelected[i] += dt * (*selectedArray)[low]; - totalTime[i] += dt * (*array)[low++]; + // we add a bin on the end since the last "incomplete" bin + // will be dropped otherwise + int count = int(ceil((arrayLength - 1) / binw))+1; + + // allocate space for data, plus beginning and ending point + QVector parameterValue(count+2, 0.0); + QVector totalTime(count+2, 0.0); + QVector totalTimeSelected(count+2, 0.0); + int i; + for (i = 1; i <= count; ++i) { + int high = i * binw; + int low = high - binw; + 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 + while (low < high && lowlow) + 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]); + totalTime[i] = 1e-9; // nonzero to accomodate log plot + parameterValue[i] = i * delta * binw; + totalTime[0] = 1e-9; + parameterValue[0] = 0; - refreshZoneLabels(); + // convert vectors from absolute time to percentage + // if the user has selected that + if (!absolutetime) { + percentify(totalTime, 1); + percentify(totalTimeSelected, 1); + } + + curve->setData(parameterValue.data(), totalTime.data(), count + 2); + curveSelected->setData(parameterValue.data(), totalTimeSelected.data(), count + 2); + + // make see through if we're shading zones + QBrush brush = curve->brush(); + QColor bcol = brush.color(); + bcol.setAlpha(shadeZones() ? 165 : 200); + brush.setColor(bcol); + curve->setBrush(brush); + + setAxisScaleDraw(QwtPlot::xBottom, new QwtScaleDraw); + setAxisScale(xBottom, 0.0, parameterValue[count + 1]); + + // we only do zone labels when using absolute values + refreshZoneLabels(); + + } else { + + // we're not binning instead we are prettyfing the columnar + // display in much the same way as the weekly summary workds + // Each zone column will have 4 points + QVector xaxis (array->size() * 4); + QVector yaxis (array->size() * 4); + QVector selectedxaxis (selectedArray->size() * 4); + QVector selectedyaxis (selectedArray->size() * 4); + + // samples to time + for (int i=0, offset=0; isize(); i++) { + + double x = (double) i - 0.5; + double y = dt * (double)(*array)[i]; + + xaxis[offset] = x +0.05; + yaxis[offset] = 0; + offset++; + xaxis[offset] = x+0.05; + yaxis[offset] = y; + offset++; + xaxis[offset] = x+0.95; + yaxis[offset] = y; + offset++; + xaxis[offset] = x +0.95; + yaxis[offset] = 0; + offset++; + } + + for (int i=0, offset=0; isize(); i++) { + double x = (double)i - 0.5; + double y = dt * (double)(*selectedArray)[i]; + + selectedxaxis[offset] = x +0.05; + selectedyaxis[offset] = 0; + offset++; + selectedxaxis[offset] = x+0.05; + selectedyaxis[offset] = y; + offset++; + selectedxaxis[offset] = x+0.95; + selectedyaxis[offset] = y; + offset++; + selectedxaxis[offset] = x +0.95; + selectedyaxis[offset] = 0; + offset++; + } + + if (!absolutetime) { + percentify(yaxis, 2); + percentify(selectedyaxis, 2); + } + // set those curves + curve->setData(xaxis.data(), yaxis.data(), xaxis.size()); + + // Opaque - we don't need to show zone shading + QBrush brush = curve->brush(); + QColor bcol = brush.color(); + bcol.setAlpha(200); + brush.setColor(bcol); + curve->setBrush(brush); + + curveSelected->setData(selectedxaxis.data(), selectedyaxis.data(), selectedxaxis.size()); + + // zone scale draw + if (selected == wattsZone && rideItem && rideItem->zones) { + setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(rideItem->zones, rideItem->zoneRange())); + setAxisScale(QwtPlot::xBottom, -0.99, rideItem->zones->numZones(rideItem->zoneRange()), 1); + } + + // hr scale draw + int hrRange; + if (selected == hrZone && rideItem && mainWindow->hrZones() && + (hrRange=mainWindow->hrZones()->whichRange(rideItem->dateTime.date())) != -1) { + setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(mainWindow->hrZones(), hrRange)); + setAxisScale(QwtPlot::xBottom, -0.99, mainWindow->hrZones()->numZones(hrRange), 1); + } + + setAxisMaxMinor(QwtPlot::xBottom, 0); + } setYMax(); replot(); @@ -442,8 +538,10 @@ PowerHist::recalc() void PowerHist::setYMax() { + double MaxY = curve->maxYValue(); + if (MaxY < curveSelected->maxYValue()) MaxY = curveSelected->maxYValue(); static const double tmin = 1.0/60; - setAxisScale(yLeft, (lny ? tmin : 0.0), curve->maxYValue() * 1.1); + setAxisScale(yLeft, (lny ? tmin : 0.0), MaxY * 1.1); } void @@ -462,14 +560,18 @@ PowerHist::setData(RideItem *_rideItem) dt = ride->recIntSecs() / 60.0; wattsArray.resize(0); + wattsZoneArray.resize(0); nmArray.resize(0); hrArray.resize(0); + hrZoneArray.resize(0); kphArray.resize(0); cadArray.resize(0); wattsSelectedArray.resize(0); + wattsZoneSelectedArray.resize(0); nmSelectedArray.resize(0); hrSelectedArray.resize(0); + hrZoneSelectedArray.resize(0); kphSelectedArray.resize(0); cadSelectedArray.resize(0); @@ -480,6 +582,7 @@ PowerHist::setData(RideItem *_rideItem) foreach(const RideFilePoint *p1, ride->dataPoints()) { bool selected = isSelected(p1, ride->recIntSecs()); + // watts array int wattsIndex = int(floor(p1->watts / wattsDelta)); if (wattsIndex >= 0 && wattsIndex < maxSize) { if (wattsIndex >= wattsArray.size()) @@ -493,6 +596,27 @@ PowerHist::setData(RideItem *_rideItem) } } + // watts zoned array + const Zones *zones = rideItem->zones; + int zoneRange = zones ? zones->whichRange(ride->startTime().date()) : -1; + + // Only calculate zones if we have a valid range and check zeroes + if (zoneRange > -1 && (withz || (!withz && p1->watts))) { + wattsIndex = zones->whichZone(zoneRange, p1->watts); + + if (wattsIndex >= 0 && wattsIndex < maxSize) { + if (wattsIndex >= wattsZoneArray.size()) + wattsZoneArray.resize(wattsIndex + 1); + wattsZoneArray[wattsIndex]++; + + if (selected) { + if (wattsIndex >= wattsZoneSelectedArray.size()) + wattsZoneSelectedArray.resize(wattsIndex + 1); + wattsZoneSelectedArray[wattsIndex]++; + } + } + } + int nmIndex = int(floor(p1->nm * torque_factor / nmDelta)); if (nmIndex >= 0 && nmIndex < maxSize) { if (nmIndex >= nmArray.size()) @@ -519,6 +643,26 @@ PowerHist::setData(RideItem *_rideItem) } } + // 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()) @@ -570,12 +714,13 @@ double PowerHist::getDelta() { switch (selected) { - case wattsShaded: - case wattsUnshaded: + case watts: + case wattsZone: return wattsDelta; case nm: return nmDelta; case hr: + case hrZone: return hrDelta; case kph: return kphDelta; @@ -589,12 +734,13 @@ int PowerHist::getDigits() { switch (selected) { - case wattsShaded: - case wattsUnshaded: + case watts: + case wattsZone: return wattsDigits; case nm: return nmDigits; case hr: + case hrZone: return hrDigits; case kph: return kphDigits; @@ -621,6 +767,7 @@ void PowerHist::setWithZeros(bool value) { withz = value; + setData(rideItem); // for zone recalculating with/without zero recalc(); } @@ -631,7 +778,7 @@ PowerHist::setlnY(bool value) // "new" in the argument list is not a leak lny=value; - if (lny) + if (lny && selected != wattsZone && selected != hrZone) { setAxisScaleEngine(yLeft, new QwtLog10ScaleEngine); curve->setBaseline(1e-6); @@ -645,45 +792,50 @@ PowerHist::setlnY(bool value) replot(); } +void +PowerHist::setSumY(bool value) +{ + absolutetime = value; + setParameterAxisTitle(); + setData(rideItem); // for zone recalculating with/without zero + recalc(); +} + void PowerHist::setParameterAxisTitle() { - setAxisTitle( - xBottom, - ((selected == wattsShaded) || - (selected == wattsUnshaded) - ) ? - "watts" : - ((selected == hr) ? - "beats/minute" : - ((selected == cad) ? - "revolutions/min" : - useMetricUnits ? - ((selected == nm) ? - "newton-meters" : - ((selected == kph) ? - "km/hr" : - "undefined" - ) - ) : - ((selected == nm) ? - "ft-lb" : - ((selected == kph) ? - "miles/hr" : - "undefined" - ) - ) - ) - ) - ); + QString axislabel; + switch (selected) { + case watts: + axislabel = tr("Power (watts)"); + break; + case wattsZone: + axislabel = tr("Power zone"); + break; + case hr: + axislabel = tr("Heartrate (bpm)"); + break; + case hrZone: + axislabel = tr("Heartrate zone"); + break; + case cad: + axislabel = tr("Cadence (rpm)"); + break; + case kph: + axislabel = QString(tr("Speed (%1)")).arg(useMetricUnits ? tr("kph") : tr("mph")); + break; + case nm: + axislabel = QString(tr("Torque (%1)")).arg(useMetricUnits ? tr("N-m") : tr("ft-lbf")); + break; + } + setAxisTitle(xBottom, axislabel); + setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)")); } void PowerHist::setSelection(Selection selection) { - if (selected == selection) - return; - selected = selection; + if (selected == wattsZone || selected == hrZone) setlnY(false); configChanged(); // set colors setParameterAxisTitle(); recalc(); @@ -696,48 +848,40 @@ PowerHist::fixSelection() { Selection s = selected; RideFile *ride = rideItem->ride(); - if (ride) - do - { - if ((s == wattsShaded) || (s == wattsUnshaded)) - { - if (ride->areDataPresent()->watts) - setSelection(s); - else - s = nm; - } + int powerRange = mainWindow->zones()->whichRange(rideItem->dateTime.date()); + int hrRange = mainWindow->hrZones()->whichRange(rideItem->dateTime.date()); - else if (s == nm) - { - if (ride->areDataPresent()->nm) - setSelection(s); - else - s = hr; - } + if (ride) do { - else if (s == hr) - { - if (ride->areDataPresent()->hr) - setSelection(s); - else - s = kph; - } + if (s == watts) { + if (ride->areDataPresent()->watts) setSelection(s); + else s = nm; - else if (s == kph) - { - if (ride->areDataPresent()->kph) - setSelection(s); - else - s = cad; - } + } 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; + } - else if (s == cad) - { - if (ride->areDataPresent()->cad) - setSelection(s); - else - s = wattsShaded; - } } while (s != selected); } @@ -747,7 +891,8 @@ bool PowerHist::shadeZones() const return ( rideItem && rideItem->ride() && - selected == wattsShaded + selected == watts && + shaded() == true ); } @@ -764,3 +909,54 @@ bool PowerHist::isSelected(const RideFilePoint *p, double sample) { } return false; } + +void +PowerHist::pointHover(QwtPlotCurve *curve, int index) +{ + if (index >= 0) { + + double xvalue = curve->x(index); + double yvalue = curve->y(index); + QString text; + + if ((selected == hrZone || selected == wattsZone) && yvalue > 0) { + // output the tooltip + text = QString("%1 %2").arg(yvalue, 0, 'f', 1).arg(absolutetime ? tr("minutes") : tr("%")); + + // set that text up + zoomer->setText(text); + return; + + } else if (yvalue > 0) { + + // output the tooltip + text = QString("%1 %2\n%3 %4") + .arg(xvalue, 0, 'f', getDigits()) + .arg(this->axisTitle(curve->xAxis()).text()) + .arg(yvalue, 0, 'f', 1) + .arg(absolutetime ? tr("minutes") : tr("%")); + + // set that text up + zoomer->setText(text); + return; + } + } + // no point + zoomer->setText(""); +} + +// because we need to effectively draw bars when showing +// time in zone (i.e. for every zone there are 2 points for each +// zone - top left and top right) we need to multiply the percentage +// values by 2 to take this into account +void +PowerHist::percentify(QVector &array, double factor) +{ + double total=0; + foreach (double current, array) total += current; + + if (total > 0) + for (int i=0; i< array.size(); i++) + if (array[i] > 0.01) // greater than 0.8s (i.e. not a double storage issue) + array[i] = factor * (array[i] / total) * (double)100.00; +} diff --git a/src/PowerHist.h b/src/PowerHist.h index 7d71370ca..7399cc5af 100644 --- a/src/PowerHist.h +++ b/src/PowerHist.h @@ -20,9 +20,11 @@ #define _GC_PowerHist_h 1 #include +#include #include #include + class QwtPlotCurve; class QwtPlotGrid; class MainWindow; @@ -30,7 +32,44 @@ class RideItem; class RideFilePoint; class PowerHistBackground; class PowerHistZoneLabel; -class QwtPlotZoomer; +class LTMCanvasPicker; +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); + } + + virtual QwtText trackerText(const QwtDoublePoint &/*pos*/) const + { + QColor bg = QColor(255,255, 170); // toolyip yellow +#if QT_VERSION >= 0x040300 + 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; } + private: + QString format; + QString tip; + }; class PowerHist : public QwtPlot { @@ -50,20 +89,27 @@ class PowerHist : public QwtPlot bool shadeZones() const; enum Selection { - wattsShaded, - wattsUnshaded, + 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(); @@ -78,6 +124,8 @@ class PowerHist : public QwtPlot void setlnY(bool value); void setWithZeros(bool value); + void setSumY(bool value); + void pointHover(QwtPlotCurve *curve, int index); void configChanged(); protected: @@ -88,16 +136,20 @@ class PowerHist : public QwtPlot // 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; @@ -108,10 +160,9 @@ class PowerHist : public QwtPlot void recalc(); void setYMax(); - QwtPlotZoomer *zoomer; + penTooltip *zoomer; private: - QSettings *settings; QVariant unit; PowerHistBackground *bg; @@ -135,6 +186,11 @@ class PowerHist : public QwtPlot bool isSelected(const RideFilePoint *p, double); bool useMetricUnits; // whether metric units are used (or imperial) + + bool absolutetime; // do we sum absolute or percentage? + void percentify(QVector &, double factor); // and a function to convert + + LTMCanvasPicker *canvasPicker; }; #endif // _GC_PowerHist_h diff --git a/src/RideCalendar.cpp b/src/RideCalendar.cpp index fd46c2d52..1b5fa39e0 100644 --- a/src/RideCalendar.cpp +++ b/src/RideCalendar.cpp @@ -104,12 +104,12 @@ void RideCalendar::paintCell(QPainter *painter, const QRect &rect, const QDate & RideIter i; if (ascending) { RideItemDateLessThan comp; - RideItem search(0, "", "", QDateTime(date), NULL, "", NULL); + RideItem search(0, "", "", QDateTime(date), NULL, NULL, "", NULL); i = std::lower_bound(begin, end, &search, comp); } else { RideItemDateGreaterThan comp; - RideItem search(0, "", "", QDateTime(date.addDays(1)), NULL, "", NULL); + RideItem search(0, "", "", QDateTime(date.addDays(1)), NULL, NULL, "", NULL); i = std::upper_bound(begin, end, &search, comp); } diff --git a/src/RideFile.cpp b/src/RideFile.cpp index 20fcf16f8..2f4aa491c 100644 --- a/src/RideFile.cpp +++ b/src/RideFile.cpp @@ -255,6 +255,7 @@ RideFile *RideFileFactory::openRideFile(QFile &file, if (result) { if (result->intervals().empty()) result->fillInIntervals(); result->setTag("Filename", file.fileName()); + result->setTag("Athlete", QFileInfo(file).dir().dirName()); DataProcessorFactory::instance().autoProcess(result); } diff --git a/src/RideItem.cpp b/src/RideItem.cpp index d311c3597..0f25cff49 100644 --- a/src/RideItem.cpp +++ b/src/RideItem.cpp @@ -22,14 +22,15 @@ #include "RideFile.h" #include "MainWindow.h" #include "Zones.h" +#include "HrZones.h" #include #include RideItem::RideItem(int type, QString path, QString fileName, const QDateTime &dateTime, - const Zones *zones, QString notesFileName, MainWindow *main) : + const Zones *zones, const HrZones *hrZones, QString notesFileName, MainWindow *main) : QTreeWidgetItem(type), ride_(NULL), main(main), isdirty(false), isedit(false), path(path), fileName(fileName), - dateTime(dateTime), zones(zones), notesFileName(notesFileName) + dateTime(dateTime), zones(zones), hrZones(hrZones), notesFileName(notesFileName) { setText(0, dateTime.toString("ddd")); setText(1, dateTime.toString("MMM d, yyyy")); @@ -121,12 +122,23 @@ int RideItem::zoneRange() return zones->whichRange(dateTime.date()); } +int RideItem::hrZoneRange() +{ + return hrZones->whichRange(dateTime.date()); +} + int RideItem::numZones() { int zone_range = zoneRange(); return (zone_range >= 0) ? zones->numZones(zone_range) : 0; } +int RideItem::numHrZones() +{ + int hr_zone_range = hrZoneRange(); + return (hr_zone_range >= 0) ? hrZones->numZones(hr_zone_range) : 0; +} + double RideItem::timeInZone(int zone) { computeMetrics(); @@ -136,6 +148,15 @@ double RideItem::timeInZone(int zone) return time_in_zone[zone]; } +double RideItem::timeInHrZone(int zone) +{ + computeMetrics(); + if (!ride()) + return 0.0; + assert(zone < numHrZones()); + return time_in_hr_zone[zone]; +} + void RideItem::freeMemory() { @@ -165,6 +186,13 @@ RideItem::computeMetrics() num_zones = zones->numZones(zone_range); time_in_zone.resize(num_zones); } + int hr_zone_range = hrZoneRange(); + int num_hr_zones = numHrZones(); + time_in_hr_zone.clear(); + if (hr_zone_range >= 0) { + num_hr_zones = hrZones->numZones(hr_zone_range); + time_in_hr_zone.resize(num_hr_zones); + } double secs_delta = ride()->recIntSecs(); foreach (const RideFilePoint *point, ride()->dataPoints()) { @@ -175,13 +203,20 @@ RideItem::computeMetrics() time_in_zone[zone] += secs_delta; } } + if (point->hr >= 0.0) { + if (num_hr_zones > 0) { + int hrZone = hrZones->whichZone(hr_zone_range, point->hr); + if (hrZone >= 0) + time_in_hr_zone[hrZone] += secs_delta; + } + } } QStringList allMetrics; const RideMetricFactory &factory = RideMetricFactory::instance(); for (int i = 0; i < factory.metricCount(); ++i) allMetrics.append(factory.metricName(i)); - metrics = RideMetric::computeMetrics(ride(), zones, allMetrics); + metrics = RideMetric::computeMetrics(ride(), zones, hrZones, allMetrics); } void diff --git a/src/RideItem.h b/src/RideItem.h index b84b94e92..f3a043eb0 100644 --- a/src/RideItem.h +++ b/src/RideItem.h @@ -27,6 +27,7 @@ class RideFile; class RideEditor; class MainWindow; class Zones; +class HrZones; // Because we have subclassed QTreeWidgetItem we // need to use our own type, this MUST be greater than @@ -42,6 +43,7 @@ class RideItem : public QObject, public QTreeWidgetItem //<< for signals/slots protected: QVector time_in_zone; + QVector time_in_hr_zone; RideFile *ride_; QStringList errors_; MainWindow *main; // to notify widgets when date/time changes @@ -63,13 +65,14 @@ class RideItem : public QObject, public QTreeWidgetItem //<< for signals/slots RideFile *ride(); const QStringList errors() { return errors_; } const Zones *zones; + const HrZones *hrZones; QString notesFileName; QHash metrics; RideItem(int type, QString path, QString fileName, const QDateTime &dateTime, - const Zones *zones, QString notesFileName, MainWindow *main); + const Zones *zones, const HrZones *hrZones, QString notesFileName, MainWindow *main); void setDirty(bool); bool isDirty() { return isdirty; } @@ -79,7 +82,10 @@ class RideItem : public QObject, public QTreeWidgetItem //<< for signals/slots void freeMemory(); int zoneRange(); + int hrZoneRange(); int numZones(); + int numHrZones(); double timeInZone(int zone); + double timeInHrZone(int zone); }; #endif // _GC_RideItem_h diff --git a/src/RideMetric.cpp b/src/RideMetric.cpp index c1de7a6a2..defe3649b 100644 --- a/src/RideMetric.cpp +++ b/src/RideMetric.cpp @@ -18,15 +18,18 @@ #include "RideMetric.h" #include "Zones.h" +#include "HrZones.h" RideMetricFactory *RideMetricFactory::_instance; QVector RideMetricFactory::noDeps; QHash -RideMetric::computeMetrics(const RideFile *ride, const Zones *zones, +RideMetric::computeMetrics(const RideFile *ride, const Zones *zones, const HrZones *hrZones, const QStringList &metrics) { int zoneRange = zones->whichRange(ride->startTime().date()); + int hrZoneRange = hrZones->whichRange(ride->startTime().date()); + const RideMetricFactory &factory = RideMetricFactory::instance(); QStringList todo = metrics; QHash done; @@ -45,7 +48,7 @@ RideMetric::computeMetrics(const RideFile *ride, const Zones *zones, if (ready) { RideMetric *m = factory.newMetric(symbol); if (!ride->dataPoints().isEmpty()) - m->compute(ride, zones, zoneRange, done); + m->compute(ride, zones, zoneRange, hrZones, hrZoneRange, done); if (ride->metricOverrides.contains(symbol)) m->override(ride->metricOverrides.value(symbol)); done.insert(symbol, m); diff --git a/src/RideMetric.h b/src/RideMetric.h index 7cf1e9cbc..62cc5e2d8 100644 --- a/src/RideMetric.h +++ b/src/RideMetric.h @@ -30,6 +30,7 @@ #include "RideFile.h" class Zones; +class HrZones; class RideMetric; typedef QSharedPointer RideMetricPtr; @@ -82,10 +83,10 @@ struct RideMetric { // Factor to multiple value to convert from metric to imperial virtual double conversion() const { return conversion_; } - // Compute the ride metric from a file. - virtual void compute(const RideFile *ride, - const Zones *zones, - int zoneRange, + // Compute the ride metric from a file (Hr Zone Version). + virtual void compute(const RideFile *ride, + const Zones *zones, int zoneRange, + const HrZones *hrZones, int hrZoneRange, const QHash &deps) = 0; // Fill in the value of the ride metric using the mapping provided. For @@ -121,7 +122,7 @@ struct RideMetric { virtual RideMetric *clone() const = 0; static QHash - computeMetrics(const RideFile *ride, const Zones *zones, + computeMetrics(const RideFile *ride, const Zones *zones, const HrZones *hrZones, const QStringList &metrics); // Initialisers for derived classes to setup basic data diff --git a/src/RideSummaryWindow.cpp b/src/RideSummaryWindow.cpp index 42c217d4a..ef6ca0d06 100644 --- a/src/RideSummaryWindow.cpp +++ b/src/RideSummaryWindow.cpp @@ -25,6 +25,7 @@ #include "TimeUtils.h" #include "Units.h" #include "Zones.h" +#include "HrZones.h" #include #include #include @@ -113,6 +114,9 @@ RideSummaryWindow::htmlSummary() const "skiba_bike_score", "daniels_points", "daniels_equivalent_power", + "trimp_points", + "trimp_100_points", + "trimp_zonal_points", "aerobic_decoupling", NULL }; @@ -166,6 +170,14 @@ RideSummaryWindow::htmlSummary() const summary += mainWindow->zones()->summarize(rideItem->zoneRange(), time_in_zone); } + if (rideItem->numHrZones() > 0) { + QVector time_in_hr_zone(rideItem->numHrZones()); + for (int i = 0; i < rideItem->numHrZones(); ++i) + time_in_hr_zone[i] = rideItem->timeInHrZone(i); + summary += tr("

Hr Zones

"); + summary += mainWindow->hrZones()->summarize(rideItem->hrZoneRange(), time_in_hr_zone); + } + if (ride->intervals().size() > 0) { bool firstRow = true; QString s; @@ -194,7 +206,7 @@ RideSummaryWindow::htmlSummary() const } QHash metrics = - RideMetric::computeMetrics(&f, mainWindow->zones(), intervalMetrics); + RideMetric::computeMetrics(&f, mainWindow->zones(), mainWindow->hrZones(), intervalMetrics); if (firstRow) { summary += ""; summary += "Interval Name"; diff --git a/src/Settings.h b/src/Settings.h index 31a29f76c..81f4d25cf 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -40,6 +40,12 @@ #define GC_DATETIME_FORMAT "ddd MMM dd, yyyy, hh:mm AP" #define GC_UNIT "unit" #define GC_LANG "lang" +#define GC_NICKNAME "nickname" +#define GC_DOB "dob" +#define GC_WEIGHT "weight" +#define GC_SEX "sex" +#define GC_BIO "bio" +#define GC_AVATAR "avatar" #define GC_SETTINGS_LAST_IMPORT_PATH "mainwindow/lastImportPath" #define GC_ALLRIDES_ASCENDING "allRidesAscending" #define GC_CRANKLENGTH "crankLength" @@ -62,6 +68,7 @@ #define GC_TRAIN_SPLITTER_SIZES "trainwindow/splitterSizes" #define GC_LTM_SPLITTER_SIZES "ltmwindow/splitterSizes" + // device Configurations NAME/SPEC/TYPE/DEFI/DEFR all get a number appended // to them to specify which configured device i.e. devices1 ... devicesn where // n is defined in GC_DEV_COUNT @@ -107,4 +114,3 @@ inline boost::shared_ptr GetApplicationSettings() } #endif // _GC_Settings_h - diff --git a/src/StressCalculator.cpp b/src/StressCalculator.cpp index ced8773dc..f32177f08 100644 --- a/src/StressCalculator.cpp +++ b/src/StressCalculator.cpp @@ -71,7 +71,7 @@ void StressCalculator::calculateStress(MainWindow *main, QString home, const QSt QList results; // refresh metrics - metricDB = new MetricAggregator(main, home, main->zones()); + metricDB = new MetricAggregator(main, home, main->zones(), main->hrZones()); results = metricDB->getAllMetricsFor(QDateTime(QDate(1900,1,1)), QDateTime(QDate(3000,1,1))); delete metricDB; diff --git a/src/TRIMPPoints.cpp b/src/TRIMPPoints.cpp new file mode 100644 index 000000000..e010a8db2 --- /dev/null +++ b/src/TRIMPPoints.cpp @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2010 Damien Grauser (Damien.Grauser@pev-geneve.ch) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "RideMetric.h" +#include "Settings.h" +#include "Zones.h" +#include "HrZones.h" +#include +#include + +#define tr(s) QObject::tr(s) + +// This is Morton/Banister with Green et al coefficient. +// +// HR_TRIMP = time(min) * (AvgHR-RHR)/(MaxHR-RHR)*0.64*EXP(Ksex*(AvgHr-RHR)/(MaxHR-RHR)) +// +// Ksex = 1.92 for man and 1.67 for woman +// RHR = resting heart rate +// +class TRIMPPoints : public RideMetric { + + double score; + + public: + + static const double K; + + TRIMPPoints() : score(0.0) + { + setSymbol("trimp_points"); + setName(tr("TRIMP Points")); + setMetricUnits(""); + setImperialUnits(""); + setType(RideMetric::Total); + } + + void compute(const RideFile *rideFile, + const Zones *, int , + const HrZones *hrZones, int hrZoneRange, + const QHash &deps) + { + if (!hrZones || hrZoneRange < 0) { + setValue(0); + return; + } + + // use resting HR from zones, but allow it to be + // overriden in ride metadata + double maxHr = hrZones->getMaxHr(hrZoneRange); + double restHr = hrZones->getRestHr(hrZoneRange); + restHr = rideFile->getTag("Rest HR", QString("%1").arg(restHr)).toDouble(); + + assert(deps.contains("time_riding")); + assert(deps.contains("average_hr")); + const RideMetric *timeRidingMetric = deps.value("time_riding"); + const RideMetric *averageHrMetric = deps.value("average_hr"); + assert(timeRidingMetric); + assert(averageHrMetric); + + double secs = timeRidingMetric->value(true); + double hr = averageHrMetric->value(true); + + //TRIMP: = t x %HRR x 0.64e1,92(%HRR) + + // Can we lookup the athletes gender? + // Default to male if we fail + QString athlete; + double ksex = 1.92; + if ((athlete = rideFile->getTag("Athlete", "unknown")) != "unknown") { + boost::shared_ptr settings = GetApplicationSettings(); + QString key = QString("%1/%2").arg(athlete).arg(GC_SEX); + if (settings->value(key).toInt() == 1) ksex = 1.67; // Female + else ksex = 1.92; // Male + } + + // ok lets work the score out + score = (secs == 0.0 || hr &deps) + { + if (!hrZones || hrZoneRange < 0) { + setValue(0); + return; + } + + // use resting HR from zones, but allow it to be + // overriden in ride metadata + double maxHr = hrZones->getMaxHr(hrZoneRange); + double restHr = hrZones->getRestHr(hrZoneRange); + double ltHr = hrZones->getLT(hrZoneRange); + restHr = rideFile->getTag("Rest HR", QString("%1").arg(restHr)).toDouble(); + + + assert(deps.contains("trimp_points")); + const RideMetric *trimpPointsMetric = deps.value("trimp_points"); + assert(trimpPointsMetric); + double trimp = trimpPointsMetric->value(true); + + //TRIMP: = t x %HRR x 0.64e1,92(%HRR) + + // Can we lookup the athletes gender? + // Default to male if we fail + QString athlete; + double ksex = 1.92; + if ((athlete = rideFile->getTag("Athlete", "unknown")) != "unknown") { + boost::shared_ptr settings = GetApplicationSettings(); + QString key = QString("%1/%2").arg(athlete).arg(GC_SEX); + if (settings->value(key).toInt() == 1) ksex = 1.67; // Female + else ksex = 1.92; // Male + } + + score = trimp == 0.0 ? 0.0 : 100 * trimp / + (60 * (ltHr-restHr)/(maxHr-restHr)*0.64*exp(ksex*(ltHr-restHr)/(ltHr-restHr)/(maxHr-restHr))); + + setValue(score); + } + RideMetric *clone() const { return new TRIMP100Points(*this); } +}; + +//0.84 (zone 1 64-76%), 1.65 (zone 2 77-83%), 2.57 (zone 3 84-89%), 4.01 (zone 4 90-94%), and 5.91 (zone 5 95-100%) +//1 (zone 1 50-60%), 1.1 (zone 2 60-70%), 1.2 (zone 3 70-80%), 2.2 (zone 4 80-90%), and 4.5 (zone 5 90-100%) + +// 0, 68, 83, 94, 105 of LT for LT 80% Max-> 0, 55, 66, 75, 84 +// 0.9 (zone 1 0-55%), 1.1 (zone 2 55-66%), 1.2 (zone 3 66-75%), 2 (zone 4 75-84%), and 5 (zone 5 84-100%) + +class TRIMPZonalPoints : public RideMetric { + + double score; + +public: + + static const double K; + + TRIMPZonalPoints() : score(0.0) + { + setSymbol("trimp_zonal_points"); + setName(tr("TRIMP Zonal Points")); + setMetricUnits(""); + setImperialUnits(""); + setType(RideMetric::Total); + } + + void compute(const RideFile *, + const Zones *, int, + const HrZones *hrZones, int hrZoneRange, + const QHash &deps) + { + if (hrZoneRange == -1) { + setValue(0); + return; + } + + QList trimpk = hrZones->getZoneTrimps(hrZoneRange); + double value = 0; + + if (trimpk.size()>0) { + assert(deps.contains("time_in_zone_H1")); + const RideMetric *time1Metric = deps.value("time_in_zone_H1"); + assert(time1Metric); + double time1 = time1Metric->value(true); + double trimpk1 = trimpk[0]; + value += trimpk1 * time1; + } + + if (trimpk.size()>1) { + assert(deps.contains("time_in_zone_H2")); + const RideMetric *time2Metric = deps.value("time_in_zone_H2"); + assert(time2Metric); + double time2 = time2Metric->value(true); + double trimpk2 = trimpk[1]; + value += trimpk2 * time2; + } + + if (trimpk.size()>2) { + assert(deps.contains("time_in_zone_H3")); + const RideMetric *time3Metric = deps.value("time_in_zone_H3"); + assert(time3Metric); + double time3 = time3Metric->value(true); + double trimpk3 = trimpk[2]; + value += trimpk3 * time3; + } + + if (trimpk.size()>3) { + assert(deps.contains("time_in_zone_H4")); + const RideMetric *time4Metric = deps.value("time_in_zone_H4"); + assert(time4Metric); + double time4 = time4Metric->value(true); + double trimpk4 = trimpk[3]; + value += trimpk4 * time4; + } + + if (trimpk.size()>4) { + assert(deps.contains("time_in_zone_H5")); + const RideMetric *time5Metric = deps.value("time_in_zone_H5"); + assert(time5Metric); + double time5 = time5Metric->value(true); + double trimpk5 = trimpk[4]; + value += trimpk5 * time5; + } + + if (trimpk.size()>5) { + assert(deps.contains("time_in_zone_H6")); + const RideMetric *time6Metric = deps.value("time_in_zone_H6"); + assert(time6Metric); + double time6 = time6Metric->value(true); + double trimpk6 = trimpk[5]; + value += trimpk6 * time6; + } + + if (trimpk.size()>6) { + assert(deps.contains("time_in_zone_H7")); + const RideMetric *time7Metric = deps.value("time_in_zone_H7"); + assert(time7Metric); + double time7 = time7Metric->value(true); + double trimpk7 = trimpk[6]; + value += trimpk7 * time7; + } + + if (trimpk.size()>7) { + assert(deps.contains("time_in_zone_H8")); + const RideMetric *time8Metric = deps.value("time_in_zone_H8"); + assert(time8Metric); + double time8 = time8Metric->value(true); + double trimpk8 = trimpk[7]; + value += trimpk8 * time8; + } + + setValue(value/60); + return; + } + RideMetric *clone() const { return new TRIMPZonalPoints(*this); } +}; + +static bool added() { + QVector deps; + deps.append("time_riding"); + deps.append("average_hr"); + RideMetricFactory::instance().addMetric(TRIMPPoints(), &deps); + + deps.append("time_riding"); + deps.append("trimp_points"); + RideMetricFactory::instance().addMetric(TRIMP100Points(), &deps); + + deps.append("time_in_zone_H1"); + deps.append("time_in_zone_H2"); + deps.append("time_in_zone_H3"); + deps.append("time_in_zone_H4"); + deps.append("time_in_zone_H5"); + RideMetricFactory::instance().addMetric(TRIMPZonalPoints(), &deps); + return true; +} + +static bool added_ = added(); diff --git a/src/TimeInZone.cpp b/src/TimeInZone.cpp index dce17578d..ccb6e834c 100644 --- a/src/TimeInZone.cpp +++ b/src/TimeInZone.cpp @@ -42,7 +42,7 @@ class ZoneTime : public RideMetric { setConversion(1.0); } void setLevel(int level) { this->level=level-1; } // zones start from zero not 1 - void compute(const RideFile *ride, const Zones *zone, int zoneRange, + void compute(const RideFile *ride, const Zones *zone, int zoneRange, const HrZones *, int, const QHash &) { seconds = 0; diff --git a/src/Units.h b/src/Units.h index b06d4edae..c7594b65e 100644 --- a/src/Units.h +++ b/src/Units.h @@ -23,6 +23,8 @@ #define MILES_PER_KM 0.62137119 #define FEET_PER_METER 3.2808399 #define METERS_PER_FOOT 0.3047999 +#define LB_PER_KG 2.20462262 +#define KG_PER_LB 0.45359237 #endif // _GC_Units_h diff --git a/src/ZoneScaleDraw.h b/src/ZoneScaleDraw.h new file mode 100644 index 000000000..34eafa3c2 --- /dev/null +++ b/src/ZoneScaleDraw.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010 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_ZoneScaleDraw_h +#define _GC_ZoneScaleDraw_h 1 + +#include +#include "Zones.h" +#include "HrZones.h" + +class ZoneScaleDraw: public QwtScaleDraw +{ + public: + ZoneScaleDraw(const Zones *zones, int range=-1) : zones(zones) { + setRange(range); + } + + // modify later if neccessary + void setZones(Zones *z) { + zones=z; + names.clear(); + from.clear(); + to.clear(); + } + + // when we set the range we are choosing the texts + void setRange(int x) { + range=x; + if (range >= 0) { + names = zones->getZoneNames(range); + from = zones->getZoneLows(range); + to = zones->getZoneHighs(range); + } else { + names.clear(); + from.clear(); + to.clear(); + } + } + + // return label + virtual QwtText label(double v) const + { + if (v <0 || v > (names.count()-1) || range < 0) return QString(""); + else { + return names[v]; +#if 0 + if (v == names.count()-1) return QString("%1\n%2w+").arg(names[v]).arg(from[v]); + else return QString("%1\n%2-%3w").arg(names[v]).arg(from[v]).arg(to[v]); +#endif + } + } + + private: + const Zones *zones; + int range; + QList names; + QList from, to; +}; + +class HrZoneScaleDraw: public QwtScaleDraw +{ + public: + HrZoneScaleDraw(const HrZones *zones, int range=-1) : zones(zones) { + setRange(range); + } + + // modify later if neccessary + void setHrZones(HrZones *z) { + zones=z; + names.clear(); + from.clear(); + to.clear(); + } + + // when we set the range we are choosing the texts + void setRange(int x) { + range=x; + if (range >= 0) { + names = zones->getZoneNames(range); + from = zones->getZoneLows(range); + to = zones->getZoneHighs(range); + } else { + names.clear(); + from.clear(); + to.clear(); + } + } + + // return label + virtual QwtText label(double v) const + { + if (v < 0 || v > (names.count()-1) || range < 0) return QString(""); + else { + return names[v]; +#if 0 + if (v == names.count()-1) return QString("%1\n%2bpm+").arg(names[v]).arg(from[v]); + else return QString("%1\n%2-%3bpm").arg(names[v]).arg(from[v]).arg(to[v]); +#endif + } + } + + private: + const HrZones *zones; + int range; + QList names; + QList from, to; + +}; +#endif diff --git a/src/application.qrc b/src/application.qrc index d8e73f467..f5b8eb3fd 100644 --- a/src/application.qrc +++ b/src/application.qrc @@ -9,6 +9,7 @@ images/update.png images/gc.png images/config.png + images/noavatar.png translations/gc_fr.qm translations/gc_ja.qm xml/charts.xml diff --git a/src/images/noavatar.png b/src/images/noavatar.png new file mode 100644 index 000000000..45d65f045 Binary files /dev/null and b/src/images/noavatar.png differ diff --git a/src/src.pro b/src/src.pro index bd92b1da7..f7b5b3f43 100644 --- a/src/src.pro +++ b/src/src.pro @@ -118,6 +118,7 @@ HEADERS += \ GcRideFile.h \ GoogleMapControl.h \ HistogramWindow.h \ + HrZones.h \ IntervalItem.h \ LogTimeScaleDraw.h \ LogTimeScaleEngine.h \ @@ -188,6 +189,7 @@ HEADERS += \ WeeklySummaryWindow.h \ WkoRideFile.h \ Zones.h \ + ZoneScaleDraw.h SOURCES += \ AerobicDecoupling.cpp \ @@ -228,6 +230,8 @@ SOURCES += \ GcRideFile.cpp \ GoogleMapControl.cpp \ HistogramWindow.cpp \ + HrTimeInZone.cpp \ + HrZones.cpp \ IntervalItem.cpp \ LogTimeScaleDraw.cpp \ LogTimeScaleEngine.cpp \ @@ -294,6 +298,7 @@ SOURCES += \ TrainTabs.cpp \ TrainTool.cpp \ TrainWindow.cpp \ + TRIMPPoints.cpp \ ViewSelection.cpp \ WeeklySummaryWindow.cpp \ WkoRideFile.cpp \