mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
This is already fixed when it is out to the right,
but still can happen to the left, continuation of b36bbdc
1679 lines
55 KiB
C++
1679 lines
55 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 "LTMChartParser.h"
|
|
#include "AbstractView.h"
|
|
#include "Context.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "Banister.h"
|
|
#include "RideCache.h"
|
|
#include "RideFileCache.h"
|
|
#include "Settings.h"
|
|
#include "cmath"
|
|
#include "float.h"
|
|
#include "Units.h" // for MILES_PER_KM
|
|
#include "HelpWhatsThis.h"
|
|
#include "GcOverlayWidget.h"
|
|
|
|
#include <QWebEngineSettings>
|
|
#include <QDesktopWidget>
|
|
|
|
#include <QtGlobal>
|
|
#include <QtGui>
|
|
#include <QString>
|
|
#include <QDebug>
|
|
#include <QStyle>
|
|
#include <QStyleFactory>
|
|
|
|
// span slider specials
|
|
#include <qxtspanslider.h>
|
|
#include <QStyleFactory>
|
|
#include <QStyle>
|
|
#include <QScrollBar>
|
|
|
|
#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), stackDirty(true), compareDirty(true), firstshow(true)
|
|
{
|
|
useToToday = useCustom = false;
|
|
plotted = DateRange(QDate(01,01,01), QDate(01,01,01));
|
|
lastRefresh = QTime::currentTime().addSecs(-10);
|
|
|
|
// the plot
|
|
QVBoxLayout *mainLayout = new QVBoxLayout;
|
|
|
|
QPalette palette;
|
|
palette.setBrush(QPalette::Background, QBrush(GColor(CTRENDPLOTBACKGROUND)));
|
|
|
|
// single plot
|
|
plotWidget = new QWidget(this);
|
|
plotWidget->setPalette(palette);
|
|
QVBoxLayout *plotLayout = new QVBoxLayout(plotWidget);
|
|
plotLayout->setSpacing(0);
|
|
plotLayout->setContentsMargins(0,0,0,0);
|
|
|
|
ltmPlot = new LTMPlot(this, context, 0);
|
|
spanSlider = new QxtSpanSlider(Qt::Horizontal, this);
|
|
spanSlider->setFocusPolicy(Qt::NoFocus);
|
|
spanSlider->setHandleMovementMode(QxtSpanSlider::NoOverlapping);
|
|
spanSlider->setLowerValue(0);
|
|
spanSlider->setUpperValue(15);
|
|
|
|
QFont smallFont;
|
|
smallFont.setPointSize(6);
|
|
|
|
scrollLeft = new QPushButton("<", this);
|
|
scrollLeft->setFont(smallFont);
|
|
scrollLeft->setAutoRepeat(true);
|
|
scrollLeft->setFixedHeight(16);
|
|
scrollLeft->setFixedWidth(16);
|
|
scrollLeft->setContentsMargins(0,0,0,0);
|
|
|
|
scrollRight = new QPushButton(">", this);
|
|
scrollRight->setFont(smallFont);
|
|
scrollRight->setAutoRepeat(true);
|
|
scrollRight->setFixedHeight(16);
|
|
scrollRight->setFixedWidth(16);
|
|
scrollRight->setContentsMargins(0,0,0,0);
|
|
|
|
QHBoxLayout *span = new QHBoxLayout;
|
|
span->addWidget(scrollLeft);
|
|
span->addWidget(spanSlider);
|
|
span->addWidget(scrollRight);
|
|
plotLayout->addWidget(ltmPlot);
|
|
plotLayout->addLayout(span);
|
|
|
|
#ifdef Q_OS_MAC
|
|
// BUG in QMacStyle and painting of spanSlider
|
|
// so we use a plain style to avoid it, but only
|
|
// on a MAC, since win and linux are fine
|
|
QStyle *style = QStyleFactory::create("fusion");
|
|
spanSlider->setStyle(style);
|
|
scrollLeft->setStyle(style);
|
|
scrollRight->setStyle(style);
|
|
#endif
|
|
|
|
// the stack of plots
|
|
plotsWidget = new QWidget(this);
|
|
plotsWidget->setPalette(palette);
|
|
plotsLayout = new QVBoxLayout(plotsWidget);
|
|
plotsLayout->setSpacing(0);
|
|
plotsLayout->setContentsMargins(0,0,0,0);
|
|
|
|
plotArea = new QScrollArea(this);
|
|
#ifdef Q_OS_WIN
|
|
QStyle *cde = QStyleFactory::create(OS_STYLE);
|
|
plotArea->setStyle(cde);
|
|
#endif
|
|
plotArea->setAutoFillBackground(false);
|
|
plotArea->setWidgetResizable(true);
|
|
plotArea->setWidget(plotsWidget);
|
|
plotArea->setFrameStyle(QFrame::NoFrame);
|
|
plotArea->setContentsMargins(0,0,0,0);
|
|
plotArea->setPalette(palette);
|
|
|
|
// the data table
|
|
QFont defaultFont; // mainwindow sets up the defaults.. we need to apply
|
|
dataSummary = new QWebEngineView(this);
|
|
// stop stealing focus!
|
|
dataSummary->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
|
|
//XXXdataSummary->setEnabled(false); // stop grabbing focus
|
|
if (dpiXFactor > 1) {
|
|
// 80 lines per page on hidpi screens (?)
|
|
int pixelsize = pixelSizeForFont(defaultFont, QApplication::desktop()->geometry().height()/80);
|
|
dataSummary->settings()->setFontSize(QWebEngineSettings::DefaultFontSize, pixelsize);
|
|
} else {
|
|
dataSummary->settings()->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFont.pointSize()+1);
|
|
}
|
|
dataSummary->settings()->setFontFamily(QWebEngineSettings::StandardFont, defaultFont.family());
|
|
dataSummary->setContentsMargins(0,0,0,0);
|
|
dataSummary->page()->view()->setContentsMargins(0,0,0,0);
|
|
dataSummary->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
dataSummary->setAcceptDrops(false);
|
|
|
|
// compare plot page
|
|
compareplotsWidget = new QWidget(this);
|
|
compareplotsWidget->setPalette(palette);
|
|
compareplotsLayout = new QVBoxLayout(compareplotsWidget);
|
|
compareplotsLayout->setSpacing(0);
|
|
compareplotsLayout->setContentsMargins(0,0,0,0);
|
|
|
|
compareplotArea = new QScrollArea(this);
|
|
#ifdef Q_OS_WIN
|
|
cde = QStyleFactory::create(OS_STYLE);
|
|
compareplotArea->setStyle(cde);
|
|
#endif
|
|
compareplotArea->setAutoFillBackground(false);
|
|
compareplotArea->setWidgetResizable(true);
|
|
compareplotArea->setWidget(compareplotsWidget);
|
|
compareplotArea->setFrameStyle(QFrame::NoFrame);
|
|
compareplotArea->setContentsMargins(0,0,0,0);
|
|
compareplotArea->setPalette(palette);
|
|
|
|
// the stack
|
|
stackWidget = new QStackedWidget(this);
|
|
stackWidget->addWidget(plotWidget);
|
|
stackWidget->addWidget(dataSummary);
|
|
stackWidget->addWidget(plotArea);
|
|
stackWidget->addWidget(compareplotArea);
|
|
stackWidget->setCurrentIndex(0);
|
|
mainLayout->addWidget(stackWidget);
|
|
setChartLayout(mainLayout);
|
|
|
|
HelpWhatsThis *helpStack = new HelpWhatsThis(stackWidget);
|
|
stackWidget->setWhatsThis(helpStack->getWhatsThisText(HelpWhatsThis::ChartTrends_MetricTrends));
|
|
|
|
// 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")
|
|
<< tr("All");
|
|
rGroupBy->setStrings(strings);
|
|
rGroupBy->setValue(0);
|
|
rGroupBy->setMinimumWidth(100);
|
|
|
|
revealLayout->addWidget(rGroupBy);
|
|
rData = new QCheckBox(tr("Data Table"), this);
|
|
rStack = new QCheckBox(tr("Stacked"), this);
|
|
QVBoxLayout *checks = new QVBoxLayout;
|
|
checks->setSpacing(2 *dpiXFactor);
|
|
checks->setContentsMargins(0,0,0,0);
|
|
checks->addWidget(rData);
|
|
checks->addWidget(rStack);
|
|
revealLayout->addLayout(checks);
|
|
revealLayout->addStretch();
|
|
setRevealLayout(revealLayout);
|
|
|
|
// add additional menu items before setting
|
|
// controls since the menu is SET from setControls
|
|
QAction *showsettings = new QAction(tr("Chart Settings..."));
|
|
addAction(showsettings);
|
|
QAction *exportData = new QAction(tr("Export Chart Data..."), this);
|
|
addAction(exportData);
|
|
|
|
// the controls
|
|
QWidget *c = new QWidget;
|
|
c->setContentsMargins(0,0,0,0);
|
|
HelpWhatsThis *helpConfig = new HelpWhatsThis(c);
|
|
c->setWhatsThis(helpConfig->getWhatsThisText(HelpWhatsThis::ChartTrends_MetricTrends));
|
|
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);
|
|
|
|
ltmTool = new LTMTool(context, &settings);
|
|
|
|
// the banister overlay
|
|
QWidget *ban=new QWidget(this);
|
|
addHelper(tr("Banister Model"), ban);
|
|
|
|
QGridLayout *bang= new QGridLayout(ban);
|
|
bang->setColumnStretch(0, 40);
|
|
bang->setColumnStretch(1, 30);
|
|
bang->setColumnStretch(2, 20);
|
|
|
|
// interactive elements
|
|
banCombo = new QComboBox(this);
|
|
banPerf = new QComboBox(this);
|
|
banT1 = new QDoubleSpinBox(this);
|
|
banT2 = new QDoubleSpinBox(this);
|
|
|
|
// labels etc
|
|
ilabel = new QLabel(tr("Impulse Metric"), this);
|
|
perflabel = new QLabel(tr("Perf. Metric"), this);
|
|
plabel = new QLabel(tr("Peak"), this);
|
|
peaklabel = new QLabel(this);
|
|
peaklabel->setText("296w on 3rd July");
|
|
t1label1 = new QLabel(tr("Positive decay"), this);
|
|
t1label2 = new QLabel(tr("days"), this);
|
|
t2label1 = new QLabel(tr("Negative decay"), this);
|
|
t2label2 = new QLabel(tr("days"), this);
|
|
RMSElabel = new QLabel(this);
|
|
RMSElabel->setText("RMSE 2.9 for 22 tests.");
|
|
|
|
// add to layout
|
|
bang->addWidget(ilabel,0,0);
|
|
bang->addWidget(banCombo,0,1,1,2,Qt::AlignLeft);
|
|
bang->addWidget(perflabel,1,0);
|
|
bang->addWidget(banPerf,1,1,1,2,Qt::AlignLeft);
|
|
bang->addWidget(plabel, 2,0);
|
|
bang->addWidget(peaklabel,2,1,1,2,Qt::AlignLeft);
|
|
bang->addWidget(t1label1,3,0);
|
|
bang->addWidget(banT1,3,1);
|
|
bang->addWidget(t1label2,3,2);
|
|
bang->addWidget(t2label1,4,0);
|
|
bang->addWidget(banT2,4,1);
|
|
bang->addWidget(t2label2,4,2);
|
|
bang->addWidget(RMSElabel,5,0,1,3);
|
|
|
|
// initialise
|
|
settings.ltmTool = ltmTool;
|
|
settings.groupBy = LTM_DAY;
|
|
settings.legend = ltmTool->showLegend->isChecked();
|
|
settings.events = ltmTool->showEvents->isChecked();
|
|
settings.shadeZones = ltmTool->shadeZones->isChecked();
|
|
settings.showData = ltmTool->showData->isChecked();
|
|
settings.stack = ltmTool->showStack->isChecked();
|
|
settings.stackWidth = ltmTool->stackSlider->value();
|
|
rData->setChecked(ltmTool->showData->isChecked());
|
|
rStack->setChecked(ltmTool->showStack->isChecked());
|
|
cl->addWidget(ltmTool);
|
|
|
|
connect(this, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange)));
|
|
connect(this, SIGNAL(styleChanged(int)), this, SLOT(styleChanged(int)));
|
|
connect(ltmTool, SIGNAL(filterChanged()), this, SLOT(filterChanged()));
|
|
connect(this, SIGNAL(perspectiveFilterChanged(QString)), this, SLOT(filterChanged()));
|
|
connect(this, SIGNAL(perspectiveChanged(Perspective*)), 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->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(ltmTool->showBanister, SIGNAL(stateChanged(int)), this, SLOT(refresh()));
|
|
connect(rData, SIGNAL(stateChanged(int)), this, SLOT(showDataClicked(int)));
|
|
connect(ltmTool->showStack, SIGNAL(stateChanged(int)), this, SLOT(showStackClicked(int)));
|
|
connect(rStack, SIGNAL(stateChanged(int)), this, SLOT(showStackClicked(int)));
|
|
connect(ltmTool->stackSlider, SIGNAL(valueChanged(int)), this, SLOT(zoomSliderChanged()));
|
|
connect(ltmTool->showLegend, SIGNAL(stateChanged(int)), this, SLOT(showLegendClicked(int)));
|
|
connect(ltmTool->showEvents, 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(filterChanged()));
|
|
connect(context, SIGNAL(refreshUpdate(QDate)), this, SLOT(refreshUpdate(QDate)));
|
|
connect(context, SIGNAL(refreshEnd()), this, SLOT(refresh()));
|
|
connect(context, SIGNAL(estimatesRefreshed()), this, SLOT(refresh()));
|
|
|
|
// comparing things
|
|
connect(context, SIGNAL(compareDateRangesStateChanged(bool)), this, SLOT(compareChanged()));
|
|
connect(context, SIGNAL(compareDateRangesChanged()), this, SLOT(compareChanged()));
|
|
|
|
connect(context, SIGNAL(rideAdded(RideItem*)), this, SLOT(refresh(void)));
|
|
connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(refresh(void)));
|
|
connect(context, SIGNAL(rideSaved(RideItem*)), this, SLOT(refresh(void)));
|
|
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
|
connect(context, SIGNAL(presetSelected(int)), this, SLOT(presetSelected(int)));
|
|
connect(context->athlete->seasons, SIGNAL(seasonsChanged()), this, SLOT(refreshPlot()));
|
|
|
|
// custom menu item
|
|
connect(exportData, SIGNAL(triggered()), this, SLOT(exportData()));
|
|
connect(showsettings, SIGNAL(triggered()), this, SIGNAL(showControls()));
|
|
|
|
// normal view
|
|
connect(spanSlider, SIGNAL(lowerPositionChanged(int)), this, SLOT(spanSliderChanged()));
|
|
connect(spanSlider, SIGNAL(upperPositionChanged(int)), this, SLOT(spanSliderChanged()));
|
|
connect(scrollLeft, SIGNAL(clicked()), this, SLOT(moveLeft()));
|
|
connect(scrollRight, SIGNAL(clicked()), this, SLOT(moveRight()));
|
|
|
|
// refresh banister data when combo changes
|
|
connect(banCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(refreshBanister()));
|
|
connect(banPerf, SIGNAL(currentIndexChanged(int)), this, SLOT(refreshBanister()));
|
|
connect(banT1, SIGNAL(valueChanged(double)), this, SLOT(tuneBanister()));
|
|
connect(banT2, SIGNAL(valueChanged(double)), this, SLOT(tuneBanister()));
|
|
configChanged(CONFIG_APPEARANCE);
|
|
}
|
|
|
|
LTMWindow::~LTMWindow()
|
|
{
|
|
delete popup;
|
|
if (dataSummary) delete dataSummary->page();
|
|
}
|
|
|
|
void
|
|
LTMWindow::hideBasic()
|
|
{
|
|
ltmTool->hideBasic();
|
|
}
|
|
|
|
void
|
|
LTMWindow::showBanister(bool relevant)
|
|
{
|
|
RideMetricFactory &factory = RideMetricFactory::instance();
|
|
|
|
if (relevant && banister()) {
|
|
|
|
// we reset the combo so lets remember where we were at
|
|
int remember=0;
|
|
if (banCombo->count()) remember=banCombo->currentIndex();
|
|
int rememberPerf=0;
|
|
if (banPerf->count()) rememberPerf=banPerf->currentIndex();
|
|
|
|
QStringList symbols;
|
|
QStringList perfSymbols;
|
|
|
|
// lets setup the widgets
|
|
banCombo->clear();
|
|
banPerf->clear();
|
|
foreach(MetricDetail metricDetail, settings.metrics) {
|
|
if (metricDetail.type == METRIC_BANISTER) {
|
|
if (!symbols.contains(metricDetail.symbol)) {
|
|
const RideMetric *m = factory.rideMetric(metricDetail.symbol);
|
|
if (m) {
|
|
symbols << metricDetail.symbol;
|
|
|
|
// bloody TM in bikescore is TEE DEE US
|
|
QString name=m->name();
|
|
if (name.startsWith("BikeScore")) name = QString("BikeScore");
|
|
|
|
banCombo->addItem(name, QVariant(metricDetail.symbol));
|
|
}
|
|
}
|
|
if (!perfSymbols.contains(metricDetail.perfSymbol)) {
|
|
const RideMetric *m = factory.rideMetric(metricDetail.perfSymbol);
|
|
if (m) {
|
|
perfSymbols << metricDetail.perfSymbol;
|
|
banPerf->addItem(m->name(), QVariant(metricDetail.perfSymbol));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// go back to remembered value
|
|
if (remember < banCombo->count()) banCombo->setCurrentIndex(remember);
|
|
if (rememberPerf < banPerf->count()) banPerf->setCurrentIndex(rememberPerf);
|
|
|
|
// now get the metric values etc
|
|
refreshBanister();
|
|
overlayWidget->show();
|
|
|
|
} else {
|
|
|
|
// ignore it
|
|
overlayWidget->hide();
|
|
}
|
|
}
|
|
|
|
bool
|
|
LTMWindow::event(QEvent *event)
|
|
{
|
|
// nasty nasty nasty hack to move widgets as soon as the widget geometry
|
|
// is set properly by the layout system, by default the width is 100 and
|
|
// we wait for it to be set properly then put our helper widget on the RHS
|
|
if (event->type() == QEvent::Resize && geometry().width() != 100) {
|
|
|
|
// put somewhere nice on first show
|
|
if (firstshow) {
|
|
firstshow = false;
|
|
helperWidget()->resize(400*dpiXFactor, 150*dpiYFactor);
|
|
helperWidget()->move(mainWidget()->geometry().width()-(500*dpiXFactor), 90*dpiYFactor);
|
|
}
|
|
|
|
// if off the screen move on screen
|
|
if (helperWidget()->geometry().x() > geometry().width() || helperWidget()->geometry().x() < geometry().x()) {
|
|
helperWidget()->move(mainWidget()->geometry().width()-(500*dpiXFactor), 90*dpiYFactor);
|
|
}
|
|
}
|
|
return QWidget::event(event);
|
|
}
|
|
|
|
void
|
|
LTMWindow::tuneBanister()
|
|
{
|
|
|
|
// if we have a banister...
|
|
if (banCombo->count() && banPerf->count() && banT1->value() >0 && banT2->value()>0) {
|
|
|
|
// lookup and set
|
|
Banister *banister = context->athlete->getBanisterFor(banCombo->currentData().toString(), banPerf->currentData().toString(),0,0);
|
|
|
|
// when user adjusts the t1/t2 parameters we need to refit
|
|
if (banT1->value() < banister->t1 || banT1->value() > banister->t1 ||
|
|
banT2->value() < banister->t2 || banT2->value() > banister->t2) {
|
|
|
|
// lets adjust it them
|
|
banister->setDecay(banT1->value(), banT2->value());
|
|
}
|
|
|
|
// replot
|
|
refreshPlot();
|
|
}
|
|
}
|
|
void
|
|
LTMWindow::refreshBanister()
|
|
{
|
|
int index = banCombo->currentIndex();
|
|
int perfIndex = banPerf->currentIndex();
|
|
|
|
if (index >= 0 && perfIndex >= 0) {
|
|
|
|
// lookup and set
|
|
Banister *banister = context->athlete->getBanisterFor(banCombo->currentData().toString(), banPerf->currentData().toString(),0,0);
|
|
banT1->setValue(banister->t1);
|
|
banT2->setValue(banister->t2);
|
|
|
|
double perf=0.0;
|
|
int CP=0;
|
|
QDate when = banister->getPeakPerf(settings.start.date(), settings.end.date(), perf, CP);
|
|
|
|
// set peak label
|
|
if (CP >0 && when != QDate()) peaklabel ->setText(QString("%1 watts on %2").arg(CP).arg(when.toString("d MMM yyyy")));
|
|
else if (perf >0.0 && when != QDate()) peaklabel ->setText(QString("%1 on %2").arg(perf, 0, 'f', 1).arg(when.toString("d MMM yyyy")));
|
|
else peaklabel->setText("");
|
|
|
|
// set RMSE for current view
|
|
int count;
|
|
double RMSE = banister->RMSE(settings.start.date(), settings.end.date(), count);
|
|
if (count && RMSE >0) RMSElabel->setText(QString("RMSE %1 for %2 tests.").arg(RMSE, 0, 'f', 2).arg(count));
|
|
else RMSElabel->setText("");
|
|
|
|
} else {
|
|
// clear
|
|
peaklabel->setText("");
|
|
RMSElabel->setText("");
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::configChanged(qint32)
|
|
{
|
|
// tinted palette for headings etc
|
|
QPalette palette;
|
|
palette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
|
|
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
|
|
setPalette(palette);
|
|
|
|
// inverted palette for data etc
|
|
QPalette whitepalette;
|
|
whitepalette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
|
|
whitepalette.setBrush(QPalette::Background, QBrush(GColor(CTRENDPLOTBACKGROUND)));
|
|
whitepalette.setColor(QPalette::WindowText, GCColor::invertColor(GColor(CTRENDPLOTBACKGROUND)));
|
|
whitepalette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
|
|
whitepalette.setColor(QPalette::Text, GCColor::invertColor(GColor(CTRENDPLOTBACKGROUND)));
|
|
|
|
QFont font;
|
|
font.setPointSize(12); // reasonably big
|
|
ilabel->setFont(font);
|
|
perflabel->setFont(font);
|
|
plabel->setFont(font);
|
|
t1label1->setFont(font);
|
|
t1label2->setFont(font);
|
|
plabel->setFont(font);
|
|
peaklabel->setFont(font);
|
|
t2label1->setFont(font);
|
|
t2label2->setFont(font);
|
|
RMSElabel->setFont(font);
|
|
banT1->setFont(font);
|
|
banT2->setFont(font);
|
|
banCombo->setFont(font);
|
|
banPerf->setFont(font);
|
|
|
|
ilabel->setPalette(palette);
|
|
perflabel->setPalette(palette);
|
|
plabel->setPalette(palette);
|
|
t1label1->setPalette(palette);
|
|
t1label2->setPalette(palette);
|
|
plabel->setPalette(palette);
|
|
peaklabel->setPalette(whitepalette);
|
|
t2label1->setPalette(palette);
|
|
t2label2->setPalette(palette);
|
|
RMSElabel->setPalette(whitepalette);
|
|
|
|
#ifndef Q_OS_MAC
|
|
banT1->setStyleSheet(AbstractView::ourStyleSheet());
|
|
banT2->setStyleSheet(AbstractView::ourStyleSheet());
|
|
banCombo->setStyleSheet(AbstractView::ourStyleSheet());
|
|
plotArea->setStyleSheet(AbstractView::ourStyleSheet());
|
|
compareplotArea->setStyleSheet(AbstractView::ourStyleSheet());
|
|
#endif
|
|
refresh();
|
|
}
|
|
|
|
void
|
|
LTMWindow::styleChanged(int style)
|
|
{
|
|
if (style) {
|
|
// hide spanslider
|
|
spanSlider->hide();
|
|
scrollLeft->hide();
|
|
scrollRight->hide();
|
|
} else {
|
|
// show spanslider
|
|
spanSlider->show();
|
|
scrollLeft->show();
|
|
scrollRight->show();
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::compareChanged()
|
|
{
|
|
if (!amVisible()) {
|
|
compareDirty = true;
|
|
return;
|
|
}
|
|
|
|
if (isCompare()) {
|
|
|
|
// refresh plot handles the compare case
|
|
refreshPlot();
|
|
|
|
} else {
|
|
|
|
// forced refresh back to normal
|
|
stackDirty = dirty = true;
|
|
filterChanged(); // forces reread etc
|
|
}
|
|
repaint();
|
|
}
|
|
|
|
void
|
|
LTMWindow::rideSelected() { } // deprecated
|
|
|
|
void
|
|
LTMWindow::presetSelected(int index)
|
|
{
|
|
// apply a preset if we are configured to do that...
|
|
if (ltmTool->usePreset->isChecked()) {
|
|
|
|
// save chart setup
|
|
int groupBy = settings.groupBy;
|
|
bool legend = settings.legend;
|
|
bool events = settings.events;
|
|
bool stack = settings.stack;
|
|
bool shadeZones = settings.shadeZones;
|
|
QDateTime start = settings.start;
|
|
QDateTime end = settings.end;
|
|
|
|
// apply preset
|
|
settings = context->athlete->presets[index];
|
|
|
|
// now get back the local chart setup
|
|
settings.ltmTool = ltmTool;
|
|
settings.bests = &bestsresults;
|
|
settings.groupBy = groupBy;
|
|
settings.legend = legend;
|
|
settings.events = events;
|
|
settings.stack = stack;
|
|
settings.shadeZones = shadeZones;
|
|
settings.start = start;
|
|
settings.end = end;
|
|
|
|
// Set the specification
|
|
FilterSet fs;
|
|
fs.addFilter(context->isfiltered, context->filters);
|
|
fs.addFilter(context->ishomefiltered, context->homeFilters);
|
|
fs.addFilter(ltmTool->isFiltered(), ltmTool->filters());
|
|
fs.addFilter(myPerspective->isFiltered(), myPerspective->filterlist(myDateRange));
|
|
settings.specification.setFilterSet(fs);
|
|
settings.specification.setDateRange(DateRange(settings.start.date(), settings.end.date()));
|
|
|
|
ltmTool->applySettings();
|
|
refresh();
|
|
|
|
setProperty("subtitle", settings.name);
|
|
|
|
} else {
|
|
|
|
setProperty("subtitle", property("title").toString());
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::refreshUpdate(QDate here)
|
|
{
|
|
if (isVisible() && here > settings.start.date() && lastRefresh.secsTo(QTime::currentTime()) > 5) {
|
|
lastRefresh = QTime::currentTime();
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::moveLeft()
|
|
{
|
|
// move across by 5% of the span, or to zero if not much left
|
|
int span = spanSlider->upperValue() - spanSlider->lowerValue();
|
|
int delta = span / 20;
|
|
if (delta > (spanSlider->lowerValue() - spanSlider->minimum()))
|
|
delta = spanSlider->lowerValue() - spanSlider->minimum();
|
|
|
|
spanSlider->setLowerValue(spanSlider->lowerValue()-delta);
|
|
spanSlider->setUpperValue(spanSlider->upperValue()-delta);
|
|
|
|
spanSliderChanged();
|
|
}
|
|
|
|
void
|
|
LTMWindow::moveRight()
|
|
{
|
|
// move across by 5% of the span, or to zero if not much left
|
|
int span = spanSlider->upperValue() - spanSlider->lowerValue();
|
|
int delta = span / 20;
|
|
if (delta > (spanSlider->maximum() - spanSlider->upperValue()))
|
|
delta = spanSlider->maximum() - spanSlider->upperValue();
|
|
|
|
spanSlider->setLowerValue(spanSlider->lowerValue()+delta);
|
|
spanSlider->setUpperValue(spanSlider->upperValue()+delta);
|
|
|
|
spanSliderChanged();
|
|
}
|
|
|
|
void
|
|
LTMWindow::spanSliderChanged()
|
|
{
|
|
// so reset the axis range for ltmPlot
|
|
ltmPlot->setAxisScale(QwtPlot::xBottom, spanSlider->lowerValue(), spanSlider->upperValue());
|
|
ltmPlot->replot();
|
|
}
|
|
|
|
void
|
|
LTMWindow::refreshPlot()
|
|
{
|
|
if (amVisible() == true) {
|
|
|
|
if (isCompare()) {
|
|
|
|
// COMPARE PLOTS
|
|
showBanister(false); // never
|
|
stackWidget->setCurrentIndex(3);
|
|
refreshCompare();
|
|
|
|
} else if (ltmTool->showData->isChecked()) {
|
|
|
|
// DATA TABLE
|
|
showBanister(false); // never
|
|
stackWidget->setCurrentIndex(1);
|
|
refreshDataTable();
|
|
|
|
} else {
|
|
|
|
if (ltmTool->showStack->isChecked()) {
|
|
|
|
// STACK PLOTS
|
|
refreshStackPlots();
|
|
stackWidget->setCurrentIndex(2);
|
|
stackDirty = false;
|
|
|
|
} else {
|
|
|
|
// NORMAL PLOTS
|
|
plotted = DateRange(settings.start.date(), settings.end.date());
|
|
ltmPlot->setData(&settings);
|
|
stackWidget->setCurrentIndex(0);
|
|
dirty = false;
|
|
|
|
spanSlider->setMinimum(ltmPlot->axisScaleDiv(QwtPlot::xBottom).lowerBound());
|
|
spanSlider->setMaximum(ltmPlot->axisScaleDiv(QwtPlot::xBottom).upperBound());
|
|
spanSlider->setLowerValue(spanSlider->minimum());
|
|
spanSlider->setUpperValue(spanSlider->maximum());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::refreshCompare()
|
|
{
|
|
// not if in compare mode
|
|
if (!isCompare()) return;
|
|
|
|
// setup stacks but only if needed
|
|
//if (!stackDirty) return; // lets come back to that!
|
|
|
|
setUpdatesEnabled(false);
|
|
|
|
// delete old and create new...
|
|
// QScrollArea *plotArea;
|
|
// QWidget *plotsWidget;
|
|
// QVBoxLayout *plotsLayout;
|
|
// QList<LTMSettings> plotSettings;
|
|
foreach (LTMPlot *p, compareplots) {
|
|
compareplotsLayout->removeWidget(p);
|
|
delete p;
|
|
}
|
|
compareplots.clear();
|
|
compareplotSettings.clear();
|
|
|
|
if (compareplotsLayout->count() == 1) {
|
|
compareplotsLayout->takeAt(0); // remove the stretch
|
|
}
|
|
|
|
// now lets create them all again
|
|
// based upon the current settings
|
|
// we create a plot for each curve
|
|
// but where they are stacked we put
|
|
// them all in the SAME plot
|
|
// so we go once through picking out
|
|
// the stacked items and once through
|
|
// for all the rest of the curves
|
|
LTMSettings plotSetting = settings;
|
|
plotSetting.metrics.clear();
|
|
foreach(MetricDetail m, settings.metrics) {
|
|
if (m.stack) plotSetting.metrics << m;
|
|
}
|
|
|
|
int position = 0;
|
|
|
|
// create ltmPlot with this
|
|
if (plotSetting.metrics.count()) {
|
|
|
|
compareplotSettings << plotSetting;
|
|
|
|
// create and setup the plot
|
|
LTMPlot *stacked = new LTMPlot(this, context, position++);
|
|
stacked->setCompareData(&compareplotSettings.last()); // setData using the compare data
|
|
stacked->setFixedHeight(200); // maybe make this adjustable later
|
|
|
|
// now add
|
|
compareplotsLayout->addWidget(stacked);
|
|
compareplots << stacked;
|
|
}
|
|
|
|
// OK, now one plot for each curve
|
|
// that isn't stacked!
|
|
foreach(MetricDetail m, settings.metrics) {
|
|
|
|
// ignore stacks
|
|
if (m.stack) continue;
|
|
|
|
plotSetting = settings;
|
|
plotSetting.metrics.clear();
|
|
plotSetting.metrics << m;
|
|
compareplotSettings << plotSetting;
|
|
|
|
// create and setup the plot
|
|
LTMPlot *plot = new LTMPlot(this, context, position++);
|
|
plot->setCompareData(&compareplotSettings.last()); // setData using the compare data
|
|
|
|
// now add
|
|
compareplotsLayout->addWidget(plot);
|
|
compareplots << plot;
|
|
}
|
|
|
|
// squash em up
|
|
compareplotsLayout->addStretch();
|
|
|
|
// set a common X-AXIS
|
|
if (settings.groupBy != LTM_TOD) {
|
|
int MAXX=0;
|
|
foreach(LTMPlot *p, compareplots) {
|
|
if (p->getMaxX() > MAXX) MAXX=p->getMaxX();
|
|
}
|
|
foreach(LTMPlot *p, compareplots) {
|
|
p->setMaxX(MAXX);
|
|
}
|
|
}
|
|
|
|
|
|
// resize to choice
|
|
zoomSliderChanged();
|
|
|
|
// we no longer dirty
|
|
compareDirty = false;
|
|
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
void
|
|
LTMWindow::refreshStackPlots()
|
|
{
|
|
// not if in compare mode
|
|
if (isCompare()) return;
|
|
|
|
// setup stacks but only if needed
|
|
//if (!stackDirty) return; // lets come back to that!
|
|
|
|
setUpdatesEnabled(false);
|
|
|
|
// delete old and create new...
|
|
// QScrollArea *plotArea;
|
|
// QWidget *plotsWidget;
|
|
// QVBoxLayout *plotsLayout;
|
|
// QList<LTMSettings> plotSettings;
|
|
foreach (LTMPlot *p, plots) {
|
|
plotsLayout->removeWidget(p);
|
|
delete p;
|
|
}
|
|
plots.clear();
|
|
plotSettings.clear();
|
|
|
|
if (plotsLayout->count() == 1) {
|
|
plotsLayout->takeAt(0); // remove the stretch
|
|
}
|
|
|
|
// now lets create them all again
|
|
// based upon the current settings
|
|
// we create a plot for each curve
|
|
// but where they are stacked we put
|
|
// them all in the SAME plot
|
|
// so we go once through picking out
|
|
// the stacked items and once through
|
|
// for all the rest of the curves
|
|
LTMSettings plotSetting = settings;
|
|
plotSetting.metrics.clear();
|
|
foreach(MetricDetail m, settings.metrics) {
|
|
if (m.stack) plotSetting.metrics << m;
|
|
}
|
|
|
|
int position = 0;
|
|
|
|
// create ltmPlot with this
|
|
if (plotSetting.metrics.count()) {
|
|
|
|
plotSettings << plotSetting;
|
|
|
|
// create and setup the plot
|
|
LTMPlot *stacked = new LTMPlot(this, context, position++);
|
|
stacked->setData(&plotSettings.last());
|
|
stacked->setFixedHeight(200); // maybe make this adjustable later
|
|
|
|
// now add
|
|
plotsLayout->addWidget(stacked);
|
|
plots << stacked;
|
|
}
|
|
|
|
// OK, now one plot for each curve
|
|
// that isn't stacked!
|
|
foreach(MetricDetail m, settings.metrics) {
|
|
|
|
// ignore stacks
|
|
if (m.stack) continue;
|
|
|
|
plotSetting = settings;
|
|
plotSetting.metrics.clear();
|
|
plotSetting.metrics << m;
|
|
plotSettings << plotSetting;
|
|
|
|
// create and setup the plot
|
|
LTMPlot *plot = new LTMPlot(this, context, position++);
|
|
plot->setData(&plotSettings.last());
|
|
|
|
// now add
|
|
plotsLayout->addWidget(plot);
|
|
plots << plot;
|
|
}
|
|
|
|
// squash em up
|
|
plotsLayout->addStretch();
|
|
|
|
// resize to choice
|
|
zoomSliderChanged();
|
|
|
|
// we no longer dirty
|
|
stackDirty = false;
|
|
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
void
|
|
LTMWindow::zoomSliderChanged()
|
|
{
|
|
static int add[] = { 0, 20, 50, 80, 100, 150, 200, 400 };
|
|
int index = ltmTool->stackSlider->value();
|
|
|
|
settings.stackWidth = ltmTool->stackSlider->value();
|
|
setUpdatesEnabled(false);
|
|
|
|
// do the compare and the noncompare plots
|
|
// at the same time, as we don't need to worry
|
|
// about optimising out as its fast anyway
|
|
foreach(LTMPlot *plot, plots) {
|
|
plot->setFixedHeight(150 + add[index]);
|
|
}
|
|
foreach(LTMPlot *plot, compareplots) {
|
|
plot->setFixedHeight(150 + add[index]);
|
|
}
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
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()
|
|
{
|
|
setProperty("color", GColor(CTRENDPLOTBACKGROUND)); // called on config change
|
|
|
|
// not if in compare mode
|
|
if (isCompare()) return;
|
|
|
|
// refresh for changes to ridefiles / zones
|
|
if (amVisible() == true) {
|
|
|
|
bestsresults.clear();
|
|
bestsresults = RideFileCache::getAllBestsFor(context, settings.metrics, settings.specification);
|
|
|
|
refreshPlot();
|
|
repaint(); // title changes color when filters change
|
|
|
|
// set spanslider to limits of ltmPlot
|
|
|
|
} else {
|
|
stackDirty = 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.bests = &bestsresults;
|
|
|
|
// we let all the state get updated, but lets not actually plot
|
|
// whilst in compare mode -- but when compare mode ends we will
|
|
// call filterChanged, so need to record the fact that the date
|
|
// range changed whilst we were in compare mode
|
|
if (!isCompare()) {
|
|
|
|
// apply filter to new date range too -- will also refresh plot
|
|
filterChanged();
|
|
} else {
|
|
|
|
// we've been told to redraw so maybe
|
|
// compare mode was switched whilst we were
|
|
// not visible, lets refresh
|
|
if (compareDirty) compareChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMWindow::filterChanged()
|
|
{
|
|
// ignore in compare mode
|
|
if (isCompare()) return;
|
|
|
|
if (amVisible() == false) 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;
|
|
|
|
// Set the specification
|
|
FilterSet fs;
|
|
fs.addFilter(context->isfiltered, context->filters);
|
|
fs.addFilter(context->ishomefiltered, context->homeFilters);
|
|
fs.addFilter(ltmTool->isFiltered(), ltmTool->filters());
|
|
fs.addFilter(myPerspective->isFiltered(), myPerspective->filterlist(myDateRange));
|
|
settings.specification.setFilterSet(fs);
|
|
settings.specification.setDateRange(DateRange(settings.start.date(), settings.end.date()));
|
|
|
|
// 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
|
|
bestsresults.clear();
|
|
bestsresults = RideFileCache::getAllBestsFor(context, settings.metrics, settings.specification);
|
|
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();
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
settings.events = bool(state);
|
|
refreshPlot();
|
|
}
|
|
|
|
void
|
|
LTMWindow::showStackClicked(int state)
|
|
{
|
|
bool checked = state;
|
|
|
|
// only change if changed, to avoid endless looping
|
|
if (ltmTool->showStack->isChecked() != checked) ltmTool->showStack->setChecked(checked);
|
|
if (rStack->isChecked() != checked) rStack->setChecked(checked);
|
|
|
|
settings.stack = checked;
|
|
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 stack = settings.stack;
|
|
bool shadeZones = settings.shadeZones;
|
|
QDateTime start = settings.start;
|
|
QDateTime end = settings.end;
|
|
|
|
// apply preset
|
|
settings = context->athlete->presets[selected];
|
|
|
|
// now get back the local chart setup
|
|
settings.ltmTool = ltmTool;
|
|
settings.bests = &bestsresults;
|
|
settings.groupBy = groupBy;
|
|
settings.legend = legend;
|
|
settings.events = events;
|
|
settings.stack = stack;
|
|
settings.shadeZones = shadeZones;
|
|
settings.start = start;
|
|
settings.end = end;
|
|
|
|
// Set the specification
|
|
FilterSet fs;
|
|
fs.addFilter(context->isfiltered, context->filters);
|
|
fs.addFilter(context->ishomefiltered, context->homeFilters);
|
|
fs.addFilter(ltmTool->isFiltered(), ltmTool->filters());
|
|
fs.addFilter(myPerspective->isFiltered(), myPerspective->filterlist(myDateRange));
|
|
settings.specification.setFilterSet(fs);
|
|
settings.specification.setDateRange(DateRange(settings.start.date(), settings.end.date()));
|
|
|
|
ltmTool->applySettings();
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
// initialize date and time to senseful boundaries
|
|
QDate start = QDate(1900,1,1);
|
|
QDate end = QDate(2999,12,31);
|
|
QTime time = QTime(0, 0, 0, 0);
|
|
|
|
// now fill the correct values for context
|
|
if (settings.groupBy != LTM_TOD) {
|
|
// get the date range for this point (for all date-dependent grouping)
|
|
LTMScaleDraw *lsd = new LTMScaleDraw(settings.start,
|
|
groupForDate(settings.start.date()),
|
|
settings.groupBy);
|
|
lsd->dateRange((int)round(curve->sample(index).x()), start, end); }
|
|
else {
|
|
// special treatment for LTM_TOD as time dependent grouping
|
|
time = QTime((int)round(curve->sample(index).x()), 0, 0, 0);
|
|
}
|
|
// feed the popup with data
|
|
ltmPopup->setData(settings, start, end, time);
|
|
popup->show();
|
|
}
|
|
|
|
class GroupedData {
|
|
|
|
public:
|
|
QVector<double> x, y; // x and y data for each metric
|
|
int maxdays;
|
|
};
|
|
|
|
void
|
|
LTMWindow::refreshDataTable()
|
|
{
|
|
// get string
|
|
QString summary = dataTable(true);
|
|
|
|
// now set it
|
|
dataSummary->page()->setHtml(summary);
|
|
}
|
|
|
|
// for storing curve data without using a curve
|
|
class TableCurveData {
|
|
public:
|
|
TableCurveData() { n=0; x.resize(0); y.resize(0); }
|
|
QVector<double> x,y;
|
|
int n;
|
|
};
|
|
|
|
QString
|
|
LTMWindow::dataTable(bool html)
|
|
{
|
|
// truncate date range to the actual data when not set to any date
|
|
if (context->athlete->rideCache->rides().count()) {
|
|
|
|
QDateTime first = context->athlete->rideCache->rides().first()->dateTime;
|
|
QDateTime last = context->athlete->rideCache->rides().last()->dateTime;
|
|
|
|
// end
|
|
if (settings.end == QDateTime() || settings.end.date() > QDate::currentDate().addYears(40))
|
|
settings.end = last;
|
|
|
|
// start
|
|
if (settings.start == QDateTime() || settings.start.date() < QDate::currentDate().addYears(-40))
|
|
settings.start = first;
|
|
}
|
|
|
|
// now set to new (avoids a weird crash)
|
|
QString summary;
|
|
|
|
QColor bgColor = GColor(CTRENDPLOTBACKGROUND);
|
|
QColor altColor = GCColor::alternateColor(bgColor);
|
|
|
|
// html page prettified with a title
|
|
if (html) {
|
|
|
|
summary = GCColor::css();
|
|
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;
|
|
case LTM_ALL :
|
|
summary += tr("All");
|
|
break;
|
|
}
|
|
summary += "</h3><p>";
|
|
}
|
|
|
|
//
|
|
// STEP1: AGGREGATE DATA INTO GROUPBY FOR EACH METRIC
|
|
// This is performed by reusing the existing code in
|
|
// LTMPlot for creating curve data, but storing it
|
|
// in columns and forceing zero values
|
|
QList<TableCurveData> columns;
|
|
bool first=true;
|
|
int rows = 0;
|
|
bool firstXvalue=true;
|
|
double lowestFirstXvalue = DBL_MAX;
|
|
double highestFirstXvalue = 0.0;
|
|
|
|
// create curve data for each metric detail to iterate over
|
|
foreach(MetricDetail metricDetail, settings.metrics) {
|
|
TableCurveData add;
|
|
|
|
ltmPlot->settings=&settings; // for stack mode ltmPlot isn't set
|
|
if (settings.groupBy != LTM_TOD)
|
|
ltmPlot->createCurveData(context, &settings, metricDetail, add.x, add.y, add.n, true);
|
|
else
|
|
ltmPlot->createTODCurveData(context, &settings, metricDetail, add.x, add.y, add.n, true);
|
|
|
|
// adjust to avoid empty chart when there is only 1 group
|
|
if (settings.groupBy != LTM_TOD) add.n++;
|
|
|
|
columns << add;
|
|
|
|
// check if "x" value of all metrics is the same for all colums and find
|
|
// the lowest "x" value and highest "x" value to which all columns need to be aligned
|
|
if (add.n > 0) {
|
|
if (firstXvalue) {
|
|
lowestFirstXvalue = highestFirstXvalue = add.x[0];
|
|
firstXvalue = false;
|
|
} else {
|
|
if (add.x[0] < lowestFirstXvalue) {
|
|
lowestFirstXvalue = add.x[0];
|
|
}
|
|
if (add.x[0] > highestFirstXvalue) {
|
|
highestFirstXvalue = add.x[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
// truncate to shortest set of rows available as
|
|
// we dont pad with zeroes in the data table
|
|
if (first) rows=add.n;
|
|
else if (add.n < rows) rows=add.n;
|
|
first=false;
|
|
}
|
|
|
|
// align the starting X values of all columns using the
|
|
// lowest xValue and highest xValue as borders
|
|
// for columns which have data at all - and if there is something to adjust
|
|
if (!firstXvalue && lowestFirstXvalue != highestFirstXvalue) {
|
|
for (int i = 0; i< columns.count(); i++) {
|
|
if (columns[i].n > 0) {
|
|
// Prepend on vector is prohibitively expensive since requires
|
|
// full vector copy for each prepend. Much faster to convert
|
|
// to Qlist, do our business, then convert back.
|
|
QList<double> tx = columns[i].x.toList();
|
|
QList<double> ty = columns[i].y.toList();
|
|
|
|
double xValue = columns[i].x[0];
|
|
while (xValue > lowestFirstXvalue) {
|
|
xValue--;
|
|
tx.prepend(xValue);
|
|
ty.prepend(0.0);
|
|
}
|
|
|
|
columns[i].x = tx.toVector();
|
|
columns[i].y = ty.toVector();
|
|
columns[i].n += tx.size();
|
|
}
|
|
}
|
|
// adjust number of visible rows in table
|
|
rows += qRound(highestFirstXvalue-lowestFirstXvalue);
|
|
}
|
|
|
|
|
|
//
|
|
// 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 (rows) {
|
|
|
|
// formatting ...
|
|
LTMScaleDraw lsd(settings.start, groupForDate(settings.start.date()), settings.groupBy);
|
|
|
|
QString sLabel = (settings.groupBy == LTM_TOD) ? tr("Time of Day") : tr("Date");
|
|
if (html) {
|
|
// table and headings 50% for 1 metric, 70% for 2 metrics, 90% for 3 metrics or more
|
|
QString tableStart = "<table border=0 cellspacing=3 width=\"%1%%\"><tr><td align=\"center\" valigne=\"top\"><b>%2</b></td>";
|
|
tableStart = tableStart.arg(settings.metrics.count() >= 3 ? 90 : (30 + (settings.metrics.count() * 20))).arg(sLabel);
|
|
|
|
summary += tableStart;
|
|
} else {
|
|
summary += sLabel;
|
|
}
|
|
|
|
QList<QVector<double> > hdatas;
|
|
QList<QString> fontcolors;
|
|
|
|
// highlight
|
|
for (int a=0; a < settings.metrics.count(); a++) {
|
|
MetricDetail metricDetail = settings.metrics[a];
|
|
|
|
int brightness = metricDetail.penColor.red() *0.299 + metricDetail.penColor.green()*0.587 + metricDetail.penColor.blue()*0.114;
|
|
fontcolors.append( brightness > 128 ? "#000" : "#fff" );
|
|
|
|
// highlight lowest / top N values
|
|
if (metricDetail.lowestN > 0 || metricDetail.topN > 0) {
|
|
QMap<double, int> sortedList;
|
|
|
|
// copy the yvalues, retaining the offset
|
|
for(int i=0; i<columns[a].y.count(); i++) {
|
|
// pmc metrics we highlight TROUGHS
|
|
if (metricDetail.type == METRIC_STRESS || metricDetail.type == METRIC_PM) {
|
|
if (i && i < (columns[a].y.count()-1) // not at start/end
|
|
&& ((columns[a].y[i-1] > columns[a].y[i] && columns[a].y[i+1] > columns[a].y[i]) || // is a trough
|
|
(columns[a].y[i-1] < columns[a].y[i] && columns[a].y[i+1] < columns[a].y[i]))) // is a peak
|
|
sortedList.insert(columns[a].y[i], i);
|
|
} else
|
|
sortedList.insert(columns[a].y[i], i);
|
|
}
|
|
|
|
// copy the top N values
|
|
QVector<double> hdata;
|
|
hdata.resize(metricDetail.topN + metricDetail.lowestN);
|
|
|
|
|
|
// QMap orders the list so start at the top and work
|
|
// backwards for topN
|
|
int counter = 0;
|
|
QMapIterator<double, int> i(sortedList);
|
|
if (metricDetail.topN) {
|
|
i.toBack();
|
|
while (i.hasPrevious() && counter < metricDetail.topN) {
|
|
i.previous();
|
|
hdata[counter] = i.value();
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
if (metricDetail.lowestN) {
|
|
i.toFront();
|
|
counter = 0; // and forwards for bottomN
|
|
while (i.hasNext() && counter < metricDetail.lowestN) {
|
|
i.next();
|
|
hdata[metricDetail.topN + counter] = i.value();
|
|
counter++;
|
|
}
|
|
}
|
|
hdatas.append(hdata);
|
|
} else {
|
|
// add an empty vector to maintain alignment with fontcolors
|
|
QVector<double> hdata;
|
|
hdatas.append(hdata);
|
|
}
|
|
}
|
|
|
|
// metric name
|
|
for (int i=0; i < settings.metrics.count(); i++) {
|
|
|
|
QString metricSummary;
|
|
|
|
if (html) metricSummary = "<td align=\"center\" style=\"font-weight:bold;background-color:%2;color:%3\" valign=\"top\">%1</td>";
|
|
else metricSummary = ", %1";
|
|
|
|
QString name = settings.metrics[i].uname;
|
|
|
|
if (name == "Coggan Acute Training Load" || name == tr("Coggan Acute Training Load")) name = "ATL";
|
|
if (name == "Coggan Chronic Training Load" || name == tr("Coggan Chronic Training Load")) name = "CTL";
|
|
if (name == "Coggan Training Stress Balance" || name == tr("Coggan Training Stress Balance")) name = "TSB";
|
|
|
|
metricSummary = metricSummary.arg(name);
|
|
|
|
if (html) {
|
|
QString bcolor = settings.metrics[i].penColor.lighter(80).name();
|
|
metricSummary = metricSummary.arg(bcolor);
|
|
metricSummary = metricSummary.arg(fontcolors.at(i));
|
|
}
|
|
|
|
summary += metricSummary;
|
|
}
|
|
|
|
if (html) {
|
|
|
|
// html table and units on next line
|
|
summary += "</tr><tr><td></td>";
|
|
|
|
// units
|
|
for (int i=0; i < settings.metrics.count(); i++) {
|
|
QString metricSummary = "<td align=\"center\" style=\"font-weight:bold;background-color:%2;color:%3\" valign=\"top\">"
|
|
"%1</td>";
|
|
QString units = settings.metrics[i].uunits;
|
|
QString bcolor = settings.metrics[i].penColor.lighter(80).name();
|
|
|
|
|
|
if (units == "seconds" || units == tr("seconds")) units = tr("hours");
|
|
if (units == settings.metrics[i].uname) units = "";
|
|
metricSummary = metricSummary.arg(units != "" ? QString("(%1)").arg(units) : "");
|
|
metricSummary = metricSummary.arg(bcolor);
|
|
metricSummary = metricSummary.arg(fontcolors.at(i));
|
|
|
|
summary += metricSummary;
|
|
}
|
|
summary += "</tr>";
|
|
|
|
} else {
|
|
|
|
// end of heading for CSV
|
|
summary += "\n";
|
|
}
|
|
|
|
for(int row=0; row<rows; row++) {
|
|
|
|
QString rowSummary;
|
|
|
|
// in day mode we don't list all the zeroes .. its too many!
|
|
bool nonzero = false;
|
|
if (settings.groupBy == LTM_DAY) {
|
|
|
|
// nonzeros?
|
|
for(int j=0; j<columns.count(); j++)
|
|
if (int(columns[j].y[row])) nonzero = true;
|
|
|
|
// skip all zeroes if day mode
|
|
if (nonzero == false) continue;
|
|
}
|
|
|
|
// alternating colors on html output
|
|
if (html) {
|
|
if (row%2) rowSummary += "<tr bgcolor='" + altColor.name() + "'>";
|
|
else rowSummary += "<tr>";
|
|
}
|
|
|
|
// First column, date / month year etc
|
|
QString sDate = lsd.label(columns[0].x[row]+0.5).text().replace("\n", " ");
|
|
if (html) rowSummary += QString("<td align=\"center\" valign=\"top\">%1</td>").arg(sDate);
|
|
else rowSummary += (settings.groupBy == LTM_ALL || settings.groupBy == LTM_TOD) ? sDate : lsd.toDate(columns[0].x[row]+0.5).toString(Qt::ISODate);
|
|
|
|
// Remaining columns - each metric value
|
|
for(int j=0; j<columns.count(); j++) {
|
|
|
|
QString metricSummary;
|
|
|
|
if (html) metricSummary += "<td align=\"center\" style=\"%2\" valign=\"top\">%1</td>";
|
|
else metricSummary += ", %1";
|
|
|
|
// now format the actual value....
|
|
QString valueString;
|
|
double value = columns[j].y[row];
|
|
|
|
// Format minutes in sexagesimal format
|
|
if (LTMPlot::isMinutes(settings.metrics[j].uunits)) {
|
|
valueString = time_to_string(value * 60, true);
|
|
} else {
|
|
int precision = 1;
|
|
const RideMetric *m = settings.metrics[j].metric;
|
|
if (m != NULL) {
|
|
|
|
// we have a metric so lets be precise ...
|
|
precision = m->precision();
|
|
|
|
// handle precision of 1 for seconds converted to hours
|
|
if (settings.metrics[j].uunits == "seconds" || settings.metrics[j].uunits == tr("seconds")) precision = 1;
|
|
}
|
|
valueString.setNum(value, 'f', precision);
|
|
}
|
|
|
|
metricSummary = metricSummary.arg(valueString);
|
|
|
|
//
|
|
if (hdatas.at(j).contains(row)) {
|
|
QString c = QString("background-color:%1;color:%2").arg(settings.metrics[j].penColor.name()).arg(fontcolors.at(j));
|
|
metricSummary = metricSummary.arg(c);
|
|
} else
|
|
metricSummary = metricSummary.arg("");
|
|
|
|
rowSummary += metricSummary;
|
|
}
|
|
|
|
// ok, this row is done
|
|
if (html) rowSummary += "</tr>";
|
|
else rowSummary += "\n"; // csv newline
|
|
|
|
summary += rowSummary;
|
|
}
|
|
|
|
// close table on html page
|
|
if (html) summary += "</table>";
|
|
}
|
|
|
|
// all done !
|
|
if (html) summary += "</center>";
|
|
|
|
return summary;
|
|
}
|
|
|
|
void
|
|
LTMWindow::exportConfig()
|
|
{
|
|
// collect the config to export
|
|
QList<LTMSettings> mine;
|
|
mine << settings;
|
|
mine[0].title = mine[0].name = title();
|
|
|
|
// get a filename
|
|
QString filename = title()+".xml";
|
|
filename = QFileDialog::getSaveFileName(this, tr("Export Chart Config"), filename, title()+".xml (*.xml)");
|
|
|
|
// export it!
|
|
if (!filename.isEmpty()) {
|
|
LTMChartParser::serialize(filename, mine);
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
LTMWindow::exportData()
|
|
{
|
|
QString filename = title()+".csv";
|
|
filename = QFileDialog::getSaveFileName(this, tr("Save Chart Data as CSV"), QString(), title()+".csv (*.csv)");
|
|
|
|
if (!filename.isEmpty()) {
|
|
|
|
// can we open the file ?
|
|
QFile f(filename);
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) return; // couldn't open file
|
|
|
|
// generate file content
|
|
QString content = dataTable(false); // want csv
|
|
|
|
// open stream and write header
|
|
QTextStream stream(&f);
|
|
stream.setCodec("UTF-8"); // Names and Units can be translated
|
|
stream << content;
|
|
|
|
// and we're done
|
|
f.close();
|
|
}
|
|
}
|