Long Term Metrics

A user configurable chart for showing ride metrics and
other calculated values over time.

* Uses SQLITE database to store metrics
* Supports any metric available from the metric factory
* Adds new MaxHr, VI, Peak Power and Time In Zone metric
* Also includes LTS/STS/SB for PM charting
* Aggregates in days, weeks, months or years
* Reads and Updates seasons.xml
* Adds cycles and adhoc date ranges in seasons.xml
* Date ranges can be selected on the plot with shift-left click
* Allows users to customise preferences for color, symbols et al
* Allows user to customise metric names and unit names
* Supports smooth curves and topN highlighting
* Has a linear regress trend line function
* Allows users to save charts to charts.xml
* A default charts.xml is built-in
* A chart manager to import/export/rename/delete charts etc
* Provides a tooltip to provide basic datapoint information
* Performance Manager adjusted to use the MetricDB
* User configurable setting for SB calculation (today/tomorrow)
This commit is contained in:
Mark Liversedge
2010-02-25 13:14:52 +00:00
committed by Sean Rhea
parent cef5cca454
commit 7bb9cf5462
46 changed files with 5334 additions and 523 deletions

View File

@@ -44,8 +44,10 @@ class AerobicDecoupling : public RideMetric {
AerobicDecoupling() : percent(0.0) {}
QString symbol() const { return "aerobic_decoupling"; }
QString name() const { return QObject::tr("Aerobic Decoupling"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return "%"; }
int precision() const { return 2; }
double conversion() const { return 1.0; }
double value(bool) const { return percent; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {

View File

@@ -30,9 +30,11 @@ class WorkoutTime : public RideMetric {
WorkoutTime() : seconds(0.0) {}
QString symbol() const { return "workout_time"; }
QString name() const { return tr("Duration"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool) const { return "seconds"; }
int precision() const { return 0; }
double value(bool) const { return seconds; }
double conversion() const { return 1.0; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
seconds = ride->dataPoints().back()->secs -
@@ -56,9 +58,11 @@ class TimeRiding : public RideMetric {
TimeRiding() : secsMovingOrPedaling(0.0) {}
QString symbol() const { return "time_riding"; }
QString name() const { return tr("Time Riding"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool) const { return "seconds"; }
int precision() const { return 0; }
double value(bool) const { return secsMovingOrPedaling; }
double conversion() const { return 1.0; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
foreach (const RideFilePoint *point, ride->dataPoints()) {
@@ -90,8 +94,10 @@ class TotalDistance : public RideMetric {
TotalDistance() : km(0.0) {}
QString symbol() const { return "total_distance"; }
QString name() const { return tr("Distance"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool metric) const { return metric ? "km" : "miles"; }
int precision() const { return 1; }
double conversion() const { return MILES_PER_KM; }
double value(bool metric) const {
return metric ? km : (km * MILES_PER_KM);
}
@@ -123,8 +129,10 @@ class ElevationGain : public RideMetric {
ElevationGain() : elegain(0.0), prevalt(0.0) {}
QString symbol() const { return "elevation_gain"; }
QString name() const { return tr("Elevation Gain"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool metric) const { return metric ? "meters" : "feet"; }
int precision() const { return 0; }
double conversion() const { return FEET_PER_METER; }
double value(bool metric) const {
return metric ? elegain : (elegain * FEET_PER_METER);
}
@@ -166,8 +174,10 @@ class TotalWork : public RideMetric {
TotalWork() : joules(0.0) {}
QString symbol() const { return "total_work"; }
QString name() const { return tr("Work"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool) const { return "kJ"; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
double value(bool) const { return joules / 1000.0; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
@@ -199,8 +209,10 @@ class AvgSpeed : public RideMetric {
AvgSpeed() : secsMoving(0.0), km(0.0) {}
QString symbol() const { return "average_speed"; }
QString name() const { return tr("Average Speed"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool metric) const { return metric ? "kph" : "mph"; }
int precision() const { return 1; }
double conversion() const { return MILES_PER_KM; }
double value(bool metric) const {
if (secsMoving == 0.0) return 0.0;
double kph = km / secsMoving * 3600.0;
@@ -233,7 +245,9 @@ struct AvgPower : public AvgRideMetric {
QString symbol() const { return "average_power"; }
QString name() const { return tr("Average Power"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return "watts"; }
double conversion() const { return 1.0; }
int precision() const { return 0; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
@@ -256,7 +270,9 @@ struct AvgHeartRate : public AvgRideMetric {
QString symbol() const { return "average_hr"; }
QString name() const { return tr("Average Heart Rate"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return "bpm"; }
double conversion() const { return 1.0; }
int precision() const { return 0; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
@@ -279,8 +295,10 @@ struct AvgCadence : public AvgRideMetric {
QString symbol() const { return "average_cad"; }
QString name() const { return tr("Average Cadence"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return "rpm"; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
foreach (const RideFilePoint *point, ride->dataPoints()) {
@@ -304,8 +322,10 @@ class MaxPower : public RideMetric {
MaxPower() : max(0.0) {}
QString symbol() const { return "max_power"; }
QString name() const { return tr("Max Power"); }
MetricType type() const { return RideMetric::Peak; }
QString units(bool) const { return "watts"; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
double value(bool) const { return max; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
@@ -322,13 +342,41 @@ static bool maxPowerAdded =
//////////////////////////////////////////////////////////////////////////////
class MaxHr : public RideMetric {
double max;
public:
MaxHr() : max(0.0) {}
QString symbol() const { return "max_heartrate"; }
QString name() const { return tr("Max Heartrate"); }
MetricType type() const { return RideMetric::Peak; }
QString units(bool) const { return "bpm"; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
double value(bool) const { return max; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
foreach (const RideFilePoint *point, ride->dataPoints()) {
if (point->hr >= max)
max = point->hr;
}
}
RideMetric *clone() const { return new MaxHr(*this); }
};
static bool maxHrAdded =
RideMetricFactory::instance().addMetric(MaxHr());
//////////////////////////////////////////////////////////////////////////////
class NinetyFivePercentHeartRate : public RideMetric {
double hr;
public:
NinetyFivePercentHeartRate() : hr(0.0) {}
QString symbol() const { return "ninety_five_percent_hr"; }
QString name() const { return tr("95% Heart Rate"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return "bpm"; }
double conversion() const { return 1.0; }
int precision() const { return 0; }
double value(bool) const { return hr; }
void compute(const RideFile *ride, const Zones *, int,

View File

@@ -38,6 +38,7 @@ class XPower : public RideMetric {
double secs;
friend class RelativeIntensity;
friend class VariabilityIndex;
friend class BikeScore;
public:
@@ -45,8 +46,10 @@ class XPower : public RideMetric {
XPower() : xpower(0.0), secs(0.0) {}
QString symbol() const { return "skiba_xpower"; }
QString name() const { return tr("xPower"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return "watts"; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
double value(bool) const { return xpower; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
@@ -97,6 +100,46 @@ class XPower : public RideMetric {
RideMetric *clone() const { return new XPower(*this); }
};
class VariabilityIndex : public RideMetric {
double vi;
double secs;
public:
VariabilityIndex() : vi(0.0), secs(0.0) {}
QString symbol() const { return "skiba_variability_index"; }
QString name() const { return tr("Skiba VI"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return ""; }
double conversion() const { return 1.0; }
int precision() const { return 3; }
double value(bool) const { return vi; }
void compute(const RideFile *, const Zones *, int,
const QHash<QString,RideMetric*> &deps) {
assert(deps.contains("skiba_xpower"));
assert(deps.contains("average_power"));
XPower *xp = dynamic_cast<XPower*>(deps.value("skiba_xpower"));
assert(xp);
RideMetric *ap = dynamic_cast<RideMetric*>(deps.value("average_power"));
assert(ap);
vi = xp->value(true) / ap->value(true);
secs = xp->secs;
}
// added djconnel: allow RI to be combined across rides
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) {
assert(symbol() == other.symbol());
const VariabilityIndex &ovi = dynamic_cast<const VariabilityIndex&>(other);
vi = secs * pow(vi, bikeScoreN) + ovi.secs * pow(ovi.vi, bikeScoreN);
secs += ovi.secs;
vi = pow(vi / secs, 1.0 / bikeScoreN);
}
// end added djconnel
RideMetric *clone() const { return new VariabilityIndex(*this); }
};
class RelativeIntensity : public RideMetric {
double reli;
double secs;
@@ -106,7 +149,9 @@ class RelativeIntensity : public RideMetric {
RelativeIntensity() : reli(0.0), secs(0.0) {}
QString symbol() const { return "skiba_relative_intensity"; }
QString name() const { return tr("Relative Intensity"); }
MetricType type() const { return RideMetric::Average; }
QString units(bool) const { return ""; }
double conversion() const { return 1.0; }
int precision() const { return 3; }
double value(bool) const { return reli; }
void compute(const RideFile *, const Zones *zones, int zoneRange,
@@ -142,8 +187,10 @@ class BikeScore : public RideMetric {
BikeScore() : score(0.0) {}
QString symbol() const { return "skiba_bike_score"; }
QString name() const { return tr("BikeScore&#8482;"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool) const { return ""; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
double value(bool) const { return score; }
void compute(const RideFile *, const Zones *zones, int zoneRange,
const QHash<QString,RideMetric*> &deps) {
@@ -168,15 +215,19 @@ class BikeScore : public RideMetric {
void aggregateWith(const RideMetric &other) { score += other.value(true); }
};
static bool addAllThree() {
static bool addAllFour() {
RideMetricFactory::instance().addMetric(XPower());
QVector<QString> deps;
deps.append("skiba_xpower");
RideMetricFactory::instance().addMetric(RelativeIntensity(), &deps);
deps.append("skiba_relative_intensity");
RideMetricFactory::instance().addMetric(BikeScore(), &deps);
deps.clear();
deps.append("skiba_xpower");
deps.append("average_power");
RideMetricFactory::instance().addMetric(VariabilityIndex(), &deps);
return true;
}
static bool allThreeAdded = addAllThree();
static bool allFourAdded = addAllFour();

View File

@@ -166,6 +166,7 @@ void ConfigDialog::save_Clicked()
settings->setValue(GC_INITIAL_LTS, cyclistPage->perfManStart->text());
settings->setValue(GC_STS_DAYS, cyclistPage->perfManSTSavg->text());
settings->setValue(GC_LTS_DAYS, cyclistPage->perfManLTSavg->text());
settings->setValue(GC_SB_TODAY, (int) cyclistPage->showSBToday->isChecked());
// set default stress names if not set:
settings->setValue(GC_STS_NAME, settings->value(GC_STS_NAME,tr("Short Term Stress")));

View File

@@ -30,6 +30,11 @@
#include <QtXml/QtXml>
#include "SummaryMetrics.h"
// DB Schema Version - YOU MUST UPDATE THIS IF THE SCHEMA VERSION CHANGES!!!
static int DBSchemaVersion = 10;
// each DB connection gets a unique session id based upon this number:
int DBAccess::session=0;
DBAccess::DBAccess(QDir home)
{
@@ -38,137 +43,186 @@ DBAccess::DBAccess(QDir home)
void DBAccess::closeConnection()
{
db.close();
dbconn.close();
}
QSqlDatabase DBAccess::initDatabase(QDir home)
void
DBAccess::initDatabase(QDir home)
{
if(db.isOpen())
return db;
db = QSqlDatabase::addDatabase("QSQLITE");
if(dbconn.isOpen()) return;
sessionid = QString("session%1").arg(session++);
db = QSqlDatabase::addDatabase("QSQLITE", sessionid);
db.setDatabaseName(home.absolutePath() + "/metricDB");
if (!db.open()) {
dbconn = db.database(sessionid);
if (!dbconn.isOpen()) {
QMessageBox::critical(0, qApp->translate("DBAccess","Cannot open database"),
qApp->translate("DBAccess","Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"This feature requires SQLite support. Please read "
"the Qt SQL driver documentation for information how "
"to build it.\n\n"
"Click Cancel to exit."), QMessageBox::Cancel);
return db;
} else {
// create database - does nothing if its already there
createDatabase();
}
return db;
}
bool DBAccess::createMetricsTable()
{
QSqlQuery query;
bool rc = query.exec("create table metrics (id integer primary key autoincrement, "
"filename varchar,"
"ride_date date,"
"ride_time double, "
"average_cad double,"
"workout_time double, "
"total_distance double,"
"x_power double,"
"average_speed double,"
"total_work double,"
"average_power double,"
"average_hr double,"
"relative_intensity double,"
"bike_score double)");
if(!rc)
qDebug() << query.lastError();
QSqlQuery query(dbconn);
bool rc;
bool createTables = true;
// does the table exist?
rc = query.exec("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
if (rc) {
while (query.next()) {
QString table = query.value(0).toString();
if (table == "metrics") {
createTables = false;
break;
}
}
}
// we need to create it!
if (rc && createTables) {
QString createMetricTable = "create table metrics (filename varchar primary key,"
"timestamp integer,"
"ride_date date";
// Add columns for all the metrics
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
createMetricTable += QString(", X%1 double").arg(factory.metricName(i));
createMetricTable += " )";
rc = query.exec(createMetricTable);
if (!rc) {
qDebug()<<"create table failed!" << query.lastError();
}
}
return rc;
}
bool DBAccess::dropMetricTable()
{
QSqlQuery query("DROP TABLE metrics", dbconn);
return query.exec();
}
bool DBAccess::createDatabase()
{
// check schema version and if missing recreate database
checkDBVersion();
bool rc = false;
rc = createMetricsTable();
if(!rc)
return rc;
rc = createIndex();
if(!rc)
return rc;
// at present only one table!
bool rc = createMetricsTable();
if(!rc) return rc;
// other tables here
return true;
}
bool DBAccess::createIndex()
void DBAccess::checkDBVersion()
{
QSqlQuery query;
query.prepare("create INDEX IDX_FILENAME on metrics(filename)");
bool rc = query.exec();
if(!rc)
qDebug() << query.lastError();
return rc;
int currentversion = 0;
// can we get a version number?
QSqlQuery query("SELECT schema_version from version;", dbconn);
bool rc = query.exec();
while(rc && query.next()) currentversion = query.value(0).toInt();
// if its not up-to-date
if (!rc || currentversion != DBSchemaVersion) {
// drop tables
QSqlQuery dropV("DROP TABLE version", dbconn);
dropV.exec();
QSqlQuery dropM("DROP TABLE metrics", dbconn);
dropM.exec();
// recreate version table and add one entry
QSqlQuery version("CREATE TABLE version ( schema_version integer primary key);", dbconn);
version.exec();
// insert current version number
QSqlQuery insert("INSERT INTO version ( schema_version ) values (?)", dbconn);
insert.addBindValue(DBSchemaVersion);
insert.exec();
}
}
bool DBAccess::importRide(SummaryMetrics *summaryMetrics )
/*----------------------------------------------------------------------
* CRUD routines for Metrics table
*----------------------------------------------------------------------*/
bool DBAccess::importRide(SummaryMetrics *summaryMetrics, bool modify)
{
QSqlQuery query;
query.prepare("insert into metrics (filename, ride_date, ride_time, average_cad, workout_time, total_distance,"
"x_power, average_speed, total_work, average_power, average_hr,"
"relative_intensity, bike_score) values (?,?,?,?,?,?,?,?,?,?,?,?,?)");
QSqlQuery query(dbconn);
QDateTime timestamp = QDateTime::currentDateTime();
if (modify) {
// zap the current row
query.prepare("DELETE FROM metrics WHERE filename = ?;");
query.addBindValue(summaryMetrics->getFileName());
query.exec();
}
// construct an insert statement
QString insertStatement = "insert into metrics ( filename, timestamp, ride_date";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
insertStatement += QString(", X%1 ").arg(factory.metricName(i));
insertStatement += " ) values (?,?,?"; // filename, timestamp, ride_date
for (int i=0; i<factory.metricCount(); i++)
insertStatement += ",?";
insertStatement += ")";
query.prepare(insertStatement);
// filename, timestamp, ride date
query.addBindValue(summaryMetrics->getFileName());
query.addBindValue(timestamp.toTime_t());
query.addBindValue(summaryMetrics->getRideDate());
query.addBindValue(summaryMetrics->getRideTime());
query.addBindValue(summaryMetrics->getCadence());
query.addBindValue(summaryMetrics->getWorkoutTime());
query.addBindValue(summaryMetrics->getDistance());
query.addBindValue(summaryMetrics->getXPower());
query.addBindValue(summaryMetrics->getSpeed());
query.addBindValue(summaryMetrics->getTotalWork());
query.addBindValue(summaryMetrics->getWatts());
query.addBindValue(summaryMetrics->getHeartRate());
query.addBindValue(summaryMetrics->getRelativeIntensity());
query.addBindValue(summaryMetrics->getBikeScore());
bool rc = query.exec();
// values
for (int i=0; i<factory.metricCount(); i++) {
query.addBindValue(summaryMetrics->getForSymbol(factory.metricName(i)));
}
// go do it!
bool rc = query.exec();
if(!rc)
{
qDebug() << query.lastError();
}
return rc;
}
QStringList DBAccess::getAllFileNames()
bool
DBAccess::deleteRide(QString name)
{
QSqlQuery query("SELECT filename from metrics");
QStringList fileList;
while(query.next())
{
QString filename = query.value(0).toString();
fileList << filename;
}
return fileList;
QSqlQuery query(dbconn);
query.prepare("DELETE FROM metrics WHERE filename = ?;");
query.addBindValue(name);
return query.exec();
}
QList<QDateTime> DBAccess::getAllDates()
{
QSqlQuery query("SELECT ride_date from metrics");
QSqlQuery query("SELECT ride_date from metrics ORDER BY ride_date;", dbconn);
QList<QDateTime> dates;
query.exec();
while(query.next())
{
QDateTime date = query.value(0).toDateTime();
@@ -180,47 +234,38 @@ QList<QDateTime> DBAccess::getAllDates()
QList<SummaryMetrics> DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
{
QList<SummaryMetrics> metrics;
QSqlQuery query("SELECT filename, ride_date, ride_time, average_cad, workout_time, total_distance,"
"x_power, average_speed, total_work, average_power, average_hr,"
"relative_intensity, bike_scoreFROM metrics WHERE ride_date >=:start AND ride_date <=:end");
query.bindValue(":start", start);
query.bindValue(":end", end);
// null date range fetches all, but not currently used by application code
// since it relies too heavily on the results of the QDateTime constructor
if (start == QDateTime()) start = QDateTime::currentDateTime().addYears(-10);
if (end == QDateTime()) end = QDateTime::currentDateTime().addYears(+10);
// construct the select statement
QString selectStatement = "SELECT filename, ride_date";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
selectStatement += " FROM metrics where DATE(ride_date) >=DATE(:start) AND DATE(ride_date) <=DATE(:end) "
" ORDER BY ride_date;";
// execute the select statement
QSqlQuery query(selectStatement, dbconn);
query.bindValue(":start", start.date());
query.bindValue(":end", end.date());
query.exec();
while(query.next())
{
SummaryMetrics summaryMetrics;
// filename and date
summaryMetrics.setFileName(query.value(0).toString());
summaryMetrics.setRideDate(query.value(1).toDateTime());
summaryMetrics.setRideTime(query.value(2).toDouble());
summaryMetrics.setCadence(query.value(3).toDouble());
summaryMetrics.setWorkoutTime(query.value(4).toDouble());
summaryMetrics.setDistance(query.value(5).toDouble());
summaryMetrics.setXPower(query.value(6).toDouble());
summaryMetrics.setSpeed(query.value(7).toDouble());
summaryMetrics.setTotalWork(query.value(8).toDouble());
summaryMetrics.setWatts(query.value(9).toDouble());
summaryMetrics.setHeartRate(query.value(10).toDouble());
summaryMetrics.setRelativeIntensity(query.value(11).toDouble());
summaryMetrics.setBikeScore(query.value(12).toDouble());
// the values
for (int i=0; i<factory.metricCount(); i++)
summaryMetrics.setForSymbol(factory.metricName(i), query.value(i+2).toDouble());
metrics << summaryMetrics;
}
return metrics;
}
bool DBAccess::dropMetricTable()
{
QStringList tableList = db.tables(QSql::Tables);
if(!tableList.contains("metrics"))
return true;
QSqlQuery query("DROP TABLE metrics");
return query.exec();
}

View File

@@ -34,23 +34,36 @@ class DBAccess
{
public:
// get connection name
QSqlDatabase connection() { return dbconn; }
// create and drop connections
DBAccess(QDir home);
typedef QHash<QString,RideMetric*> MetricMap;
void importAllRides(QDir path, Zones *zones);
bool importRide(SummaryMetrics *summaryMetrics);
bool createDatabase();
QStringList getAllFileNames();
void closeConnection();
// Create/Delete Records
bool importRide(SummaryMetrics *summaryMetrics, bool);
bool deleteRide(QString);
// Query Records
QList<QDateTime> getAllDates();
QList<SummaryMetrics> getAllMetricsFor(QDateTime start, QDateTime end);
bool createMetricsTable();
QList<Season> getAllSeasons();
bool dropMetricTable();
private:
QSqlDatabase db;
QSqlDatabase db, dbconn;
typedef QHash<QString,RideMetric*> MetricMap;
bool createDatabase();
bool createMetricsTable();
bool dropMetricTable();
bool createIndex();
QSqlDatabase initDatabase(QDir home);
void checkDBVersion();
void initDatabase(QDir home);
QString sessionid;
static int session;
};
#endif
#endif

View File

@@ -43,8 +43,10 @@ class DanielsPoints : public RideMetric {
DanielsPoints() : score(0.0) {}
QString symbol() const { return "daniels_points"; }
QString name() const { return QObject::tr("Daniels Points"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool) const { return ""; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
double value(bool) const { return score; }
void compute(const RideFile *ride, const Zones *zones,
int zoneRange, const QHash<QString,RideMetric*> &) {
@@ -119,7 +121,9 @@ class DanielsEquivalentPower : public RideMetric {
QString name() const { return QObject::tr("Daniels EqP"); }
QString units(bool) const { return "watts"; }
int precision() const { return 0; }
double conversion() const { return 1.0; }
double value(bool) const { return watts; }
MetricType type() const { return RideMetric::Average; }
void compute(const RideFile *, const Zones *zones, int zoneRange,
const QHash<QString,RideMetric*> &deps) {
if (!zones || zoneRange < 0)

111
src/LTMCanvasPicker.cpp Normal file
View File

@@ -0,0 +1,111 @@
// code borrowed from event_filter qwt examples
// and modified for GC LTM purposes
#include <qapplication.h>
#include <qevent.h>
#include <qwhatsthis.h>
#include <qpainter.h>
#include <qwt_plot.h>
#include <qwt_symbol.h>
#include <qwt_scale_map.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot_curve.h>
#include "LTMCanvasPicker.h"
#include <QDebug>
LTMCanvasPicker::LTMCanvasPicker(QwtPlot *plot):
QObject(plot),
d_selectedCurve(NULL),
d_selectedPoint(-1)
{
QwtPlotCanvas *canvas = plot->canvas();
canvas->installEventFilter(this);
// We want the focus, but no focus rect. The
canvas->setFocusPolicy(Qt::StrongFocus);
canvas->setFocusIndicator(QwtPlotCanvas::ItemFocusIndicator);
canvas->setFocus();
}
bool LTMCanvasPicker::event(QEvent *e)
{
if ( e->type() == QEvent::User )
{
//showCursor(true);
return true;
}
return QObject::event(e);
}
bool LTMCanvasPicker::eventFilter(QObject *object, QEvent *e)
{
if ( object != (QObject *)plot()->canvas() )
return false;
switch(e->type())
{
default:
QApplication::postEvent(this, new QEvent(QEvent::User));
break;
case QEvent::MouseButtonPress:
select(((QMouseEvent *)e)->pos(), true);
break;
case QEvent::MouseMove:
select(((QMouseEvent *)e)->pos(), false);
break;
}
return QObject::eventFilter(object, e);
}
// Select the point at a position. If there is no point
// deselect the selected point
void LTMCanvasPicker::select(const QPoint &pos, bool clicked)
{
QwtPlotCurve *curve = NULL;
double dist = 10e10;
int index = -1;
const QwtPlotItemList& itmList = plot()->itemList();
for ( QwtPlotItemIterator it = itmList.begin();
it != itmList.end(); ++it )
{
if ( (*it)->rtti() == QwtPlotItem::Rtti_PlotCurve )
{
QwtPlotCurve *c = (QwtPlotCurve*)(*it);
double d;
int idx = c->closestPoint(pos, &d);
if ( d < dist )
{
curve = c;
index = idx;
dist = d;
}
}
}
d_selectedCurve = NULL;
d_selectedPoint = -1;
if ( curve && dist < 10 ) // 10 pixels tolerance
{
// picked one
d_selectedCurve = curve;
d_selectedPoint = index;
if (clicked)
pointClicked(curve, index); // emit
else
pointHover(curve, index); // emit
} else {
// didn't
if (clicked)
pointClicked(NULL, -1); // emit
else
pointHover(NULL, -1); // emit
}
}

34
src/LTMCanvasPicker.h Normal file
View File

@@ -0,0 +1,34 @@
// code stolen from the event_filter qwt example
// and modified for GC LTM
#ifndef GC_LTMCanvasPicker_H
#define GC_LTMCanvasPicker_H 1
#include <qobject.h>
class QPoint;
class QCustomEvent;
class QwtPlot;
class QwtPlotCurve;
class LTMCanvasPicker: public QObject
{
Q_OBJECT
public:
LTMCanvasPicker(QwtPlot *plot);
virtual bool eventFilter(QObject *, QEvent *);
virtual bool event(QEvent *);
signals:
void pointClicked(QwtPlotCurve *, int);
void pointHover(QwtPlotCurve *, int);
private:
void select(const QPoint &, bool);
QwtPlot *plot() { return (QwtPlot *)parent(); }
const QwtPlot *plot() const { return (QwtPlot *)parent(); }
QwtPlotCurve *d_selectedCurve;
int d_selectedPoint;
};
#endif

293
src/LTMChartParser.cpp Normal file
View File

@@ -0,0 +1,293 @@
/*
* 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
*/
#include "LTMChartParser.h"
#include "LTMSettings.h"
#include "LTMTool.h"
#include <QDate>
#include <QDebug>
#include <assert.h>
// local helper functions to convert Qwt enums to ints and back
static int curveToInt(QwtPlotCurve::CurveStyle x)
{
switch (x) {
case QwtPlotCurve::NoCurve : return 0;
case QwtPlotCurve::Lines : return 1;
case QwtPlotCurve::Sticks : return 2;
case QwtPlotCurve::Steps : return 3;
case QwtPlotCurve::Dots : return 4;
default : return 100;
}
}
static QwtPlotCurve::CurveStyle intToCurve(int x)
{
switch (x) {
default:
case 0 : return QwtPlotCurve::NoCurve;
case 1 : return QwtPlotCurve::Lines;
case 2 : return QwtPlotCurve::Sticks;
case 3 : return QwtPlotCurve::Steps;
case 4 : return QwtPlotCurve::Dots;
case 100: return QwtPlotCurve::UserCurve;
}
}
static int symbolToInt(QwtSymbol::Style x)
{
switch (x) {
default:
case QwtSymbol::NoSymbol: return -1;
case QwtSymbol::Ellipse: return 0;
case QwtSymbol::Rect: return 1;
case QwtSymbol::Diamond: return 2;
case QwtSymbol::Triangle: return 3;
case QwtSymbol::DTriangle: return 4;
case QwtSymbol::UTriangle: return 5;
case QwtSymbol::LTriangle: return 6;
case QwtSymbol::RTriangle: return 7;
case QwtSymbol::Cross: return 8;
case QwtSymbol::XCross: return 9;
case QwtSymbol::HLine: return 10;
case QwtSymbol::VLine: return 11;
case QwtSymbol::Star1: return 12;
case QwtSymbol::Star2: return 13;
case QwtSymbol::Hexagon: return 14;
case QwtSymbol::StyleCnt: return 15;
}
}
static QwtSymbol::Style intToSymbol(int x)
{
switch (x) {
default:
case -1: return QwtSymbol::NoSymbol;
case 0 : return QwtSymbol::Ellipse;
case 1 : return QwtSymbol::Rect;
case 2 : return QwtSymbol::Diamond;
case 3 : return QwtSymbol::Triangle;
case 4 : return QwtSymbol::DTriangle;
case 5 : return QwtSymbol::UTriangle;
case 6 : return QwtSymbol::LTriangle;
case 7 : return QwtSymbol::RTriangle;
case 8 : return QwtSymbol::Cross;
case 9 : return QwtSymbol::XCross;
case 10 : return QwtSymbol::HLine;
case 11 : return QwtSymbol::VLine;
case 12 : return QwtSymbol::Star1;
case 13 : return QwtSymbol::Star2;
case 14 : return QwtSymbol::Hexagon;
case 15 : return QwtSymbol::StyleCnt;
}
}
bool LTMChartParser::startDocument()
{
buffer.clear();
return TRUE;
}
static QString unprotect(QString buffer)
{
// get local TM character code
QTextEdit trademark("&#8482;"); // process html encoding of(TM)
QString tm = trademark.toPlainText();
// remove quotes
QString t = buffer.trimmed();
QString s = t.mid(1,t.length()-2);
// replace html (TM) with local TM character
s.replace( "&#8482;", tm );
// html special chars are automatically handled
// XXX other special characters will not work
// cross-platform but will work locally, so not a biggie
// i.e. if thedefault charts.xml has a special character
// in it it should be added here
return s;
}
// to see the format of the charts.xml file, look at the serialize()
// function at the bottom of this source file.
bool LTMChartParser::endElement( const QString&, const QString&, const QString &qName )
{
//
// Single Attribute elements
//
if(qName == "chartname") setting.name = unprotect(buffer);
else if(qName == "metricname") metric.symbol = buffer.trimmed();
else if(qName == "metricdesc") metric.name = unprotect(buffer);
else if(qName == "metricuname") metric.uname = unprotect(buffer);
else if(qName == "metricuunits") metric.uunits = unprotect(buffer);
else if(qName == "metricbaseline") metric.baseline = buffer.trimmed().toDouble();
else if(qName == "metricsmooth") metric.smooth = buffer.trimmed().toInt();
else if(qName == "metrictrend") metric.trend = buffer.trimmed().toInt();
else if(qName == "metrictopn") metric.topN = buffer.trimmed().toInt();
else if(qName == "metriccurve") metric.curveStyle = intToCurve(buffer.trimmed().toInt());
else if(qName == "metricsymbol") metric.symbolStyle = intToSymbol(buffer.trimmed().toInt());
else if(qName == "metricpencolor") {
// the r,g,b values are in red="xx",green="xx" and blue="xx" attributes
// of this element and captured in startelement below
metric.penColor = QColor(red,green,blue);
}
else if(qName == "metricpenalpha") metric.penAlpha = buffer.trimmed().toInt();
else if(qName == "metricpenwidth") metric.penWidth = buffer.trimmed().toInt();
else if(qName == "metricpenstyle") metric.penStyle = buffer.trimmed().toInt();
else if(qName == "metricbrushcolor") {
// the r,g,b values are in red="xx",green="xx" and blue="xx" attributes
// of this element and captured in startelement below
metric.brushColor = QColor(red,green,blue);
} else if(qName == "metricbrushalpha") metric.penAlpha = buffer.trimmed().toInt();
//
// Complex Elements
//
else if(qName == "metric") // <metric></metric> block
setting.metrics.append(metric);
else if (qName == "LTM-chart") // <LTM-chart></LTM-chart> block
settings.append(setting);
else if (qName == "charts") { // <charts></charts> block top-level
} // do nothing for now
return TRUE;
}
bool LTMChartParser::startElement( const QString&, const QString&, const QString &name, const QXmlAttributes &attrs )
{
buffer.clear();
if(name == "charts")
; // do nothing for now
else if (name == "LTM-chart")
setting = LTMSettings();
else if (name == "metric")
metric = MetricDetail();
else if (name == "metricpencolor" || name == "metricbrushcolor") {
// red="x" green="x" blue="x" attributes for pen/brush color
for(int i=0; i<attrs.count(); i++) {
if (attrs.qName(i) == "red") red=attrs.value(i).toInt();
if (attrs.qName(i) == "green") green=attrs.value(i).toInt();
if (attrs.qName(i) == "blue") blue=attrs.value(i).toInt();
}
}
return TRUE;
}
bool LTMChartParser::characters( const QString& str )
{
buffer += str;
return TRUE;
}
QList<LTMSettings>
LTMChartParser::getSettings()
{
return settings;
}
bool LTMChartParser::endDocument()
{
return TRUE;
}
// static helper to protect special xml characters
// ideally we would use XMLwriter to do this but
// the file format is trivial and this implementation
// is easier to follow and modify... for now.
static QString xmlprotect(QString string)
{
QTextEdit trademark("&#8482;"); // process html encoding of(TM)
QString tm = trademark.toPlainText();
QString s = string;
s.replace( tm, "&#8482;" );
s.replace( "&", "&amp;" );
s.replace( ">", "&gt;" );
s.replace( "<", "&lt;" );
s.replace( "\"", "&quot;" );
s.replace( "\'", "&apos;" );
return s;
}
//
// Write out the charts.xml file
//
void
LTMChartParser::serialize(QString filename, QList<LTMSettings> charts)
{
// open file - truncate contents
QFile file(filename);
file.open(QFile::WriteOnly);
file.resize(0);
QTextStream out(&file);
// begin document
out << "<charts>\n";
// write out to file
foreach (LTMSettings chart, charts) {
// chart name
out<<QString("\t<LTM-chart>\n\t\t<chartname>\"%1\"</chartname>\n").arg(xmlprotect(chart.name));
// all the metrics
foreach (MetricDetail metric, chart.metrics) {
out<<QString("\t\t<metric>\n");
out<<QString("\t\t\t<metricdesc>\"%1\"</metricdesc>\n").arg(xmlprotect(metric.name));
out<<QString("\t\t\t<metricname>%1</metricname>\n").arg(metric.symbol);
out<<QString("\t\t\t<metricuname>\"%1\"</metricuname>\n").arg(xmlprotect(metric.uname));
out<<QString("\t\t\t<metricuunits>\"%1\"</metricuunits>\n").arg(xmlprotect(metric.uunits));
// SMOOTH, TREND, TOPN
out<<QString("\t\t\t<metricsmooth>%1</metricsmooth>\n").arg(metric.smooth);
out<<QString("\t\t\t<metrictrend>%1</metrictrend>\n").arg(metric.trend);
out<<QString("\t\t\t<metrictopn>%1</metrictopn>\n").arg(metric.topN);
out<<QString("\t\t\t<metricbaseline>%1</metricbaseline>\n").arg(metric.baseline);
// CURVE, SYMBOL
out<<QString("\t\t\t<metriccurve>%1</metriccurve>\n").arg(curveToInt(metric.curveStyle));
out<<QString("\t\t\t<metricsymbol>%1</metricsymbol>\n").arg(symbolToInt(metric.symbolStyle));
// PEN
out<<QString("\t\t\t<metricpencolor red=\"%1\" green=\"%3\" blue=\"%4\"></metricpencolor>\n")
.arg(metric.penColor.red())
.arg(metric.penColor.green())
.arg(metric.penColor.blue());
out<<QString("\t\t\t<metricpenalpha>%1</metricpenalpha>\n").arg(metric.penAlpha);
out<<QString("\t\t\t<metricpenwidth>%1</metricpenwidth>\n").arg(metric.penWidth);
out<<QString("\t\t\t<metricpenstyle>%1</metricpenstyle>\n").arg(metric.penStyle);
// BRUSH
out<<QString("\t\t\t<metricbrushcolor red=\"%1\" green=\"%3\" blue=\"%4\"></metricbrushcolor>\n")
.arg(metric.brushColor.red())
.arg(metric.brushColor.green())
.arg(metric.brushColor.blue());
out<<QString("\t\t\t<metricbrushalpha>%1</metricbrushalpha>\n").arg(metric.brushAlpha);
out<<QString("\t\t</metric>\n");
}
out<<QString("\t</LTM-chart>\n");
}
// end document
out << "</charts>\n";
// close file
file.close();
}

47
src/LTMChartParser.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* 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_LTMChartParser_h
#define _GC_LTMChartParser_h 1
#include <QXmlDefaultHandler>
#include "LTMSettings.h"
#include "LTMTool.h"
class LTMChartParser : public QXmlDefaultHandler
{
public:
static void serialize(QString, QList<LTMSettings>);
// unmarshall
bool startDocument();
bool endDocument();
bool endElement( const QString&, const QString&, const QString &qName );
bool startElement( const QString&, const QString&, const QString &name, const QXmlAttributes &attrs );
bool characters( const QString& str );
QList<LTMSettings> getSettings();
protected:
QString buffer;
LTMSettings setting;
MetricDetail metric;
int red, green, blue;
QList<LTMSettings> settings;
};
#endif

916
src/LTMPlot.cpp Normal file
View File

@@ -0,0 +1,916 @@
/*
* 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
*/
#include "LTMPlot.h"
#include "LTMTool.h"
#include "LTMTrend.h"
#include "LTMWindow.h"
#include "MetricAggregator.h"
#include "SummaryMetrics.h"
#include "RideMetric.h"
#include "Settings.h"
#include "StressCalculator.h" // for LTS/STS calculation
#include <QSettings>
#include <qwt_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_curve_fitter.h>
#include <qwt_plot_grid.h>
#include <qwt_symbol.h>
#include <math.h> // for isinf() isnan()
#include <boost/shared_ptr.hpp>
static int supported_axes[] = { QwtPlot::yLeft, QwtPlot::yRight, QwtPlot::yLeft1, QwtPlot::yRight1, QwtPlot::yLeft2, QwtPlot::yRight2, QwtPlot::yLeft3, QwtPlot::yRight3 };
LTMPlot::LTMPlot(LTMWindow *parent, MainWindow *main, QDir home) : bg(NULL), parent(parent), main(main),
home(home), highlighter(NULL)
{
// get application settings
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
useMetricUnits = appsettings->value(GC_UNIT).toString() == "Metric";
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setCanvasBackground(Qt::white);
setAxisTitle(yLeft, tr(""));
setAxisTitle(xBottom, "Date");
setAxisMaxMinor(QwtPlot::xBottom,-1);
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(QDateTime::currentDateTime(), 0, LTM_DAY));
grid = new QwtPlotGrid();
grid->enableX(false);
QPen gridPen;
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
grid->attach(this);
settings = NULL;
connect(main, SIGNAL(configChanged()), this, SLOT(configUpdate()));
}
LTMPlot::~LTMPlot()
{
}
void
LTMPlot::configUpdate()
{
// get application settings
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
useMetricUnits = appsettings->value(GC_UNIT).toString() == "Metric";
}
void
LTMPlot::setData(LTMSettings *set)
{
settings = set;
// crop dates to at least within a year of the data available, but only if we have some data
if (settings->data != NULL && (*settings->data).count() != 0) {
// if dates are null we need to set them from the available data
// end
if (settings->end == QDateTime() ||
settings->end > (*settings->data).last().getRideDate().addDays(365)) {
if (settings->end < QDateTime::currentDateTime()) {
settings->end = QDateTime::currentDateTime();
} else {
settings->end = (*settings->data).last().getRideDate();
}
}
// start
if (settings->start == QDateTime() ||
settings->start < (*settings->data).first().getRideDate().addDays(-365)) {
settings->start = (*settings->data).first().getRideDate();
}
}
setTitle(settings->title);
// wipe existing curves/axes details
QHashIterator<QString, QwtPlotCurve*> c(curves);
while (c.hasNext()) {
c.next();
QString symbol = c.key();
QwtPlotCurve *current = c.value();
//current->detach(); // the destructor does this for you
delete current;
}
curves.clear();
if (highlighter) {
highlighter->detach();
delete highlighter;
highlighter = NULL;
}
// disable all y axes until we have populated
for (int i=0; i<8; i++) enableAxis(supported_axes[i], false);
axes.clear();
// reset all min/max Y values
for (int i=0; i<10; i++) minY[i]=0, maxY[i]=0;
// no data to display so that all folks
if (settings->data == NULL || (*settings->data).count() == 0) {
// tidy up the bottom axis
maxX = groupForDate(settings->end.date(), settings->groupBy) -
groupForDate(settings->start.date(), settings->groupBy);
setAxisScale(xBottom, 0, maxX);
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
// remove the shading if it exists
refreshZoneLabels(-1);
replot();
return;
}
// setup the curves
int count;
foreach (MetricDetail metricDetail, settings->metrics) {
QVector<double> xdata, ydata;
createCurveData(settings, metricDetail, xdata, ydata, count);
// Create a curve
QwtPlotCurve *current = new QwtPlotCurve(metricDetail.uname);
curves.insert(metricDetail.symbol, current);
current->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen cpen = QPen(metricDetail.penColor);
cpen.setWidth(1.0);
current->setPen(cpen);
current->setStyle(metricDetail.curveStyle);
QwtSymbol sym;
sym.setStyle(metricDetail.symbolStyle);
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
// fill the bars
QColor brushColor = metricDetail.penColor;
brushColor.setAlpha(100);
QBrush brush = QBrush(brushColor);
current->setBrush(brush);
current->setPen(cpen);
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
// XXX Symbol for steps looks horrible
sym.setStyle(QwtSymbol::Ellipse);
if (settings->groupBy == LTM_DAY)
sym.setSize(3);
else
sym.setSize(6);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(metricDetail.penColor));
current->setSymbol(sym);
// XXX FUDGE QWT's LACK OF A BAR CHART
// add a zero point at the head and tail so the
// histogram columns look nice.
// and shift all the x-values left by 0.5 so that
// they centre over x-axis labels
int i=0;
for (i=0; i<=count; i++) xdata[i] -= 0.5;
// now add a final 0 value to get the last
// column drawn - no resize neccessary
// since it is always sized for 1 + maxnumber of entries
xdata[i] = xdata[i-1] + 1;
ydata[i] = 0;
count++;
// END OF FUDGE
} else if (metricDetail.curveStyle == QwtPlotCurve::Lines) {
QPen cpen = QPen(metricDetail.penColor);
cpen.setWidth(2.0);
sym.setSize(6);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(metricDetail.penColor));
current->setSymbol(sym);
current->setPen(cpen);
} else if (metricDetail.curveStyle == QwtPlotCurve::Dots) {
sym.setSize(6);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(metricDetail.penColor));
current->setSymbol(sym);
} else if (metricDetail.curveStyle == QwtPlotCurve::Sticks) {
sym.setSize(4);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(Qt::white));
current->setSymbol(sym);
}
// smoothing
if (metricDetail.smooth == true) {
current->setCurveAttribute(QwtPlotCurve::Fitted, true);
}
// set the data series
current->setData(xdata.data(),ydata.data(), count+1);
current->setBaseline(metricDetail.baseline);
// choose the axis
int axisid = chooseYAxis(metricDetail.uunits);
current->setYAxis(axisid);
// update min/max Y values for the chosen axis
if (current->maxYValue() > maxY[axisid]) maxY[axisid] = current->maxYValue();
if (current->minYValue() < minY[axisid]) minY[axisid] = current->minYValue();
current->attach(this);
// trend - clone the data for the curve and add a curvefitted
// curve with no symbols and use a dashed pen
// need more than 2 points for a trend line
if (metricDetail.trend == true && count > 2) {
QString trendName = QString("%1 trend").arg(metricDetail.uname);
QString trendSymbol = QString("%1_trend").arg(metricDetail.symbol);
QwtPlotCurve *trend = new QwtPlotCurve(trendName);
// cosmetics
QPen cpen = QPen(metricDetail.penColor.darker(200));
cpen.setWidth(4.0);
cpen.setStyle(Qt::DotLine);
trend->setPen(cpen);
trend->setRenderHint(QwtPlotItem::RenderAntialiased);
trend->setBaseline(0);
trend->setYAxis(axisid);
trend->setStyle(QwtPlotCurve::Lines);
// perform linear regression
LTMTrend regress(xdata.data(), ydata.data(), count);
double xtrend[2], ytrend[2];
xtrend[0] = 0.0;
ytrend[0] = regress.getYforX(0.0);
xtrend[1] = xdata[count];
ytrend[1] = regress.getYforX(xdata[count]);
trend->setData(xtrend,ytrend, 2);
trend->attach(this);
curves.insert(trendSymbol, trend);
}
// highlight top N values
if (metricDetail.topN > 0) {
QMap<double, int> sortedList;
// copy the yvalues, retaining the offset
for(int i=0; i<ydata.count(); i++)
sortedList.insert(ydata[i], i);
// copy the top N values
QVector<double> hxdata, hydata;
hxdata.resize(metricDetail.topN);
hydata.resize(metricDetail.topN);
// QMap orders the list so start at the top and work
// backwards
QMapIterator<double, int> i(sortedList);
i.toBack();
int counter = 0;
while (i.hasPrevious() && counter < metricDetail.topN) {
i.previous();
if (ydata[i.value()]) {
hxdata[counter] = xdata[i.value()];
hydata[counter] = ydata[i.value()];
counter++;
}
}
// lets setup a curve with this data then!
QString topName;
if (counter > 1)
topName = QString("%1 Best %2")
.arg(metricDetail.uname)
.arg(counter); // starts from zero
else
topName = QString("Best %1").arg(metricDetail.uname);
QString topSymbol = QString("%1_topN").arg(metricDetail.symbol);
QwtPlotCurve *top = new QwtPlotCurve(topName);
curves.insert(topSymbol, top);
top->setRenderHint(QwtPlotItem::RenderAntialiased);
top->setStyle(QwtPlotCurve::Dots);
// we might have hidden the symbols for this curve
// if its set to none then default to a rectangle
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol)
sym.setStyle(QwtSymbol::Rect);
sym.setSize(12);
QColor lighter = metricDetail.penColor;
lighter.setAlpha(50);
sym.setPen(metricDetail.penColor);
sym.setBrush(lighter);
top->setSymbol(sym);
top->setData(hxdata.data(),hydata.data(), counter);
top->setBaseline(0);
top->setYAxis(axisid);
top->attach(this);
}
}
// setup the xaxis at the bottom
int tics;
maxX = 0.5 + groupForDate(settings->end.date(), settings->groupBy) -
groupForDate(settings->start.date(), settings->groupBy);
if (maxX < 14) {
tics = 1;
} else {
tics = 1 + maxX/10;
}
setAxisScale(xBottom, -0.5, maxX, tics);
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
// run through the Y axis
for (int i=0; i<10; i++) {
// set the scale on the axis
if (i != xBottom && i != xTop) {
maxY[i] *= 1.1; // add 10% headroom
setAxisScale(i, minY[i], maxY[i]);
}
}
QString format = axisTitle(yLeft).text();
parent->toolTip()->setAxis(xBottom, yLeft);
parent->toolTip()->setFormat(format);
// draw zone labels axisid of -1 means delete whats there
// cause no watts are being displayed
if (settings->shadeZones == true)
refreshZoneLabels(axes.value("watts", -1));
else
refreshZoneLabels(-1); // turn em off
// plot
replot();
}
void
LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n)
{
QList<SummaryMetrics> *data;
// resize the curve array to maximum possible size
int maxdays = groupForDate(settings->end.date(), settings->groupBy)
- groupForDate(settings->start.date(), settings->groupBy);
x.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail
y.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail
// Get metric data, either from metricDB for RideFile metrics
// or from StressCalculator for PM type metrics
QList<SummaryMetrics> PMCdata;
if (metricDetail.type == METRIC_DB) {
data = settings->data;
} else if (metricDetail.type == METRIC_PM) {
createPMCCurveData(settings, metricDetail, PMCdata);
data = &PMCdata;
}
n=-1;
int lastDay=0;
unsigned long secondsPerGroupBy=0;
bool wantZero = (metricDetail.curveStyle == QwtPlotCurve::Steps);
foreach (SummaryMetrics rideMetrics, *data) {
// day we are on
int currentDay = groupForDate(rideMetrics.getRideDate().date(), settings->groupBy);
// value for day
double value = rideMetrics.getForSymbol(metricDetail.symbol);
// check values are bounded to stop QWT going berserk
if (isnan(value) || isinf(value)) value = 0;
// Special computed metrics (LTS/STS) have a null metric pointer
if (metricDetail.metric) {
// convert from stored metric value to imperial
if (useMetricUnits == false) value *= metricDetail.metric->conversion();
// convert seconds to hours
if (metricDetail.metric->units(true) == "seconds") value /= 3600;
}
if (value || wantZero) {
unsigned long seconds = rideMetrics.getForSymbol("workout_time");
if (currentDay > lastDay) {
if (lastDay && wantZero) {
while (lastDay<currentDay) {
lastDay++;
n++;
x[n]=lastDay - groupForDate(settings->start.date(), settings->groupBy);
y[n]=0;
}
} else {
n++;
}
y[n] = value;
x[n] = currentDay - groupForDate(settings->start.date(), settings->groupBy);
secondsPerGroupBy = seconds; // reset for new group
} else {
// sum totals, average averages and choose best for Peaks
int type = metricDetail.metric ? metricDetail.metric->type() : RideMetric::Average;
if (metricDetail.uunits == "Ramp") type = RideMetric::Total;
switch (type) {
case RideMetric::Total:
y[n] += value;
break;
case RideMetric::Average:
{
// average should be calculated taking into account
// the duration of the ride, otherwise high value but
// short rides will skew the overall average
y[n] = ((y[n]*secondsPerGroupBy)+(seconds*value)) / (secondsPerGroupBy+seconds);
break;
}
case RideMetric::Peak:
if (value > y[n]) y[n] = value;
break;
}
secondsPerGroupBy += seconds; // increment for same group
}
lastDay = currentDay;
}
}
}
void
LTMPlot::createPMCCurveData(LTMSettings *settings, MetricDetail metricDetail,
QList<SummaryMetrics> &customData)
{
QDate earliest, latest; // rides
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
QString scoreType;
// create a custom set of summary metric data!
if (metricDetail.name.startsWith("Skiba")) {
scoreType = "skiba_bike_score";
} else if (metricDetail.name.startsWith("Daniels")) {
scoreType = "daniels_points";
}
// create the Stress Calculation List
// FOR ALL RIDE FILES
StressCalculator *sc = new StressCalculator(
settings->start,
settings->end,
(appsettings->value(GC_INITIAL_STS)).toInt(),
(appsettings->value(GC_INITIAL_LTS)).toInt(),
(appsettings->value(GC_STS_DAYS,7)).toInt(),
(appsettings->value(GC_LTS_DAYS,42)).toInt());
sc->calculateStress(main, home.absolutePath(), scoreType);
// pick out any data that is in the date range selected
// convert to SummaryMetric Format used on the plot
for (int i=0; i< sc->n(); i++) {
SummaryMetrics add = SummaryMetrics();
add.setRideDate(settings->start.addDays(i));
if (scoreType == "skiba_bike_score") {
add.setForSymbol("skiba_lts", sc->getLTSvalues()[i]);
add.setForSymbol("skiba_sts", sc->getSTSvalues()[i]);
add.setForSymbol("skiba_sb", sc->getSBvalues()[i]);
add.setForSymbol("skiba_sr", sc->getSRvalues()[i]);
add.setForSymbol("skiba_lr", sc->getLRvalues()[i]);
} else if (scoreType == "daniels_points") {
add.setForSymbol("daniels_lts", sc->getLTSvalues()[i]);
add.setForSymbol("daniels_sts", sc->getSTSvalues()[i]);
add.setForSymbol("daniels_sb", sc->getSBvalues()[i]);
add.setForSymbol("daniels_sr", sc->getSRvalues()[i]);
add.setForSymbol("daniels_lr", sc->getLRvalues()[i]);
}
add.setForSymbol("workout_time", 1.0); // averaging is per day
customData << add;
}
delete sc;
}
int
LTMPlot::chooseYAxis(QString units)
{
int chosen;
// return the YAxis to use
if ((chosen = axes.value(units, -1)) != -1) return chosen;
else if (axes.count() < 8) {
chosen = supported_axes[axes.count()];
if (units == "seconds") setAxisTitle(chosen, "hours"); // we convert seconds to hours
else setAxisTitle(chosen, units);
enableAxis(chosen, true);
axes.insert(units, chosen);
return chosen;
} else {
// eek!
return yLeft; // just re-use the current yLeft axis
}
}
int
LTMPlot::groupForDate(QDate date, int groupby)
{
switch(groupby) {
case LTM_WEEK:
{
// must start from 1 not zero!
return 1 + ((date.toJulianDay() - settings->start.date().toJulianDay()) / 7);
}
case LTM_MONTH: return (date.year()*12) + date.month();
case LTM_YEAR: return date.year();
case LTM_DAY:
default:
return date.toJulianDay();
}
}
void
LTMPlot::pointHover(QwtPlotCurve *curve, int index)
{
if (index >= 0 && curve != highlighter) {
const RideMetricFactory &factory = RideMetricFactory::instance();
double value;
QString units;
QString datestr;
LTMScaleDraw *lsd = new LTMScaleDraw(settings->start, groupForDate(settings->start.date(), settings->groupBy), settings->groupBy);
QwtText startText = lsd->label((int)(curve->x(index)+0.5));
QwtText endText;
endText = lsd->label((int)(curve->x(index)+1.5));
if (settings->groupBy != LTM_WEEK)
datestr = startText.text();
else
datestr = QString("%1 - %2").arg(startText.text()).arg(endText.text());
datestr = datestr.replace('\n', ' ');
// we reference the metric definitions of name and
// units to decide on the level of precision required
QHashIterator<QString, QwtPlotCurve*> c(curves);
while (c.hasNext()) {
c.next();
if (c.value() == curve) {
const RideMetric *metric =factory.rideMetric(c.key());
units = metric ? metric->units(useMetricUnits) : "";
// BikeScore, RI and Daniels Points have no units
if (units == "" && metric != NULL) {
QTextEdit processHTML(factory.rideMetric(c.key())->name());
units = processHTML.toPlainText();
}
}
}
if (units == "seconds") {
units = "hours"; // we translate from seconds to hours
value = ceil(curve->y(index)*10.0)/10.0;
} else if (units.contains("Relative Intensity") ||
units.endsWith("VI"))
value = ceil(curve->y(index)*100.00)/100.00;
else value = (int)curve->y(index);
// but then we use the user defined values when we
// output the tooltip
QString text = QString("%1\n%2\n%3 %4")
.arg(datestr)
.arg(curve->title().text())
.arg(value)
.arg(this->axisTitle(curve->yAxis()).text());
// set that text up
parent->toolTip()->setText(text);
} else {
// no point
parent->toolTip()->setText("");
}
}
// start of date range selection
void
LTMPlot::pickerAppended(QPoint pos)
{
// ony work once we have a chart to do it on
if (settings == NULL) return;
// allow user to select a date range across the plot
if (highlighter) {
// detach and delete
highlighter->detach();
delete highlighter;
}
highlighter = new QwtPlotCurve("Date Selection");
double curveDataX[4]; // simple 4 point line
double curveDataY[4]; // simple 4 point line
// get x
int x = invTransform(xBottom, pos.x());
// trying to select a range on anull plot
if (maxY[yLeft] == 0) {
enableAxis(yLeft, true);
setAxisTitle(yLeft, tr("watts")); // as good as any
setAxisScale(yLeft, 0, 1000);
maxY[yLeft] = 1000;
}
// get min/max y
curveDataX[0]=x;
curveDataY[0]=minY[yLeft];
curveDataX[1]=x;
curveDataY[1]=maxY[yLeft];
// no right then down - updated by pickerMoved
curveDataX[2]=curveDataX[1];
curveDataY[2]=curveDataY[1];
curveDataX[3]=curveDataX[0];
curveDataY[3]=curveDataY[3];
// color
QColor ccol(Qt::blue);
ccol.setAlpha(64);
QPen cpen = QPen(ccol);
cpen.setWidth(1.0);
QBrush cbrush = QBrush(ccol);
highlighter->setPen(cpen);
highlighter->setBrush(cbrush);
highlighter->setStyle(QwtPlotCurve::Lines);
highlighter->setData(curveDataX,curveDataY, 4);
// axis etc
highlighter->setYAxis(QwtPlot::yLeft);
highlighter->attach(this);
highlighter->show();
// what is the start date?
LTMScaleDraw *lsd = new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(),
settings->groupBy),
settings->groupBy);
start = lsd->toDate((int)x);
end = start.addYears(10);
name = QString("%1 - ").arg(start.toString("d MMM yy"));
seasonid = settings->ltmTool->newSeason(name, start, end, Season::adhoc);
replot();
}
// end of date range selection
void
LTMPlot::pickerMoved(QPoint pos)
{
if (settings == NULL) return;
// allow user to select a date range across the plot
double curveDataX[4]; // simple 4 point line
double curveDataY[4]; // simple 4 point line
// get x
int x = invTransform(xBottom, pos.x());
// update to reflect new x position
curveDataX[0]=highlighter->x(0);
curveDataY[0]=highlighter->y(0);
curveDataX[1]=highlighter->x(0);
curveDataY[1]=highlighter->y(1);
curveDataX[2]=x;
curveDataY[2]=curveDataY[1];
curveDataX[3]=x;
curveDataY[3]=curveDataY[3];
// what is the end date?
LTMScaleDraw *lsd = new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(),
settings->groupBy),
settings->groupBy);
end = lsd->toDate((int)x);
name = QString("%1 - %2").arg(start.toString("d MMM yy"))
.arg(end.toString("d MMM yy"));
settings->ltmTool->updateSeason(seasonid, name, start, end, Season::adhoc);
// update and replot highlighter
highlighter->setData(curveDataX,curveDataY, 4);
replot();
}
/*----------------------------------------------------------------------
* Draw Power Zone Shading on Background (here to end of source file)
*
* THANKS TO DAMIEN GRAUSER FOR GETTING THIS WORKING TO SHOW
* ZONE SHADING OVER TIME. WHEN CP CHANGES THE ZONE SHADING AND
* LABELLING CHANGES TOO. NEAT.
*--------------------------------------------------------------------*/
class LTMPlotBackground: public QwtPlotItem
{
private:
LTMPlot *parent;
public:
LTMPlotBackground(LTMPlot *_parent, int axisid)
{
setAxis(QwtPlot::xBottom, axisid);
setZ(0.0);
parent = _parent;
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
virtual void draw(QPainter *painter,
const QwtScaleMap &xMap, const QwtScaleMap &yMap,
const QRect &rect) const
{
const Zones *zones = parent->parent->main->zones();
int zone_range_size = parent->parent->main->zones()->getRangeSize();
//fprintf(stderr, "size: %d\n",zone_range_size);
if (zone_range_size >= 0) { //parent->shadeZones() &&
for (int i = 0; i < zone_range_size; i ++) {
int zone_range = i;
//int zone_range = zones->whichRange(parent->settings->start.addDays((parent->settings->end.date().toJulianDay()-parent->settings->start.date().toJulianDay())/2).date()); // XXX Damien fixup
int left = xMap.transform(parent->groupForDate(zones->getStartDate(zone_range), parent->settings->groupBy)- parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
//fprintf(stderr, "%d left: %d\n",i,left);
//int right = xMap.transform(parent->groupForDate(zones->getEndDate(zone_range), parent->settings->groupBy)- parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
//fprintf(stderr, "%d right: %d\n",i,right);
/* The +50 pixels is for a QWT bug? cover the little gap on the right? */
int right = xMap.transform(parent->maxX + 0.5) + 50;
if (right<0)
right= xMap.transform(parent->groupForDate(parent->settings->end.date(), parent->settings->groupBy) - parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
QList <int> zone_lows = zones->getZoneLows(zone_range);
int num_zones = zone_lows.size();
if (num_zones > 0) {
for (int z = 0; z < num_zones; z ++) {
QRect r = rect;
r.setLeft(left);
r.setRight(right);
QColor shading_color = zoneColor(z, num_zones);
shading_color.setHsv(
shading_color.hue(),
shading_color.saturation() / 4,
shading_color.value()
);
r.setBottom(yMap.transform(zone_lows[z]));
if (z + 1 < num_zones)
r.setTop(yMap.transform(zone_lows[z + 1]));
if (r.top() <= r.bottom())
painter->fillRect(r, shading_color);
}
}
}
}
}
};
// Zone labels are drawn if power zone bands are enabled, automatically
// at the center of the plot
class LTMPlotZoneLabel: public QwtPlotItem
{
private:
LTMPlot *parent;
int zone_number;
double watts;
QwtText text;
public:
LTMPlotZoneLabel(LTMPlot *_parent, int _zone_number, int axisid, LTMSettings *settings)
{
parent = _parent;
zone_number = _zone_number;
const Zones *zones = parent->parent->main->zones();
//int zone_range = 0; //parent->parent->mainWindow->zoneRange();
int zone_range = zones->whichRange(settings->start.addDays((settings->end.date().toJulianDay()-settings->start.date().toJulianDay())/2).date()); // XXX Damien Fixup
// which axis has watts?
setAxis(QwtPlot::xBottom, axisid);
// create new zone labels if we're shading
if (zone_range >= 0) { //parent->shadeZones()
QList <int> zone_lows = zones->getZoneLows(zone_range);
QList <QString> zone_names = zones->getZoneNames(zone_range);
int num_zones = zone_lows.size();
assert(zone_names.size() == num_zones);
if (zone_number < num_zones) {
watts =
(
(zone_number + 1 < num_zones) ?
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
(
(zone_number > 0) ?
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
2.0 * zone_lows[zone_number]
)
);
text = QwtText(zone_names[zone_number]);
text.setFont(QFont("Helvetica",24, QFont::Bold));
QColor text_color = zoneColor(zone_number, num_zones);
text_color.setAlpha(64);
text.setColor(text_color);
}
}
setZ(1.0 + zone_number / 100.0);
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
void draw(QPainter *painter,
const QwtScaleMap &, const QwtScaleMap &yMap,
const QRect &rect) const
{
if (true) {//parent->shadeZones()
int x = (rect.left() + rect.right()) / 2;
int y = yMap.transform(watts);
// the following code based on source for QwtPlotMarker::draw()
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
tr.moveCenter(QPoint(x, y));
text.draw(painter, tr);
}
}
};
void
LTMPlot::refreshZoneLabels(int axisid)
{
foreach(LTMPlotZoneLabel *label, zoneLabels) {
label->detach();
delete label;
}
zoneLabels.clear();
if (bg) {
bg->detach();
delete bg;
bg = NULL;
}
if (axisid == -1) return; // our job is done - no zones to plot
const Zones *zones = main->zones();
if (zones == NULL || zones->getRangeSize()==0) return; // no zones to plot
int zone_range = 0; // first range
// generate labels for existing zones
if (zone_range >= 0) {
int num_zones = zones->numZones(zone_range);
for (int z = 0; z < num_zones; z ++) {
LTMPlotZoneLabel *label = new LTMPlotZoneLabel(this, z, axisid, settings);
label->attach(this);
zoneLabels.append(label);
}
}
bg = new LTMPlotBackground(this, axisid);
bg->attach(this);
}

168
src/LTMPlot.h Normal file
View File

@@ -0,0 +1,168 @@
/*
* 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_LTMPlot_h
#define _GC_LTMPlot_h 1
#include <QtGui>
#include <qwt_plot.h>
#include <qwt_plot_marker.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_scale_draw.h>
#include "LTMTool.h"
#include "LTMSettings.h"
#include "MetricAggregator.h"
#include "MainWindow.h"
class LTMPlotBackground;
class LTMPlotZoneLabel;
class LTMScaleDraw;
class LTMPlot : public QwtPlot
{
Q_OBJECT
public:
LTMPlot(LTMWindow *, MainWindow *main, QDir home);
~LTMPlot();
void setData(LTMSettings *);
public slots:
void pointHover(QwtPlotCurve*, int);
void pickerMoved(QPoint);
void pickerAppended(QPoint);
void configUpdate();
protected:
friend class ::LTMPlotBackground;
friend class ::LTMPlotZoneLabel;
LTMPlotBackground *bg;
QList <LTMPlotZoneLabel *> zoneLabels;
LTMWindow *parent;
double minY[10], maxY[10], maxX; // for all possible 10 curves
private:
MainWindow *main;
QDir home;
bool useMetricUnits;
struct LTMSettings *settings;
// date range selection
int selection, seasonid;
QString name;
QDate start, end;
QwtPlotCurve *highlighter;
QHash<QString, QwtPlotCurve*> curves; // metric symbol with curve object
QHash<QString, int> axes; // units and associated axis
LTMScaleDraw *scale;
QwtPlotGrid *grid;
QDate firstDate,
lastDate;
int groupForDate(QDate , int);
void createCurveData(LTMSettings *, MetricDetail,
QVector<double>&, QVector<double>&, int&);
void createPMCCurveData(LTMSettings *, MetricDetail, QList<SummaryMetrics> &);
int chooseYAxis(QString);
void refreshZoneLabels(int);
};
// Produce Labels for X-Axis
class LTMScaleDraw: public QwtScaleDraw
{
public:
LTMScaleDraw(const QDateTime &base, int startGroup, int groupBy) :
baseTime(base), groupBy(groupBy), startGroup(startGroup) {
}
virtual QwtText label(double v) const {
int group = startGroup + (int) v;
QString label;
QDateTime upTime;
switch (groupBy) {
case LTM_DAY:
upTime = baseTime.addDays((int)v);
label = upTime.toString("MMM dd\nyyyy");
break;
case LTM_WEEK:
{
QDate week = baseTime.date().addDays((int)v*7);
label = week.toString("MMM dd\nyyyy");
}
break;
case LTM_MONTH:
{ // month is count of months since year 0 starting from month 0
int year=group/12;
int month=group%12;
if (!month) { year--; month=12; }
label = QString("%1\n%2").arg(QDate::shortMonthName(month)).arg(year);
}
break;
case LTM_YEAR:
label = QString("%1").arg(group);
break;
}
return label;
}
QDate toDate(double v)
{
int group = startGroup + (int) v;
switch (groupBy) {
default: // meaningless but keeps the compiler happy
case LTM_DAY:
return baseTime.addDays((int)v).date();
break;
case LTM_WEEK:
return baseTime.date().addDays((int)v*7);
break;
case LTM_MONTH:
{
int year=group/12;
int month=group%12;
if (!month) { year--; month=12; }
return QDate(year, month, 1);
break;
}
case LTM_YEAR:
return QDate(group, 1, 1);
break;
}
}
private:
QDateTime baseTime;
int groupBy, startGroup;
};
#endif // _GC_LTMPlot_h

330
src/LTMSettings.cpp Normal file
View File

@@ -0,0 +1,330 @@
/*
* 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
*/
#include "LTMSettings.h"
#include "LTMTool.h"
#include "MainWindow.h"
#include "LTMChartParser.h"
#include <QtGui>
#include <qwt_plot.h>
#include <qwt_symbol.h>
#include <qwt_plot_curve.h>
/*----------------------------------------------------------------------
* EDIT CHART DIALOG
*--------------------------------------------------------------------*/
EditChartDialog::EditChartDialog(MainWindow *mainWindow, LTMSettings *settings, QList<LTMSettings>presets) :
QDialog(mainWindow, Qt::Dialog), mainWindow(mainWindow), settings(settings), presets(presets)
{
setWindowTitle(tr("Enter Chart Name"));
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// Metric Name
mainLayout->addSpacing(5);
chartName = new QLineEdit;
mainLayout->addWidget(chartName);
mainLayout->addStretch();
// Buttons
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
okButton = new QPushButton(tr("&OK"), this);
cancelButton = new QPushButton(tr("&Cancel"), this);
buttonLayout->addWidget(cancelButton);
buttonLayout->addWidget(okButton);
mainLayout->addLayout(buttonLayout);
// make it wide enough
setMinimumWidth(250);
// connect up slots
connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
}
void
EditChartDialog::okClicked()
{
// mustn't be blank
if (chartName->text() == "") {
QMessageBox::warning( 0, "Entry Error", "Name is blank");
return;
}
// does it already exist?
foreach (LTMSettings chart, presets) {
if (chart.name == chartName->text()) {
QMessageBox::warning( 0, "Entry Error", "Chart already exists");
return;
}
}
settings->name = chartName->text();
accept();
}
void
EditChartDialog::cancelClicked()
{
reject();
}
/*----------------------------------------------------------------------
* CHART MANAGER DIALOG
*--------------------------------------------------------------------*/
ChartManagerDialog::ChartManagerDialog(MainWindow *mainWindow, QList<LTMSettings>*presets) :
QDialog(mainWindow, Qt::Dialog), mainWindow(mainWindow), presets(presets)
{
setWindowTitle(tr("Manage Charts"));
QGridLayout *mainLayout = new QGridLayout(this);
importButton = new QPushButton(tr("Import..."));
exportButton = new QPushButton(tr("Export..."));
upButton = new QPushButton(tr("Move up"));
downButton = new QPushButton(tr("Move down"));
renameButton = new QPushButton(tr("Rename"));
deleteButton = new QPushButton(tr("Delete"));
QVBoxLayout *actionButtons = new QVBoxLayout;
actionButtons->addWidget(renameButton);
actionButtons->addWidget(deleteButton);
actionButtons->addWidget(upButton);
actionButtons->addWidget(downButton);
actionButtons->addStretch();
actionButtons->addWidget(importButton);
actionButtons->addWidget(exportButton);
charts = new QTreeWidget;
charts->headerItem()->setText(0, "Charts");
charts->setColumnCount(1);
charts->setSelectionMode(QAbstractItemView::SingleSelection);
charts->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit
charts->setIndentation(0);
foreach(LTMSettings chart, *presets) {
QTreeWidgetItem *add;
add = new QTreeWidgetItem(charts->invisibleRootItem());
add->setFlags(add->flags() | Qt::ItemIsEditable);
add->setText(0, chart.name);
}
charts->setCurrentItem(charts->invisibleRootItem()->child(0));
// Cancel/ OK Buttons
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
okButton = new QPushButton(tr("&OK"), this);
cancelButton = new QPushButton(tr("&Cancel"), this);
buttonLayout->addWidget(cancelButton);
buttonLayout->addWidget(okButton);
mainLayout->addWidget(charts, 0,0);
mainLayout->addLayout(actionButtons, 0,1);
mainLayout->addLayout(buttonLayout,1,0);
// seems reasonable...
setMinimumHeight(350);
// connect up slots
connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
connect(upButton, SIGNAL(clicked()), this, SLOT(upClicked()));
connect(downButton, SIGNAL(clicked()), this, SLOT(downClicked()));
connect(renameButton, SIGNAL(clicked()), this, SLOT(renameClicked()));
connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked()));
connect(importButton, SIGNAL(clicked()), this, SLOT(importClicked()));
connect(exportButton, SIGNAL(clicked()), this, SLOT(exportClicked()));
}
void
ChartManagerDialog::okClicked()
{
// take the edited versions of the name first
for(int i=0; i<charts->invisibleRootItem()->childCount(); i++)
(*presets)[i].name = charts->invisibleRootItem()->child(i)->text(0);
accept();
}
void
ChartManagerDialog::cancelClicked()
{
reject();
}
void
ChartManagerDialog::importClicked()
{
QFileDialog existing(this);
existing.setFileMode(QFileDialog::ExistingFile);
existing.setNameFilter(tr("Chart File (*.xml)"));
if (existing.exec()){
// we will only get one (ExistingFile not ExistingFiles)
QStringList filenames = existing.selectedFiles();
if (QFileInfo(filenames[0]).exists()) {
QList<LTMSettings> imported;
QFile chartsFile(filenames[0]);
// setup XML processor
QXmlInputSource source( &chartsFile );
QXmlSimpleReader xmlReader;
LTMChartParser (handler);
xmlReader.setContentHandler(&handler);
xmlReader.setErrorHandler(&handler);
// parse and get return values
xmlReader.parse(source);
imported = handler.getSettings();
// now append to the QList and QTreeWidget
*presets += imported;
foreach (LTMSettings chart, imported) {
QTreeWidgetItem *add;
add = new QTreeWidgetItem(charts->invisibleRootItem());
add->setFlags(add->flags() | Qt::ItemIsEditable);
add->setText(0, chart.name);
}
} else {
// oops non existant - does this ever happen?
QMessageBox::warning( 0, "Entry Error", QString("Selected file (%1) does not exist").arg(filenames[0]));
}
}
}
void
ChartManagerDialog::exportClicked()
{
QFileDialog newone(this);
newone.setFileMode(QFileDialog::AnyFile);
newone.setNameFilter(tr("Chart File (*.xml)"));
if (newone.exec()){
// we will only get one (ExistingFile not ExistingFiles)
QStringList filenames = newone.selectedFiles();
// if exists confirm overwrite
if (QFileInfo(filenames[0]).exists()) {
QMessageBox msgBox;
msgBox.setText(QString("The selected file (%1) exists.").arg(filenames[0]));
msgBox.setInformativeText("Do you want to overwrite it?");
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
msgBox.setIcon(QMessageBox::Warning);
if (msgBox.exec() != QMessageBox::Ok)
return;
}
LTMChartParser::serialize(filenames[0], *presets);
}
}
void
ChartManagerDialog::upClicked()
{
if (charts->currentItem()) {
int index = charts->invisibleRootItem()->indexOfChild(charts->currentItem());
if (index == 0) return; // its at the top already
// movin on up!
QTreeWidgetItem *moved;
charts->invisibleRootItem()->insertChild(index-1, moved=charts->invisibleRootItem()->takeChild(index));
charts->setCurrentItem(moved);
LTMSettings save = (*presets)[index];
presets->removeAt(index);
presets->insert(index-1, save);
}
}
void
ChartManagerDialog::downClicked()
{
if (charts->currentItem()) {
int index = charts->invisibleRootItem()->indexOfChild(charts->currentItem());
if (index == (charts->invisibleRootItem()->childCount()-1)) return; // its at the bottom already
// movin on up!
QTreeWidgetItem *moved;
charts->invisibleRootItem()->insertChild(index+1, moved=charts->invisibleRootItem()->takeChild(index));
charts->setCurrentItem(moved);
LTMSettings save = (*presets)[index];
presets->removeAt(index);
presets->insert(index+1, save);
}
}
void
ChartManagerDialog::renameClicked()
{
// which one is selected?
if (charts->currentItem()) charts->editItem(charts->currentItem(), 0);
}
void
ChartManagerDialog::deleteClicked()
{
// must have at least 1 child
if (charts->invisibleRootItem()->childCount() == 1) {
QMessageBox::warning(0, "Error", "You must have at least one chart");
return;
} else if (charts->currentItem()) {
int index = charts->invisibleRootItem()->indexOfChild(charts->currentItem());
// zap!
presets->removeAt(index);
delete charts->invisibleRootItem()->takeChild(index);
}
}
/*----------------------------------------------------------------------
* Write to charts.xml
*--------------------------------------------------------------------*/
void
LTMSettings::writeChartXML(QDir home, QList<LTMSettings> charts)
{
LTMChartParser::serialize(QString(home.path() + "/charts.xml"), charts);
}
/*----------------------------------------------------------------------
* Read charts.xml
*--------------------------------------------------------------------*/
void
LTMSettings::readChartXML(QDir home, QList<LTMSettings> &charts)
{
QFileInfo chartFile(home.absolutePath() + "/charts.xml");
QFile chartsFile;
// if it doesn't exist use our built-in default version
if (chartFile.exists())
chartsFile.setFileName(chartFile.filePath());
else
chartsFile.setFileName(":/xml/charts.xml");
QXmlInputSource source( &chartsFile );
QXmlSimpleReader xmlReader;
LTMChartParser( handler );
xmlReader.setContentHandler(&handler);
xmlReader.setErrorHandler(&handler);
xmlReader.parse( source );
charts = handler.getSettings();
}

155
src/LTMSettings.h Normal file
View File

@@ -0,0 +1,155 @@
/*
* 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_LTMSettings_h
#define _GC_LTMSettings_h 1
#include <QtGui>
#include <QList>
#include <qwt_plot.h>
#include <qwt_plot_marker.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_scale_draw.h>
#include <qwt_symbol.h>
class LTMTool;
class SummaryMetrics;
class MainWindow;
class RideMetric;
// group by settings
#define LTM_DAY 1
#define LTM_WEEK 2
#define LTM_MONTH 3
#define LTM_YEAR 4
// type of metric
// is it from the ridemetric factory or PMC stresscalculator
#define METRIC_DB 1
#define METRIC_PM 2
// We catalogue each metric and the curve settings etc here
class MetricDetail {
public:
MetricDetail() : type(METRIC_DB), name(""), metric(NULL), smooth(false), trend(false), topN(0),
baseline(0.0), curveStyle(QwtPlotCurve::Lines), symbolStyle(QwtSymbol::NoSymbol),
penColor(Qt::black), penAlpha(0), penWidth(1.0), penStyle(0),
brushColor(Qt::black), brushAlpha(0) {}
bool operator< (MetricDetail right) const { return name < right.name; }
int type;
QString symbol, name;
const RideMetric *metric;
QString uname, uunits; // user specified name and units (axis choice)
// user configurable settings
bool smooth, // smooth the curve
trend; // add a trend line
int topN; // highlight top N points
double baseline; // baseline for chart
// curve type and symbol
QwtPlotCurve::CurveStyle curveStyle; // how should this metric be plotted?
QwtSymbol::Style symbolStyle; // display a symbol
// pen
QColor penColor;
int penAlpha;
double penWidth;
int penStyle;
// brush
QColor brushColor;
int brushAlpha;
};
// used to maintain details about the metrics being plotted
class LTMSettings {
public:
void writeChartXML(QDir, QList<LTMSettings>);
void readChartXML(QDir, QList<LTMSettings>&charts);
QString name;
QString title;
QDateTime start;
QDateTime end;
int groupBy;
bool shadeZones;
QList<MetricDetail> metrics;
QList<SummaryMetrics> *data;
LTMTool *ltmTool;
};
class EditChartDialog : public QDialog
{
Q_OBJECT
public:
EditChartDialog(MainWindow *, LTMSettings *, QList<LTMSettings>);
public slots:
void okClicked();
void cancelClicked();
private:
MainWindow *mainWindow;
LTMSettings *settings;
QList<LTMSettings> presets;
QLineEdit *chartName;
QPushButton *okButton, *cancelButton;
};
class ChartManagerDialog : public QDialog
{
Q_OBJECT
public:
ChartManagerDialog(MainWindow *, QList<LTMSettings> *);
public slots:
void okClicked();
void cancelClicked();
void exportClicked();
void importClicked();
void upClicked();
void downClicked();
void renameClicked();
void deleteClicked();
private:
MainWindow *mainWindow;
QList<LTMSettings> *presets;
QLineEdit *chartName;
QTreeWidget *charts;
QPushButton *importButton, *exportButton;
QPushButton *upButton, *downButton, *renameButton, *deleteButton;
QPushButton *okButton, *cancelButton;
};
#endif

848
src/LTMTool.cpp Normal file
View File

@@ -0,0 +1,848 @@
/*
* 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
*/
#include "LTMTool.h"
#include "MainWindow.h"
#include "Settings.h"
#include "Units.h"
#include <assert.h>
#include <QApplication>
#include <QtGui>
// seasons support
#include "Season.h"
#include "SeasonParser.h"
#include <QXmlInputSource>
#include <QXmlSimpleReader>
LTMTool::LTMTool(MainWindow *parent, const QDir &home) : QWidget(parent), home(home), main(parent)
{
// get application settings
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
useMetricUnits = appsettings->value(GC_UNIT).toString() == "Metric";
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0,0,0,0);
dateRangeTree = new QTreeWidget;
dateRangeTree->setColumnCount(1);
dateRangeTree->setSelectionMode(QAbstractItemView::SingleSelection);
dateRangeTree->header()->hide();
dateRangeTree->setAlternatingRowColors (true);
dateRangeTree->setIndentation(5);
allDateRanges = new QTreeWidgetItem(dateRangeTree, ROOT_TYPE);
allDateRanges->setText(0, tr("Date Range"));
readSeasons();
dateRangeTree->expandItem(allDateRanges);
dateRangeTree->setContextMenuPolicy(Qt::CustomContextMenu);
metricTree = new QTreeWidget;
metricTree->setColumnCount(1);
metricTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
metricTree->header()->hide();
metricTree->setAlternatingRowColors (true);
metricTree->setIndentation(5);
allMetrics = new QTreeWidgetItem(metricTree, ROOT_TYPE);
allMetrics->setText(0, tr("Metric"));
metricTree->setContextMenuPolicy(Qt::CustomContextMenu);
// initialise the metrics catalogue and user selector
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i = 0; i < factory.metricCount(); ++i) {
// metrics catalogue and settings
MetricDetail adds;
QColor cHSV;
adds.symbol = factory.metricName(i);
adds.metric = factory.rideMetric(factory.metricName(i));
qsrand(QTime::currentTime().msec());
cHSV.setHsv((i%6)*(255/(factory.metricCount()/5)), 255, 255);
adds.penColor = cHSV.convertTo(QColor::Rgb);
adds.curveStyle = curveStyle(factory.metricType(i));
adds.symbolStyle = symbolStyle(factory.metricType(i));
adds.smooth = false;
adds.trend = false;
adds.topN = 5; // show top 5 by default always
QTextEdit processHTML(adds.metric->name()); // process html encoding of(TM)
adds.name = processHTML.toPlainText();
// set default for the user overiddable fields
adds.uname = adds.name;
adds.uunits = adds.metric->units(useMetricUnits);
// default units to metric name if it is blank
if (adds.uunits == "") adds.uunits = adds.name;
metrics.append(adds);
}
//
// Add PM metrics, which are calculated over the metric dataset
//
// SKIBA LTS
MetricDetail skibaLTS;
skibaLTS.type = METRIC_PM;
skibaLTS.symbol = "skiba_lts";
skibaLTS.metric = NULL; // not a factory metric
skibaLTS.penColor = QColor(Qt::blue);
skibaLTS.curveStyle = QwtPlotCurve::Lines;
skibaLTS.symbolStyle = QwtSymbol::NoSymbol;
skibaLTS.smooth = false;
skibaLTS.trend = false;
skibaLTS.topN = 5;
skibaLTS.uname = skibaLTS.name = "Skiba Long Term Stress";
skibaLTS.uunits = "Stress";
metrics.append(skibaLTS);
MetricDetail skibaSTS;
skibaSTS.type = METRIC_PM;
skibaSTS.symbol = "skiba_sts";
skibaSTS.metric = NULL; // not a factory metric
skibaSTS.penColor = QColor(Qt::magenta);
skibaSTS.curveStyle = QwtPlotCurve::Lines;
skibaSTS.symbolStyle = QwtSymbol::NoSymbol;
skibaSTS.smooth = false;
skibaSTS.trend = false;
skibaSTS.topN = 5;
skibaSTS.uname = skibaSTS.name = "Skiba Short Term Stress";
skibaSTS.uunits = "Stress";
metrics.append(skibaSTS);
MetricDetail skibaSB;
skibaSB.type = METRIC_PM;
skibaSB.symbol = "skiba_sb";
skibaSB.metric = NULL; // not a factory metric
skibaSB.penColor = QColor(Qt::yellow);
skibaSB.curveStyle = QwtPlotCurve::Steps;
skibaSB.symbolStyle = QwtSymbol::NoSymbol;
skibaSB.smooth = false;
skibaSB.trend = false;
skibaSB.topN = 1;
skibaSB.uname = skibaSB.name = "Skiba Stress Balance";
skibaSB.uunits = "Stress Balance";
metrics.append(skibaSB);
MetricDetail skibaSTR;
skibaSTR.type = METRIC_PM;
skibaSTR.symbol = "skiba_sr";
skibaSTR.metric = NULL; // not a factory metric
skibaSTR.penColor = QColor(Qt::darkGreen);
skibaSTR.curveStyle = QwtPlotCurve::Steps;
skibaSTR.symbolStyle = QwtSymbol::NoSymbol;
skibaSTR.smooth = false;
skibaSTR.trend = false;
skibaSTR.topN = 1;
skibaSTR.uname = skibaSTR.name = "Skiba STS Ramp";
skibaSTR.uunits = "Ramp";
metrics.append(skibaSTR);
MetricDetail skibaLTR;
skibaLTR.type = METRIC_PM;
skibaLTR.symbol = "skiba_lr";
skibaLTR.metric = NULL; // not a factory metric
skibaLTR.penColor = QColor(Qt::darkBlue);
skibaLTR.curveStyle = QwtPlotCurve::Steps;
skibaLTR.symbolStyle = QwtSymbol::NoSymbol;
skibaLTR.smooth = false;
skibaLTR.trend = false;
skibaLTR.topN = 1;
skibaLTR.uname = skibaLTR.name = "Skiba LTS Ramp";
skibaLTR.uunits = "Ramp";
metrics.append(skibaLTR);
// DANIELS LTS
MetricDetail danielsLTS;
danielsLTS.type = METRIC_PM;
danielsLTS.symbol = "daniels_lts";
danielsLTS.metric = NULL; // not a factory metric
danielsLTS.penColor = QColor(Qt::blue);
danielsLTS.curveStyle = QwtPlotCurve::Lines;
danielsLTS.symbolStyle = QwtSymbol::NoSymbol;
danielsLTS.smooth = false;
danielsLTS.trend = false;
danielsLTS.topN = 5;
danielsLTS.uname = danielsLTS.name = "Daniels Long Term Stress";
danielsLTS.uunits = "Stress";
metrics.append(danielsLTS);
MetricDetail danielsSTS;
danielsSTS.type = METRIC_PM;
danielsSTS.symbol = "daniels_sts";
danielsSTS.metric = NULL; // not a factory metric
danielsSTS.penColor = QColor(Qt::magenta);
danielsSTS.curveStyle = QwtPlotCurve::Lines;
danielsSTS.symbolStyle = QwtSymbol::NoSymbol;
danielsSTS.smooth = false;
danielsSTS.trend = false;
danielsSTS.topN = 5;
danielsSTS.uname = danielsSTS.name = "Daniels Short Term Stress";
danielsSTS.uunits = "Stress";
metrics.append(danielsSTS);
MetricDetail danielsSB;
danielsSB.type = METRIC_PM;
danielsSB.symbol = "daniels_sb";
danielsSB.metric = NULL; // not a factory metric
danielsSB.penColor = QColor(Qt::yellow);
danielsSB.curveStyle = QwtPlotCurve::Steps;
danielsSB.symbolStyle = QwtSymbol::NoSymbol;
danielsSB.smooth = false;
danielsSB.trend = false;
danielsSB.topN = 1;
danielsSB.uname = danielsSB.name = "Daniels Stress Balance";
danielsSB.uunits = "Stress Balance";
metrics.append(danielsSB);
MetricDetail danielsSTR;
danielsSTR.type = METRIC_PM;
danielsSTR.symbol = "daniels_sr";
danielsSTR.metric = NULL; // not a factory metric
danielsSTR.penColor = QColor(Qt::darkGreen);
danielsSTR.curveStyle = QwtPlotCurve::Steps;
danielsSTR.symbolStyle = QwtSymbol::NoSymbol;
danielsSTR.smooth = false;
danielsSTR.trend = false;
danielsSTR.topN = 1;
danielsSTR.uname = danielsSTR.name = "Daniels STS Ramp";
danielsSTR.uunits = "Ramp";
metrics.append(danielsSTR);
MetricDetail danielsLTR;
danielsLTR.type = METRIC_PM;
danielsLTR.symbol = "daniels_lr";
danielsLTR.metric = NULL; // not a factory metric
danielsLTR.penColor = QColor(Qt::darkBlue);
danielsLTR.curveStyle = QwtPlotCurve::Steps;
danielsLTR.symbolStyle = QwtSymbol::NoSymbol;
danielsLTR.smooth = false;
danielsLTR.trend = false;
danielsLTR.topN = 1;
danielsLTR.uname = danielsLTR.name = "Daniels LTS Ramp";
danielsLTR.uunits = "Ramp";
metrics.append(danielsLTR);
// sort the list
qSort(metrics);
foreach(MetricDetail metric, metrics) {
QTreeWidgetItem *add;
add = new QTreeWidgetItem(allMetrics, METRIC_TYPE);
add->setText(0, metric.name);
}
metricTree->expandItem(allMetrics);
configChanged(); // will reset the metric tree
ltmSplitter = new QSplitter;
ltmSplitter->setContentsMargins(0,0,0,0);
ltmSplitter->setOrientation(Qt::Vertical);
mainLayout->addWidget(ltmSplitter);
ltmSplitter->addWidget(dateRangeTree);
ltmSplitter->setCollapsible(0, true);
ltmSplitter->addWidget(metricTree);
ltmSplitter->setCollapsible(1, true);
connect(dateRangeTree,SIGNAL(itemSelectionChanged()),
this, SLOT(dateRangeTreeWidgetSelectionChanged()));
connect(metricTree,SIGNAL(itemSelectionChanged()),
this, SLOT(metricTreeWidgetSelectionChanged()));
connect(main, SIGNAL(configChanged()),
this, SLOT(configChanged()));
connect(dateRangeTree,SIGNAL(customContextMenuRequested(const QPoint &)),
this, SLOT(dateRangePopup(const QPoint &)));
connect(metricTree,SIGNAL(customContextMenuRequested(const QPoint &)),
this, SLOT(metricTreePopup(const QPoint &)));
connect(dateRangeTree,SIGNAL(itemChanged(QTreeWidgetItem *,int)),
this, SLOT(dateRangeChanged(QTreeWidgetItem*, int)));
}
void
LTMTool::selectDateRange(int index)
{
allDateRanges->child(index)->setSelected(true);
}
QwtPlotCurve::CurveStyle
LTMTool::curveStyle(RideMetric::MetricType type)
{
switch (type) {
case RideMetric::Average : return QwtPlotCurve::Lines;
case RideMetric::Total : return QwtPlotCurve::Steps;
case RideMetric::Peak : return QwtPlotCurve::Lines;
default : return QwtPlotCurve::Lines;
}
}
QwtSymbol::Style
LTMTool::symbolStyle(RideMetric::MetricType type)
{
switch (type) {
case RideMetric::Average : return QwtSymbol::Ellipse;
case RideMetric::Total : return QwtSymbol::Ellipse;
case RideMetric::Peak : return QwtSymbol::Rect;
default : return QwtSymbol::XCross;
}
}
void
LTMTool::configChanged()
{
}
/*----------------------------------------------------------------------
* Selections Made
*----------------------------------------------------------------------*/
void
LTMTool::dateRangeTreeWidgetSelectionChanged()
{
if (dateRangeTree->selectedItems().isEmpty()) dateRange = NULL;
else {
QTreeWidgetItem *which = dateRangeTree->selectedItems().first();
if (which != allDateRanges) {
dateRange = &seasons.at(allDateRanges->indexOfChild(which));
} else {
dateRange = NULL;
}
}
dateRangeSelected(dateRange);
}
void
LTMTool::metricTreeWidgetSelectionChanged()
{
metricSelected();
}
/*----------------------------------------------------------------------
* Date ranges from Seasons.xml
*--------------------------------------------------------------------*/
void LTMTool::readSeasons()
{
QFile seasonFile(home.absolutePath() + "/seasons.xml");
QXmlInputSource source( &seasonFile );
QXmlSimpleReader xmlReader;
SeasonParser( handler );
xmlReader.setContentHandler(&handler);
xmlReader.setErrorHandler(&handler);
xmlReader.parse( source );
seasons = handler.getSeasons();
int i;
for (i=0; i <seasons.count(); i++) {
Season season = seasons.at(i);
QTreeWidgetItem *add = new QTreeWidgetItem(allDateRanges, USER_DATE);
add->setText(0, season.getName());
}
Season season;
QDate today = QDate::currentDate();
QDate eom = QDate::QDate(today.year(), today.month(), today.daysInMonth());
// add Default Date Ranges
season.setName(tr("All Dates"));
season.setType(Season::temporary);
season.setStart(QDate::currentDate().addYears(-50));
season.setEnd(QDate::currentDate().addYears(50));
seasons.append(season);
season.setName(tr("This Year"));
season.setType(Season::temporary);
season.setStart(QDate::QDate(today.year(), 1,1));
season.setEnd(QDate::QDate(today.year(), 12, 31));
seasons.append(season);
season.setName(tr("This Month"));
season.setType(Season::temporary);
season.setStart(QDate::QDate(today.year(), today.month(),1));
season.setEnd(eom);
seasons.append(season);
season.setName(tr("This Week"));
season.setType(Season::temporary);
// from Mon-Sun
QDate wstart = QDate::currentDate();
wstart = wstart.addDays(Qt::Monday - wstart.dayOfWeek());
QDate wend = wstart.addDays(6); // first day + 6 more
season.setStart(wstart);
season.setEnd(wend);
seasons.append(season);
season.setName(tr("Last 7 days"));
season.setType(Season::temporary);
season.setStart(today.addDays(-6)); // today plus previous 6
season.setEnd(today);
seasons.append(season);
season.setName(tr("Last 14 days"));
season.setType(Season::temporary);
season.setStart(today.addDays(-13));
season.setEnd(today);
seasons.append(season);
season.setName(tr("Last 28 days"));
season.setType(Season::temporary);
season.setStart(today.addDays(-27));
season.setEnd(today);
seasons.append(season);
season.setName(tr("Last 3 months"));
season.setType(Season::temporary);
season.setEnd(today);
season.setStart(today.addMonths(-3));
seasons.append(season);
season.setName(tr("Last 6 months"));
season.setType(Season::temporary);
season.setEnd(today);
season.setStart(today.addMonths(-6));
seasons.append(season);
season.setName(tr("Last 12 months"));
season.setType(Season::temporary);
season.setEnd(today);
season.setStart(today.addMonths(-12));
seasons.append(season);
for (;i <seasons.count(); i++) {
Season season = seasons.at(i);
QTreeWidgetItem *add = new QTreeWidgetItem(allDateRanges, SYS_DATE);
add->setText(0, season.getName());
}
dateRangeTree->expandItem(allDateRanges);
}
QString
LTMTool::metricName(QTreeWidgetItem *item)
{
int idx = allMetrics->indexOfChild(item);
if (idx >= 0) return metrics[idx].name;
else return tr("Unknown Metric");
}
QString
LTMTool::metricSymbol(QTreeWidgetItem *item)
{
int idx = allMetrics->indexOfChild(item);
if (idx >= 0) return metrics[idx].symbol;
else return tr("Unknown Metric");
}
MetricDetail
LTMTool::metricDetails(QTreeWidgetItem *item)
{
MetricDetail empty;
int idx = allMetrics->indexOfChild(item);
if (idx >= 0) return metrics[idx];
else return empty;
}
int
LTMTool::newSeason(QString name, QDate start, QDate end, int type)
{
Season add;
add.setName(name);
add.setStart(start);
add.setEnd(end);
add.setType(type);
seasons.insert(0, add);
// save changes away
writeSeasons();
QTreeWidgetItem *item = new QTreeWidgetItem(USER_DATE);
item->setText(0, add.getName());
allDateRanges->insertChild(0, item);
return 0; // always add at the top
}
void
LTMTool::updateSeason(int index, QString name, QDate start, QDate end, int type)
{
seasons[index].setName(name);
seasons[index].setStart(start);
seasons[index].setEnd(end);
seasons[index].setType(type);
allDateRanges->child(index)->setText(0, name);
// save changes away
writeSeasons();
}
void
LTMTool::dateRangePopup(QPoint pos)
{
QTreeWidgetItem *item = dateRangeTree->itemAt(pos);
if (item != NULL && item->type() != ROOT_TYPE && item->type() != SYS_DATE) {
// save context
activeDateRange = item;
// create context menu
QMenu menu(dateRangeTree);
QAction *rename = new QAction(tr("Rename range"), dateRangeTree);
QAction *edit = new QAction(tr("Edit details"), dateRangeTree);
QAction *del = new QAction(tr("Delete range"), dateRangeTree);
menu.addAction(rename);
menu.addAction(edit);
menu.addAction(del);
// connect menu to functions
connect(rename, SIGNAL(triggered(void)), this, SLOT(renameRange(void)));
connect(edit, SIGNAL(triggered(void)), this, SLOT(editRange(void)));
connect(del, SIGNAL(triggered(void)), this, SLOT(deleteRange(void)));
// execute the menu
menu.exec(dateRangeTree->mapToGlobal(pos));
}
}
void
LTMTool::renameRange()
{
// go edit the name
activeDateRange->setFlags(activeDateRange->flags() | Qt::ItemIsEditable);
dateRangeTree->editItem(activeDateRange, 0);
}
void
LTMTool::dateRangeChanged(QTreeWidgetItem*item, int)
{
if (item != activeDateRange) return;
int index = allDateRanges->indexOfChild(item);
seasons[index].setName(item->text(0));
// save changes away
writeSeasons();
// signal date selected changed
dateRangeSelected(&seasons[index]);
}
void
LTMTool::editRange()
{
// throw up modal dialog box to edit all the season
// fields.
int index = allDateRanges->indexOfChild(activeDateRange);
EditSeasonDialog dialog(main, &seasons[index]);
if (dialog.exec()) {
// update name
activeDateRange->setText(0, seasons[index].getName());
// save changes away
writeSeasons();
// signal its changed!
dateRangeSelected(&seasons[index]);
}
}
void
LTMTool::deleteRange()
{
// now delete!
int index = allDateRanges->indexOfChild(activeDateRange);
delete allDateRanges->takeChild(index);
seasons.removeAt(index);
// now update season.xml
writeSeasons();
}
void
LTMTool::writeSeasons()
{
// update seasons.xml
QString file = QString(home.absolutePath() + "/seasons.xml");
SeasonParser::serialize(file, seasons);
}
void
LTMTool::metricTreePopup(QPoint pos)
{
QTreeWidgetItem *item = metricTree->itemAt(pos);
if (item != NULL && item->type() != ROOT_TYPE) {
// save context
activeMetric = item;
// create context menu
QMenu menu(metricTree);
QAction *color = new QAction(tr("Pick Color"), metricTree);
QAction *edit = new QAction(tr("Settings"), metricTree);
menu.addAction(color);
menu.addAction(edit);
// connect menu to functions
connect(color, SIGNAL(triggered(void)), this, SLOT(colorPicker(void)));
connect(edit, SIGNAL(triggered(void)), this, SLOT(editMetric(void)));
// execute the menu
menu.exec(metricTree->mapToGlobal(pos));
}
}
void
LTMTool::editMetric()
{
int index = allMetrics->indexOfChild(activeMetric);
EditMetricDetailDialog dialog(main, &metrics[index]);
if (dialog.exec()) {
// notify of change
metricSelected();
}
}
void
LTMTool::colorPicker()
{
int index = allMetrics->indexOfChild(activeMetric);
QColorDialog picker(main);
picker.setCurrentColor(metrics[index].penColor);
QColor color = picker.getColor();
// if we got a good color use it and notify others
if (color.isValid()) {
metrics[index].penColor = color;
metricSelected();
}
}
void
LTMTool::applySettings(LTMSettings *settings)
{
disconnect(metricTree,SIGNAL(itemSelectionChanged()), this, SLOT(metricTreeWidgetSelectionChanged()));
metricTree->clearSelection(); // de-select everything
foreach (MetricDetail metricDetail, settings->metrics) {
// get index for the symbol
for (int i=0; i<metrics.count(); i++) {
if (metrics[i].symbol == metricDetail.symbol) {
// rather than copy each member one by one
// we save the ridemetric pointer and metric type
// copy across them all then re-instate the saved point
RideMetric *saved = (RideMetric*)metrics[i].metric;
int type = metrics[i].type;
metrics[i] = metricDetail;
metrics[i].metric = saved;
metrics[i].type = type;
// units may need to be adjusted if
// usemetricUnits changed since charts.xml was
// written
if (saved && saved->conversion() != 1.0 &&
metrics[i].uunits.contains(saved->units(!useMetricUnits)))
metrics[i].uunits.replace(saved->units(!useMetricUnits), saved->units(useMetricUnits));
// select it on the tool
allMetrics->child(i)->setSelected(true);
break;
}
}
}
connect(metricTree,SIGNAL(itemSelectionChanged()), this, SLOT(metricTreeWidgetSelectionChanged()));
metricTreeWidgetSelectionChanged();
}
/*----------------------------------------------------------------------
* EDIT METRIC DETAIL DIALOG
*--------------------------------------------------------------------*/
EditMetricDetailDialog::EditMetricDetailDialog(MainWindow *mainWindow, MetricDetail *metricDetail) :
QDialog(mainWindow, Qt::Dialog), mainWindow(mainWindow), metricDetail(metricDetail)
{
setWindowTitle(tr("Settings"));
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// Metric Name
mainLayout->addSpacing(5);
QLabel *metricName = new QLabel(metricDetail->name, this);
metricName->setAlignment(Qt::AlignHCenter);
QFont def;
def.setBold(true);
metricName->setFont(def);
mainLayout->addWidget(metricName);
mainLayout->addSpacing(5);
// Grid
QGridLayout *grid = new QGridLayout;
QLabel *name = new QLabel("Name");
QLabel *units = new QLabel("Axis Label / Units");
userName = new QLineEdit(this);
userName->setText(metricDetail->uname);
userUnits = new QLineEdit(this);
userUnits->setText(metricDetail->uunits);
QLabel *style = new QLabel("Curve");
curveStyle = new QComboBox(this);
curveStyle->addItem("Bar", QwtPlotCurve::Steps);
curveStyle->addItem("Line", QwtPlotCurve::Lines);
curveStyle->addItem("Sticks", QwtPlotCurve::Sticks);
curveStyle->addItem("Dots", QwtPlotCurve::Dots);
curveStyle->setCurrentIndex(curveStyle->findData(metricDetail->curveStyle));
QLabel *symbol = new QLabel("Symbol");
curveSymbol = new QComboBox(this);
curveSymbol->addItem("None", QwtSymbol::NoSymbol);
curveSymbol->addItem("Circle", QwtSymbol::Ellipse);
curveSymbol->addItem("Square", QwtSymbol::Rect);
curveSymbol->addItem("Diamond", QwtSymbol::Diamond);
curveSymbol->addItem("Triangle", QwtSymbol::Triangle);
curveSymbol->addItem("Cross", QwtSymbol::XCross);
curveSymbol->addItem("Hexagon", QwtSymbol::Hexagon);
curveSymbol->addItem("Star", QwtSymbol::Star1);
curveSymbol->setCurrentIndex(curveSymbol->findData(metricDetail->symbolStyle));
QLabel *color = new QLabel("Color");
curveColor = new QPushButton(this);
// color background...
penColor = metricDetail->penColor;
setButtonIcon(penColor);
QLabel *topN = new QLabel("Highlight Best");
showBest = new QDoubleSpinBox(this);
showBest->setDecimals(0);
showBest->setMinimum(0);
showBest->setMaximum(999);
showBest->setSingleStep(1.0);
showBest->setValue(metricDetail->topN);
QLabel *baseline = new QLabel("Baseline");
baseLine = new QDoubleSpinBox(this);
baseLine->setDecimals(0);
baseLine->setMinimum(-999999);
baseLine->setMaximum(999999);
baseLine->setSingleStep(1.0);
baseLine->setValue(metricDetail->baseline);
curveSmooth = new QCheckBox("Smooth Curve", this);
curveSmooth->setChecked(metricDetail->smooth);
curveTrend = new QCheckBox("Trend Line", this);
curveTrend->setChecked(metricDetail->trend);
// add to grid
grid->addWidget(name, 0,0);
grid->addWidget(userName, 0,1);
grid->addWidget(units, 1,0);
grid->addWidget(userUnits, 1,1);
grid->addWidget(style, 2,0);
grid->addWidget(curveStyle, 2,1);
grid->addWidget(symbol, 3,0);
grid->addWidget(curveSymbol, 3,1);
grid->addWidget(color, 4,0);
grid->addWidget(curveColor, 4,1);
grid->addWidget(topN, 5,0);
grid->addWidget(showBest, 5,1);
grid->addWidget(baseline, 6, 0);
grid->addWidget(baseLine, 6,1);
grid->addWidget(curveSmooth, 7,1);
grid->addWidget(curveTrend, 8,1);
mainLayout->addLayout(grid);
mainLayout->addStretch();
// Buttons
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
applyButton = new QPushButton(tr("&OK"), this);
cancelButton = new QPushButton(tr("&Cancel"), this);
buttonLayout->addWidget(cancelButton);
buttonLayout->addWidget(applyButton);
mainLayout->addLayout(buttonLayout);
// connect up slots
connect(applyButton, SIGNAL(clicked()), this, SLOT(applyClicked()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
connect(curveColor, SIGNAL(clicked()), this, SLOT(colorClicked()));
}
// uh. i hate enums when you need to modify from ints
// this is fugly and prone to error. Tied directly to the
// combo box above. all better solutions gratefully received
// but wanna get this code running for now
static QwtPlotCurve::CurveStyle styleMap[] = { QwtPlotCurve::Steps, QwtPlotCurve::Lines,
QwtPlotCurve::Sticks, QwtPlotCurve::Dots };
static QwtSymbol::Style symbolMap[] = { QwtSymbol::NoSymbol, QwtSymbol::Ellipse, QwtSymbol::Rect,
QwtSymbol::Diamond, QwtSymbol::Triangle, QwtSymbol::XCross,
QwtSymbol::Hexagon, QwtSymbol::Star1 };
void
EditMetricDetailDialog::applyClicked()
{
// get the values back
metricDetail->smooth = curveSmooth->isChecked();
metricDetail->trend = curveTrend->isChecked();
metricDetail->topN = showBest->value();
metricDetail->baseline = baseLine->value();
metricDetail->curveStyle = styleMap[curveStyle->currentIndex()];
metricDetail->symbolStyle = symbolMap[curveSymbol->currentIndex()];
metricDetail->penColor = penColor;
metricDetail->uname = userName->text();
metricDetail->uunits = userUnits->text();
accept();
}
void
EditMetricDetailDialog::cancelClicked()
{
reject();
}
void
EditMetricDetailDialog::colorClicked()
{
QColorDialog picker(mainWindow);
picker.setCurrentColor(penColor);
QColor color = picker.getColor();
if (color.isValid()) {
setButtonIcon(penColor=color);
}
}
void
EditMetricDetailDialog::setButtonIcon(QColor color)
{
// create an icon
QPixmap pix(24, 24);
QPainter painter(&pix);
if (color.isValid()) {
painter.setPen(Qt::gray);
painter.setBrush(QBrush(color));
painter.drawRect(0, 0, 24, 24);
}
QIcon icon;
icon.addPixmap(pix);
curveColor->setIcon(icon);
curveColor->setContentsMargins(2,2,2,2);
curveColor->setFixedWidth(34);
}

140
src/LTMTool.h Normal file
View File

@@ -0,0 +1,140 @@
/*
* 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_LTMTool_h
#define _GC_LTMTool_h 1
#include "MainWindow.h"
#include "Season.h"
#include "RideMetric.h"
#include "LTMSettings.h"
#include <QDir>
#include <QtGui>
// tree widget types
#define ROOT_TYPE 1
#define DATE_TYPE 2
#define METRIC_TYPE 3
#define SYS_DATE 1
#define USER_DATE 2
class LTMTool : public QWidget
{
Q_OBJECT
public:
LTMTool(MainWindow *parent, const QDir &home);
const Season *currentDateRange() { return dateRange; }
void selectDateRange(int);
QList<QTreeWidgetItem *> selectedMetrics() { return metricTree->selectedItems(); }
QString metricName(QTreeWidgetItem *);
QString metricSymbol(QTreeWidgetItem *);
MetricDetail metricDetails(QTreeWidgetItem *);
// allow others to create and update season structures
int newSeason(QString, QDate, QDate, int);
void updateSeason(int, QString, QDate, QDate, int);
// apply settings to the metric selector
void applySettings(LTMSettings *);
signals:
void dateRangeSelected(const Season *);
void metricSelected();
private slots:
void dateRangeTreeWidgetSelectionChanged();
void dateRangePopup(QPoint);
void dateRangeChanged(QTreeWidgetItem *, int);
void renameRange();
void editRange();
void deleteRange();
void metricTreeWidgetSelectionChanged();
void metricTreePopup(QPoint);
void colorPicker();
void editMetric();
void configChanged();
void readSeasons();
void writeSeasons();
private:
QwtPlotCurve::CurveStyle curveStyle(RideMetric::MetricType);
QwtSymbol::Style symbolStyle(RideMetric::MetricType);
const QDir home;
MainWindow *main;
bool useMetricUnits;
QList<Season> seasons;
QTreeWidget *dateRangeTree;
QTreeWidgetItem *allDateRanges;
const Season *dateRange;
QList<MetricDetail> metrics;
QTreeWidget *metricTree;
QTreeWidgetItem *allMetrics;
QTreeWidgetItem *activeDateRange; // when using context menus
QTreeWidgetItem *activeMetric; // when using context menus
QSplitter *ltmSplitter;
};
class EditMetricDetailDialog : public QDialog
{
Q_OBJECT
public:
EditMetricDetailDialog(MainWindow *, MetricDetail *);
public slots:
void colorClicked();
void applyClicked();
void cancelClicked();
private:
MainWindow *mainWindow;
MetricDetail *metricDetail;
QLineEdit *userName,
*userUnits;
QComboBox *curveStyle,
*curveSymbol;
QPushButton *curveColor;
QDoubleSpinBox *showBest,
*baseLine;
QCheckBox *curveSmooth,
*curveTrend;
QPushButton *applyButton, *cancelButton;
QColor penColor; // chosen from color Picker
void setButtonIcon(QColor);
};
#endif // _GC_LTMTool_h

56
src/LTMTrend.cpp Normal file
View File

@@ -0,0 +1,56 @@
/*
* 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
*/
#include <math.h>
#include <float.h>
#include "LTMTrend.h"
#include <QDebug>
LTMTrend::LTMTrend(double *xdata, double *ydata, int count) :
points(0.0), sumX(0.0), sumY(0.0), sumXsquared(0.0),
sumYsquared(0.0), sumXY(0.0), a(0.0), b(0.0)
{
if (count == 0) return;
for (int i = 0; i < count; i++) addXY(xdata[i], ydata[i]);
}
void
LTMTrend::addXY(double& x, double& y)
{
points++;
sumX += x;
sumY += y;
sumXsquared += x * x;
sumYsquared += y * y;
sumXY += x * y;
calc();
}
void
LTMTrend::calc()
{
if (points > 2) {
if (fabs( double(points) * sumXsquared - sumX * sumX) > DBL_EPSILON) {
b = ( double(points) * sumXY - sumY * sumX) /
( double(points) * sumXsquared - sumX * sumX);
a = (sumY - b * sumX) / double(points);
}
}
}

43
src/LTMTrend.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* 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_LTMTrend_h
#define _GC_LTMTrend_h 1
class LTMTrend
{
public:
// Constructor using arrays of x values and y values
LTMTrend(double *, double *, int);
void addXY(double&, double&);
double getYforX(double x) const { return (a + b * x); }
protected:
long points;
double sumX, sumY;
double sumXsquared,
sumYsquared;
double sumXY;
double a, b; // a = intercept, b = slope
void calc(); // calculate coefficients
};
#endif

306
src/LTMWindow.cpp Normal file
View File

@@ -0,0 +1,306 @@
/*
* 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
*/
#include "LTMWindow.h"
#include "LTMTool.h"
#include "LTMPlot.h"
#include "LTMSettings.h"
#include "MainWindow.h"
#include "SummaryMetrics.h"
#include "Settings.h"
#include "math.h"
#include "Units.h" // for MILES_PER_KM
#include <QtGui>
#include <QString>
#include <qwt_plot_panner.h>
#include <qwt_plot_zoomer.h>
#include <qwt_plot_picker.h>
#include <qwt_plot_marker.h>
LTMWindow::LTMWindow(MainWindow *parent, bool useMetricUnits, const QDir &home) :
QWidget(parent), main(parent), home(home),
useMetricUnits(useMetricUnits), active(false), dirty(true), metricDB(NULL)
{
QVBoxLayout *mainLayout = new QVBoxLayout;
setLayout(mainLayout);
// widgets
ltmPlot = new LTMPlot(this, main, home);
ltmZoomer = new QwtPlotZoomer(ltmPlot->canvas());
ltmZoomer->setRubberBand(QwtPicker::RectRubberBand);
ltmZoomer->setRubberBandPen(QColor(Qt::black));
ltmZoomer->setSelectionFlags(QwtPicker::DragSelection
| QwtPicker::CornerToCorner);
ltmZoomer->setTrackerMode(QwtPicker::AlwaysOff);
ltmZoomer->setEnabled(false);
ltmZoomer->setMousePattern(QwtEventPattern::MouseSelect2,
Qt::RightButton, Qt::ControlModifier);
ltmZoomer->setMousePattern(QwtEventPattern::MouseSelect3,
Qt::RightButton);
picker = new LTMToolTip(QwtPlot::xBottom, QwtPlot::yLeft,
QwtPicker::PointSelection,
QwtPicker::VLineRubberBand,
QwtPicker::AlwaysOn,
ltmPlot->canvas(),
"");
picker->setMousePattern(QwtEventPattern::MouseSelect1,
Qt::LeftButton, Qt::ShiftModifier);
picker->setTrackerPen(QColor(Qt::black));
QColor inv(Qt::white);
inv.setAlpha(0);
picker->setRubberBandPen(inv); // make it invisible
picker->setEnabled(true);
_canvasPicker = new LTMCanvasPicker(ltmPlot);
ltmTool = new LTMTool(parent, home);
settings.ltmTool = ltmTool;
ltmSplitter = new QSplitter(this);
ltmSplitter->addWidget(ltmPlot);
ltmSplitter->addWidget(ltmTool);
// splitter sizing
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
QVariant splitterSizes = appsettings->value(GC_LTM_SPLITTER_SIZES);
if (splitterSizes != QVariant())
ltmSplitter->restoreState(splitterSizes.toByteArray());
else {
QList<int> sizes;
sizes.append(390);
sizes.append(150);
ltmSplitter->setSizes(sizes);
}
// initialise
settings.data = NULL;
settings.groupBy = LTM_DAY;
settings.shadeZones = true;
mainLayout->addWidget(ltmSplitter);
// controls
QHBoxLayout *controls = new QHBoxLayout;
saveButton = new QPushButton("Add");
manageButton = new QPushButton("Manage");
QLabel *presetLabel = new QLabel("Chart");
presetPicker = new QComboBox;
presetPicker->setSizeAdjustPolicy(QComboBox::AdjustToContents);
// read charts.xml and populate the picker
LTMSettings reader;
reader.readChartXML(home, presets);
for(int i=0; i<presets.count(); i++)
presetPicker->addItem(presets[i].name, i);
groupBy = new QComboBox;
groupBy->addItem("Days", LTM_DAY);
groupBy->addItem("Weeks", LTM_WEEK);
groupBy->addItem("Months", LTM_MONTH);
groupBy->addItem("Years", LTM_YEAR);
groupBy->setCurrentIndex(0);
shadeZones = new QCheckBox("Shade Zones");
shadeZones->setChecked(true);
controls->addWidget(saveButton);
controls->addWidget(manageButton);
controls->addStretch();
controls->addWidget(presetLabel);
controls->addWidget(presetPicker);
controls->addWidget(groupBy);
controls->addWidget(shadeZones);
controls->addStretch();
mainLayout->addLayout(controls);
connect(ltmTool, SIGNAL(dateRangeSelected(const Season *)), this, SLOT(dateRangeSelected(const Season *)));
connect(ltmTool, SIGNAL(metricSelected()), this, SLOT(metricSelected()));
connect(ltmSplitter, SIGNAL(splitterMoved(int,int)), this, SLOT(splitterMoved()));
connect(groupBy, SIGNAL(currentIndexChanged(int)), this, SLOT(groupBySelected(int)));
connect(saveButton, SIGNAL(clicked(bool)), this, SLOT(saveClicked(void)));
connect(manageButton, SIGNAL(clicked(bool)), this, SLOT(manageClicked(void)));
connect(presetPicker, SIGNAL(currentIndexChanged(int)), this, SLOT(chartSelected(int)));
connect(shadeZones, SIGNAL(stateChanged(int)), this, SLOT(shadeZonesClicked(int)));
// connect pickers to ltmPlot
connect(_canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), ltmPlot, SLOT(pointHover(QwtPlotCurve*, int)));
connect(picker, SIGNAL(moved(QPoint)), ltmPlot, SLOT(pickerMoved(QPoint)));
connect(picker, SIGNAL(appended(const QPoint &)), ltmPlot, SLOT(pickerAppended(const QPoint &)));
// config changes or ride file activities cause a redraw/refresh (but only if active)
connect(main, SIGNAL(rideAdded(RideItem*)), this, SLOT(refresh(void)));
connect(main, SIGNAL(rideDeleted(RideItem*)), this, SLOT(refresh(void)));
connect(main, SIGNAL(configChanged()), this, SLOT(refresh()));
}
LTMWindow::~LTMWindow()
{
if (metricDB != NULL) delete metricDB;
}
void
LTMWindow::setActive(bool me)
{
active = me;
if (active == true && metricDB == NULL) {
metricDB = new MetricAggregator(main, home, main->zones());
// mimic user first selection now that
// we are active - choose a chart and
// use the first available date range
ltmTool->selectDateRange(0);
chartSelected(0);
} else if (active == true && dirty == true) {
// plot needs to be redrawn
refresh();
}
}
void
LTMWindow::refreshPlot()
{
if (active == true) ltmPlot->setData(&settings);
}
// total redraw, reread data etc
void
LTMWindow::refresh()
{
// if config has changed get new useMetricUnits
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
useMetricUnits = appsettings->value(GC_UNIT).toString() == "Metric";
// refresh for changes to ridefiles / zones
if (active == true && metricDB != NULL) {
results.clear(); // clear any old data
results = metricDB->getAllMetricsFor(settings.start, settings.end);
refreshPlot();
dirty = false;
} else {
dirty = true;
}
}
void
LTMWindow::metricSelected()
{
// wipe existing settings
settings.metrics.clear();
foreach(QTreeWidgetItem *metric, ltmTool->selectedMetrics()) {
if (metric->type() != ROOT_TYPE) {
QString symbol = ltmTool->metricSymbol(metric);
settings.metrics.append(ltmTool->metricDetails(metric));
}
}
refreshPlot();
}
void
LTMWindow::dateRangeSelected(const Season *selected)
{
if (selected) {
Season dateRange = *selected;
settings.start = QDateTime(dateRange.getStart(), QTime(0,0));
settings.end = QDateTime(dateRange.getEnd(), QTime(24,0,0));
settings.title = dateRange.getName();
settings.data = &results;
// if we want weeks and start is not a monday go back to the monday
int dow = dateRange.getStart().dayOfWeek();
if (settings.groupBy == LTM_WEEK && dow >1 && dateRange.getStart() != QDate())
settings.start = settings.start.addDays(-1*(dow-1));
// get the data
results.clear(); // clear any old data
results = metricDB->getAllMetricsFor(settings.start, settings.end);
refreshPlot();
}
}
void
LTMWindow::groupBySelected(int selected)
{
if (selected >= 0) {
settings.groupBy = groupBy->itemData(selected).toInt();
refreshPlot();
}
}
void
LTMWindow::shadeZonesClicked(int state)
{
settings.shadeZones = state;
refreshPlot();
}
void
LTMWindow::splitterMoved()
{
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
appsettings->setValue(GC_LTM_SPLITTER_SIZES, ltmSplitter->saveState());
}
void
LTMWindow::chartSelected(int selected)
{
if (selected >= 0) {
// what is the index of the chart?
int chartid = presetPicker->itemData(selected).toInt();
ltmTool->applySettings(&presets[chartid]);
}
}
void
LTMWindow::saveClicked()
{
EditChartDialog editor(main, &settings, presets);
if (editor.exec()) {
presets.append(settings);
settings.writeChartXML(main->home, presets);
presetPicker->insertItem(presets.count()-1, settings.name, presets.count()-1);
presetPicker->setCurrentIndex(presets.count()-1);
}
}
void
LTMWindow::manageClicked()
{
QList<LTMSettings> charts = presets; // get current
ChartManagerDialog editor(main, &charts);
if (editor.exec()) {
// wipe the current and add the new
presets = charts;
presetPicker->clear();
// update the presets to reflect the change
for(int i=0; i<presets.count(); i++)
presetPicker->addItem(presets[i].name, i);
// update charts.xml
settings.writeChartXML(main->home, presets);
}
}

123
src/LTMWindow.h Normal file
View File

@@ -0,0 +1,123 @@
/*
* 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_LTMWindow_h
#define _GC_LTMWindow_h 1
#include <QtGui>
#include <QTimer>
#include "MainWindow.h"
#include "MetricAggregator.h"
#include "Season.h"
#include "LTMPlot.h"
#include "LTMTool.h"
#include "LTMSettings.h"
#include "LTMCanvasPicker.h"
#include <math.h>
#include <qwt_plot_picker.h>
#include <qwt_text_engine.h>
// track the cursor and display the value for the chosen axis
class LTMToolTip : public QwtPlotPicker
{
public:
LTMToolTip(int xaxis, int yaxis, int sflags,
RubberBand rb, DisplayMode dm, QwtPlotCanvas *pc, QString fmt) :
QwtPlotPicker(xaxis, yaxis, sflags, rb, dm, pc),
format(fmt) {}
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 LTMWindow : public QWidget
{
Q_OBJECT
public:
MainWindow *main; // used by zones shader
LTMWindow(MainWindow *, bool, const QDir &);
~LTMWindow();
void setActive(bool);
LTMToolTip *toolTip() { return picker; }
public slots:
void refreshPlot();
void splitterMoved();
void dateRangeSelected(const Season *);
void metricSelected();
void groupBySelected(int);
void shadeZonesClicked(int);
void chartSelected(int);
void saveClicked();
void manageClicked();
void refresh();
private:
// passed from MainWindow
QDir home;
bool useMetricUnits;
// qwt picker
LTMToolTip *picker;
LTMCanvasPicker *_canvasPicker; // allow point selection/hover
// preset charts
QList<LTMSettings> presets;
// local state
bool active;
bool dirty;
LTMSettings settings; // all the plot settings
QList<SummaryMetrics> results;
// Widgets
QSplitter *ltmSplitter;
LTMPlot *ltmPlot;
QwtPlotZoomer *ltmZoomer;
LTMTool *ltmTool;
QComboBox *presetPicker;
QComboBox *groupBy;
QCheckBox *shadeZones;
QPushButton *saveButton;
QPushButton *manageButton;
MetricAggregator *metricDB;
};
#endif // _GC_LTMWindow_h

View File

@@ -27,6 +27,7 @@
#include "ConfigDialog.h"
#include "CriticalPowerWindow.h"
#include "GcRideFile.h"
#include "LTMWindow.h"
#include "PfPvWindow.h"
#include "DownloadRideDialog.h"
#include "ManualRideDialog.h"
@@ -289,10 +290,17 @@ MainWindow::MainWindow(const QDir &home) :
weeklySummaryWindow = new WeeklySummaryWindow(useMetricUnits, this);
tabWidget->addTab(weeklySummaryWindow, tr("Weekly Summary"));
//////////////////////// LTM ////////////////////////
// long term metrics window
metricDB = new MetricAggregator(this, home, zones()); // just to catch config updates!
ltmWindow = new LTMWindow(this, useMetricUnits, home);
tabWidget->addTab(ltmWindow, tr("Metrics"));
//////////////////////// Performance Manager ////////////////////////
performanceManagerWindow = new PerformanceManagerWindow(this);
tabWidget->addTab(performanceManagerWindow, tr("Performance Manager"));
tabWidget->addTab(performanceManagerWindow, tr("PM"));
///////////////////////////// Aerolab //////////////////////////////////
@@ -461,6 +469,7 @@ MainWindow::addRide(QString name, bool bSelect /*=true*/)
tabWidget->setCurrentIndex(0);
treeWidget->setCurrentItem(last);
}
rideAdded(last);
}
void
@@ -473,6 +482,8 @@ MainWindow::removeCurrentRide()
return;
RideItem *item = reinterpret_cast<RideItem*>(_item);
rideDeleted(item);
QTreeWidgetItem *itemToSelect = NULL;
for (x=0; x<allRides->childCount(); ++x)
{
@@ -1154,6 +1165,7 @@ MainWindow::tabChanged(int index)
{
criticalPowerWindow->setActive(index == 2);
performanceManagerWindow->setActive(tabWidget->widget(index) == performanceManagerWindow);
ltmWindow->setActive(tabWidget->widget(index) == ltmWindow);
#ifdef GC_HAVE_QWTPLOT3D
modelWindow->setActive(tabWidget->widget(index) == modelWindow);
#endif
@@ -1188,14 +1200,12 @@ MainWindow::aboutDialog()
void MainWindow::importRideToDB()
{
MetricAggregator aggregator;
aggregator.aggregateRides(home, zones());
metricDB->refreshMetrics();
}
void MainWindow::scanForMissing()
{
MetricAggregator aggregator;
aggregator.scanForMissing(home, zones());
metricDB->refreshMetrics();
}

View File

@@ -36,6 +36,8 @@ class PfPvWindow;
class QwtPlotPanner;
class QwtPlotPicker;
class QwtPlotZoomer;
class LTMWindow;
class MetricAggregator;
class ModelWindow;
class RealtimeWindow;
class RideFile;
@@ -94,6 +96,8 @@ class MainWindow : public QMainWindow
void zonesChanged();
void configChanged();
void viewChanged(int);
void rideAdded(RideItem *);
void rideDeleted(RideItem *);
private slots:
void rideTreeWidgetSelectionChanged();
@@ -158,6 +162,8 @@ class MainWindow : public QMainWindow
AllPlotWindow *allPlotWindow;
HistogramWindow *histogramWindow;
WeeklySummaryWindow *weeklySummaryWindow;
MetricAggregator *metricDB;
LTMWindow *ltmWindow;
CriticalPowerWindow *criticalPowerWindow;
ModelWindow *modelWindow;
AerolabWindow *aerolabWindow;

View File

@@ -16,8 +16,6 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "MetricAggregator.h"
#include "DBAccess.h"
#include "RideFile.h"
@@ -29,45 +27,109 @@
#include <assert.h>
#include <math.h>
#include <QtXml/QtXml>
#include <QProgressDialog>
MetricAggregator::MetricAggregator()
bool MetricAggregator::isclean = false;
MetricAggregator::MetricAggregator(MainWindow *parent, QDir home, const Zones *zones) : QWidget(parent), parent(parent), home(home), zones(zones)
{
dbaccess = new DBAccess(home);
connect(parent, SIGNAL(configChanged()), this, SLOT(update()));
connect(parent, SIGNAL(rideAdded(RideItem*)), this, SLOT(update(void)));
connect(parent, SIGNAL(rideDeleted(RideItem*)), this, SLOT(update(void)));
}
void MetricAggregator::aggregateRides(QDir home, const Zones *zones)
MetricAggregator::~MetricAggregator()
{
qDebug() << QDateTime::currentDateTime();
DBAccess *dbaccess = new DBAccess(home);
dbaccess->dropMetricTable();
dbaccess->createDatabase();
// close the database connection
if (dbaccess != NULL) {
dbaccess->closeConnection();
delete dbaccess;
}
}
/*----------------------------------------------------------------------
* Refresh the database -- only updates metrics when they are out
* of date or missing altogether or where
* the ride file no longer exists
*----------------------------------------------------------------------*/
void MetricAggregator::refreshMetrics()
{
// only if we have established a connection to the database
if (dbaccess == NULL || isclean==true) return;
// Get a list of the ride files
QRegExp rx = RideFileFactory::instance().rideFileRegExp();
QStringList errors;
QStringListIterator i(RideFileFactory::instance().listRideFiles(home));
QStringList filenames = RideFileFactory::instance().listRideFiles(home);
QStringListIterator i(filenames);
// get a Hash map of statistic records and timestamps
QSqlQuery query(dbaccess->connection());
QHash <QString, unsigned long> dbStatus;
bool rc = query.exec("SELECT filename, timestamp FROM metrics ORDER BY ride_date;");
while (rc && query.next()) {
QString filename = query.value(0).toString();
unsigned long timestamp = query.value(1).toInt();
dbStatus.insert(filename, timestamp);
}
// Delete statistics for non-existant ride files
QHash<QString, unsigned long>::iterator d;
for (d = dbStatus.begin(); d != dbStatus.end(); ++d) {
if (QFile(home.absolutePath() + "/" + d.key()).exists() == false) {
dbaccess->deleteRide(d.key());
}
}
// get power.zones timestamp to refresh on CP changes
unsigned long zonesTimeStamp = 0;
QString zonesfile = home.absolutePath() + "/power.zones";
if (QFileInfo(zonesfile).exists())
zonesTimeStamp = QFileInfo(zonesfile).lastModified().toTime_t();
// update statistics for ride files which are out of date
// showing a progress bar as we go
QProgressDialog bar("Refreshing Metrics Database...", "Abort", 0, filenames.count(), parent);
bar.setWindowModality(Qt::WindowModal);
int processed=0;
while (i.hasNext()) {
QString name = i.next();
QFile file(home.absolutePath() + "/" + name);
RideFile *ride = RideFileFactory::instance().openRideFile(file, errors);
importRide(home, zones, ride, name, dbaccess);
}
dbaccess->closeConnection();
delete dbaccess;
qDebug() << QDateTime::currentDateTime();
// if it s missing or out of date then update it!
unsigned long dbTimeStamp = dbStatus.value(name, 0);
if (dbTimeStamp < QFileInfo(file).lastModified().toTime_t() ||
dbTimeStamp < zonesTimeStamp) {
// read file and process it
RideFile *ride = RideFileFactory::instance().openRideFile(file, errors);
if (ride != NULL) {
importRide(home, ride, name, (dbTimeStamp > 0));
delete ride;
}
}
// update progress bar
bar.setValue(++processed);
QApplication::processEvents();
if (bar.wasCanceled())
break;
}
isclean = true;
}
bool MetricAggregator::importRide(QDir path, const Zones *zones, RideFile *ride, QString fileName, DBAccess *dbaccess)
/*----------------------------------------------------------------------
* Calculate the metrics for a ride file using the metrics factory
*----------------------------------------------------------------------*/
bool MetricAggregator::importRide(QDir path, RideFile *ride, QString fileName, bool modify)
{
SummaryMetrics *summaryMetric = new SummaryMetrics();
QFile file(path.absolutePath() + "/" + fileName);
QRegExp rx = RideFileFactory::instance().rideFileRegExp();
if (!rx.exactMatch(fileName)) {
fprintf(stderr, "bad name: %s\n", fileName.toAscii().constData());
assert(false);
return false;
return false; // not a ridefile!
}
summaryMetric->setFileName(fileName);
assert(rx.numCaptures() == 7);
@@ -77,89 +139,37 @@ bool MetricAggregator::importRide(QDir path, const Zones *zones, RideFile *ride,
summaryMetric->setRideDate(dateTime);
int zone_range = zones->whichRange(dateTime.date());
const RideMetricFactory &factory = RideMetricFactory::instance();
QSet<QString> todo;
QStringList metrics;
for (int i = 0; i < factory.metricCount(); ++i)
todo.insert(factory.metricName(i));
metrics << factory.metricName(i);
// compute all the metrics
QHash<QString, RideMetricPtr> computed = RideMetric::computeMetrics(ride, zones, metrics);
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(ride, zones, zone_range, metrics);
metrics.insert(name, metric);
i.remove();
double value = metric->value(true);
if(name == "workout_time")
summaryMetric->setWorkoutTime(value);
else if(name == "average_cad")
summaryMetric->setCadence(value);
else if(name == "total_distance")
summaryMetric->setDistance(value);
else if(name == "skiba_xpower")
summaryMetric->setXPower(value);
else if(name == "average_speed")
summaryMetric->setSpeed(value);
else if(name == "total_work")
summaryMetric->setTotalWork(value);
else if(name == "average_power")
summaryMetric->setWatts(value);
else if(name == "time_riding")
summaryMetric->setRideTime(value);
else if(name == "average_hr")
summaryMetric->setHeartRate(value);
else if(name == "skiba_relative_intensity")
summaryMetric->setRelativeIntensity(value);
else if(name == "skiba_bike_score")
summaryMetric->setBikeScore(value);
}
// get metrics into summaryMetric QMap
for(int i = 0; i < factory.metricCount(); ++i) {
summaryMetric->setForSymbol(factory.metricName(i), computed.value(factory.metricName(i))->value(true));
}
dbaccess->importRide(summaryMetric);
dbaccess->importRide(summaryMetric, modify);
delete summaryMetric;
return true;
}
void MetricAggregator::scanForMissing(QDir home, const Zones *zones)
/*----------------------------------------------------------------------
* Query functions are wrappers around DBAccess functions
*----------------------------------------------------------------------*/
QList<SummaryMetrics>
MetricAggregator::getAllMetricsFor(QDateTime start, QDateTime end)
{
QStringList errors;
DBAccess *dbaccess = new DBAccess(home);
QStringList filenames = dbaccess->getAllFileNames();
QRegExp rx = RideFileFactory::instance().rideFileRegExp();
QStringListIterator i(RideFileFactory::instance().listRideFiles(home));
while (i.hasNext()) {
QString name = i.next();
if(!filenames.contains(name))
{
qDebug() << "Found missing file: " << name;
QFile file(home.absolutePath() + "/" + name);
RideFile *ride = RideFileFactory::instance().openRideFile(file, errors);
importRide(home, zones, ride, name, dbaccess);
if (isclean == false) refreshMetrics(); // get them up-to-date
}
}
dbaccess->closeConnection();
delete dbaccess;
QList<SummaryMetrics> empty;
// only if we have established a connection to the database
if (dbaccess == NULL) return empty;
return dbaccess->getAllMetricsFor(start, end);
}
void MetricAggregator::resetMetricTable(QDir home)
{
DBAccess dbAccess(home);
dbAccess.dropMetricTable();
}

View File

@@ -20,30 +20,41 @@
#ifndef METRICAGGREGATOR_H_
#define METRICAGGREGATOR_H_
#include <QMap>
#include "RideFile.h"
#include <QDir>
#include "Zones.h"
#include "RideMetric.h"
#include "SummaryMetrics.h"
#include "MainWindow.h"
#include "DBAccess.h"
class MetricAggregator
class MetricAggregator : public QWidget
{
Q_OBJECT
public:
MetricAggregator();
void aggregateRides(QDir home, const Zones *zones);
MetricAggregator(MainWindow *, QDir , const Zones *);
~MetricAggregator();
void refreshMetrics();
void getFirstLast(QDate &, QDate &);
QList<SummaryMetrics> getAllMetricsFor(QDateTime start, QDateTime end);
public slots:
void update() { isclean = false; }
private:
QWidget *parent;
DBAccess *dbaccess;
QDir home;
const Zones *zones;
static bool isclean;
typedef QHash<QString,RideMetric*> MetricMap;
bool importRide(QDir path, const Zones *zones, RideFile *ride, QString fileName, DBAccess *dbaccess);
MetricMap metrics;
void scanForMissing(QDir home, const Zones *zones);
void resetMetricTable(QDir home);
bool importRide(QDir path, RideFile *ride, QString fileName, bool modify);
MetricMap metrics;
};
#endif /* METRICAGGREGATOR_H_ */

View File

@@ -208,6 +208,9 @@ CyclistPage::CyclistPage(const Zones *_zones):
lblCurRange->setText(QString("Current Zone Range: %1").arg(currentRange + 1));
perfManLabel = new QLabel(tr("Performance Manager"));
showSBToday = new QCheckBox(tr("Show Stress Balance Today"), this);
showSBToday->setChecked(settings->value(GC_SB_TODAY).toInt());
perfManStartLabel = new QLabel(tr("Starting LTS"));
perfManSTSLabel = new QLabel(tr("STS average (days)"));
perfManLTSLabel = new QLabel(tr("LTS average (days)"));
@@ -216,6 +219,7 @@ CyclistPage::CyclistPage(const Zones *_zones):
perfManLTSavgValidator = new QIntValidator(7,56,this);
QVariant perfManStartVal = settings->value(GC_INITIAL_STS);
QVariant perfManSTSVal = settings->value(GC_STS_DAYS);
if (perfManSTSVal.isNull() || perfManSTSVal.toInt() == 0)
perfManSTSVal = 7;
QVariant perfManLTSVal = settings->value(GC_LTS_DAYS);
@@ -279,6 +283,7 @@ CyclistPage::CyclistPage(const Zones *_zones):
perfManSTSavgLayout->addWidget(perfManSTSavg);
perfManLTSavgLayout->addWidget(perfManLTSLabel);
perfManLTSavgLayout->addWidget(perfManLTSavg);
perfManLayout->addWidget(showSBToday);
perfManLayout->addLayout(perfManStartValLayout);
perfManLayout->addLayout(perfManSTSavgLayout);
perfManLayout->addLayout(perfManLTSavgLayout);

View File

@@ -86,6 +86,7 @@ class CyclistPage : public QWidget
QLineEdit *perfManStart;
QLineEdit *perfManSTSavg;
QLineEdit *perfManLTSavg;
QCheckBox *showSBToday;
int getCurrentRange();
bool isNewMode();

158
src/PeakPower.cpp Normal file
View File

@@ -0,0 +1,158 @@
/*
* 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
*/
#include "RideMetric.h"
#include "BestIntervalDialog.h"
#include "Zones.h"
#include <math.h>
#define tr(s) QObject::tr(s)
class PeakPower : public RideMetric {
double watts;
double secs;
public:
PeakPower() : watts(0.0), secs(3600.0) {}
QString symbol() const { return "60m_critical_power"; }
QString name() const { return tr("60 min Peak Power"); }
MetricType type() const { return RideMetric::Peak; }
QString units(bool) const { return "watts"; }
int precision() const { return 0; }
void setSecs(double secs) { this->secs=secs; }
double conversion() const { return 1.0; }
double value(bool) const { return watts; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
QList<BestIntervalDialog::BestInterval> results;
BestIntervalDialog::findBests(ride, secs, 1, results);
if (results.count() > 0 && results.first().avg < 3000) watts = results.first().avg;
else watts = 0.0;
}
bool canAggregate() const { return false; }
void aggregateWith(const RideMetric &) {}
RideMetric *clone() const { return new PeakPower(*this); }
};
class PeakPower1s : public PeakPower {
public:
PeakPower1s() { setSecs(1); }
QString symbol() const { return "1s_critical_power"; }
QString name() const { return tr("1 sec Peak Power"); }
RideMetric *clone() const { return new PeakPower1s(*this); }
};
class PeakPower5s : public PeakPower {
public:
PeakPower5s() { setSecs(5); }
QString symbol() const { return "5s_critical_power"; }
QString name() const { return tr("5 sec Peak Power"); }
RideMetric *clone() const { return new PeakPower5s(*this); }
};
class PeakPower10s : public PeakPower {
public:
PeakPower10s() { setSecs(10); }
QString symbol() const { return "10s_critical_power"; }
QString name() const { return tr("10 sec Peak Power"); }
RideMetric *clone() const { return new PeakPower10s(*this); }
};
class PeakPower15s : public PeakPower {
public:
PeakPower15s() { setSecs(15); }
QString symbol() const { return "15s_critical_power"; }
QString name() const { return tr("15 sec Peak Power"); }
RideMetric *clone() const { return new PeakPower15s(*this); }
};
class PeakPower20s : public PeakPower {
public:
PeakPower20s() { setSecs(20); }
QString symbol() const { return "20s_critical_power"; }
QString name() const { return tr("20 sec Peak Power"); }
RideMetric *clone() const { return new PeakPower20s(*this); }
};
class PeakPower30s : public PeakPower {
public:
PeakPower30s() { setSecs(30); }
QString symbol() const { return "30s_critical_power"; }
QString name() const { return tr("30 sec Peak Power"); }
RideMetric *clone() const { return new PeakPower30s(*this); }
};
class PeakPower1m : public PeakPower {
public:
PeakPower1m() { setSecs(60); }
QString symbol() const { return "1m_critical_power"; }
QString name() const { return tr("1 min Peak Power"); }
RideMetric *clone() const { return new PeakPower1m(*this); }
};
class PeakPower5m : public PeakPower {
public:
PeakPower5m() { setSecs(300); }
QString symbol() const { return "5m_critical_power"; }
QString name() const { return tr("5 min Peak Power"); }
RideMetric *clone() const { return new PeakPower5m(*this); }
};
class PeakPower10m : public PeakPower {
public:
PeakPower10m() { setSecs(600); }
QString symbol() const { return "10m_critical_power"; }
QString name() const { return tr("10 min Peak Power"); }
RideMetric *clone() const { return new PeakPower10m(*this); }
};
class PeakPower20m : public PeakPower {
public:
PeakPower20m() { setSecs(1200); }
QString symbol() const { return "20m_critical_power"; }
QString name() const { return tr("20 min Peak Power"); }
RideMetric *clone() const { return new PeakPower20m(*this); }
};
class PeakPower30m : public PeakPower {
public:
PeakPower30m() { setSecs(1800); }
QString symbol() const { return "30m_critical_power"; }
QString name() const { return tr("30 min Peak Power"); }
RideMetric *clone() const { return new PeakPower30m(*this); }
};
static bool addAllPeaks() {
RideMetricFactory::instance().addMetric(PeakPower1s());
RideMetricFactory::instance().addMetric(PeakPower5s());
RideMetricFactory::instance().addMetric(PeakPower10s());
RideMetricFactory::instance().addMetric(PeakPower15s());
RideMetricFactory::instance().addMetric(PeakPower20s());
RideMetricFactory::instance().addMetric(PeakPower30s());
RideMetricFactory::instance().addMetric(PeakPower1m());
RideMetricFactory::instance().addMetric(PeakPower5m());
RideMetricFactory::instance().addMetric(PeakPower10m());
RideMetricFactory::instance().addMetric(PeakPower20m());
RideMetricFactory::instance().addMetric(PeakPower30m());
RideMetricFactory::instance().addMetric(PeakPower());
return true;
}
static bool allPeaksAdded = addAllPeaks();

View File

@@ -34,6 +34,7 @@ PerfPlot::PerfPlot() : STScurve(NULL), LTScurve(NULL), SBcurve(NULL), DAYcurve(N
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setCanvasBackground(Qt::white);
setTitle(tr("Performance Manager"));
setAxisTitle(yLeft, "Stress (BS/Day)");
setAxisTitle(xBottom, "Time (days)");
setAxisTitle(yRight, "Stress (Daily)");

View File

@@ -8,7 +8,7 @@
PerformanceManagerWindow::PerformanceManagerWindow(MainWindow *mainWindow) :
QWidget(mainWindow), mainWindow(mainWindow)
QWidget(mainWindow), mainWindow(mainWindow), active(false)
{
days = count = 0;
sc = NULL;
@@ -111,9 +111,10 @@ PerformanceManagerWindow::~PerformanceManagerWindow()
void PerformanceManagerWindow::configChanged()
{
mainWindow->home.remove("stress.cache");
days = 0; // force replot
replot();
if (active) {
days = 0; // force replot
replot();
}
}
void PerformanceManagerWindow::metricChanged()
@@ -126,8 +127,7 @@ void PerformanceManagerWindow::metricChanged()
void PerformanceManagerWindow::setActive(bool value)
{
if (value)
replot();
if (active=value) replot();
}
void PerformanceManagerWindow::replot()
@@ -186,7 +186,7 @@ void PerformanceManagerWindow::replot()
(settings->value(GC_STS_DAYS,7)).toInt(),
(settings->value(GC_LTS_DAYS,42)).toInt());
sc->calculateStress(this,home.absolutePath(),allRides,newMetric);
sc->calculateStress(mainWindow,home.absolutePath(),newMetric);
perfplot->setStressCalculator(sc);

View File

@@ -62,6 +62,8 @@ class PerformanceManagerWindow : public QWidget
StressCalculator *sc;
MainWindow *mainWindow;
bool active;
PerfPlot *perfplot;
QLineEdit *PMSTSValue;
QLineEdit *PMLTSValue;

View File

@@ -159,7 +159,7 @@ RideFileFactory::rideFileRegExp() const
{
QStringList suffixList = RideFileFactory::instance().suffixes();
QString s("^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.(%1)$");
return QRegExp(s.arg(suffixList.join("|")));
return QRegExp(s.arg(suffixList.join("|")), Qt::CaseInsensitive);
}
RideFile *RideFileFactory::openRideFile(QFile &file,

View File

@@ -45,6 +45,13 @@ struct RideMetric {
// using QObject::tr().
virtual QString name() const = 0;
// What type of metric is this?
// Drives the way metrics combined over a day or week in the
// Long term metrics charts
enum metrictype { Total, Average, Peak } types;
typedef enum metrictype MetricType;
virtual MetricType type() const = 0;
// The units in which this RideMetric is expressed. It should be
// translated using QObject::tr().
virtual QString units(bool metric) const = 0;
@@ -56,6 +63,9 @@ struct RideMetric {
// The actual value of this ride metric, in the units above.
virtual double value(bool metric) const = 0;
// Factor to multiple value to convert from metric to imperial
virtual double conversion() const = 0;
// Compute the ride metric from a file.
virtual void compute(const RideFile *ride,
const Zones *zones,
@@ -98,6 +108,7 @@ class AvgRideMetric : public RideMetric {
if (count == 0) return 0.0;
return total / count;
}
int type() { return RideMetric::Average; }
void aggregateWith(const RideMetric &other) {
assert(symbol() == other.symbol());
const AvgRideMetric &as = dynamic_cast<const AvgRideMetric&>(other);
@@ -112,6 +123,7 @@ class RideMetricFactory {
static QVector<QString> noDeps;
QVector<QString> metricNames;
QVector<RideMetric::MetricType> metricTypes;
QHash<QString,RideMetric*> metrics;
QHash<QString,QVector<QString>*> dependencyMap;
bool dependenciesChecked;
@@ -140,6 +152,8 @@ class RideMetricFactory {
int metricCount() const { return metricNames.size(); }
const QString &metricName(int i) const { return metricNames[i]; }
const RideMetric::MetricType &metricType(int i) const { return metricTypes[i]; }
const RideMetric *rideMetric(QString name) const { return metrics.value(name, NULL); }
bool haveMetric(const QString &symbol) const {
return metrics.contains(symbol);
@@ -156,6 +170,7 @@ class RideMetricFactory {
assert(!metrics.contains(metric.symbol()));
metrics.insert(metric.symbol(), metric.clone());
metricNames.append(metric.symbol());
metricTypes.append(metric.type());
if (deps) {
QVector<QString> *copy = new QVector<QString>;
for (int i = 0; i < deps->size(); ++i)

View File

@@ -25,7 +25,7 @@
Season::Season()
{
type = season; // by default seasons are of type season
}
QString Season::getName() {
@@ -43,6 +43,11 @@ QDate Season::getEnd()
return end;
}
int Season::getType()
{
return type;
}
void Season::setEnd(QDate _end)
{
end = _end;
@@ -57,3 +62,80 @@ void Season::setName(QString _name)
{
name = _name;
}
void Season::setType(int _type)
{
type = _type;
}
/*----------------------------------------------------------------------
* EDIT SEASON DIALOG
*--------------------------------------------------------------------*/
EditSeasonDialog::EditSeasonDialog(MainWindow *mainWindow, Season *season) :
QDialog(mainWindow, Qt::Dialog), mainWindow(mainWindow), season(season)
{
setWindowTitle(tr("Edit Date Range"));
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// Grid
QGridLayout *grid = new QGridLayout;
QLabel *name = new QLabel("Name");
QLabel *type = new QLabel("Type");
QLabel *from = new QLabel("From");
QLabel *to = new QLabel("To");
nameEdit = new QLineEdit(this);
nameEdit->setText(season->getName());
typeEdit = new QComboBox;
typeEdit->addItem("Season", Season::season);
typeEdit->addItem("Cycle", Season::cycle);
typeEdit->addItem("Adhoc", Season::adhoc);
typeEdit->setCurrentIndex(typeEdit->findData(season->getType()));
fromEdit = new QDateEdit(this);
fromEdit->setDate(season->getStart());
toEdit = new QDateEdit(this);
toEdit->setDate(season->getEnd());
grid->addWidget(name, 0,0);
grid->addWidget(nameEdit, 0,1);
grid->addWidget(type, 1,0);
grid->addWidget(typeEdit, 1,1);
grid->addWidget(from, 2,0);
grid->addWidget(fromEdit, 2,1);
grid->addWidget(to, 3,0);
grid->addWidget(toEdit, 3,1);
mainLayout->addLayout(grid);
// Buttons
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
applyButton = new QPushButton(tr("&OK"), this);
cancelButton = new QPushButton(tr("&Cancel"), this);
buttonLayout->addWidget(cancelButton);
buttonLayout->addWidget(applyButton);
mainLayout->addLayout(buttonLayout);
// connect up slots
connect(applyButton, SIGNAL(clicked()), this, SLOT(applyClicked()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
}
void
EditSeasonDialog::applyClicked()
{
// get the values back
season->setName(nameEdit->text());
season->setType(typeEdit->itemData(typeEdit->currentIndex()).toInt());
season->setStart(fromEdit->date());
season->setEnd(toEdit->date());
accept();
}
void
EditSeasonDialog::cancelClicked()
{
reject();
}

View File

@@ -23,21 +23,51 @@
#include <QDate>
#include <QFile>
#include "MainWindow.h"
class Season
{
public:
enum SeasonType { season=0, cycle=1, adhoc=2, temporary=3 };
//typedef enum seasontype SeasonType;
Season();
QDate getStart();
QDate getEnd();
QString getName();
int getType();
void setStart(QDate _start);
void setEnd(QDate _end);
void setName(QString _name);
void setType(int _type);
private:
QDate start;
QDate end;
QString name;
int type;
};
class EditSeasonDialog : public QDialog
{
Q_OBJECT
public:
EditSeasonDialog(MainWindow *, Season *);
public slots:
void applyClicked();
void cancelClicked();
private:
MainWindow *mainWindow;
Season *season;
QPushButton *applyButton, *cancelButton;
QLineEdit *nameEdit;
QComboBox *typeEdit;
QDateEdit *fromEdit, *toEdit;
};
#endif /* SEASON_H_ */

View File

@@ -33,10 +33,18 @@ bool SeasonParser::endElement( const QString&, const QString&, const QString &qN
season.setName(buffer.trimmed());
else if(qName == "startdate")
season.setStart(seasonDateToDate(buffer.trimmed()));
else if(qName == "enddate")
season.setEnd(seasonDateToDate(buffer.trimmed()));
else if (qName == "type")
season.setType(buffer.trimmed().toInt());
else if(qName == "season")
{
if(seasons.size() >= 1)
seasons[seasons.size()-1].setEnd(season.getStart());
if(seasons.size() >= 1) {
// only set end date for previous season if
// it is not null
if (seasons[seasons.size()-1].getEnd() == QDate())
seasons[seasons.size()-1].setEnd(season.getStart());
}
seasons.append(season);
}
return TRUE;
@@ -84,7 +92,47 @@ QDate SeasonParser::seasonDateToDate(QString seasonDate)
}
bool SeasonParser::endDocument()
{
// Go 10 years into the future (GC's version of infinity)
seasons[seasons.size()-1].setEnd(QDate::currentDate().addYears(10));
// Go 10 years into the future if not defined in the file
if (seasons.size() > 0) {
if (seasons[seasons.size()-1].getEnd() == QDate())
seasons[seasons.size()-1].setEnd(QDate::currentDate().addYears(10));
}
return TRUE;
}
bool
SeasonParser::serialize(QString filename, QList<Season>Seasons)
{
// open file - truncate contents
QFile file(filename);
file.open(QFile::WriteOnly);
file.resize(0);
QTextStream out(&file);
// begin document
out << "<seasons>\n";
// write out to file
foreach (Season season, Seasons) {
if (season.getType() != Season::temporary) {
out<<QString("\t<season>\n"
"\t\t<name>%1</name>\n"
"\t\t<startdate>%2</startdate>\n"
"\t\t<enddate>%3</enddate>\n"
"\t\t<type>%4</type>\n"
"\t</season>\n")
.arg(season.getName())
.arg(season.getStart().toString("yyyy-MM-dd"))
.arg(season.getEnd().toString("yyyy-MM-dd"))
.arg(season.getType());
}
}
// end document
out << "</seasons>\n";
// close file
file.close();
return true; // success
}

View File

@@ -26,6 +26,10 @@ class SeasonParser : public QXmlDefaultHandler
{
public:
// marshall
static bool serialize(QString, QList<Season>);
// unmarshall
bool startDocument();
bool endDocument();
bool endElement( const QString&, const QString&, const QString &qName );

View File

@@ -42,6 +42,7 @@
#define GC_CRANKLENGTH "crankLength"
#define GC_BIKESCOREDAYS "bikeScoreDays"
#define GC_BIKESCOREMODE "bikeScoreMode"
#define GC_SB_TODAY "PMshowSBtoday"
#define GC_INITIAL_LTS "initialLTS"
#define GC_INITIAL_STS "initialSTS"
#define GC_LTS_DAYS "LTSdays"
@@ -56,6 +57,7 @@
#define GC_WARNEXIT "warnexit"
#define GC_WORKOUTDIR "workoutDir"
#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

View File

@@ -1,7 +1,10 @@
#include "StressCalculator.h"
#include "MetricAggregator.h"
#include "RideMetric.h"
#include "RideItem.h"
#include "MainWindow.h"
#include <stdio.h>
#include <QSharedPointer>
@@ -18,19 +21,24 @@ StressCalculator::StressCalculator (
longTermDays(longTermDays),
initialSTS(initialSTS), initialLTS(initialLTS), lastDaysIndex(-1)
{
// calc SB for today or tomorrow?
settings = GetApplicationSettings();
showSBToday = settings->value(GC_SB_TODAY).toInt();
days = startDate.daysTo(endDate);
// make vectors 1 larger in case there is a ride for today.
// see calculateStress()
stsvalues.resize(days+1);
ltsvalues.resize(days+1);
sbvalues.resize(days+1);
xdays.resize(days+1);
list.resize(days+1);
stsvalues.resize(days+2);
ltsvalues.resize(days+2);
sbvalues.resize(days+2);
xdays.resize(days+2);
list.resize(days+2);
ltsramp.resize(days+2);
stsramp.resize(days+2);
lte = (double)exp(-1.0/longTermDays);
ste = (double)exp(-1.0/shortTermDays);
settings = GetApplicationSettings();
}
@@ -57,163 +65,70 @@ double StressCalculator::min(void) {
void StressCalculator::calculateStress(QWidget *mw,
QString homePath, const QTreeWidgetItem * rides,
const QString &metric)
void StressCalculator::calculateStress(MainWindow *main, QString home, const QString &metric)
{
QSharedPointer<QProgressDialog> progress;
int endingOffset = 0;
bool aborted = false;
bool showProgress = false;
RideItem *item;
// refresh metrics
metricDB = new MetricAggregator(main, home, main->zones());
// get all metric data from the year 1900 - 3000
QList<SummaryMetrics> results;
results = metricDB->getAllMetricsFor(QDateTime(QDate(1900,1,1)), QDateTime(QDate(3000,1,1)));
// set up cache file
QString cachePath = homePath + "/" + "stress.cache";
QFile cacheFile(cachePath);
QMap<QString,QMap<QString,float> > cache;
// set start and enddate to maximum maximum required date range
// remember the date range required so we can truncate afterwards
QDateTime startDateNeeded = startDate;
QDateTime endDateNeeded = endDate;
startDate = startDate < results[0].getRideDate() ? startDate : results[0].getRideDate();
endDate = endDate > results[results.count()-1].getRideDate() ? endDate : results[results.count()-1].getRideDate();
const QString bs_name = "skiba_bike_score";
const QString dp_name = "daniels_points";
assert(metric == bs_name || metric == dp_name);
int maxarray = startDate.daysTo(endDate) +2; // from zero plus tomorrows SB!
stsvalues.resize(maxarray);
ltsvalues.resize(maxarray);
sbvalues.resize(maxarray);
xdays.resize(maxarray);
list.resize(maxarray);
ltsramp.resize(maxarray);
stsramp.resize(maxarray);
if (cacheFile.exists() && cacheFile.size() > 0) {
if (cacheFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
fprintf(stderr,"reading stress cache file\n");
QTextStream in(&cacheFile);
bool first = true;
QMap<int,QString> columnToMetric;
while(! in.atEnd()) {
QString line = in.readLine();
QStringList fields = line.split(",");
if (first) {
first = false;
if (fields[0] != "Date")
break; // rescan to get DanielsPoints
for (int i = 1; i < fields.size(); ++i)
columnToMetric.insert(i, fields[i]);
continue;
}
else {
QString date = fields[0];
for (int i = 1; i < fields.size(); ++i)
cache[date][columnToMetric[i]] = fields[i].toFloat();
}
}
cacheFile.close();
}
for (int i=0; i<results.count(); i++)
addRideData(results[i].getForSymbol(metric), results[i].getRideDate());
// ensure the last day is covered ...
addRideData(0.0, endDate);
// now truncate the data series to the requested date range
int firstindex = startDate.daysTo(startDateNeeded);
int lastindex = startDate.daysTo(endDateNeeded)+2; // for today and tomorrow SB
// zap the back
if (lastindex < maxarray) {
stsvalues.remove(lastindex, maxarray-lastindex);
ltsvalues.remove(lastindex, maxarray-lastindex);
sbvalues.remove(lastindex, maxarray-lastindex);
xdays.remove(lastindex, maxarray-lastindex);
list.remove(lastindex, maxarray-lastindex);
stsramp.remove(lastindex, maxarray-lastindex);
ltsramp.remove(lastindex, maxarray-lastindex);
}
if (cache.isEmpty()) {
// set up progress bar only if no cache file
progress = QSharedPointer<QProgressDialog>(new
QProgressDialog(QString(tr("Computing stress.\n")),
tr("Abort"),0,days,mw));
endingOffset = progress->labelText().size();
showProgress = true;
// now zap the front
if (firstindex) {
stsvalues.remove(0, firstindex);
ltsvalues.remove(0, firstindex);
ltsramp.remove(0, firstindex);
stsramp.remove(0, firstindex);
sbvalues.remove(0, firstindex);
xdays.remove(0, firstindex);
list.remove(0, firstindex);
}
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked);
if(isAscending.toInt() > 0 ){
item = (RideItem*) rides->child(0);
} else {
item = (RideItem*) rides->child(rides->childCount()-1);
}
for (int i = 0; i < rides->childCount(); ++i) {
if(isAscending.toInt() > 0 ){
item = (RideItem*) rides->child(i);
} else {
item = (RideItem*) rides->child(rides->childCount()-1-i);
}
// calculate using rides within date range
if (item->dateTime.daysTo(startDate) <= 0 &&
item->dateTime.daysTo(endDate) >= 0) { // inclusive of end date
QString ridedatestring = item->dateTime.toString();
double bs = 0.0, dp = 0.0;
if (showProgress) {
QString existing = progress->labelText();
existing.chop(progress->labelText().size() - endingOffset);
progress->setLabelText( existing +
QString(tr("Processing %1...")).arg(item->fileName));
}
// get new value if not in cache
if (cache.contains(ridedatestring)) {
bs = cache[ridedatestring][bs_name];
dp = cache[ridedatestring][dp_name];
}
else {
item->computeMetrics();
RideMetricPtr m;
if ((m = item->metrics.value(bs_name)) && m->value(true))
bs = m->value(true);
if ((m = item->metrics.value(dp_name)) && m->value(true))
dp = m->value(true);
cache[ridedatestring][bs_name] = bs;
cache[ridedatestring][dp_name] = dp;
// only delete if the ride is clean (i.e. no pending ave)
if (item->isDirty() == false) item->freeMemory();
}
addRideData(metric == bs_name ? bs : dp,item->dateTime);
// check progress
if (showProgress) {
QCoreApplication::processEvents();
if (progress->wasCanceled()) {
aborted = true;
goto done;
}
// set progress from 0 to days
progress->setValue(startDate.daysTo(item->dateTime));
}
}
}
// fill in any days from last ride up to YESTERDAY but not today.
// we want to show todays ride if there is a ride but don't fill in
// a zero for it if there is no ride
if (item->dateTime.daysTo(endDate) > 0)
{
/*
fprintf(stderr,"filling in up to date = %s\n",
endDate.toString().toAscii().data());
*/
addRideData(0.0,endDate.addDays(-1));
}
else
{
// there was a ride for today, increment the count so
// we will show it:
days++;
}
done:
if (!aborted) {
// write cache file
if (cacheFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
cacheFile.resize(0); // truncate
QTextStream out(&cacheFile);
out << "Date," << bs_name << "," << dp_name << endl;
QMap<QString,QMap<QString,float> >::const_iterator i = cache.constBegin();
while (i != cache.constEnd()) {
out << i.key() << "," << i.value()[bs_name]
<< "," << i.value()[dp_name] << endl;
++i;
}
cacheFile.close();
}
}
// reapply the requested date range
startDate = startDateNeeded;
endDate = endDateNeeded;
days = startDate.daysTo(endDate) + 1; // include today
// will close the connection, alledgedly
delete metricDB;
}
/*
@@ -227,25 +142,20 @@ done:
void StressCalculator::addRideData(double BS, QDateTime rideDate) {
int daysIndex = startDate.daysTo(rideDate);
/*
fprintf(stderr,"addRideData date = %s\n",
rideDate.toString().toAscii().data());
*/
// fill in any missing days before today
int d;
for (d = lastDaysIndex + 1; d < daysIndex ; d++) {
list[d] = 0.0; // no ride
calculate(d);
list[d] = 0.0; // no ride
calculate(d);
}
// do this ride (may be more than one ride in a day)
// ignore stuff from before start date
if(daysIndex < 0) return;
// today
list[daysIndex] += BS;
calculate(daysIndex);
lastDaysIndex = daysIndex;
// fprintf(stderr,"addRideData (%.2f, %d)\n",BS,daysIndex);
}
@@ -278,9 +188,16 @@ void StressCalculator::calculate(int daysIndex) {
stsvalues[daysIndex] = (list[daysIndex] * (1.0 - ste)) + (lastSTS * ste);
// SB (stress balance) long term - short term
sbvalues[daysIndex] = ltsvalues[daysIndex] - stsvalues[daysIndex] ;
// XXX FIXED BUG WHERE SB WAS NOT SHOWN ON THE NEXT DAY!
if (daysIndex == 0) sbvalues[daysIndex]=0;
sbvalues[daysIndex+(showSBToday ? 0 : 1)] = ltsvalues[daysIndex] - stsvalues[daysIndex] ;
// xdays
xdays[daysIndex] = daysIndex+1;
}
// ramp
if (daysIndex > 0) {
stsramp[daysIndex] = stsvalues[daysIndex] - stsvalues[daysIndex-1];
ltsramp[daysIndex] = ltsvalues[daysIndex] - ltsvalues[daysIndex-1];
}
}

View File

@@ -15,6 +15,7 @@
#include <QDateTime>
#include <QTreeWidgetItem>
#include "Settings.h"
#include "MetricAggregator.h"
class StressCalculator:public QObject {
@@ -30,10 +31,13 @@ class StressCalculator:public QObject {
double ste, lte;
int lastDaysIndex;
bool showSBToday;
// graph axis arrays
QVector<double> stsvalues;
QVector<double> ltsvalues;
QVector<double> ltsramp;
QVector<double> stsramp;
QVector<double> sbvalues;
QVector<double> xdays;
// averaging array
@@ -50,14 +54,15 @@ class StressCalculator:public QObject {
double initialSTS, double initialLTS,
int shortTermDays, int longTermDays);
void calculateStress(QWidget *mw, QString homePath,
const QTreeWidgetItem * rides, const QString &metric);
void calculateStress(MainWindow *, QString, const QString &metric);
// x axes:
double *getSTSvalues() { return stsvalues.data(); }
double *getLTSvalues() { return ltsvalues.data(); }
double *getSBvalues() { return sbvalues.data(); }
double *getDAYvalues() { return list.data(); }
double *getLRvalues() { return ltsramp.data(); }
double *getSRvalues() { return stsramp.data(); }
// y axis
double *getDays() { return xdays.data(); }
@@ -71,8 +76,8 @@ class StressCalculator:public QObject {
QDateTime getStartDate(void) { return startDate; }
QDateTime getEndDate(void) { return startDate.addDays(days); }
// use metricDB pre-calculated values
MetricAggregator *metricDB;
};

View File

@@ -1,52 +0,0 @@
/*
* Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca)
*
* 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 "SummaryMetrics.h"
SummaryMetrics::SummaryMetrics() { }
double SummaryMetrics::getDistance() { return distance; }
double SummaryMetrics::getSpeed() { return speed; }
double SummaryMetrics::getWatts() { return watts; }
double SummaryMetrics::getBikeScore() { return bikeScore; }
double SummaryMetrics::getXPower() { return xPower; }
double SummaryMetrics::getCadence() { return cadence; }
double SummaryMetrics::getHeartRate() { return heartRate; }
double SummaryMetrics::getRideTime() { return rideTime; }
QString SummaryMetrics::getFileName() { return fileName; }
double SummaryMetrics::getTotalWork() { return totalWork; }
double SummaryMetrics::getWorkoutTime() { return workoutTime; }
double SummaryMetrics::getRelativeIntensity() { return relativeIntensity; }
QDateTime SummaryMetrics::getRideDate() { return rideDate; }
void SummaryMetrics::setSpeed(double _speed) { speed = _speed; }
void SummaryMetrics::setWatts(double _watts) { watts = _watts; }
void SummaryMetrics::setBikeScore(double _bikescore) { bikeScore = _bikescore; }
void SummaryMetrics::setXPower(double _xPower) { xPower = _xPower; }
void SummaryMetrics::setCadence(double _cadence) { cadence = _cadence; }
void SummaryMetrics::setDistance(double _distance) { distance = _distance; }
void SummaryMetrics::setRideTime(double _rideTime) { rideTime = _rideTime; }
void SummaryMetrics::setTotalWork(double _totalWork) { totalWork = _totalWork; }
void SummaryMetrics::setFileName(QString _fileName) { fileName = _fileName; }
void SummaryMetrics::setWorkoutTime(double _workoutTime) { workoutTime = _workoutTime; }
void SummaryMetrics::setRelativeIntensity(double _relativeIntensity) { relativeIntensity = _relativeIntensity; }
void SummaryMetrics::setHeartRate(double _heartRate) { heartRate = _heartRate; }
void SummaryMetrics::setRideDate(QDateTime _rideDate) { rideDate = _rideDate; }

View File

@@ -25,51 +25,22 @@
class SummaryMetrics
{
public:
SummaryMetrics();
QString getFileName();
double getDistance();
double getSpeed();
double getWatts();
double getBikeScore();
double getXPower();
double getCadence();
double getHeartRate();
double getRideTime();
double getWorkoutTime();
double getTotalWork();
double getRelativeIntensity();
QDateTime getRideDate();
void setDistance(double _distance);
void setSpeed(double _speed);
void setWatts(double _watts);
void setBikeScore(double _bikescore);
void setXPower(double _xPower);
void setCadence(double _cadence);
void setHeartRate(double _heartRate);
void setWorkoutTime(double _workoutTime);
void setRideTime(double _rideTime);
void setFileName(QString _filename);
void setTotalWork(double _totalWork);
void setRelativeIntensity(double _relativeIntensity);
void setRideDate(QDateTime _rideDate);
// filename
QString getFileName() { return fileName; }
void setFileName(QString fileName) { this->fileName = fileName; }
// ride date
QDateTime getRideDate() { return rideDate; }
void setRideDate(QDateTime rideDate) { this->rideDate = rideDate; }
// metric values
void setForSymbol(QString symbol, double v) { value.insert(symbol, v); }
double getForSymbol(QString symbol) { return value.value(symbol, 0.0); }
private:
double distance;
double speed;
double watts;
double bikeScore;
double xPower;
double cadence;
double heartRate;
double rideTime;
QString fileName;
double totalWork;
double workoutTime;
double relativeIntensity;
QDateTime rideDate;
QMap<QString, double> value;
};

131
src/TimeInZone.cpp Normal file
View File

@@ -0,0 +1,131 @@
/*
* 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
*/
#include "RideMetric.h"
#include "BestIntervalDialog.h"
#include "Zones.h"
#include <math.h>
#define tr(s) QObject::tr(s)
class ZoneTime : public RideMetric {
int level;
double seconds;
QList<int> lo;
QList<int> hi;
public:
ZoneTime() : level(0), seconds(0.0) {}
QString symbol() const { return "time_in_zone"; }
QString name() const { return tr("Time In Zone"); }
MetricType type() const { return RideMetric::Total; }
QString units(bool) const { return "seconds"; }
int precision() const { return 0; }
void setLevel(int level) { this->level=level-1; } // zones start from zero not 1
double conversion() const { return 1.0; }
double value(bool) const { return seconds; }
void compute(const RideFile *ride, const Zones *zone, int zoneRange,
const QHash<QString,RideMetric*> &)
{
// get zone ranges
if (zone && zoneRange >= 0) {
// iterate and compute
foreach(const RideFilePoint *point, ride->dataPoints()) {
if (zone->whichZone(zoneRange, point->watts) == level)
seconds += ride->recIntSecs();
}
}
}
bool canAggregate() const { return false; }
void aggregateWith(const RideMetric &) {}
RideMetric *clone() const { return new ZoneTime(*this); }
};
class ZoneTime1 : public ZoneTime {
public:
ZoneTime1() { setLevel(1); }
QString symbol() const { return "time_in_zone_L1"; }
QString name() const { return tr("L1 Time in Zone"); }
RideMetric *clone() const { return new ZoneTime1(*this); }
};
class ZoneTime2 : public ZoneTime {
public:
ZoneTime2() { setLevel(2); }
QString symbol() const { return "time_in_zone_L2"; }
QString name() const { return tr("L2 Time in Zone"); }
RideMetric *clone() const { return new ZoneTime2(*this); }
};
class ZoneTime3 : public ZoneTime {
public:
ZoneTime3() { setLevel(3); }
QString symbol() const { return "time_in_zone_L3"; }
QString name() const { return tr("L3 Time in Zone"); }
RideMetric *clone() const { return new ZoneTime3(*this); }
};
class ZoneTime4 : public ZoneTime {
public:
ZoneTime4() { setLevel(4); }
QString symbol() const { return "time_in_zone_L4"; }
QString name() const { return tr("L4 Time in Zone"); }
RideMetric *clone() const { return new ZoneTime4(*this); }
};
class ZoneTime5 : public ZoneTime {
public:
ZoneTime5() { setLevel(5); }
QString symbol() const { return "time_in_zone_L5"; }
QString name() const { return tr("L5 Time in Zone"); }
RideMetric *clone() const { return new ZoneTime5(*this); }
};
class ZoneTime6 : public ZoneTime {
public:
ZoneTime6() { setLevel(6); }
QString symbol() const { return "time_in_zone_L6"; }
QString name() const { return tr("L6 Time in Zone"); }
RideMetric *clone() const { return new ZoneTime6(*this); }
};
class ZoneTime7 : public ZoneTime {
public:
ZoneTime7() { setLevel(7); }
QString symbol() const { return "time_in_zone_L7"; }
QString name() const { return tr("L7 Time in Zone"); }
RideMetric *clone() const { return new ZoneTime7(*this); }
};
static bool addAllZones() {
RideMetricFactory::instance().addMetric(ZoneTime1());
RideMetricFactory::instance().addMetric(ZoneTime2());
RideMetricFactory::instance().addMetric(ZoneTime3());
RideMetricFactory::instance().addMetric(ZoneTime4());
RideMetricFactory::instance().addMetric(ZoneTime5());
RideMetricFactory::instance().addMetric(ZoneTime6());
RideMetricFactory::instance().addMetric(ZoneTime7());
return true;
}
static bool allZonesAdded = addAllZones();

View File

@@ -10,5 +10,6 @@
<file>images/config.png</file>
<file>translations/gc_fr.qm</file>
<file>translations/gc_ja.qm</file>
<file>xml/charts.xml</file>
</qresource>
</RCC>

View File

@@ -78,6 +78,13 @@ HEADERS += \
IntervalItem.h \
LogTimeScaleDraw.h \
LogTimeScaleEngine.h \
LTMCanvasPicker.h \
LTMChartParser.h \
LTMPlot.h \
LTMSettings.h \
LTMTool.h \
LTMTrend.h \
LTMWindow.h \
MainWindow.h \
ManualRideDialog.h \
ManualRideFile.h \
@@ -162,11 +169,19 @@ SOURCES += \
IntervalItem.cpp \
LogTimeScaleDraw.cpp \
LogTimeScaleEngine.cpp \
LTMCanvasPicker.cpp \
LTMChartParser.cpp \
LTMPlot.cpp \
LTMSettings.cpp \
LTMTool.cpp \
LTMTrend.cpp \
LTMWindow.cpp \
MainWindow.cpp \
ManualRideDialog.cpp \
ManualRideFile.cpp \
MetricAggregator.cpp \
Pages.cpp \
PeakPower.cpp \
PerfPlot.cpp \
PerformanceManagerWindow.cpp \
PfPvPlot.cpp \
@@ -199,9 +214,9 @@ SOURCES += \
SplitRideDialog.cpp \
SrmRideFile.cpp \
StressCalculator.cpp \
SummaryMetrics.cpp \
TcxParser.cpp \
TcxRideFile.cpp \
TimeInZone.cpp \
TimeUtils.cpp \
ToolsDialog.cpp \
TrainTabs.cpp \

704
src/xml/charts.xml Normal file
View File

@@ -0,0 +1,704 @@
<charts>
<LTM-chart>
<chartname>"Aerobic Power"</chartname>
<metric>
<metricdesc>"60 min Peak Power"</metricdesc>
<metricname>60m_critical_power</metricname>
<metricuname>"60 min Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"30 min Peak Power"</metricdesc>
<metricname>30m_critical_power</metricname>
<metricuname>"30 min Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="85" green="170" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"20 min Peak Power"</metricdesc>
<metricname>20m_critical_power</metricname>
<metricuname>"20 min Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="0" green="85" blue="127"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"10 min Peak Power"</metricdesc>
<metricname>10m_critical_power</metricname>
<metricuname>"10 min Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="0" green="170" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Anaerobic Power"</chartname>
<metric>
<metricdesc>"1 min Peak Power"</metricdesc>
<metricname>1m_critical_power</metricname>
<metricuname>"1 min Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="255" green="170" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"1 sec Peak Power"</metricdesc>
<metricname>1s_critical_power</metricname>
<metricuname>"1 sec Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"10 sec Peak Power"</metricdesc>
<metricname>10s_critical_power</metricname>
<metricuname>"10 sec Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="0" green="255" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"15 sec Peak Power"</metricdesc>
<metricname>15s_critical_power</metricname>
<metricuname>"15 sec Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="0" green="170" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"20 sec Peak Power"</metricdesc>
<metricname>20s_critical_power</metricname>
<metricuname>"20 sec Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="0" green="170" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"30 sec Peak Power"</metricdesc>
<metricname>30s_critical_power</metricname>
<metricuname>"30 sec Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="170" green="85" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Critical Power Trend"</chartname>
<metric>
<metricdesc>"60 min Peak Power"</metricdesc>
<metricname>60m_critical_power</metricname>
<metricuname>"60 min Peak Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>1</metrictrend>
<metrictopn>10</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>4</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Power &amp; Speed Trend"</chartname>
<metric>
<metricdesc>"Average Power"</metricdesc>
<metricname>average_power</metricname>
<metricuname>"Average Power"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>1</metrictrend>
<metrictopn>25</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>4</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Average Speed"</metricdesc>
<metricname>average_speed</metricname>
<metricuname>"Average Speed"</metricuname>
<metricuunits>"kph"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>1</metrictrend>
<metrictopn>25</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>4</metriccurve>
<metricsymbol>2</metricsymbol>
<metricpencolor red="0" green="85" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Cardiovascular Response"</chartname>
<metric>
<metricdesc>"95% Heart Rate"</metricdesc>
<metricname>ninety_five_percent_hr</metricname>
<metricuname>"95% Heart Rate"</metricuname>
<metricuunits>"bpm"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="170" green="170" blue="170"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Aerobic Decoupling"</metricdesc>
<metricname>aerobic_decoupling</metricname>
<metricuname>"Aerobic Decoupling"</metricuname>
<metricuunits>"%"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="170" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Average Heart Rate"</metricdesc>
<metricname>average_hr</metricname>
<metricuname>"Average Heart Rate"</metricuname>
<metricuunits>"bpm"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Max Heartrate"</metricdesc>
<metricname>max_heartrate</metricname>
<metricuname>"Max Heartrate"</metricuname>
<metricuunits>"bpm"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="0" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Training Mix"</chartname>
<metric>
<metricdesc>"Skiba VI"</metricdesc>
<metricname>skiba_variability_index</metricname>
<metricuname>"Skiba VI"</metricuname>
<metricuunits>"Skiba VI"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Relative Intensity"</metricdesc>
<metricname>skiba_relative_intensity</metricname>
<metricuname>"Relative Intensity"</metricuname>
<metricuunits>"Relative Intensity"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="0" green="255" blue="102"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Work"</metricdesc>
<metricname>total_work</metricname>
<metricuname>"Work"</metricuname>
<metricuunits>"kJ"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="170" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Tempo &amp; Threshold Time"</chartname>
<metric>
<metricdesc>"L3 Time in Zone"</metricdesc>
<metricname>time_in_zone_L3</metricname>
<metricuname>"L3 Time in Zone"</metricuname>
<metricuunits>"seconds"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="170" blue="127"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"L4 Time in Zone"</metricdesc>
<metricname>time_in_zone_L4</metricname>
<metricuname>"L4 Time in Zone"</metricuname>
<metricuunits>"seconds"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="0" green="85" blue="127"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Time &amp; Distance"</chartname>
<metric>
<metricdesc>"Duration"</metricdesc>
<metricname>workout_time</metricname>
<metricuname>"Duration"</metricuname>
<metricuunits>"seconds"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="0" green="255" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Distance"</metricdesc>
<metricname>total_distance</metricname>
<metricuname>"Distance"</metricuname>
<metricuunits>"km"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>1</metricsymbol>
<metricpencolor red="170" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Time Riding"</metricdesc>
<metricname>time_riding</metricname>
<metricuname>"Time Riding"</metricuname>
<metricuunits>"seconds"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="0" green="85" blue="127"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Daniels Power"</chartname>
<metric>
<metricdesc>"Daniels EqP"</metricdesc>
<metricname>daniels_equivalent_power</metricname>
<metricuname>"Daniels EqP"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="170" green="85" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Daniels Points"</metricdesc>
<metricname>daniels_points</metricname>
<metricuname>"Daniels Points"</metricuname>
<metricuunits>"Daniels Points"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>0</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="255" green="170" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Skiba Power"</chartname>
<metric>
<metricdesc>"BikeScore&amp;#8482;"</metricdesc>
<metricname>skiba_bike_score</metricname>
<metricuname>"BikeScore&amp;#8482;"</metricuname>
<metricuunits>"BikeScore&amp;#8482;"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="0" green="170" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"xPower"</metricdesc>
<metricname>skiba_xpower</metricname>
<metricuname>"xPower"</metricuname>
<metricuunits>"watts"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Skiba PM"</chartname>
<metric>
<metricdesc>"Skiba Long Term Stress"</metricdesc>
<metricname>skiba_lts</metricname>
<metricuname>"Skiba Long Term Stress"</metricuname>
<metricuunits>"Stress"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="0" green="0" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Skiba Short Term Stress"</metricdesc>
<metricname>skiba_sts</metricname>
<metricuname>"Skiba Short Term Stress"</metricuname>
<metricuunits>"Stress"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="255" green="0" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Skiba Stress Balance"</metricdesc>
<metricname>skiba_sb</metricname>
<metricuname>"Skiba Stress Balance"</metricuname>
<metricuunits>"Stress Balance"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>1</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="255" green="255" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"BikeScore&amp;#8482;"</metricdesc>
<metricname>skiba_bike_score</metricname>
<metricuname>"BikeScore&amp;#8482;"</metricuname>
<metricuunits>"BikeScore&amp;#8482;"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>5</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>3</metriccurve>
<metricsymbol>0</metricsymbol>
<metricpencolor red="85" green="0" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
<LTM-chart>
<chartname>"Daniels PM"</chartname>
<metric>
<metricdesc>"Daniels Points"</metricdesc>
<metricname>daniels_points</metricname>
<metricuname>"Daniels Points"</metricuname>
<metricuunits>"Daniels Points"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>0</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>2</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="255" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Daniels Long Term Stress"</metricdesc>
<metricname>daniels_lts</metricname>
<metricuname>"Daniels Long Term Stress"</metricuname>
<metricuunits>"Stress"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>0</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="0" green="255" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Daniels Stress Balance"</metricdesc>
<metricname>daniels_sb</metricname>
<metricuname>"Daniels Stress Balance"</metricuname>
<metricuunits>"Stress"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>0</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="0" green="0" blue="0"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
<metric>
<metricdesc>"Daniels Short Term Stress"</metricdesc>
<metricname>daniels_sts</metricname>
<metricuname>"Daniels Short Term Stress"</metricuname>
<metricuunits>"Stress"</metricuunits>
<metricsmooth>0</metricsmooth>
<metrictrend>0</metrictrend>
<metrictopn>0</metrictopn>
<metricbaseline>0</metricbaseline>
<metriccurve>1</metriccurve>
<metricsymbol>-1</metricsymbol>
<metricpencolor red="0" green="85" blue="255"></metricpencolor>
<metricpenalpha>0</metricpenalpha>
<metricpenwidth>1</metricpenwidth>
<metricpenstyle>0</metricpenstyle>
<metricbrushcolor red="0" green="0" blue="0"></metricbrushcolor>
<metricbrushalpha>0</metricbrushalpha>
</metric>
</LTM-chart>
</charts>