added zones to the weekly summary, consolidated and cleaned up code

This commit is contained in:
Sean C. Rhea
2007-04-25 19:30:55 +00:00
parent cfdbe3cc30
commit e103824538
9 changed files with 319 additions and 115 deletions

View File

@@ -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

View File

@@ -27,6 +27,8 @@
#include "RawFile.h"
#include "RideItem.h"
#include "Settings.h"
#include "Time.h"
#include "Zones.h"
#include <assert.h>
#include <QApplication>
#include <QtGui>
@@ -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(
"<center>"
"<h2>Week of %1 through %2</h2>"
"<h2>Summary</h2>"
@@ -477,15 +509,30 @@ MainWindow::rideSelected()
"<tr><td>Total work (kJ):</td>"
" <td align=\"right\">%6</td></tr>"
"</table>"
"</center>"
// 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 += "<h2>Power Zones</h2>";
if (!zones_ok)
summary += "Error: Week spans more than one zone range.";
else {
summary +=
zones->summarize(zone_range, time_in_zone, num_zones);
}
}
summary += "</center>";
// 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())));
}

View File

@@ -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

View File

@@ -21,41 +21,26 @@
#include "RideItem.h"
#include "RawFile.h"
#include "Settings.h"
#include "Time.h"
#include "Zones.h"
#include <assert.h>
#include <math.h>
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 += "<p><table align=\"center\" width=\"60%\" border=0>";
summary += "<tr><td>Total workout time:</td><td align=\"right\">" +
summary += "<p>";
summary += "<table align=\"center\" width=\"90%\" border=0>";
summary += "<tr><td align=\"center\"><h2>Totals</h2></td>";
summary += "<td align=\"center\"><h2>Averages</h2></td></tr>";
summary += "<tr><td>";
summary += "<table align=\"center\" width=\"70%\" border=0>";
summary += "<tr><td>Workout time:</td><td align=\"right\">" +
time_to_string(raw->points.back()->secs);
summary += "<tr><td>Total time riding:</td><td align=\"right\">" +
summary += "<tr><td>Time riding:</td><td align=\"right\">" +
time_to_string(secs_moving_or_pedaling) + "</td></tr>";
summary += QString("<tr><td>Total distance (miles):</td>"
summary += QString("<tr><td>Distance (miles):</td>"
"<td align=\"right\">%1</td></tr>")
.arg(total_distance, 0, 'f', 1);
summary += QString("<tr><td>Total work (kJ):</td>"
summary += QString("<tr><td>Work (kJ):</td>"
"<td align=\"right\">%1</td></tr>")
.arg((unsigned) round(total_work));
summary += QString("<tr><td>Average speed (mph):</td>"
summary += "</table></td><td>";
summary += "<table align=\"center\" width=\"70%\" border=0>";
summary += QString("<tr><td>Speed (mph):</td>"
"<td align=\"right\">%1</td></tr>")
.arg(((secs_moving == 0.0) ? 0.0
: raw->points.back()->miles / secs_moving * 3600.0),
0, 'f', 1);
summary += QString("<tr><td>Average power (watts):</td>"
summary += QString("<tr><td>Power (watts):</td>"
"<td align=\"right\">%1</td></tr>")
.arg((unsigned) avg_watts);
summary +=QString("<tr><td>Average heart rate (bpm):</td>"
summary +=QString("<tr><td>Heart rate (bpm):</td>"
"<td align=\"right\">%1</td></tr>")
.arg((unsigned) ((secs_hr == 0.0) ? 0.0
: round(total_hr / secs_hr)));
summary += QString("<tr><td>Average cadence (rpm):</td>"
summary += QString("<tr><td>Cadence (rpm):</td>"
"<td align=\"right\">%1</td></tr>")
.arg((unsigned) ((secs_cad == 0.0) ? 0.0
: round(total_cad / secs_cad)));
summary += "</table></td></tr>";
summary += "</table>";
if (zones) {
summary += "<h2>Power Zones</h2>";
summary += zones->summarize(zone_range, time_in_zone, num_zones);
}
if (last_interval > 0) {
summary += "<p><h2>Intervals</h2>\n<p>\n";
summary += "<table align=\"center\" width=\"90%\" ";
@@ -278,4 +316,3 @@ RideItem::htmlSummary()
return summary;
}

View File

@@ -24,6 +24,7 @@
#include <QtGui>
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

48
src/gui/Time.cpp Normal file
View File

@@ -0,0 +1,48 @@
#include "Time.h"
#include <math.h>
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;
}

30
src/gui/Time.h Normal file
View File

@@ -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>
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

View File

@@ -19,7 +19,9 @@
*/
#include "Zones.h"
#include "Time.h"
#include <assert.h>
#include <math.h>
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<ZoneRange*> 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<ZoneRange*> 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 += "<table align=\"center\" width=\"70%\" ";
summary += "border=\"0\">";
summary += "<tr>";
summary += "<td align=\"center\">Zone</td>";
summary += "<td align=\"center\">Description</td>";
summary += "<td align=\"center\">Low</td>";
summary += "<td align=\"center\">High</td>";
summary += "<td align=\"center\">Time</td>";
summary += "</tr>";
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 += "<tr>";
summary += QString("<td align=\"center\">%1</td>").arg(name);
summary += QString("<td align=\"center\">%1</td>").arg(desc);
summary += QString("<td align=\"center\">%1</td>").arg(lo);
if (hi == INT_MAX)
summary += "<td align=\"center\">MAX</td>";
else
summary += QString("<td align=\"center\">%1</td>").arg(hi);
summary += QString("<td align=\"center\">%1</td>")
.arg(time_to_string((unsigned) round(time_in_zone[zone])));
summary += "</tr>";
}
}
summary += "</table>";
return summary;
}

View File

@@ -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;
};