/* * 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 #include #include #include #include #include #include #include #include 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 filteredresults; foreach (SummaryMetrics x, results) { if (ltmTool->filters().contains(x.getFileName())) filteredresults << x; } results = filteredresults; // metrics filtering QList 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 filteredresults; foreach (SummaryMetrics x, results) { if (context->homeFilters.contains(x.getFileName())) filteredresults << x; } results = filteredresults; // metrics filtering QList 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 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 = "
"; // device summary for ride summary, otherwise how many activities? summary += "

" + 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 += "

"; // // 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 aggregates; foreach (MetricDetail metricDetail, settings.metrics) { QList *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 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) && lastDaytype() : 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 += ""; 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 += ""; 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 += ""; // units for (int i=0; i < settings.metrics.count(); i++) { summary += ""; QString units = settings.metrics[i].uunits; if (units == tr("seconds")) units = tr("hours"); summary = summary.arg(units != "" ? QString("(%1)").arg(units) : ""); } summary += ""; for(int i=0; i"; else summary += ""; // date / month year etc summary += ""; summary = summary.arg(lsd.label(aggregates[0].x[i]+0.5).text()); // each metric value for(int j=0; j%1"; // 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 += ""; } summary += "
Date" "%1
" "%1
%1
"; } summary += "

"; // now set it dataSummary->page()->mainFrame()->setHtml(summary); }