mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
Separated out BikeScore as a RideMetric.
This commit is contained in:
@@ -12,9 +12,11 @@ class WorkoutTime : public RideMetric {
|
||||
QString name() const { return "workout_time"; }
|
||||
QString units(bool) const { return "seconds"; }
|
||||
double value(bool) const { return seconds; }
|
||||
void compute(const RawFile *raw, const Zones *, int) {
|
||||
void compute(const RawFile *raw, const Zones *, int,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
seconds = raw->points.back()->secs;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) { seconds += other->value(true); }
|
||||
RideMetric *clone() const { return new WorkoutTime(*this); }
|
||||
};
|
||||
@@ -38,6 +40,7 @@ class TimeRiding : public PointwiseRideMetric {
|
||||
if ((point->mph > 0.0) || (point->cad > 0.0))
|
||||
secsMovingOrPedaling += secsDelta;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
secsMovingOrPedaling += other->value(true);
|
||||
}
|
||||
@@ -60,9 +63,11 @@ class TotalDistance : public RideMetric {
|
||||
double value(bool metric) const {
|
||||
return metric ? (miles / MILES_PER_KM) : miles;
|
||||
}
|
||||
void compute(const RawFile *raw, const Zones *, int) {
|
||||
void compute(const RawFile *raw, const Zones *, int,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
miles = raw->points.back()->miles;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) { miles += other->value(false); }
|
||||
RideMetric *clone() const { return new TotalDistance(*this); }
|
||||
};
|
||||
@@ -86,6 +91,7 @@ class TotalWork : public PointwiseRideMetric {
|
||||
if (point->watts >= 0.0)
|
||||
joules += point->watts * secsDelta;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
assert(name() == other->name());
|
||||
TotalWork *tw = dynamic_cast<TotalWork*>(other);
|
||||
@@ -113,14 +119,16 @@ class AvgSpeed : public PointwiseRideMetric {
|
||||
double mph = miles / secsMoving * 3600.0;
|
||||
return metric ? (mph / MILES_PER_KM) : mph;
|
||||
}
|
||||
void compute(const RawFile *raw, const Zones *zones, int zoneRange) {
|
||||
PointwiseRideMetric::compute(raw, zones, zoneRange);
|
||||
void compute(const RawFile *raw, const Zones *zones, int zoneRange,
|
||||
const QHash<QString,RideMetric*> &deps) {
|
||||
PointwiseRideMetric::compute(raw, zones, zoneRange, deps);
|
||||
miles = raw->points.back()->miles;
|
||||
}
|
||||
void perPoint(const RawFilePoint *point, double secsDelta,
|
||||
const RawFile *, const Zones *, int) {
|
||||
if (point->mph > 0.0) secsMoving += secsDelta;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
assert(name() == other->name());
|
||||
AvgSpeed *as = dynamic_cast<AvgSpeed*>(other);
|
||||
|
||||
107
src/gui/BikeScore.cpp
Normal file
107
src/gui/BikeScore.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
#include "RideMetric.h"
|
||||
#include "Zones.h"
|
||||
#include <math.h>
|
||||
|
||||
class XPower : public RideMetric {
|
||||
double xpower;
|
||||
double secs;
|
||||
|
||||
friend class RelativeIntensity;
|
||||
|
||||
public:
|
||||
|
||||
XPower() : xpower(0.0), secs(0) {}
|
||||
QString name() const { return "skiba_xpower"; }
|
||||
QString units(bool) const { return "watts"; }
|
||||
double value(bool) const { return xpower; }
|
||||
void compute(const RawFile *raw, const Zones *, int,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
double total = 0.0;
|
||||
int count = 0;
|
||||
// we're interested in a 25 second moving average
|
||||
int movingAvgLen = (int) ceil(25000 / raw->rec_int_ms);
|
||||
double *movingAvgWatts = new double[movingAvgLen];
|
||||
int j = 0; // an index for the moving average function
|
||||
QListIterator<RawFilePoint*> i(raw->points);
|
||||
double secsDelta = raw->rec_int_ms / 1000.0;
|
||||
while (i.hasNext()) {
|
||||
const RawFilePoint *point = i.next();
|
||||
if (point->watts >= 0.0) {
|
||||
secs += secsDelta;
|
||||
// 25 second moving average
|
||||
// populate the array in the first 25 seconds
|
||||
if (secs <= 25)
|
||||
movingAvgWatts[j] = point->watts;
|
||||
else {
|
||||
// add the most recent value, and calculate average
|
||||
movingAvgWatts[j] = point->watts;
|
||||
double avg = 0.0;
|
||||
for (int jj = 0; jj < movingAvgLen; jj++)
|
||||
avg += movingAvgWatts[jj];
|
||||
total += pow(avg / movingAvgLen, 4.0);
|
||||
count++;
|
||||
}
|
||||
j = (j + 1) % movingAvgLen;
|
||||
}
|
||||
}
|
||||
xpower = pow(total / count, 0.25);
|
||||
delete [] movingAvgWatts;
|
||||
}
|
||||
RideMetric *clone() const { return new XPower(*this); }
|
||||
};
|
||||
|
||||
class RelativeIntensity : public RideMetric {
|
||||
double reli;
|
||||
|
||||
public:
|
||||
|
||||
RelativeIntensity() : reli(0.0) {}
|
||||
QString name() const { return "skiba_relative_intensity"; }
|
||||
QString units(bool) const { return ""; }
|
||||
double value(bool) const { return reli; }
|
||||
void compute(const RawFile *, const Zones *zones, int zoneRange,
|
||||
const QHash<QString,RideMetric*> &deps) {
|
||||
if (zones) {
|
||||
assert(deps.contains("skiba_xpower"));
|
||||
RideMetric *xp = deps.value("skiba_xpower");
|
||||
assert(xp);
|
||||
reli = xp->value(true) / zones->getFTP(zoneRange);
|
||||
}
|
||||
}
|
||||
RideMetric *clone() const { return new RelativeIntensity(*this); }
|
||||
};
|
||||
|
||||
class BikeScore : public RideMetric {
|
||||
double score;
|
||||
|
||||
public:
|
||||
|
||||
BikeScore() : score(0.0) {}
|
||||
QString name() const { return "skiba_bike_score"; }
|
||||
QString units(bool) const { return ""; }
|
||||
double value(bool) const { return score; }
|
||||
void compute(const RawFile *raw, const Zones *, int,
|
||||
const QHash<QString,RideMetric*> &deps) {
|
||||
assert(deps.contains("skiba_relative_intensity"));
|
||||
RideMetric *ri = deps.value("skiba_relative_intensity");
|
||||
assert(ri);
|
||||
score = ((raw->points.back()->secs / 60) / 60)
|
||||
* (pow(ri->value(true), 2)) * 100.0;
|
||||
}
|
||||
RideMetric *clone() const { return new BikeScore(*this); }
|
||||
};
|
||||
|
||||
static bool addAllThree() {
|
||||
RideMetricFactory::instance().addMetric(XPower());
|
||||
QVector<QString> deps;
|
||||
deps.append("skiba_xpower");
|
||||
RideMetricFactory::instance().addMetric(RelativeIntensity(), &deps);
|
||||
deps.clear();
|
||||
deps.append("skiba_relative_intensity");
|
||||
RideMetricFactory::instance().addMetric(BikeScore(), &deps);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool allThreeAdded = addAllThree();
|
||||
|
||||
@@ -155,6 +155,14 @@ static const char *metricsXml =
|
||||
" <metric name=\"average_cad\" display_name=\"Cadence\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" </metric_group>\n"
|
||||
" <metric_group name=\"Dr. Skiba's BikeScore\">\n"
|
||||
" <metric name=\"skiba_xpower\" display_name=\"xPower\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" <metric name=\"skiba_relative_intensity\"\n"
|
||||
" display_name=\"Relative Intensity\" precision=\"3\"/>\n"
|
||||
" <metric name=\"skiba_bike_score\" display_name=\"BikeScore\"\n"
|
||||
" precision=\"2\"/>\n"
|
||||
" </metric_group>\n"
|
||||
"</metrics>\n";
|
||||
|
||||
QString
|
||||
@@ -193,13 +201,24 @@ RideItem::htmlSummary()
|
||||
QVariant unit = settings.value(GC_UNIT);
|
||||
|
||||
const RideMetricFactory &factory = RideMetricFactory::instance();
|
||||
RideMetricFactory::RideMetricIter metricIter(factory.metrics());
|
||||
while (metricIter.hasNext()) {
|
||||
metricIter.next();
|
||||
QString name = metricIter.key();
|
||||
RideMetric *metric = factory.newMetric(name);
|
||||
metric->compute(raw, zones, zone_range);
|
||||
metrics.insert(name, metric);
|
||||
QSet<QString> todo;
|
||||
for (int i = 0; i < factory.metricCount(); ++i)
|
||||
todo.insert(factory.metricName(i));
|
||||
|
||||
while (!todo.empty()) {
|
||||
QMutableSetIterator<QString> i(todo);
|
||||
later:
|
||||
while (i.hasNext()) {
|
||||
const QString &name = i.next();
|
||||
const QVector<QString> &deps = factory.dependencies(name);
|
||||
for (int j = 0; j < deps.size(); ++j)
|
||||
if (!metrics.contains(deps[j]))
|
||||
goto later;
|
||||
RideMetric *metric = factory.newMetric(name);
|
||||
metric->compute(raw, zones, zone_range, metrics);
|
||||
metrics.insert(name, metric);
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
double secs_watts = 0.0;
|
||||
@@ -214,22 +233,8 @@ RideItem::htmlSummary()
|
||||
double int_secs_hr = 0.0;
|
||||
double int_max_power = 0.0;
|
||||
|
||||
// for computing "Bike Score"
|
||||
// we're interested in a 25 second moving average
|
||||
double moving_avg_len = 25000 / raw->rec_int_ms;
|
||||
double moving_avg_watts[int(ceil(moving_avg_len))];
|
||||
double fourthPower_watts = 0.0;
|
||||
double fourth_counter = 0.0;
|
||||
double int_fourth_watts = 0.0;
|
||||
double int_fourth_counter = 0.0;
|
||||
double xpower = 0.0;
|
||||
double relative_intensity = 0.0;
|
||||
double bike_score = 0.0;
|
||||
|
||||
double time_start, time_end, mile_start, mile_end, int_dur;
|
||||
|
||||
int j = 0; // an index for the moving average function
|
||||
|
||||
QListIterator<RawFilePoint*> i(raw->points);
|
||||
while (i.hasNext()) {
|
||||
RawFilePoint *point = i.next();
|
||||
@@ -257,25 +262,6 @@ RideItem::htmlSummary()
|
||||
}
|
||||
if (point->watts >= 0.0) {
|
||||
secs_watts += secs_delta;
|
||||
|
||||
// 25 second moving average
|
||||
// populate the array in the first 25 seconds
|
||||
if (secs_watts <= 25)
|
||||
moving_avg_watts[j] = point->watts;
|
||||
else {
|
||||
// add the most recent value, and calculate average
|
||||
moving_avg_watts[j] = point->watts;
|
||||
double mov_avg = 0.0;
|
||||
for(int jj = 0;jj < moving_avg_len; jj++)
|
||||
mov_avg += moving_avg_watts[jj];
|
||||
double current_fourth = pow(mov_avg / moving_avg_len, 4.0);
|
||||
fourthPower_watts += current_fourth;
|
||||
fourth_counter++;
|
||||
int_fourth_watts += current_fourth;
|
||||
int_fourth_counter++;
|
||||
}
|
||||
j = (j + 1) % ((int)(moving_avg_len));
|
||||
|
||||
int_watts_sum += point->watts * secs_delta;
|
||||
if (point->watts > int_max_power)
|
||||
int_max_power = point->watts;
|
||||
@@ -302,13 +288,6 @@ RideItem::htmlSummary()
|
||||
int_hr_sum, int_cad_sum, int_mph_sum,
|
||||
int_secs_hr, int_max_power, int_dur);
|
||||
|
||||
if (zones) {
|
||||
xpower = pow(int_fourth_watts/int_fourth_counter,0.25);
|
||||
relative_intensity = xpower / ((float) zones->getFTP(zone_range));
|
||||
bike_score = ((raw->points.back()->secs / 60) / 60)
|
||||
* (pow(relative_intensity, 2)) * 100.0;
|
||||
}
|
||||
|
||||
summary += "<p>";
|
||||
|
||||
bool metricUnits = (unit.toString() == "Metric");
|
||||
@@ -353,9 +332,10 @@ RideItem::htmlSummary()
|
||||
summary += s;
|
||||
}
|
||||
else {
|
||||
QString s("<tr><td>%1 (%2):</td><td "
|
||||
"align=\"right\">%3</td></tr>");
|
||||
s = s.arg(displayName).arg(m->units(metricUnits));
|
||||
QString s = "<tr><td>" + displayName;
|
||||
if (m->units(metricUnits) != "")
|
||||
s += " (" + m->units(metricUnits) + ")";
|
||||
s += ":</td><td align=\"right\">%1</td></tr>";
|
||||
if (precision == 0)
|
||||
s = s.arg((unsigned) round(m->value(metricUnits)));
|
||||
else
|
||||
@@ -368,22 +348,6 @@ RideItem::htmlSummary()
|
||||
summary += "</tr></table>";
|
||||
}
|
||||
|
||||
if (xpower != 0) {
|
||||
summary += "<h2>Dr. Skiba's BikeScore</h2>";
|
||||
summary += "<table align=\"center\" width=\"40%\" ";
|
||||
summary += "cellspacing=0 border=0>";
|
||||
summary += QString("<tr><td>xPower:</td>"
|
||||
"<td align=\"left\">%1</td></tr>")
|
||||
.arg((unsigned) xpower);
|
||||
summary += QString("<tr><td>Relative Intensity:</td>"
|
||||
"<td align=\"left\">%1</td></tr>")
|
||||
.arg(relative_intensity, 0, 'f', 3);
|
||||
summary += QString("<tr><td>Bike Score: </td>"
|
||||
"<td align=\"left\">%1</td></tr>")
|
||||
.arg(bike_score, 0, 'f', 2);
|
||||
summary += "</table>";
|
||||
}
|
||||
|
||||
if (zones) {
|
||||
summary += "<h2>Power Zones</h2>";
|
||||
summary += zones->summarize(zone_range, time_in_zone, num_zones);
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
#include "RideMetric.h"
|
||||
|
||||
RideMetricFactory *RideMetricFactory::_instance;
|
||||
QVector<QString> RideMetricFactory::noDeps;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <assert.h>
|
||||
|
||||
#include "RawFile.h"
|
||||
@@ -16,13 +17,20 @@ struct RideMetric {
|
||||
virtual QString units(bool metric) const = 0;
|
||||
virtual double value(bool metric) const = 0;
|
||||
virtual void compute(const RawFile *raw,
|
||||
const Zones *zones, int zoneRange) = 0;
|
||||
virtual void aggregateWith(RideMetric *other) = 0;
|
||||
const Zones *zones,
|
||||
int zoneRange,
|
||||
const QHash<QString,RideMetric*> &deps) = 0;
|
||||
virtual bool canAggregate() const { return false; }
|
||||
virtual void aggregateWith(RideMetric *other) {
|
||||
(void) other;
|
||||
assert(false);
|
||||
}
|
||||
virtual RideMetric *clone() const = 0;
|
||||
};
|
||||
|
||||
struct PointwiseRideMetric : public RideMetric {
|
||||
void compute(const RawFile *raw, const Zones *zones, int zoneRange) {
|
||||
void compute(const RawFile *raw, const Zones *zones, int zoneRange,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
QListIterator<RawFilePoint*> i(raw->points);
|
||||
double secsDelta = raw->rec_int_ms / 1000.0;
|
||||
while (i.hasNext()) {
|
||||
@@ -59,15 +67,12 @@ class AvgRideMetric : public PointwiseRideMetric {
|
||||
|
||||
class RideMetricFactory {
|
||||
|
||||
public:
|
||||
|
||||
typedef QHash<QString,RideMetric*> RideMetricMap;
|
||||
typedef QHashIterator<QString,RideMetric*> RideMetricIter;
|
||||
|
||||
private:
|
||||
|
||||
static RideMetricFactory *_instance;
|
||||
RideMetricMap _metrics;
|
||||
static QVector<QString> noDeps;
|
||||
|
||||
QVector<QString> metricNames;
|
||||
QHash<QString,RideMetric*> metrics;
|
||||
QHash<QString,QVector<QString>*> dependencyMap;
|
||||
|
||||
RideMetricFactory() {}
|
||||
RideMetricFactory(const RideMetricFactory &other);
|
||||
@@ -81,20 +86,36 @@ class RideMetricFactory {
|
||||
return *_instance;
|
||||
}
|
||||
|
||||
const RideMetricMap &metrics() const { return _metrics; }
|
||||
int metricCount() const { return metricNames.size(); }
|
||||
|
||||
const QString &metricName(int i) const { return metricNames[i]; }
|
||||
|
||||
RideMetric *newMetric(const QString &name) const {
|
||||
if (!_metrics.contains(name))
|
||||
return NULL;
|
||||
RideMetric *metric = _metrics.value(name);
|
||||
return metric->clone();
|
||||
assert(metrics.contains(name));
|
||||
return metrics.value(name)->clone();
|
||||
}
|
||||
|
||||
bool addMetric(const RideMetric &metric) {
|
||||
assert(!_metrics.contains(metric.name()));
|
||||
_metrics.insert(metric.name(), metric.clone());
|
||||
bool addMetric(const RideMetric &metric,
|
||||
const QVector<QString> *deps = NULL) {
|
||||
assert(!metrics.contains(metric.name()));
|
||||
metrics.insert(metric.name(), metric.clone());
|
||||
metricNames.append(metric.name());
|
||||
if (deps) {
|
||||
QVector<QString> *copy = new QVector<QString>;
|
||||
for (int i = 0; i < deps->size(); ++i) {
|
||||
assert(metrics.contains((*deps)[i]));
|
||||
copy->append((*deps)[i]);
|
||||
}
|
||||
dependencyMap.insert(metric.name(), copy);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const QVector<QString> &dependencies(const QString &name) const {
|
||||
assert(metrics.contains(name));
|
||||
QVector<QString> *result = dependencyMap.value(name);
|
||||
return result ? *result : noDeps;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // _GC_RideMetric_h
|
||||
|
||||
@@ -44,6 +44,7 @@ HEADERS += \
|
||||
SOURCES += \
|
||||
AllPlot.cpp \
|
||||
BasicRideMetrics.cpp \
|
||||
BikeScore.cpp \
|
||||
ChooseCyclistDialog.cpp \
|
||||
CpintPlot.cpp \
|
||||
CsvRideFile.cpp \
|
||||
|
||||
Reference in New Issue
Block a user