mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
.. lots of crashes fixed .. AVOID data table for PMC for now as they are a mess and will fix tomorrow night
774 lines
26 KiB
C++
774 lines
26 KiB
C++
/*
|
|
* 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 "Context.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "RideFileCache.h"
|
|
#include "SummaryMetrics.h"
|
|
#include "Settings.h"
|
|
#include "math.h"
|
|
#include "Units.h" // for MILES_PER_KM
|
|
|
|
#include <QtGui>
|
|
#include <QString>
|
|
#include <QDebug>
|
|
#include <QWebView>
|
|
#include <QWebFrame>
|
|
|
|
#include <qwt_plot_panner.h>
|
|
#include <qwt_plot_zoomer.h>
|
|
#include <qwt_plot_picker.h>
|
|
#include <qwt_plot_marker.h>
|
|
|
|
LTMWindow::LTMWindow(Context *context) :
|
|
GcChartWindow(context), context(context), dirty(true)
|
|
{
|
|
useToToday = useCustom = false;
|
|
plotted = DateRange(QDate(01,01,01), QDate(01,01,01));
|
|
|
|
// the plot
|
|
QVBoxLayout *mainLayout = new QVBoxLayout;
|
|
ltmPlot = new LTMPlot(this, context);
|
|
|
|
// the data table
|
|
dataSummary = new QWebView(this);
|
|
dataSummary->setContentsMargins(0,0,0,0);
|
|
dataSummary->page()->view()->setContentsMargins(0,0,0,0);
|
|
dataSummary->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
dataSummary->setAcceptDrops(false);
|
|
|
|
QFont defaultFont; // mainwindow sets up the defaults.. we need to apply
|
|
dataSummary->settings()->setFontSize(QWebSettings::DefaultFontSize, defaultFont.pointSize()+1);
|
|
dataSummary->settings()->setFontFamily(QWebSettings::StandardFont, defaultFont.family());
|
|
|
|
|
|
// the stack
|
|
stack = new QStackedWidget(this);
|
|
stack->addWidget(ltmPlot);
|
|
stack->addWidget(dataSummary);
|
|
stack->setCurrentIndex(0);
|
|
mainLayout->addWidget(stack);
|
|
setChartLayout(mainLayout);
|
|
|
|
// reveal controls
|
|
QHBoxLayout *revealLayout = new QHBoxLayout;
|
|
revealLayout->setContentsMargins(0,0,0,0);
|
|
revealLayout->addStretch();
|
|
revealLayout->addWidget(new QLabel(tr("Group by"),this));
|
|
|
|
rGroupBy = new QxtStringSpinBox(this);
|
|
QStringList strings;
|
|
strings << tr("Days")
|
|
<< tr("Weeks")
|
|
<< tr("Months")
|
|
<< tr("Years")
|
|
<< tr("Time Of Day");
|
|
rGroupBy->setStrings(strings);
|
|
rGroupBy->setValue(0);
|
|
|
|
revealLayout->addWidget(rGroupBy);
|
|
rData = new QCheckBox(tr("Data Table"), this);
|
|
rEvents = new QCheckBox(tr("Show events"), this);
|
|
QVBoxLayout *checks = new QVBoxLayout;
|
|
checks->setSpacing(2);
|
|
checks->setContentsMargins(0,0,0,0);
|
|
checks->addWidget(rData);
|
|
checks->addWidget(rEvents);
|
|
revealLayout->addLayout(checks);
|
|
revealLayout->addStretch();
|
|
setRevealLayout(revealLayout);
|
|
|
|
// the controls
|
|
QWidget *c = new QWidget;
|
|
c->setContentsMargins(0,0,0,0);
|
|
QVBoxLayout *cl = new QVBoxLayout(c);
|
|
cl->setContentsMargins(0,0,0,0);
|
|
cl->setSpacing(0);
|
|
setControls(c);
|
|
|
|
// the popup
|
|
popup = new GcPane();
|
|
ltmPopup = new LTMPopup(context);
|
|
QVBoxLayout *popupLayout = new QVBoxLayout();
|
|
popupLayout->addWidget(ltmPopup);
|
|
popup->setLayout(popupLayout);
|
|
|
|
picker = new LTMToolTip(QwtPlot::xBottom, QwtPlot::yLeft,
|
|
QwtPicker::VLineRubberBand,
|
|
QwtPicker::AlwaysOn,
|
|
ltmPlot->canvas(),
|
|
"");
|
|
picker->setMousePattern(QwtEventPattern::MouseSelect1,
|
|
Qt::LeftButton);
|
|
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(context, &settings);
|
|
|
|
// initialise
|
|
settings.ltmTool = ltmTool;
|
|
settings.data = NULL;
|
|
settings.groupBy = LTM_DAY;
|
|
settings.legend = ltmTool->showLegend->isChecked();
|
|
settings.events = ltmTool->showEvents->isChecked();
|
|
settings.shadeZones = ltmTool->shadeZones->isChecked();
|
|
settings.showData = ltmTool->showData->isChecked();
|
|
rData->setChecked(ltmTool->showData->isChecked());
|
|
rEvents->setChecked(ltmTool->showEvents->isChecked());
|
|
cl->addWidget(ltmTool);
|
|
|
|
connect(this, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange)));
|
|
connect(ltmTool, SIGNAL(filterChanged()), this, SLOT(filterChanged()));
|
|
connect(context, SIGNAL(homeFilterChanged()), this, SLOT(filterChanged()));
|
|
connect(ltmTool->groupBy, SIGNAL(currentIndexChanged(int)), this, SLOT(groupBySelected(int)));
|
|
connect(rGroupBy, SIGNAL(valueChanged(int)), this, SLOT(rGroupBySelected(int)));
|
|
//!!! connect(ltmTool->saveButton, SIGNAL(clicked(bool)), this, SLOT(saveClicked(void)));
|
|
connect(ltmTool->applyButton, SIGNAL(clicked(bool)), this, SLOT(applyClicked(void)));
|
|
connect(ltmTool->shadeZones, SIGNAL(stateChanged(int)), this, SLOT(shadeZonesClicked(int)));
|
|
connect(ltmTool->showData, SIGNAL(stateChanged(int)), this, SLOT(showDataClicked(int)));
|
|
connect(rData, SIGNAL(stateChanged(int)), this, SLOT(showDataClicked(int)));
|
|
connect(ltmTool->showLegend, SIGNAL(stateChanged(int)), this, SLOT(showLegendClicked(int)));
|
|
connect(ltmTool->showEvents, SIGNAL(stateChanged(int)), this, SLOT(showEventsClicked(int)));
|
|
connect(rEvents, SIGNAL(stateChanged(int)), this, SLOT(showEventsClicked(int)));
|
|
connect(ltmTool, SIGNAL(useCustomRange(DateRange)), this, SLOT(useCustomRange(DateRange)));
|
|
connect(ltmTool, SIGNAL(useThruToday()), this, SLOT(useThruToday()));
|
|
connect(ltmTool, SIGNAL(useStandardRange()), this, SLOT(useStandardRange()));
|
|
connect(ltmTool, SIGNAL(curvesChanged()), this, SLOT(refresh()));
|
|
connect(context, SIGNAL(filterChanged()), this, SLOT(refresh()));
|
|
|
|
// connect pickers to ltmPlot
|
|
connect(_canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), ltmPlot, SLOT(pointHover(QwtPlotCurve*, int)));
|
|
connect(_canvasPicker, SIGNAL(pointClicked(QwtPlotCurve*, int)), ltmPlot, SLOT(pointClicked(QwtPlotCurve*, int)));
|
|
|
|
connect(context, SIGNAL(rideAdded(RideItem*)), this, SLOT(refresh(void)));
|
|
connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(refresh(void)));
|
|
connect(context, SIGNAL(configChanged()), this, SLOT(refresh()));
|
|
}
|
|
|
|
LTMWindow::~LTMWindow()
|
|
{
|
|
delete popup;
|
|
}
|
|
|
|
void
|
|
LTMWindow::rideSelected() { } // deprecated
|
|
|
|
void
|
|
LTMWindow::refreshPlot()
|
|
{
|
|
if (amVisible() == true) {
|
|
|
|
if (ltmTool->showData->isChecked()) {
|
|
|
|
refreshDataTable();
|
|
|
|
} else {
|
|
plotted = DateRange(settings.start.date(), settings.end.date());
|
|
ltmPlot->setData(&settings);
|
|
dirty = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::useCustomRange(DateRange range)
|
|
{
|
|
// plot using the supplied range
|
|
useCustom = true;
|
|
useToToday = false;
|
|
custom = range;
|
|
dateRangeChanged(custom);
|
|
}
|
|
|
|
void
|
|
LTMWindow::useStandardRange()
|
|
{
|
|
useToToday = useCustom = false;
|
|
dateRangeChanged(myDateRange);
|
|
}
|
|
|
|
void
|
|
LTMWindow::useThruToday()
|
|
{
|
|
// plot using the supplied range
|
|
useCustom = false;
|
|
useToToday = true;
|
|
custom = myDateRange;
|
|
if (custom.to > QDate::currentDate()) custom.to = QDate::currentDate();
|
|
dateRangeChanged(custom);
|
|
}
|
|
|
|
// total redraw, reread data etc
|
|
void
|
|
LTMWindow::refresh()
|
|
{
|
|
|
|
// refresh for changes to ridefiles / zones
|
|
if (amVisible() == true && context->athlete->metricDB != NULL) {
|
|
|
|
results.clear(); // clear any old data
|
|
results = context->athlete->metricDB->getAllMetricsFor(settings.start, settings.end);
|
|
measures.clear(); // clear any old data
|
|
measures = context->athlete->metricDB->getAllMeasuresFor(settings.start, settings.end);
|
|
bestsresults.clear();
|
|
bestsresults = RideFileCache::getAllBestsFor(context, settings.metrics, settings.start, settings.end);
|
|
refreshPlot();
|
|
repaint(); // title changes color when filters change
|
|
dirty = false;
|
|
|
|
} else {
|
|
dirty = true;
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::dateRangeChanged(DateRange range)
|
|
{
|
|
// do we need to use custom range?
|
|
if (useCustom || useToToday) range = custom;
|
|
|
|
// we already plotted that date range
|
|
if (amVisible() || dirty || range.from != plotted.from || range.to != plotted.to) {
|
|
|
|
settings.data = &results;
|
|
settings.measures = &measures;
|
|
settings.bests = &bestsresults;
|
|
|
|
// apply filter to new date range too -- will also refresh plot
|
|
filterChanged();
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::filterChanged()
|
|
{
|
|
if (amVisible() == false || context->athlete->metricDB == NULL) return;
|
|
|
|
if (useCustom) {
|
|
|
|
settings.start = QDateTime(custom.from, QTime(0,0));
|
|
settings.end = QDateTime(custom.to, QTime(24,0,0));
|
|
|
|
} else if (useToToday) {
|
|
|
|
settings.start = QDateTime(myDateRange.from, QTime(0,0));
|
|
settings.end = QDateTime(myDateRange.to, QTime(24,0,0));
|
|
|
|
QDate today = QDate::currentDate();
|
|
if (settings.end.date() > today) settings.end = QDateTime(today, QTime(24,0,0));
|
|
|
|
} else {
|
|
|
|
settings.start = QDateTime(myDateRange.from, QTime(0,0));
|
|
settings.end = QDateTime(myDateRange.to, QTime(24,0,0));
|
|
|
|
}
|
|
settings.title = myDateRange.name;
|
|
settings.data = &results;
|
|
settings.bests = &bestsresults;
|
|
settings.measures = &measures;
|
|
|
|
// if we want weeks and start is not a monday go back to the monday
|
|
int dow = settings.start.date().dayOfWeek();
|
|
if (settings.groupBy == LTM_WEEK && dow >1 && settings.start != QDateTime(QDate(), QTime(0,0)))
|
|
settings.start = settings.start.addDays(-1*(dow-1));
|
|
|
|
// we need to get data again and apply filter
|
|
results.clear(); // clear any old data
|
|
results = context->athlete->metricDB->getAllMetricsFor(settings.start, settings.end);
|
|
measures.clear(); // clear any old data
|
|
measures = context->athlete->metricDB->getAllMeasuresFor(settings.start, settings.end);
|
|
bestsresults.clear();
|
|
bestsresults = RideFileCache::getAllBestsFor(context, settings.metrics, settings.start, settings.end);
|
|
|
|
// loop through results removing any not in stringlist..
|
|
if (ltmTool->isFiltered()) {
|
|
|
|
// metrics filtering
|
|
QList<SummaryMetrics> filteredresults;
|
|
foreach (SummaryMetrics x, results) {
|
|
if (ltmTool->filters().contains(x.getFileName()))
|
|
filteredresults << x;
|
|
}
|
|
results = filteredresults;
|
|
|
|
// metrics filtering
|
|
QList<SummaryMetrics> filteredbestsresults;
|
|
foreach (SummaryMetrics x, bestsresults) {
|
|
if (ltmTool->filters().contains(x.getFileName()))
|
|
filteredbestsresults << x;
|
|
}
|
|
bestsresults = filteredbestsresults;
|
|
|
|
settings.data = &results;
|
|
settings.measures = &measures;
|
|
settings.bests = &bestsresults;
|
|
}
|
|
|
|
if (context->ishomefiltered) {
|
|
|
|
// metrics filtering
|
|
QList<SummaryMetrics> filteredresults;
|
|
foreach (SummaryMetrics x, results) {
|
|
if (context->homeFilters.contains(x.getFileName()))
|
|
filteredresults << x;
|
|
}
|
|
results = filteredresults;
|
|
|
|
// metrics filtering
|
|
QList<SummaryMetrics> filteredbestsresults;
|
|
foreach (SummaryMetrics x, bestsresults) {
|
|
if (context->homeFilters.contains(x.getFileName()))
|
|
filteredbestsresults << x;
|
|
}
|
|
bestsresults = filteredbestsresults;
|
|
|
|
settings.data = &results;
|
|
settings.measures = &measures;
|
|
settings.bests = &bestsresults;
|
|
}
|
|
|
|
refreshPlot();
|
|
|
|
repaint(); // just for the title..
|
|
}
|
|
|
|
void
|
|
LTMWindow::rGroupBySelected(int selected)
|
|
{
|
|
if (selected >= 0) {
|
|
settings.groupBy = ltmTool->groupBy->itemData(selected).toInt();
|
|
ltmTool->groupBy->setCurrentIndex(selected);
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::groupBySelected(int selected)
|
|
{
|
|
if (selected >= 0) {
|
|
settings.groupBy = ltmTool->groupBy->itemData(selected).toInt();
|
|
rGroupBy->setValue(selected);
|
|
refreshPlot();
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::showDataClicked(int state)
|
|
{
|
|
bool checked = state;
|
|
|
|
// only change if changed, to avoid endless looping
|
|
if (ltmTool->showData->isChecked() != checked) ltmTool->showData->setChecked(checked);
|
|
if (rData->isChecked()!=checked) rData->setChecked(checked);
|
|
|
|
if (settings.showData != checked) {
|
|
|
|
settings.showData = checked;
|
|
refreshPlot();
|
|
stack->setCurrentIndex(checked ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::shadeZonesClicked(int state)
|
|
{
|
|
bool checked = state;
|
|
|
|
// only change if changed, to avoid endless looping
|
|
if (ltmTool->shadeZones->isChecked() != checked) ltmTool->shadeZones->setChecked(checked);
|
|
settings.shadeZones = state;
|
|
refreshPlot();
|
|
}
|
|
|
|
void
|
|
LTMWindow::showLegendClicked(int state)
|
|
{
|
|
settings.legend = state;
|
|
refreshPlot();
|
|
}
|
|
|
|
void
|
|
LTMWindow::showEventsClicked(int state)
|
|
{
|
|
bool checked = state;
|
|
|
|
// only change if changed, to avoid endless looping
|
|
if (ltmTool->showEvents->isChecked() != checked) ltmTool->showEvents->setChecked(checked);
|
|
if (rEvents->isChecked() != checked) rEvents->setChecked(checked);
|
|
settings.events = state;
|
|
refreshPlot();
|
|
}
|
|
|
|
void
|
|
LTMWindow::applyClicked()
|
|
{
|
|
if (ltmTool->charts->selectedItems().count() == 0) return;
|
|
|
|
int selected = ltmTool->charts->invisibleRootItem()->indexOfChild(ltmTool->charts->selectedItems().first());
|
|
if (selected >= 0) {
|
|
|
|
// save chart setup
|
|
int groupBy = settings.groupBy;
|
|
bool legend = settings.legend;
|
|
bool events = settings.events;
|
|
bool shadeZones = settings.shadeZones;
|
|
QDateTime start = settings.start;
|
|
QDateTime end = settings.end;
|
|
|
|
// apply preset
|
|
settings = ltmTool->presets[selected];
|
|
|
|
// now get back the local chart setup
|
|
settings.ltmTool = ltmTool;
|
|
settings.data = &results;
|
|
settings.measures = &measures;
|
|
settings.groupBy = groupBy;
|
|
settings.legend = legend;
|
|
settings.events = events;
|
|
settings.shadeZones = shadeZones;
|
|
settings.start = start;
|
|
settings.end = end;
|
|
|
|
ltmTool->applySettings();
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::saveClicked()
|
|
{
|
|
EditChartDialog editor(context, &settings, ltmTool->presets);
|
|
if (editor.exec()) {
|
|
ltmTool->presets.append(settings);
|
|
settings.writeChartXML(context->athlete->home, ltmTool->presets);
|
|
//ltmTool->presetPicker->insertItem(ltmTool->presets.count()-1, settings.name, ltmTool->presets.count()-1);
|
|
//ltmTool->presetPicker->setCurrentIndex(ltmTool->presets.count()-1);
|
|
}
|
|
}
|
|
|
|
int
|
|
LTMWindow::groupForDate(QDate date)
|
|
{
|
|
switch(settings.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
|
|
LTMWindow::pointClicked(QwtPlotCurve*curve, int index)
|
|
{
|
|
// get the date range for this point
|
|
QDate start, end;
|
|
LTMScaleDraw *lsd = new LTMScaleDraw(settings.start,
|
|
groupForDate(settings.start.date()),
|
|
settings.groupBy);
|
|
lsd->dateRange((int)round(curve->sample(index).x()), start, end);
|
|
ltmPopup->setData(settings, start, end);
|
|
popup->show();
|
|
}
|
|
|
|
class GroupedData {
|
|
|
|
public:
|
|
QVector<double> x, y; // x and y data for each metric
|
|
int maxdays;
|
|
};
|
|
|
|
void
|
|
LTMWindow::refreshDataTable()
|
|
{
|
|
// truncate date range to the actual data!
|
|
if (settings.data != NULL && (*settings.data).count() != 0) {
|
|
// end
|
|
if (settings.end == QDateTime() || settings.end > (*settings.data).last().getRideDate())
|
|
settings.end = (*settings.data).last().getRideDate();
|
|
|
|
// start
|
|
if (settings.start == QDateTime() || settings.start < (*settings.data).first().getRideDate())
|
|
settings.start = (*settings.data).first().getRideDate();
|
|
}
|
|
|
|
// update the webview to the data table
|
|
dataSummary->page()->mainFrame()->setHtml("");
|
|
|
|
// now set to new (avoids a weird crash)
|
|
QString summary;
|
|
|
|
summary = "<center>";
|
|
|
|
// device summary for ride summary, otherwise how many activities?
|
|
summary += "<p><h3>" + settings.title + tr(" grouped by ");
|
|
|
|
switch (settings.groupBy) {
|
|
case LTM_DAY :
|
|
summary += tr("day");
|
|
break;
|
|
case LTM_WEEK :
|
|
summary += tr("week");
|
|
break;
|
|
case LTM_MONTH :
|
|
summary += tr("month");
|
|
break;
|
|
case LTM_YEAR :
|
|
summary += tr("year");
|
|
break;
|
|
case LTM_TOD :
|
|
summary += tr("time of day");
|
|
break;
|
|
}
|
|
summary += "</h3><p>";
|
|
|
|
//
|
|
// STEP1: AGGREGATE DATA INTO GROUPBY FOR EACH METRIC
|
|
// This is essentially a refactored version of createCurveData
|
|
// from LTMPlot, but updated to produce aggregates for all metrics
|
|
// at once, so we can embed into an HTML table
|
|
//
|
|
QList<GroupedData> aggregates;
|
|
|
|
foreach (MetricDetail metricDetail, settings.metrics) {
|
|
|
|
QList<SummaryMetrics> *data = NULL; // source data (metrics, bests etc)
|
|
GroupedData a; // aggregated data
|
|
|
|
// resize the curve array to maximum possible size
|
|
a.maxdays = groupForDate(settings.end.date()) - groupForDate(settings.start.date());
|
|
a.x.resize(a.maxdays+1);
|
|
a.y.resize(a.maxdays+1);
|
|
|
|
// set source for data
|
|
QList<SummaryMetrics> PMCdata;
|
|
if (metricDetail.type == METRIC_DB || metricDetail.type == METRIC_META) {
|
|
data = settings.data;
|
|
} else if (metricDetail.type == METRIC_MEASURE) {
|
|
data = settings.measures;
|
|
} else if (metricDetail.type == METRIC_PM) {
|
|
// PMC fixup later
|
|
ltmPlot->createPMCCurveData(&settings, metricDetail, PMCdata);
|
|
data = &PMCdata;
|
|
} else if (metricDetail.type == METRIC_BEST) {
|
|
data = settings.bests;
|
|
}
|
|
|
|
// initialise before looping through the data for this metric
|
|
int n=-1;
|
|
int lastDay=groupForDate(settings.start.date());
|
|
unsigned long secondsPerGroupBy=0;
|
|
bool wantZero = settings.groupBy != LTM_DAY; // not zeros for days -- too many blank lines!
|
|
|
|
foreach (SummaryMetrics rideMetrics, *data) {
|
|
|
|
// filter out unwanted rides but not for PMC type metrics
|
|
// because that needs to be done in the stress calculator
|
|
if (metricDetail.type != METRIC_PM && context->isfiltered &&
|
|
!context->filters.contains(rideMetrics.getFileName())) continue;
|
|
|
|
// day we are on
|
|
int currentDay = groupForDate(rideMetrics.getRideDate().date());
|
|
|
|
// value for day -- measures are stored differently
|
|
double value;
|
|
if (metricDetail.type == METRIC_MEASURE)
|
|
value = rideMetrics.getText(metricDetail.symbol, "0.0").toDouble();
|
|
else if (metricDetail.type == METRIC_BEST)
|
|
value = rideMetrics.getForSymbol(metricDetail.bestSymbol);
|
|
else
|
|
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.type != METRIC_BEST && metricDetail.metric) {
|
|
// convert from stored metric value to imperial
|
|
if (context->athlete->useMetricUnits == false) {
|
|
value *= metricDetail.metric->conversion();
|
|
value += metricDetail.metric->conversionSum();
|
|
}
|
|
|
|
// convert seconds to hours
|
|
if (metricDetail.metric->units(true) == "seconds" ||
|
|
metricDetail.metric->units(true) == tr("seconds")) value /= 3600;
|
|
}
|
|
|
|
if (value || wantZero) {
|
|
unsigned long seconds = rideMetrics.getForSymbol("workout_time");
|
|
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_MEASURE) seconds = 1;
|
|
if (n < a.x.size() && currentDay > lastDay) {
|
|
if (lastDay && wantZero) {
|
|
while (n<(a.x.size()-1) && lastDay<currentDay) {
|
|
lastDay++;
|
|
n++;
|
|
a.x[n]=lastDay - groupForDate(settings.start.date());
|
|
a.y[n]=0;
|
|
}
|
|
} else {
|
|
n++;
|
|
}
|
|
|
|
a.y[n] = value;
|
|
a.x[n] = currentDay - groupForDate(settings.start.date());
|
|
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" ||
|
|
metricDetail.uunits == tr("Ramp")) type = RideMetric::Total;
|
|
|
|
if (metricDetail.type == METRIC_BEST) type = RideMetric::Peak;
|
|
|
|
// just in case
|
|
if (n < 0) n=0;
|
|
|
|
switch (type) {
|
|
case RideMetric::Total:
|
|
a.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
|
|
a.y[n] = ((a.y[n]*secondsPerGroupBy)+(seconds*value)) / (secondsPerGroupBy+seconds);
|
|
break;
|
|
}
|
|
case RideMetric::Low:
|
|
if (value < a.y[n]) a.y[n] = value;
|
|
break;
|
|
case RideMetric::Peak:
|
|
if (value > a.y[n]) a.y[n] = value;
|
|
break;
|
|
}
|
|
secondsPerGroupBy += seconds; // increment for same group
|
|
}
|
|
lastDay = currentDay;
|
|
}
|
|
}
|
|
|
|
// truncate if skipping zeros
|
|
if (n>0 && !wantZero) {
|
|
a.x.resize(n);
|
|
a.y.resize(n);
|
|
}
|
|
|
|
// save to our list
|
|
aggregates << a;
|
|
}
|
|
|
|
//
|
|
// STEP 2: PREPARE HTML TABLE FROM AGGREGATED DATA
|
|
// But note there will be no data if there are no curves of if there
|
|
// is no date range selected of no data anyway!
|
|
//
|
|
if (aggregates.count()) {
|
|
|
|
// formatting ...
|
|
QColor color = QApplication::palette().alternateBase().color();
|
|
color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value());
|
|
LTMScaleDraw lsd(settings.start, groupForDate(settings.start.date()), settings.groupBy);
|
|
|
|
// table and headings 50% for 1 metric, 70% for 2 metrics, 90% for 3 metrics or more
|
|
summary += "<table border=0 cellspacing=3 width=\"%1%%\"><tr><td align=\"center\" valigne=\"top\"><b>Date</b></td>";
|
|
summary = summary.arg(settings.metrics.count() >= 3 ? 90 : (30 + (settings.metrics.count() * 20)));
|
|
|
|
// metric name
|
|
for (int i=0; i < settings.metrics.count(); i++) {
|
|
summary += "<td align=\"center\" valign=\"top\">"
|
|
"<b>%1</b></td>";
|
|
|
|
QString name = settings.metrics[i].name;
|
|
if (name == "Coggan Acute Training Load") name = "ATL";
|
|
if (name == "Coggan Chronic Training Load") name = "CTL";
|
|
if (name == "Coggan Training Stress Balance") name = "TSB";
|
|
|
|
summary = summary.arg(name);
|
|
}
|
|
summary += "</tr><tr><td></td>";
|
|
|
|
// units
|
|
for (int i=0; i < settings.metrics.count(); i++) {
|
|
summary += "<td align=\"center\" valign=\"top\">"
|
|
"<b>%1</b></td>";
|
|
QString units = settings.metrics[i].uunits;
|
|
if (units == tr("seconds")) units = tr("hours");
|
|
summary = summary.arg(units != "" ? QString("(%1)").arg(units) : "");
|
|
}
|
|
summary += "</tr>";
|
|
|
|
for(int i=0; i<aggregates[0].y.count(); i++) {
|
|
|
|
if (i%2) summary += "<tr bgcolor='" + color.name() + "'>";
|
|
else summary += "<tr>";
|
|
|
|
// date / month year etc
|
|
summary += "<td align=\"center\" valign=\"top\">%1</td>";
|
|
summary = summary.arg(lsd.label(aggregates[0].x[i]+0.5).text());
|
|
|
|
// each metric value
|
|
for(int j=0; j<aggregates.count(); j++) {
|
|
summary += "<td align=\"center\" valign=\"top\">%1</td>";
|
|
|
|
// now format the actual value....
|
|
const RideMetric *m = settings.metrics[j].metric;
|
|
if (m != NULL) {
|
|
|
|
// handle precision of 1 for seconds converted to hours
|
|
int precision = m->precision();
|
|
if (settings.metrics[j].uunits == "seconds") precision=1;
|
|
|
|
// we have a metric so lets be precise ...
|
|
QString v = QString("%1").arg(aggregates[j].y[i] * (context->athlete->useMetricUnits ? 1 : m->conversion())
|
|
+ (context->athlete->useMetricUnits ? 0 : m->conversionSum()), 0, 'f', precision);
|
|
|
|
summary = summary.arg(v);
|
|
|
|
} else {
|
|
// no precision
|
|
summary = summary.arg(QString("%1").arg(aggregates[j].y[i], 0, 'f', 0));
|
|
}
|
|
}
|
|
summary += "</tr>";
|
|
}
|
|
summary += "</table>";
|
|
}
|
|
summary += "</center>";
|
|
|
|
// now set it
|
|
dataSummary->page()->mainFrame()->setHtml(summary);
|
|
}
|