diff --git a/src/AerobicDecoupling.cpp b/src/AerobicDecoupling.cpp index 9f289cce6..2c5819dc6 100644 --- a/src/AerobicDecoupling.cpp +++ b/src/AerobicDecoupling.cpp @@ -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 &) { diff --git a/src/BasicRideMetrics.cpp b/src/BasicRideMetrics.cpp index b470844e8..ffb29d05a 100644 --- a/src/BasicRideMetrics.cpp +++ b/src/BasicRideMetrics.cpp @@ -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 &) { 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 &) { 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 &) { @@ -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 &) { @@ -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 &) { @@ -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 &) { 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 &) { @@ -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 &) { + 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, diff --git a/src/BikeScore.cpp b/src/BikeScore.cpp index c7ad9fbb9..1bb65ab7a 100644 --- a/src/BikeScore.cpp +++ b/src/BikeScore.cpp @@ -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 &) { @@ -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 &deps) { + assert(deps.contains("skiba_xpower")); + assert(deps.contains("average_power")); + XPower *xp = dynamic_cast(deps.value("skiba_xpower")); + assert(xp); + RideMetric *ap = dynamic_cast(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(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 &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 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(); diff --git a/src/ConfigDialog.cpp b/src/ConfigDialog.cpp index f8d32b925..d1a0456b9 100644 --- a/src/ConfigDialog.cpp +++ b/src/ConfigDialog.cpp @@ -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"))); diff --git a/src/DBAccess.cpp b/src/DBAccess.cpp index 24ee5550f..17db31f47 100644 --- a/src/DBAccess.cpp +++ b/src/DBAccess.cpp @@ -30,6 +30,11 @@ #include #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; igetFileName()); + query.exec(); + } + + // construct an insert statement + QString insertStatement = "insert into metrics ( filename, timestamp, ride_date"; + const RideMetricFactory &factory = RideMetricFactory::instance(); + for (int i=0; igetFileName()); + 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; igetForSymbol(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 DBAccess::getAllDates() { - QSqlQuery query("SELECT ride_date from metrics"); + QSqlQuery query("SELECT ride_date from metrics ORDER BY ride_date;", dbconn); QList dates; - + + query.exec(); while(query.next()) { QDateTime date = query.value(0).toDateTime(); @@ -180,47 +234,38 @@ QList DBAccess::getAllDates() QList DBAccess::getAllMetricsFor(QDateTime start, QDateTime end) { QList 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 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 getAllDates(); QList getAllMetricsFor(QDateTime start, QDateTime end); - bool createMetricsTable(); QList getAllSeasons(); - bool dropMetricTable(); private: - QSqlDatabase db; + QSqlDatabase db, dbconn; + typedef QHash 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 \ No newline at end of file +#endif diff --git a/src/DanielsPoints.cpp b/src/DanielsPoints.cpp index a70d1194e..3f1f3f017 100644 --- a/src/DanielsPoints.cpp +++ b/src/DanielsPoints.cpp @@ -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 &) { @@ -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 &deps) { if (!zones || zoneRange < 0) diff --git a/src/LTMCanvasPicker.cpp b/src/LTMCanvasPicker.cpp new file mode 100644 index 000000000..b362a9ae5 --- /dev/null +++ b/src/LTMCanvasPicker.cpp @@ -0,0 +1,111 @@ +// code borrowed from event_filter qwt examples +// and modified for GC LTM purposes + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "LTMCanvasPicker.h" + +#include + +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 + + } +} diff --git a/src/LTMCanvasPicker.h b/src/LTMCanvasPicker.h new file mode 100644 index 000000000..c2309b97d --- /dev/null +++ b/src/LTMCanvasPicker.h @@ -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 + +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 diff --git a/src/LTMChartParser.cpp b/src/LTMChartParser.cpp new file mode 100644 index 000000000..f7e13a4d9 --- /dev/null +++ b/src/LTMChartParser.cpp @@ -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 +#include +#include + +// 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") // block + setting.metrics.append(metric); + else if (qName == "LTM-chart") // block + settings.append(setting); + else if (qName == "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 +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 charts) +{ + // open file - truncate contents + QFile file(filename); + file.open(QFile::WriteOnly); + file.resize(0); + QTextStream out(&file); + + // begin document + out << "\n"; + + // write out to file + foreach (LTMSettings chart, charts) { + // chart name + out<\n\t\t\"%1\"\n").arg(xmlprotect(chart.name)); + + // all the metrics + foreach (MetricDetail metric, chart.metrics) { + out<\n"); + out<\"%1\"\n").arg(xmlprotect(metric.name)); + out<%1\n").arg(metric.symbol); + out<\"%1\"\n").arg(xmlprotect(metric.uname)); + out<\"%1\"\n").arg(xmlprotect(metric.uunits)); + + // SMOOTH, TREND, TOPN + out<%1\n").arg(metric.smooth); + out<%1\n").arg(metric.trend); + out<%1\n").arg(metric.topN); + out<%1\n").arg(metric.baseline); + + // CURVE, SYMBOL + out<%1\n").arg(curveToInt(metric.curveStyle)); + out<%1\n").arg(symbolToInt(metric.symbolStyle)); + + // PEN + out<\n") + .arg(metric.penColor.red()) + .arg(metric.penColor.green()) + .arg(metric.penColor.blue()); + out<%1\n").arg(metric.penAlpha); + out<%1\n").arg(metric.penWidth); + out<%1\n").arg(metric.penStyle); + + // BRUSH + out<\n") + .arg(metric.brushColor.red()) + .arg(metric.brushColor.green()) + .arg(metric.brushColor.blue()); + out<%1\n").arg(metric.brushAlpha); + + out<\n"); + } + out<\n"); + } + + // end document + out << "\n"; + + // close file + file.close(); +} diff --git a/src/LTMChartParser.h b/src/LTMChartParser.h new file mode 100644 index 000000000..cb4787360 --- /dev/null +++ b/src/LTMChartParser.h @@ -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 +#include "LTMSettings.h" +#include "LTMTool.h" + +class LTMChartParser : public QXmlDefaultHandler +{ + +public: + static void serialize(QString, QList); + + // 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 getSettings(); + +protected: + QString buffer; + LTMSettings setting; + MetricDetail metric; + int red, green, blue; + QList settings; +}; +#endif diff --git a/src/LTMPlot.cpp b/src/LTMPlot.cpp new file mode 100644 index 000000000..38814d303 --- /dev/null +++ b/src/LTMPlot.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +#include // for isinf() isnan() +#include + +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 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 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 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 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 sortedList; + + // copy the yvalues, retaining the offset + for(int i=0; i hxdata, hydata; + hxdata.resize(metricDetail.topN); + hydata.resize(metricDetail.topN); + + // QMap orders the list so start at the top and work + // backwards + QMapIterator 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&x,QVector&y,int&n) +{ + QList *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 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 (lastDaystart.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 &customData) +{ + QDate earliest, latest; // rides + boost::shared_ptr 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 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 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 zone_lows = zones->getZoneLows(zone_range); + QList 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); +} + diff --git a/src/LTMPlot.h b/src/LTMPlot.h new file mode 100644 index 000000000..99d34a08f --- /dev/null +++ b/src/LTMPlot.h @@ -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 +#include +#include +#include +#include +#include + +#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 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 curves; // metric symbol with curve object + QHash axes; // units and associated axis + LTMScaleDraw *scale; + QwtPlotGrid *grid; + QDate firstDate, + lastDate; + + int groupForDate(QDate , int); + void createCurveData(LTMSettings *, MetricDetail, + QVector&, QVector&, int&); + void createPMCCurveData(LTMSettings *, MetricDetail, QList &); + 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 + diff --git a/src/LTMSettings.cpp b/src/LTMSettings.cpp new file mode 100644 index 000000000..0739b5f88 --- /dev/null +++ b/src/LTMSettings.cpp @@ -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 +#include +#include +#include + + +/*---------------------------------------------------------------------- + * EDIT CHART DIALOG + *--------------------------------------------------------------------*/ +EditChartDialog::EditChartDialog(MainWindow *mainWindow, LTMSettings *settings, QListpresets) : + 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*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; iinvisibleRootItem()->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 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 charts) +{ + LTMChartParser::serialize(QString(home.path() + "/charts.xml"), charts); +} + + +/*---------------------------------------------------------------------- + * Read charts.xml + *--------------------------------------------------------------------*/ + +void +LTMSettings::readChartXML(QDir home, QList &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(); +} diff --git a/src/LTMSettings.h b/src/LTMSettings.h new file mode 100644 index 000000000..529836a45 --- /dev/null +++ b/src/LTMSettings.h @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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); + void readChartXML(QDir, QList&charts); + + QString name; + QString title; + QDateTime start; + QDateTime end; + int groupBy; + bool shadeZones; + QList metrics; + QList *data; + LTMTool *ltmTool; +}; + +class EditChartDialog : public QDialog +{ + Q_OBJECT + + public: + EditChartDialog(MainWindow *, LTMSettings *, QList); + + public slots: + void okClicked(); + void cancelClicked(); + + private: + MainWindow *mainWindow; + LTMSettings *settings; + + QList presets; + QLineEdit *chartName; + QPushButton *okButton, *cancelButton; +}; + +class ChartManagerDialog : public QDialog +{ + Q_OBJECT + + public: + ChartManagerDialog(MainWindow *, QList *); + + public slots: + void okClicked(); + void cancelClicked(); + void exportClicked(); + void importClicked(); + void upClicked(); + void downClicked(); + void renameClicked(); + void deleteClicked(); + + private: + MainWindow *mainWindow; + QList *presets; + + QLineEdit *chartName; + + QTreeWidget *charts; + + QPushButton *importButton, *exportButton; + QPushButton *upButton, *downButton, *renameButton, *deleteButton; + QPushButton *okButton, *cancelButton; +}; + +#endif diff --git a/src/LTMTool.cpp b/src/LTMTool.cpp new file mode 100644 index 000000000..751716a84 --- /dev/null +++ b/src/LTMTool.cpp @@ -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 +#include +#include + +// seasons support +#include "Season.h" +#include "SeasonParser.h" +#include +#include + +LTMTool::LTMTool(MainWindow *parent, const QDir &home) : QWidget(parent), home(home), main(parent) +{ + // get application settings + boost::shared_ptr 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 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 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; iconversion() != 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); +} diff --git a/src/LTMTool.h b/src/LTMTool.h new file mode 100644 index 000000000..1b5d06c5c --- /dev/null +++ b/src/LTMTool.h @@ -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 +#include + +// 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 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 seasons; + QTreeWidget *dateRangeTree; + QTreeWidgetItem *allDateRanges; + const Season *dateRange; + + QList 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 + diff --git a/src/LTMTrend.cpp b/src/LTMTrend.cpp new file mode 100644 index 000000000..46f8a2a6b --- /dev/null +++ b/src/LTMTrend.cpp @@ -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 +#include +#include "LTMTrend.h" + +#include + +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); + } + } +} diff --git a/src/LTMTrend.h b/src/LTMTrend.h new file mode 100644 index 000000000..1c14e0a94 --- /dev/null +++ b/src/LTMTrend.h @@ -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 diff --git a/src/LTMWindow.cpp b/src/LTMWindow.cpp new file mode 100644 index 000000000..cedd727cd --- /dev/null +++ b/src/LTMWindow.cpp @@ -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 +#include + +#include +#include +#include +#include + +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 appsettings = GetApplicationSettings(); + QVariant splitterSizes = appsettings->value(GC_LTM_SPLITTER_SIZES); + if (splitterSizes != QVariant()) + ltmSplitter->restoreState(splitterSizes.toByteArray()); + else { + QList 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; iaddItem(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 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 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 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; iaddItem(presets[i].name, i); + + // update charts.xml + settings.writeChartXML(main->home, presets); + } +} diff --git a/src/LTMWindow.h b/src/LTMWindow.h new file mode 100644 index 000000000..17df4d27e --- /dev/null +++ b/src/LTMWindow.h @@ -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 +#include +#include "MainWindow.h" +#include "MetricAggregator.h" +#include "Season.h" +#include "LTMPlot.h" +#include "LTMTool.h" +#include "LTMSettings.h" +#include "LTMCanvasPicker.h" +#include + +#include +#include + +// 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 presets; + + // local state + bool active; + bool dirty; + LTMSettings settings; // all the plot settings + QList 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 diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 0dd7ba1c2..84de77420 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -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(_item); + rideDeleted(item); + QTreeWidgetItem *itemToSelect = NULL; for (x=0; xchildCount(); ++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(); } diff --git a/src/MainWindow.h b/src/MainWindow.h index aa89206d1..7ee695afb 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -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; diff --git a/src/MetricAggregator.cpp b/src/MetricAggregator.cpp index f2c72f6be..3e026e488 100644 --- a/src/MetricAggregator.cpp +++ b/src/MetricAggregator.cpp @@ -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 #include #include +#include -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 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::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 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 computed = RideMetric::computeMetrics(ride, zones, metrics); - while (!todo.empty()) { - QMutableSetIterator i(todo); -later: - while (i.hasNext()) { - const QString &name = i.next(); - const QVector &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 +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 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(); -} - diff --git a/src/MetricAggregator.h b/src/MetricAggregator.h index 700c57c20..ba35e3213 100644 --- a/src/MetricAggregator.h +++ b/src/MetricAggregator.h @@ -20,30 +20,41 @@ #ifndef METRICAGGREGATOR_H_ #define METRICAGGREGATOR_H_ - #include #include "RideFile.h" #include #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 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 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_ */ diff --git a/src/Pages.cpp b/src/Pages.cpp index 434859572..f14381648 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -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); diff --git a/src/Pages.h b/src/Pages.h index 3297df855..2ef90625c 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -86,6 +86,7 @@ class CyclistPage : public QWidget QLineEdit *perfManStart; QLineEdit *perfManSTSavg; QLineEdit *perfManLTSavg; + QCheckBox *showSBToday; int getCurrentRange(); bool isNewMode(); diff --git a/src/PeakPower.cpp b/src/PeakPower.cpp new file mode 100644 index 000000000..d9e86c0df --- /dev/null +++ b/src/PeakPower.cpp @@ -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 + +#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 &) { + QList 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(); diff --git a/src/PerfPlot.cpp b/src/PerfPlot.cpp index fb8d5a378..a44a0776b 100644 --- a/src/PerfPlot.cpp +++ b/src/PerfPlot.cpp @@ -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)"); diff --git a/src/PerformanceManagerWindow.cpp b/src/PerformanceManagerWindow.cpp index f32a9c122..b14183cd7 100644 --- a/src/PerformanceManagerWindow.cpp +++ b/src/PerformanceManagerWindow.cpp @@ -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); diff --git a/src/PerformanceManagerWindow.h b/src/PerformanceManagerWindow.h index e379de2bf..11fc7a098 100644 --- a/src/PerformanceManagerWindow.h +++ b/src/PerformanceManagerWindow.h @@ -62,6 +62,8 @@ class PerformanceManagerWindow : public QWidget StressCalculator *sc; MainWindow *mainWindow; + bool active; + PerfPlot *perfplot; QLineEdit *PMSTSValue; QLineEdit *PMLTSValue; diff --git a/src/RideFile.cpp b/src/RideFile.cpp index 5f5d1dcb5..0413a2d9d 100644 --- a/src/RideFile.cpp +++ b/src/RideFile.cpp @@ -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, diff --git a/src/RideMetric.h b/src/RideMetric.h index 8f950a03b..4e3f86421 100644 --- a/src/RideMetric.h +++ b/src/RideMetric.h @@ -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(other); @@ -112,6 +123,7 @@ class RideMetricFactory { static QVector noDeps; QVector metricNames; + QVector metricTypes; QHash metrics; QHash*> 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 *copy = new QVector; for (int i = 0; i < deps->size(); ++i) diff --git a/src/Season.cpp b/src/Season.cpp index 81aca73d9..baf90484d 100644 --- a/src/Season.cpp +++ b/src/Season.cpp @@ -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(); +} diff --git a/src/Season.h b/src/Season.h index 17a650f37..fe2f26fae 100644 --- a/src/Season.h +++ b/src/Season.h @@ -23,21 +23,51 @@ #include #include +#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_ */ diff --git a/src/SeasonParser.cpp b/src/SeasonParser.cpp index 7923dc831..2d49f9152 100644 --- a/src/SeasonParser.cpp +++ b/src/SeasonParser.cpp @@ -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, QListSeasons) +{ + // open file - truncate contents + QFile file(filename); + file.open(QFile::WriteOnly); + file.resize(0); + QTextStream out(&file); + + // begin document + out << "\n"; + + // write out to file + foreach (Season season, Seasons) { + if (season.getType() != Season::temporary) { + out<\n" + "\t\t%1\n" + "\t\t%2\n" + "\t\t%3\n" + "\t\t%4\n" + "\t\n") + .arg(season.getName()) + .arg(season.getStart().toString("yyyy-MM-dd")) + .arg(season.getEnd().toString("yyyy-MM-dd")) + .arg(season.getType()); + } + } + + // end document + out << "\n"; + + // close file + file.close(); + + return true; // success +} diff --git a/src/SeasonParser.h b/src/SeasonParser.h index 4293eb06f..0681c6e90 100644 --- a/src/SeasonParser.h +++ b/src/SeasonParser.h @@ -26,6 +26,10 @@ class SeasonParser : public QXmlDefaultHandler { public: + // marshall + static bool serialize(QString, QList); + + // unmarshall bool startDocument(); bool endDocument(); bool endElement( const QString&, const QString&, const QString &qName ); diff --git a/src/Settings.h b/src/Settings.h index ace3e55a4..a0cc367bc 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -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 diff --git a/src/StressCalculator.cpp b/src/StressCalculator.cpp index 3e6c122fb..4d1f1e4c7 100644 --- a/src/StressCalculator.cpp +++ b/src/StressCalculator.cpp @@ -1,7 +1,10 @@ #include "StressCalculator.h" +#include "MetricAggregator.h" #include "RideMetric.h" #include "RideItem.h" +#include "MainWindow.h" + #include #include @@ -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 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 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 > 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 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(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 >::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]; + } +} diff --git a/src/StressCalculator.h b/src/StressCalculator.h index c1db09b6f..7cde6bb05 100644 --- a/src/StressCalculator.h +++ b/src/StressCalculator.h @@ -15,6 +15,7 @@ #include #include #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 stsvalues; QVector ltsvalues; + QVector ltsramp; + QVector stsramp; QVector sbvalues; QVector 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; }; diff --git a/src/SummaryMetrics.cpp b/src/SummaryMetrics.cpp deleted file mode 100644 index ea6a98ca4..000000000 --- a/src/SummaryMetrics.cpp +++ /dev/null @@ -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; } \ No newline at end of file diff --git a/src/SummaryMetrics.h b/src/SummaryMetrics.h index cd8e6cfca..aa83a1c62 100644 --- a/src/SummaryMetrics.h +++ b/src/SummaryMetrics.h @@ -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 value; }; diff --git a/src/TimeInZone.cpp b/src/TimeInZone.cpp new file mode 100644 index 000000000..bfe04ce0a --- /dev/null +++ b/src/TimeInZone.cpp @@ -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 + +#define tr(s) QObject::tr(s) + +class ZoneTime : public RideMetric { + int level; + double seconds; + + QList lo; + QList 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 &) + { + // 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(); diff --git a/src/application.qrc b/src/application.qrc index 45fb5dfaf..074cdd6dc 100644 --- a/src/application.qrc +++ b/src/application.qrc @@ -10,5 +10,6 @@ images/config.png translations/gc_fr.qm translations/gc_ja.qm + xml/charts.xml diff --git a/src/src.pro b/src/src.pro index 4ce6a6e48..23c8e2c28 100644 --- a/src/src.pro +++ b/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 \ diff --git a/src/xml/charts.xml b/src/xml/charts.xml new file mode 100644 index 000000000..8c0d5b1c4 --- /dev/null +++ b/src/xml/charts.xml @@ -0,0 +1,704 @@ + + + "Aerobic Power" + + "60 min Peak Power" + 60m_critical_power + "60 min Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "30 min Peak Power" + 30m_critical_power + "30 min Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "20 min Peak Power" + 20m_critical_power + "20 min Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "10 min Peak Power" + 10m_critical_power + "10 min Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + + "Anaerobic Power" + + "1 min Peak Power" + 1m_critical_power + "1 min Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "1 sec Peak Power" + 1s_critical_power + "1 sec Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "10 sec Peak Power" + 10s_critical_power + "10 sec Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "15 sec Peak Power" + 15s_critical_power + "15 sec Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "20 sec Peak Power" + 20s_critical_power + "20 sec Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "30 sec Peak Power" + 30s_critical_power + "30 sec Peak Power" + "watts" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + + "Critical Power Trend" + + "60 min Peak Power" + 60m_critical_power + "60 min Peak Power" + "watts" + 0 + 1 + 10 + 0 + 4 + 0 + + 0 + 1 + 0 + + 0 + + + + "Power & Speed Trend" + + "Average Power" + average_power + "Average Power" + "watts" + 0 + 1 + 25 + 0 + 4 + 0 + + 0 + 1 + 0 + + 0 + + + "Average Speed" + average_speed + "Average Speed" + "kph" + 0 + 1 + 25 + 0 + 4 + 2 + + 0 + 1 + 0 + + 0 + + + + "Cardiovascular Response" + + "95% Heart Rate" + ninety_five_percent_hr + "95% Heart Rate" + "bpm" + 0 + 0 + 5 + 0 + 1 + 0 + + 0 + 1 + 0 + + 0 + + + "Aerobic Decoupling" + aerobic_decoupling + "Aerobic Decoupling" + "%" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + "Average Heart Rate" + average_hr + "Average Heart Rate" + "bpm" + 0 + 0 + 5 + 0 + 1 + 0 + + 0 + 1 + 0 + + 0 + + + "Max Heartrate" + max_heartrate + "Max Heartrate" + "bpm" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + + "Training Mix" + + "Skiba VI" + skiba_variability_index + "Skiba VI" + "Skiba VI" + 0 + 0 + 5 + 0 + 1 + 0 + + 0 + 1 + 0 + + 0 + + + "Relative Intensity" + skiba_relative_intensity + "Relative Intensity" + "Relative Intensity" + 0 + 0 + 5 + 0 + 1 + 0 + + 0 + 1 + 0 + + 0 + + + "Work" + total_work + "Work" + "kJ" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + + "Tempo & Threshold Time" + + "L3 Time in Zone" + time_in_zone_L3 + "L3 Time in Zone" + "seconds" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + "L4 Time in Zone" + time_in_zone_L4 + "L4 Time in Zone" + "seconds" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + + "Time & Distance" + + "Duration" + workout_time + "Duration" + "seconds" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + "Distance" + total_distance + "Distance" + "km" + 0 + 0 + 5 + 0 + 1 + 1 + + 0 + 1 + 0 + + 0 + + + "Time Riding" + time_riding + "Time Riding" + "seconds" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + + "Daniels Power" + + "Daniels EqP" + daniels_equivalent_power + "Daniels EqP" + "watts" + 0 + 0 + 5 + 0 + 1 + 0 + + 0 + 1 + 0 + + 0 + + + "Daniels Points" + daniels_points + "Daniels Points" + "Daniels Points" + 0 + 0 + 0 + 0 + 3 + -1 + + 0 + 1 + 0 + + 0 + + + + "Skiba Power" + + "BikeScore&#8482;" + skiba_bike_score + "BikeScore&#8482;" + "BikeScore&#8482;" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + "xPower" + skiba_xpower + "xPower" + "watts" + 0 + 0 + 5 + 0 + 1 + 0 + + 0 + 1 + 0 + + 0 + + + + "Skiba PM" + + "Skiba Long Term Stress" + skiba_lts + "Skiba Long Term Stress" + "Stress" + 0 + 0 + 5 + 0 + 1 + -1 + + 0 + 1 + 0 + + 0 + + + "Skiba Short Term Stress" + skiba_sts + "Skiba Short Term Stress" + "Stress" + 0 + 0 + 5 + 0 + 1 + -1 + + 0 + 1 + 0 + + 0 + + + "Skiba Stress Balance" + skiba_sb + "Skiba Stress Balance" + "Stress Balance" + 0 + 0 + 1 + 0 + 3 + -1 + + 0 + 1 + 0 + + 0 + + + "BikeScore&#8482;" + skiba_bike_score + "BikeScore&#8482;" + "BikeScore&#8482;" + 0 + 0 + 5 + 0 + 3 + 0 + + 0 + 1 + 0 + + 0 + + + + "Daniels PM" + + "Daniels Points" + daniels_points + "Daniels Points" + "Daniels Points" + 0 + 0 + 0 + 0 + 2 + -1 + + 0 + 1 + 0 + + 0 + + + "Daniels Long Term Stress" + daniels_lts + "Daniels Long Term Stress" + "Stress" + 0 + 0 + 0 + 0 + 1 + -1 + + 0 + 1 + 0 + + 0 + + + "Daniels Stress Balance" + daniels_sb + "Daniels Stress Balance" + "Stress" + 0 + 0 + 0 + 0 + 1 + -1 + + 0 + 1 + 0 + + 0 + + + "Daniels Short Term Stress" + daniels_sts + "Daniels Short Term Stress" + "Stress" + 0 + 0 + 0 + 0 + 1 + -1 + + 0 + 1 + 0 + + 0 + + +