mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 08:08:42 +00:00
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:
committed by
Sean Rhea
parent
cef5cca454
commit
7bb9cf5462
@@ -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*> &) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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™"); }
|
||||
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();
|
||||
|
||||
|
||||
@@ -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")));
|
||||
|
||||
289
src/DBAccess.cpp
289
src/DBAccess.cpp
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
111
src/LTMCanvasPicker.cpp
Normal 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
34
src/LTMCanvasPicker.h
Normal 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
293
src/LTMChartParser.cpp
Normal 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("™"); // 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( "™", 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("™"); // process html encoding of(TM)
|
||||
QString tm = trademark.toPlainText();
|
||||
|
||||
QString s = string;
|
||||
s.replace( tm, "™" );
|
||||
s.replace( "&", "&" );
|
||||
s.replace( ">", ">" );
|
||||
s.replace( "<", "<" );
|
||||
s.replace( "\"", """ );
|
||||
s.replace( "\'", "'" );
|
||||
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
47
src/LTMChartParser.h
Normal 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
916
src/LTMPlot.cpp
Normal 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
168
src/LTMPlot.h
Normal 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
330
src/LTMSettings.cpp
Normal 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
155
src/LTMSettings.h
Normal 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
848
src/LTMTool.cpp
Normal 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
140
src/LTMTool.h
Normal 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
56
src/LTMTrend.cpp
Normal 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
43
src/LTMTrend.h
Normal 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
306
src/LTMWindow.cpp
Normal 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
123
src/LTMWindow.h
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
158
src/PeakPower.cpp
Normal 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();
|
||||
@@ -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)");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ class PerformanceManagerWindow : public QWidget
|
||||
StressCalculator *sc;
|
||||
|
||||
MainWindow *mainWindow;
|
||||
bool active;
|
||||
|
||||
PerfPlot *perfplot;
|
||||
QLineEdit *PMSTSValue;
|
||||
QLineEdit *PMLTSValue;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
32
src/Season.h
32
src/Season.h
@@ -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_ */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
@@ -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
131
src/TimeInZone.cpp
Normal 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();
|
||||
@@ -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>
|
||||
|
||||
17
src/src.pro
17
src/src.pro
@@ -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
704
src/xml/charts.xml
Normal 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 & 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 & 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 & 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&#8482;"</metricdesc>
|
||||
<metricname>skiba_bike_score</metricname>
|
||||
<metricuname>"BikeScore&#8482;"</metricuname>
|
||||
<metricuunits>"BikeScore&#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&#8482;"</metricdesc>
|
||||
<metricname>skiba_bike_score</metricname>
|
||||
<metricuname>"BikeScore&#8482;"</metricuname>
|
||||
<metricuunits>"BikeScore&#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>
|
||||
Reference in New Issue
Block a user