From e103824538e841a4b41efef0fe82b0100af304cd Mon Sep 17 00:00:00 2001 From: "Sean C. Rhea" Date: Wed, 25 Apr 2007 19:30:55 +0000 Subject: [PATCH] added zones to the weekly summary, consolidated and cleaned up code --- src/gui/GoldenCheetah.pro | 2 + src/gui/MainWindow.cpp | 90 ++++++++++++++++---------- src/gui/MainWindow.h | 2 + src/gui/RideItem.cpp | 109 ++++++++++++++++++++----------- src/gui/RideItem.h | 12 +++- src/gui/Time.cpp | 48 ++++++++++++++ src/gui/Time.h | 30 +++++++++ src/gui/Zones.cpp | 131 ++++++++++++++++++++++++++------------ src/gui/Zones.h | 10 ++- 9 files changed, 319 insertions(+), 115 deletions(-) create mode 100644 src/gui/Time.cpp create mode 100644 src/gui/Time.h diff --git a/src/gui/GoldenCheetah.pro b/src/gui/GoldenCheetah.pro index 97f95cdfe..ebb902d68 100644 --- a/src/gui/GoldenCheetah.pro +++ b/src/gui/GoldenCheetah.pro @@ -26,6 +26,7 @@ HEADERS += \ PowerHist.h \ RawFile.h \ RideItem.h \ + Time.h \ Zones.h SOURCES += \ @@ -39,6 +40,7 @@ SOURCES += \ PowerHist.cpp \ RawFile.cpp \ RideItem.cpp \ + Time.cpp \ Zones.cpp \ \ main.cpp diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 58fbee597..2a98c5a64 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -27,6 +27,8 @@ #include "RawFile.h" #include "RideItem.h" #include "Settings.h" +#include "Time.h" +#include "Zones.h" #include #include #include @@ -47,6 +49,17 @@ MainWindow::MainWindow(const QDir &home) : setWindowTitle(home.dirName()); settings.setValue(GC_SETTINGS_LAST, home.dirName()); + QFile zonesFile(home.absolutePath() + "/power.zones"); + if (zonesFile.exists()) { + zones = new Zones(); + if (!zones->read(zonesFile)) { + QMessageBox::warning(this, tr("Zones File Error"), + zones->errorString()); + delete zones; + zones = NULL; + } + } + QVariant geom = settings.value(GC_SETTINGS_MAIN_GEOM); if (geom == QVariant()) resize(640, 480); @@ -80,7 +93,8 @@ MainWindow::MainWindow(const QDir &home) : QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt()); QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt()); QDateTime dt(date, time); - last = new RideItem(allRides, RIDE_TYPE, home.path(), name, dt); + last = new RideItem(allRides, RIDE_TYPE, home.path(), + name, dt, zones); } } @@ -280,7 +294,8 @@ MainWindow::addRide(QString name) QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt()); QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt()); QDateTime dt(date, time); - RideItem *last = new RideItem(allRides, RIDE_TYPE, home.path(), name, dt); + RideItem *last = new RideItem(allRides, RIDE_TYPE, home.path(), + name, dt, zones); cpintPlot->needToScanRides = true; tabWidget->setCurrentIndex(0); treeWidget->setCurrentItem(last); @@ -447,6 +462,11 @@ MainWindow::rideSelected() double weeklyDistance = 0.0; double weeklyWork = 0.0; + double *time_in_zone = NULL; + int zone_range = -1; + int num_zones = -1; + bool zones_ok = true; + for (int i = 0; i < allRides->childCount(); ++i) { if (allRides->child(i)->type() == RIDE_TYPE) { RideItem *item = (RideItem*) allRides->child(i); @@ -455,6 +475,18 @@ MainWindow::rideSelected() weeklySeconds += item->secsMovingOrPedaling(); weeklyDistance += item->totalDistance(); weeklyWork += item->totalWork(); + if (zone_range == -1) { + zone_range = item->zoneRange(); + num_zones = item->numZones(); + time_in_zone = new double[num_zones]; + } + else if (item->zoneRange() != zone_range) { + zones_ok = false; + } + if (zone_range != -1) { + for (int j = 0; j < num_zones; ++j) + time_in_zone[j] += item->timeInZone(j); + } } } } @@ -464,7 +496,7 @@ MainWindow::rideSelected() minutes %= 60; const char *dateFormat = "MM/dd/yyyy"; - weeklySummary->setHtml(tr( + QString summary = tr( "
" "

Week of %1 through %2

" "

Summary

" @@ -477,15 +509,30 @@ MainWindow::rideSelected() "Total work (kJ):" " %6" "" - "
" + // TODO: add averages ) .arg(wstart.toString(dateFormat)) .arg(wstart.addDays(6).toString(dateFormat)) .arg(hours) .arg(minutes, 2, 10, QLatin1Char('0')) .arg((unsigned) round(weeklyDistance)) - .arg((unsigned) round(weeklyWork)) - ); + .arg((unsigned) round(weeklyWork)); + + if (zone_range != -1) { + summary += "

Power Zones

"; + if (!zones_ok) + summary += "Error: Week spans more than one zone range."; + else { + summary += + zones->summarize(zone_range, time_in_zone, num_zones); + } + } + + summary += ""; + + // TODO: add daily breakdown + + weeklySummary->setHtml(summary); return; } @@ -564,31 +611,6 @@ MainWindow::tabChanged(int index) } } -static QString -time_to_string(double secs) { - if (secs < 60.0) - return QString("%1s").arg(secs, 0, 'f', 2, QLatin1Char('0')); - QString result; - unsigned rounded = (unsigned) round(secs); - bool needs_colon = false; - if (rounded >= 3600) { - result += QString("%1h").arg(rounded / 3600); - rounded %= 3600; - needs_colon = true; - } - if (needs_colon || rounded >= 60) { - if (needs_colon) - result += " "; - result += QString("%1m").arg(rounded / 60, 2, 10, QLatin1Char('0')); - rounded %= 60; - needs_colon = true; - } - if (needs_colon) - result += " "; - result += QString("%1s").arg(rounded, 2, 10, QLatin1Char('0')); - return result; -} - static unsigned curve_to_point(double x, const QwtPlotCurve *curve) { @@ -617,10 +639,10 @@ MainWindow::pickerMoved(const QPoint &pos) { double minutes = cpintPlot->invTransform(QwtPlot::xBottom, pos.x()); cpintTimeLabel->setText(tr("Interval Duration: %1") - .arg(time_to_string(60.0*minutes))); - cpintAllLabel->setText(tr("All Rides: %1 watts").arg( - curve_to_point(minutes, cpintPlot->getAllCurve()))); + .arg(interval_to_str(60.0*minutes))); cpintTodayLabel->setText(tr("Today: %1 watts").arg( curve_to_point(minutes, cpintPlot->getThisCurve()))); + cpintAllLabel->setText(tr("All Rides: %1 watts").arg( + curve_to_point(minutes, cpintPlot->getAllCurve()))); } diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 0eb036532..2f9d41573 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -28,6 +28,7 @@ class AllPlot; class CpintPlot; class PowerHist; class QwtPlotPicker; +class Zones; class MainWindow : public QMainWindow { @@ -78,6 +79,7 @@ class MainWindow : public QMainWindow QLineEdit *binWidthLineEdit; QTreeWidgetItem *allRides; PowerHist *powerHist; + Zones *zones; }; #endif // _GC_MainWindow_h diff --git a/src/gui/RideItem.cpp b/src/gui/RideItem.cpp index b79875336..675ecf61d 100644 --- a/src/gui/RideItem.cpp +++ b/src/gui/RideItem.cpp @@ -21,41 +21,26 @@ #include "RideItem.h" #include "RawFile.h" #include "Settings.h" +#include "Time.h" +#include "Zones.h" +#include #include -static QString -time_to_string(double secs) { - QString result; - unsigned rounded = (unsigned) round(secs); - bool needs_colon = false; - if (rounded >= 3600) { - result += QString("%1").arg(rounded / 3600); - rounded %= 3600; - needs_colon = true; - } - if (needs_colon || rounded >= 60) { - if (needs_colon) - result += ":"; - result += QString("%1").arg(rounded / 60, 2, 10, QLatin1Char('0')); - rounded %= 60; - needs_colon = true; - } - if (needs_colon) - result += ":"; - result += QString("%1").arg(rounded, 2, 10, QLatin1Char('0')); - return result; -} - RideItem::RideItem(QTreeWidgetItem *parent, int type, QString path, - QString fileName, const QDateTime &dateTime) : + QString fileName, const QDateTime &dateTime, + const Zones *zones) : QTreeWidgetItem(parent, type), path(path), - fileName(fileName), dateTime(dateTime) + fileName(fileName), dateTime(dateTime), zones(zones) { setText(0, dateTime.toString("ddd")); setText(1, dateTime.toString("MMM d, yyyy")); setText(2, dateTime.toString("h:mm AP")); setTextAlignment(1, Qt::AlignRight); setTextAlignment(2, Qt::AlignRight); + + time_in_zone = NULL; + num_zones = -1; + zone_range = -1; } static void summarize(QString &intervals, @@ -122,6 +107,30 @@ double RideItem::totalWork() return total_work; } +int RideItem::zoneRange() +{ + if (summary.isEmpty()) + htmlSummary(); + return zone_range; +} + +int RideItem::numZones() +{ + if (summary.isEmpty()) + htmlSummary(); + assert(zone_range >= 0); + return num_zones; +} + +double RideItem::timeInZone(int zone) +{ + if (summary.isEmpty()) + htmlSummary(); + assert(zone_range >= 0); + assert(zone < num_zones); + return time_in_zone[zone]; +} + QString RideItem::htmlSummary() { @@ -137,6 +146,16 @@ RideItem::htmlSummary() return summary; } + if (zones) { + zone_range = zones->whichRange(dateTime.date()); + if (zone_range >= 0) { + num_zones = zones->numZones(zone_range); + time_in_zone = new double[num_zones]; + for (int i = 0; i < num_zones; ++i) + time_in_zone[i] = 0.0; + } + } + secs_moving_or_pedaling = 0.0; double secs_moving = 0.0; double total_watts = 0.0; @@ -174,14 +193,20 @@ RideItem::htmlSummary() } double secs_delta = raw->rec_int_ms / 1000.0; - if ((point->mph > 0.0) || (point->cad > 0.0)) + if ((point->mph > 0.0) || (point->cad > 0.0)) { secs_moving_or_pedaling += secs_delta; + } if (point->mph > 0.0) secs_moving += secs_delta; if (point->watts >= 0.0) { total_watts += point->watts * secs_delta; secs_watts += secs_delta; int_watts_sum += point->watts * secs_delta; + if (zones) { + int zone = zones->whichZone(zone_range, point->watts); + if (zone >= 0) + time_in_zone[zone] += secs_delta; + } } if (point->hr > 0) { total_hr += point->hr * secs_delta; @@ -211,35 +236,48 @@ RideItem::htmlSummary() total_distance = raw->points.back()->miles; total_work = total_watts / 1000.0; - summary += "

"; - summary += "
Total workout time:" + + summary += "

"; + summary += ""; + summary += ""; + summary += ""; + summary += ""; summary += "

Totals

Averages

"; + summary += ""; + summary += ""; - summary += QString("" + summary += QString("" "") .arg(total_distance, 0, 'f', 1); - summary += QString("" + summary += QString("" "") .arg((unsigned) round(total_work)); - summary += QString("" + summary += "
Workout time:" + time_to_string(raw->points.back()->secs); - summary += "
Total time riding:" + + summary += "
Time riding:" + time_to_string(secs_moving_or_pedaling) + "
Total distance (miles):
Distance (miles):%1
Total work (kJ):
Work (kJ):%1
Average speed (mph):
"; + summary += ""; + summary += QString("" "") .arg(((secs_moving == 0.0) ? 0.0 : raw->points.back()->miles / secs_moving * 3600.0), 0, 'f', 1); - summary += QString("" + summary += QString("" "") .arg((unsigned) avg_watts); - summary +=QString("" + summary +=QString("" "") .arg((unsigned) ((secs_hr == 0.0) ? 0.0 : round(total_hr / secs_hr))); - summary += QString("" + summary += QString("" "") .arg((unsigned) ((secs_cad == 0.0) ? 0.0 : round(total_cad / secs_cad))); + summary += "
Speed (mph):%1
Average power (watts):
Power (watts):%1
Average heart rate (bpm):
Heart rate (bpm):%1
Average cadence (rpm):
Cadence (rpm):%1
"; + if (zones) { + summary += "

Power Zones

"; + summary += zones->summarize(zone_range, time_in_zone, num_zones); + } + if (last_interval > 0) { summary += "

Intervals

\n

\n"; summary += " class RawFile; +class Zones; class RideItem : public QTreeWidgetItem { @@ -32,6 +33,9 @@ class RideItem : public QTreeWidgetItem { double secs_moving_or_pedaling; double total_distance; double total_work; + double *time_in_zone; + int num_zones; + int zone_range; public: @@ -40,14 +44,20 @@ class RideItem : public QTreeWidgetItem { QDateTime dateTime; QString summary; RawFile *raw; + const Zones *zones; RideItem(QTreeWidgetItem *parent, int type, QString path, - QString fileName, const QDateTime &dateTime); + QString fileName, const QDateTime &dateTime, + const Zones *zones); QString htmlSummary(); double secsMovingOrPedaling(); double totalDistance(); double totalWork(); + + int zoneRange(); + int numZones(); + double timeInZone(int zone); }; #endif // _GC_RideItem_h diff --git a/src/gui/Time.cpp b/src/gui/Time.cpp new file mode 100644 index 000000000..464250281 --- /dev/null +++ b/src/gui/Time.cpp @@ -0,0 +1,48 @@ + +#include "Time.h" +#include + +QString time_to_string(double secs) +{ + QString result; + unsigned rounded = (unsigned) round(secs); + bool needs_colon = false; + if (rounded >= 3600) { + result += QString("%1").arg(rounded / 3600); + rounded %= 3600; + needs_colon = true; + } + if (needs_colon) + result += ":"; + result += QString("%1").arg(rounded / 60, 2, 10, QLatin1Char('0')); + rounded %= 60; + result += ":"; + result += QString("%1").arg(rounded, 2, 10, QLatin1Char('0')); + return result; +} + +QString interval_to_str(double secs) +{ + if (secs < 60.0) + return QString("%1s").arg(secs, 0, 'f', 2, QLatin1Char('0')); + QString result; + unsigned rounded = (unsigned) round(secs); + bool needs_colon = false; + if (rounded >= 3600) { + result += QString("%1h").arg(rounded / 3600); + rounded %= 3600; + needs_colon = true; + } + if (needs_colon || rounded >= 60) { + if (needs_colon) + result += " "; + result += QString("%1m").arg(rounded / 60, 2, 10, QLatin1Char('0')); + rounded %= 60; + needs_colon = true; + } + if (needs_colon) + result += " "; + result += QString("%1s").arg(rounded, 2, 10, QLatin1Char('0')); + return result; +} + diff --git a/src/gui/Time.h b/src/gui/Time.h new file mode 100644 index 000000000..4839adb81 --- /dev/null +++ b/src/gui/Time.h @@ -0,0 +1,30 @@ +/* + * $Id: RideItem.cpp,v 1.3 2006/07/09 15:30:34 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _Time_h +#define _Time_h + +#include + +QString interval_to_str(double secs); // output like 1h 2m 3s +QString time_to_string(double secs); // output like 1:02:03 + +#endif // _Time_h + diff --git a/src/gui/Zones.cpp b/src/gui/Zones.cpp index f2c213aaf..14371935d 100644 --- a/src/gui/Zones.cpp +++ b/src/gui/Zones.cpp @@ -19,7 +19,9 @@ */ #include "Zones.h" +#include "Time.h" #include +#include bool Zones::read(QFile &file) { @@ -51,17 +53,17 @@ bool Zones::read(QFile &file) int pos = commentrx.indexIn(line, 0); if (pos != -1) { line = line.left(pos); - fprintf(stderr, "removing comment line %d; now: \"%s\"\n", - lineno, line.toAscii().constData()); + // fprintf(stderr, "removing comment line %d; now: \"%s\"\n", + // lineno, line.toAscii().constData()); } if (blankrx.indexIn(line, 0) == 0) { - fprintf(stderr, "line %d: blank\n", lineno); + // fprintf(stderr, "line %d: blank\n", lineno); } else if (rangerx.indexIn(line, 0) != -1) { - fprintf(stderr, "line %d: matched range: %s to %s\n", - lineno, - rangerx.cap(1).toAscii().constData(), - rangerx.cap(5).toAscii().constData()); + // fprintf(stderr, "line %d: matched range: %s to %s\n", + // lineno, + // rangerx.cap(1).toAscii().constData(), + // rangerx.cap(5).toAscii().constData()); QDate begin, end; if (rangerx.cap(1) == "BEGIN") begin = QDate::currentDate().addYears(-1000); @@ -77,9 +79,9 @@ bool Zones::read(QFile &file) rangerx.cap(7).toInt(), rangerx.cap(8).toInt()); } - fprintf(stderr, "begin=%s, end=%s\n", - begin.toString().toAscii().constData(), - end.toString().toAscii().constData()); + // fprintf(stderr, "begin=%s, end=%s\n", + // begin.toString().toAscii().constData(), + // end.toString().toAscii().constData()); if (range) { if (range->zones.empty()) { err = tr("line %1: read new range without reading " @@ -106,12 +108,12 @@ bool Zones::read(QFile &file) hi = zonerx.cap(4).toInt(); ZoneInfo *zone = new ZoneInfo(zonerx.cap(1), zonerx.cap(2), lo, hi); - fprintf(stderr, "line %d: matched zones: " - "\"%s\", \"%s\", %s, %s\n", lineno, - zonerx.cap(1).toAscii().constData(), - zonerx.cap(2).toAscii().constData(), - zonerx.cap(3).toAscii().constData(), - zonerx.cap(4).toAscii().constData()); + // fprintf(stderr, "line %d: matched zones: " + // "\"%s\", \"%s\", %s, %s\n", lineno, + // zonerx.cap(1).toAscii().constData(), + // zonerx.cap(2).toAscii().constData(), + // zonerx.cap(3).toAscii().constData(), + // zonerx.cap(4).toAscii().constData()); range->zones.append(zone); } } @@ -128,39 +130,86 @@ bool Zones::read(QFile &file) return true; } -int Zones::whichZone(const QDate &date, double value) const +int Zones::whichRange(const QDate &date) const { + int rnum = 0; QListIterator i(ranges); while (i.hasNext()) { ZoneRange *range = i.next(); - if ((date >= range->begin) && (date < range->end)) { - for (int j = 0; j < range->zones.size(); ++j) { - ZoneInfo *info = range->zones[j]; - if ((value >= info->lo) && (value < info->hi)) - return j; - } - } + if ((date >= range->begin) && (date < range->end)) + return rnum; + ++rnum; } return -1; } -void Zones::zoneInfo(const QDate &date, int zone, - QString &name, QString &description, - int &low, int &high) +int Zones::numZones(int rnum) const { - QListIterator i(ranges); - while (i.hasNext()) { - ZoneRange *range = i.next(); - if ((date >= range->begin) && (date < range->end)) { - assert(zone < range->zones.size()); - ZoneInfo *info = range->zones[zone]; - name = info->name; - description = info->desc; - low = info->lo; - high = info->hi; - return; - } - } - assert(false); + assert(rnum < ranges.size()); + return ranges[rnum]->zones.size(); } +int Zones::whichZone(int rnum, double value) const +{ + assert(rnum < ranges.size()); + ZoneRange *range = ranges[rnum]; + for (int j = 0; j < range->zones.size(); ++j) { + ZoneInfo *info = range->zones[j]; + if ((value >= info->lo) && (value < info->hi)) + return j; + } + return -1; +} + +void Zones::zoneInfo(int rnum, int znum, + QString &name, QString &description, + int &low, int &high) const +{ + assert(rnum < ranges.size()); + ZoneRange *range = ranges[rnum]; + assert(znum < range->zones.size()); + ZoneInfo *zone = range->zones[znum]; + name = zone->name; + description = zone->desc; + low = zone->lo; + high = zone->hi; +} + +QString Zones::summarize(int rnum, double *time_in_zone, int num_zones) const +{ + assert(rnum < ranges.size()); + ZoneRange *range = ranges[rnum]; + assert(num_zones == range->zones.size()); + QString summary; + summary += "
"; + summary += ""; + summary += ""; + summary += ""; + summary += ""; + summary += ""; + summary += ""; + summary += ""; + for (int zone = 0; zone < num_zones; ++zone) { + if (time_in_zone[zone] > 0.0) { + QString name, desc; + int lo, hi; + zoneInfo(rnum, zone, name, desc, lo, hi); + 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; +} + + diff --git a/src/gui/Zones.h b/src/gui/Zones.h index ebe34fae5..c08a6ab91 100644 --- a/src/gui/Zones.h +++ b/src/gui/Zones.h @@ -62,10 +62,14 @@ class Zones : public QObject bool read(QFile &file); const QString &errorString() const { return err; } - int whichZone(const QDate &date, double value) const; - void zoneInfo(const QDate &date, int zone, + + int whichRange(const QDate &date) const; + int numZones(int range) const; + int whichZone(int range, double value) const; + void zoneInfo(int range, int zone, QString &name, QString &description, - int &low, int &high); + int &low, int &high) const; + QString summarize(int rnum, double *time_in_zone, int num_zones) const; };