mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
Plot seasons / date ranges on Histogram Plot
The recent RideFileCache patches added functions to pre-compute mean-max and distributions. This enabled this patch to add plotting histograms for a date range rather than a specific ride. It supports all the same data series as before but will allow you to select a season from a new combo box. I have refactored a fair amount of the code, but kept the original code in PowerHist as close to unchanged as I could since I did not want to disturb existing functionality. There is no support for Zoning historic data -- this requires an update to the RideFileCache.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net)
|
||||
* 2011 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
|
||||
@@ -29,8 +30,7 @@
|
||||
#include "Zones.h"
|
||||
#include "HrZones.h"
|
||||
|
||||
HistogramWindow::HistogramWindow(MainWindow *mainWindow) :
|
||||
GcWindow(mainWindow), mainWindow(mainWindow)
|
||||
HistogramWindow::HistogramWindow(MainWindow *mainWindow) : GcWindow(mainWindow), mainWindow(mainWindow), source(NULL)
|
||||
{
|
||||
setInstanceName("Histogram Window");
|
||||
|
||||
@@ -60,119 +60,141 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) :
|
||||
binWidthLayout->addWidget(binWidthSlider);
|
||||
cl->addLayout(binWidthLayout);
|
||||
|
||||
lnYHistCheckBox = new QCheckBox;
|
||||
lnYHistCheckBox->setText(tr("Log Y"));
|
||||
cl->addWidget(lnYHistCheckBox);
|
||||
showLnY = new QCheckBox;
|
||||
showLnY->setText(tr("Log Y"));
|
||||
cl->addWidget(showLnY);
|
||||
|
||||
withZerosCheckBox = new QCheckBox;
|
||||
withZerosCheckBox->setText(tr("With zeros"));
|
||||
cl->addWidget(withZerosCheckBox);
|
||||
showZeroes = new QCheckBox;
|
||||
showZeroes->setText(tr("With zeros"));
|
||||
cl->addWidget(showZeroes);
|
||||
|
||||
histShadeZones = new QCheckBox;
|
||||
histShadeZones->setText(tr("Shade zones"));
|
||||
cl->addWidget(histShadeZones);
|
||||
shadeZones = new QCheckBox;
|
||||
shadeZones->setText(tr("Shade zones"));
|
||||
shadeZones->setChecked(powerHist->shade);
|
||||
cl->addWidget(shadeZones);
|
||||
|
||||
histParameterCombo = new QComboBox();
|
||||
histParameterCombo->addItem(tr("Watts"));
|
||||
histParameterCombo->addItem(tr("Watts (by Zone)"));
|
||||
histParameterCombo->addItem(tr("Torque"));
|
||||
histParameterCombo->addItem(tr("Heartrate"));
|
||||
histParameterCombo->addItem(tr("Heartrate (by Zone)"));
|
||||
histParameterCombo->addItem(tr("Speed"));
|
||||
histParameterCombo->addItem(tr("Cadence"));
|
||||
histParameterCombo->setCurrentIndex(0);
|
||||
cl->addWidget(histParameterCombo);
|
||||
showInZones = new QCheckBox;
|
||||
showInZones->setText(tr("Show in zones"));
|
||||
cl->addWidget(showInZones);
|
||||
|
||||
histSumY = new QComboBox();
|
||||
histSumY->addItem(tr("Absolute Time"));
|
||||
histSumY->addItem(tr("Percentage Time"));
|
||||
cComboSeason = new QComboBox(this);
|
||||
seriesCombo = new QComboBox();
|
||||
addSeries();
|
||||
cl->addWidget(seriesCombo);
|
||||
|
||||
showSumY = new QComboBox();
|
||||
showSumY->addItem(tr("Absolute Time"));
|
||||
showSumY->addItem(tr("Percentage Time"));
|
||||
seasonCombo = new QComboBox(this);
|
||||
addSeasons();
|
||||
cl->addWidget(histSumY);
|
||||
cl->addWidget(cComboSeason);
|
||||
seasonCombo->setCurrentIndex(0); // default to current ride selected
|
||||
cl->addWidget(showSumY);
|
||||
cl->addWidget(seasonCombo);
|
||||
cl->addStretch();
|
||||
|
||||
// sort out default values
|
||||
setHistTextValidator();
|
||||
lnYHistCheckBox->setChecked(powerHist->islnY());
|
||||
withZerosCheckBox->setChecked(powerHist->withZeros());
|
||||
showLnY->setChecked(powerHist->islnY());
|
||||
showZeroes->setChecked(powerHist->withZeros());
|
||||
binWidthSlider->setValue(powerHist->binWidth());
|
||||
setHistBinWidthText();
|
||||
|
||||
connect(binWidthSlider, SIGNAL(valueChanged(int)),
|
||||
this, SLOT(setBinWidthFromSlider()));
|
||||
connect(binWidthLineEdit, SIGNAL(editingFinished()),
|
||||
this, SLOT(setBinWidthFromLineEdit()));
|
||||
connect(lnYHistCheckBox, SIGNAL(stateChanged(int)),
|
||||
this, SLOT(setlnYHistFromCheckBox()));
|
||||
connect(withZerosCheckBox, SIGNAL(stateChanged(int)),
|
||||
this, SLOT(setWithZerosFromCheckBox()));
|
||||
connect(histParameterCombo, SIGNAL(currentIndexChanged(int)),
|
||||
this, SLOT(setHistSelection(int)));
|
||||
connect(histShadeZones, SIGNAL(stateChanged(int)),
|
||||
this, SLOT(setHistSelection(int)));
|
||||
connect(histSumY, SIGNAL(currentIndexChanged(int)),
|
||||
this, SLOT(setSumY(int)));
|
||||
//connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
|
||||
// set the defaults etc
|
||||
updateChart();
|
||||
|
||||
// the bin slider/input update each other
|
||||
// only the input box triggers an update to the chart
|
||||
connect(binWidthSlider, SIGNAL(valueChanged(int)), this, SLOT(setBinWidthFromSlider()));
|
||||
connect(binWidthLineEdit, SIGNAL(editingFinished()), this, SLOT(setBinWidthFromLineEdit()));
|
||||
|
||||
// when season changes we need to retrieve data from the cache then update the chart
|
||||
connect(seasonCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int)));
|
||||
|
||||
// if any of the controls change we pass the chart everything
|
||||
connect(showLnY, SIGNAL(stateChanged(int)), this, SLOT(updateChart()));
|
||||
connect(showZeroes, SIGNAL(stateChanged(int)), this, SLOT(updateChart()));
|
||||
connect(seriesCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateChart()));
|
||||
connect(showInZones, SIGNAL(stateChanged(int)), this, SLOT(updateChart()));
|
||||
connect(shadeZones, SIGNAL(stateChanged(int)), this, SLOT(updateChart()));
|
||||
connect(showSumY, SIGNAL(currentIndexChanged(int)), this, SLOT(updateChart()));
|
||||
|
||||
connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected()));
|
||||
connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected()));
|
||||
connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged()));
|
||||
connect(mainWindow, SIGNAL(configChanged()), powerHist, SLOT(configChanged()));
|
||||
connect(cComboSeason, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int)));
|
||||
}
|
||||
|
||||
void
|
||||
HistogramWindow::rideSelected()
|
||||
{
|
||||
if (!amVisible())
|
||||
return;
|
||||
if (!amVisible()) return;
|
||||
|
||||
RideItem *ride = myRideItem;
|
||||
if (!ride)
|
||||
return;
|
||||
if (!ride || seasonCombo->currentIndex() != 0) return;
|
||||
|
||||
// get range that applies to this ride
|
||||
powerRange = mainWindow->zones()->whichRange(ride->dateTime.date());
|
||||
hrRange = mainWindow->hrZones()->whichRange(ride->dateTime.date());
|
||||
|
||||
// set the histogram data
|
||||
powerHist->setData(ride);
|
||||
// update
|
||||
updateChart();
|
||||
}
|
||||
|
||||
void
|
||||
HistogramWindow::intervalSelected()
|
||||
{
|
||||
if (!amVisible())
|
||||
return;
|
||||
RideItem *ride = myRideItem;
|
||||
if (!ride) return;
|
||||
if (!amVisible()) return;
|
||||
|
||||
// set the histogram data
|
||||
powerHist->setData(ride);
|
||||
RideItem *ride = myRideItem;
|
||||
|
||||
// null? or not plotting current ride, ignore signal
|
||||
if (!ride || seasonCombo->currentIndex() != 0) return;
|
||||
|
||||
// update
|
||||
interval = true;
|
||||
updateChart();
|
||||
}
|
||||
|
||||
void
|
||||
HistogramWindow::zonesChanged()
|
||||
{
|
||||
if (!amVisible())
|
||||
return;
|
||||
if (!amVisible()) return;
|
||||
|
||||
powerHist->refreshZoneLabels();
|
||||
powerHist->replot();
|
||||
}
|
||||
|
||||
void HistogramWindow::seasonSelected(int index)
|
||||
{
|
||||
RideFileCache *old = source;
|
||||
|
||||
if (index > 0) {
|
||||
index--; // it is now an index into the season array
|
||||
|
||||
// Set data from BESTS
|
||||
Season season = seasons.at(index);
|
||||
QDate start = season.getStart();
|
||||
QDate end = season.getEnd();
|
||||
if (end == QDate()) end = QDate(3000,12,31);
|
||||
if (start == QDate()) start = QDate(1900,1,1);
|
||||
source = new RideFileCache(mainWindow, start, end);
|
||||
|
||||
} else if (!index) {
|
||||
// Set data from RIDE
|
||||
|
||||
if (old) delete old; // guarantee source pointer changes
|
||||
}
|
||||
updateChart();
|
||||
}
|
||||
|
||||
void HistogramWindow::addSeries()
|
||||
{
|
||||
// setup series list
|
||||
seriesList << RideFile::watts
|
||||
<< RideFile::hr
|
||||
<< RideFile::kph
|
||||
<< RideFile::cad
|
||||
<< RideFile::nm;
|
||||
|
||||
foreach (RideFile::SeriesType x, seriesList)
|
||||
seriesCombo->addItem(RideFile::seriesName(x), static_cast<int>(x));
|
||||
}
|
||||
void HistogramWindow::addSeasons()
|
||||
{
|
||||
QFile seasonFile(mainWindow->home.absolutePath() + "/seasons.xml");
|
||||
@@ -182,52 +204,26 @@ void HistogramWindow::addSeasons()
|
||||
xmlReader.setContentHandler(&handler);
|
||||
xmlReader.setErrorHandler(&handler);
|
||||
bool ok = xmlReader.parse( source );
|
||||
if(!ok)
|
||||
qWarning("Failed to parse seasons.xml");
|
||||
if(!ok) qWarning("Failed to parse seasons.xml");
|
||||
|
||||
seasons = handler.getSeasons();
|
||||
Season season;
|
||||
season.setName(tr("All Seasons"));
|
||||
seasons.insert(0,season);
|
||||
|
||||
cComboSeason->addItem("Selected Ride");
|
||||
seasonCombo->addItem("Selected Ride");
|
||||
foreach (Season season, seasons)
|
||||
cComboSeason->addItem(season.getName());
|
||||
if (!seasons.empty()) {
|
||||
cComboSeason->setCurrentIndex(cComboSeason->count() - 1);
|
||||
Season season = seasons.last();
|
||||
// set default parameters here
|
||||
// XXX todo
|
||||
}
|
||||
seasonCombo->addItem(season.getName());
|
||||
}
|
||||
|
||||
void
|
||||
HistogramWindow::setBinWidthFromSlider()
|
||||
{
|
||||
if (powerHist->binWidth() != binWidthSlider->value()) {
|
||||
powerHist->setBinWidth(binWidthSlider->value());
|
||||
setHistBinWidthText();
|
||||
}
|
||||
}
|
||||
setHistBinWidthText();
|
||||
|
||||
void
|
||||
HistogramWindow::setlnYHistFromCheckBox()
|
||||
{
|
||||
if (powerHist->islnY() != lnYHistCheckBox->isChecked())
|
||||
powerHist->setlnY(! powerHist->islnY());
|
||||
}
|
||||
|
||||
void
|
||||
HistogramWindow::setSumY(int index)
|
||||
{
|
||||
if (index < 0) return; // being destroyed
|
||||
else powerHist->setSumY(index == 0);
|
||||
}
|
||||
|
||||
void
|
||||
HistogramWindow::setWithZerosFromCheckBox()
|
||||
{
|
||||
if (powerHist->withZeros() != withZerosCheckBox->isChecked()) {
|
||||
powerHist->setWithZeros(withZerosCheckBox->isChecked());
|
||||
updateChart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,67 +238,21 @@ HistogramWindow::setHistTextValidator()
|
||||
{
|
||||
double delta = powerHist->getDelta();
|
||||
int digits = powerHist->getDigits();
|
||||
binWidthLineEdit->setValidator(
|
||||
(digits == 0) ?
|
||||
(QValidator *) (
|
||||
new QIntValidator(binWidthSlider->minimum() * delta,
|
||||
binWidthSlider->maximum() * delta,
|
||||
binWidthLineEdit
|
||||
)
|
||||
) :
|
||||
(QValidator *) (
|
||||
new QDoubleValidator(binWidthSlider->minimum() * delta,
|
||||
binWidthSlider->maximum() * delta,
|
||||
digits,
|
||||
binWidthLineEdit
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
QValidator *validator;
|
||||
if (digits == 0) {
|
||||
|
||||
void
|
||||
HistogramWindow::setHistSelection(int /*id*/)
|
||||
{
|
||||
// Set shading first, since the dataseries selection
|
||||
// below will trigger a redraw, and we need to have
|
||||
// set the shading beforehand. OK, so we could make
|
||||
// either change trigger it, but this makes for simpler
|
||||
// code here and in powerhist.cpp
|
||||
if (histShadeZones->isChecked()) powerHist->setShading(true);
|
||||
else powerHist->setShading(false);
|
||||
validator = new QIntValidator(binWidthSlider->minimum() * delta,
|
||||
binWidthSlider->maximum() * delta,
|
||||
binWidthLineEdit);
|
||||
} else {
|
||||
|
||||
// Which data series are we plotting?
|
||||
switch (histParameterCombo->currentIndex()) {
|
||||
default:
|
||||
case 0 :
|
||||
powerHist->setSelection(PowerHist::watts);
|
||||
break;
|
||||
|
||||
case 1 :
|
||||
powerHist->setSelection(PowerHist::wattsZone);
|
||||
break;
|
||||
|
||||
case 2 :
|
||||
powerHist->setSelection(PowerHist::nm);
|
||||
break;
|
||||
|
||||
case 3 :
|
||||
powerHist->setSelection(PowerHist::hr);
|
||||
break;
|
||||
|
||||
case 4 :
|
||||
powerHist->setSelection(PowerHist::hrZone);
|
||||
break;
|
||||
case 5 :
|
||||
powerHist->setSelection(PowerHist::kph);
|
||||
break;
|
||||
|
||||
case 6 :
|
||||
powerHist->setSelection(PowerHist::cad);
|
||||
validator = new QDoubleValidator(binWidthSlider->minimum() * delta,
|
||||
binWidthSlider->maximum() * delta,
|
||||
digits,
|
||||
binWidthLineEdit);
|
||||
}
|
||||
setHistBinWidthText();
|
||||
setHistTextValidator();
|
||||
binWidthLineEdit->setValidator(validator);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -310,7 +260,43 @@ HistogramWindow::setBinWidthFromLineEdit()
|
||||
{
|
||||
double value = binWidthLineEdit->text().toDouble();
|
||||
if (value != powerHist->binWidth()) {
|
||||
binWidthSlider->setValue(powerHist->setBinWidthRealUnits(value));
|
||||
setHistBinWidthText();
|
||||
binWidthSlider->setValue(powerHist->setBinWidthRealUnits(value));
|
||||
setHistBinWidthText();
|
||||
|
||||
updateChart();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
HistogramWindow::updateChart()
|
||||
{
|
||||
// set data
|
||||
if (seasonCombo->currentIndex() != 0 && source)
|
||||
powerHist->setData(source);
|
||||
else if (seasonCombo->currentIndex() == 0 && myRideItem)
|
||||
powerHist->setData(myRideItem, interval); // intervals selected forces data to
|
||||
// be recomputed since interval selection
|
||||
// has changed.
|
||||
|
||||
// and now the controls
|
||||
powerHist->setShading(shadeZones->isChecked() ? true : false);
|
||||
powerHist->setZoned(showInZones->isChecked() ? true : false);
|
||||
powerHist->setlnY(showLnY->isChecked() ? true : false);
|
||||
powerHist->setWithZeros(showZeroes->isChecked() ? true : false);
|
||||
powerHist->setSumY(showSumY->currentIndex()== 0 ? true : false);
|
||||
powerHist->setBinWidth(binWidthLineEdit->text().toDouble());
|
||||
|
||||
// and which series to plot
|
||||
powerHist->setSeries(static_cast<RideFile::SeriesType>(seriesCombo->itemData(seriesCombo->currentIndex()).toInt()));
|
||||
|
||||
// now go plot yourself
|
||||
//powerHist->setAxisTitle(int axis, QString label);
|
||||
powerHist->recalc(interval); // interval changed? force recalc
|
||||
powerHist->replot();
|
||||
|
||||
interval = false;// we force a recalc whem called coz intervals
|
||||
// have been selected. The recalc routine in
|
||||
// powerhist optimises out, but doesn't keep track
|
||||
// of interval selection -- simplifies the setters
|
||||
// and getters, so worth this 'hack'.
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net)
|
||||
* 2011 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
|
||||
@@ -28,6 +29,7 @@
|
||||
class MainWindow;
|
||||
class PowerHist;
|
||||
class RideItem;
|
||||
class RideFileCache;
|
||||
|
||||
class HistogramWindow : public GcWindow
|
||||
{
|
||||
@@ -40,24 +42,30 @@ class HistogramWindow : public GcWindow
|
||||
Q_PROPERTY(bool logY READ logY WRITE setLogY USER true)
|
||||
Q_PROPERTY(bool zeroes READ zeroes WRITE setZeroes USER true)
|
||||
Q_PROPERTY(bool shade READ shade WRITE setShade USER true)
|
||||
Q_PROPERTY(bool zoned READ zoned WRITE setZoned USER true)
|
||||
Q_PROPERTY(bool scope READ scope WRITE setScope USER true)
|
||||
|
||||
public:
|
||||
|
||||
HistogramWindow(MainWindow *mainWindow);
|
||||
|
||||
// get/set properties
|
||||
int series() const { return histParameterCombo->currentIndex(); }
|
||||
void setSeries(int x) { histParameterCombo->setCurrentIndex(x); }
|
||||
int percent() const { return histSumY->currentIndex(); }
|
||||
void setPercent(int x) { histSumY->setCurrentIndex(x); }
|
||||
int series() const { return seriesCombo->currentIndex(); }
|
||||
void setSeries(int x) { seriesCombo->setCurrentIndex(x); }
|
||||
int percent() const { return showSumY->currentIndex(); }
|
||||
void setPercent(int x) { showSumY->setCurrentIndex(x); }
|
||||
double bin() const { return binWidthSlider->value(); }
|
||||
void setBin(double x) { binWidthSlider->setValue(x); }
|
||||
bool logY() const { return lnYHistCheckBox->isChecked(); }
|
||||
void setLogY(bool x) { lnYHistCheckBox->setChecked(x); }
|
||||
bool zeroes() const { return withZerosCheckBox->isChecked(); }
|
||||
void setZeroes(bool x) { withZerosCheckBox->setChecked(x); }
|
||||
bool shade() const { return histShadeZones->isChecked(); }
|
||||
void setShade(bool x) { histShadeZones->setChecked(x); }
|
||||
bool logY() const { return showLnY->isChecked(); }
|
||||
void setLogY(bool x) { showLnY->setChecked(x); }
|
||||
bool zeroes() const { return showZeroes->isChecked(); }
|
||||
void setZeroes(bool x) { showZeroes->setChecked(x); }
|
||||
bool shade() const { return shadeZones->isChecked(); }
|
||||
void setShade(bool x) { shadeZones->setChecked(x); }
|
||||
bool zoned() const { return showInZones->isChecked(); }
|
||||
void setZoned(bool x) { return showInZones->setChecked(x); }
|
||||
int scope() const { return seasonCombo->currentIndex(); }
|
||||
void setScope(int x) { seasonCombo->setCurrentIndex(x); }
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -69,13 +77,10 @@ class HistogramWindow : public GcWindow
|
||||
|
||||
void setBinWidthFromSlider();
|
||||
void setBinWidthFromLineEdit();
|
||||
void setlnYHistFromCheckBox();
|
||||
void setWithZerosFromCheckBox();
|
||||
void setHistSelection(int id);
|
||||
void setSumY(int);
|
||||
void seasonSelected(int season);
|
||||
void updateChart();
|
||||
|
||||
protected:
|
||||
private:
|
||||
|
||||
QList<Season> seasons;
|
||||
void setHistTextValidator();
|
||||
@@ -83,17 +88,25 @@ class HistogramWindow : public GcWindow
|
||||
|
||||
MainWindow *mainWindow;
|
||||
PowerHist *powerHist;
|
||||
QSlider *binWidthSlider;
|
||||
QLineEdit *binWidthLineEdit;
|
||||
QCheckBox *lnYHistCheckBox;
|
||||
QCheckBox *withZerosCheckBox;
|
||||
QCheckBox *histShadeZones;
|
||||
QComboBox *histParameterCombo;
|
||||
QComboBox *histSumY;
|
||||
QComboBox *cComboSeason;
|
||||
|
||||
QSlider *binWidthSlider; // seet Bin Width from a slider
|
||||
QLineEdit *binWidthLineEdit; // set Bin Width from the line edit
|
||||
QCheckBox *showLnY; // set show as Log(y)
|
||||
QCheckBox *showZeroes; // Include zeroes
|
||||
QComboBox *showSumY; // ??
|
||||
QCheckBox *shadeZones; // Shade zone background
|
||||
QCheckBox *showInZones; // Plot by Zone
|
||||
QComboBox *seriesCombo; // Which data series to plot
|
||||
QComboBox *seasonCombo; // Plot for Date range or current ride
|
||||
|
||||
QList<RideFile::SeriesType> seriesList;
|
||||
void addSeasons();
|
||||
void addSeries();
|
||||
|
||||
int powerRange, hrRange;
|
||||
|
||||
RideFileCache *source;
|
||||
bool interval;
|
||||
};
|
||||
|
||||
#endif // _GC_HistogramWindow_h
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
510
src/PowerHist.h
510
src/PowerHist.h
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
* 2011 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
|
||||
@@ -19,6 +20,10 @@
|
||||
#ifndef _GC_PowerHist_h
|
||||
#define _GC_PowerHist_h 1
|
||||
#include "GoldenCheetah.h"
|
||||
#include "RideFile.h"
|
||||
#include "MainWindow.h"
|
||||
#include "Zones.h"
|
||||
#include "HrZones.h"
|
||||
|
||||
#include <qwt_plot.h>
|
||||
#include <qwt_plot_zoomer.h>
|
||||
@@ -31,6 +36,7 @@ class QwtPlotGrid;
|
||||
class MainWindow;
|
||||
class RideItem;
|
||||
class RideFilePoint;
|
||||
class RideFileCache;
|
||||
class PowerHistBackground;
|
||||
class PowerHistZoneLabel;
|
||||
class HrHistBackground;
|
||||
@@ -41,167 +47,437 @@ class ZoneScaleDraw;
|
||||
class penTooltip: public QwtPlotZoomer
|
||||
{
|
||||
public:
|
||||
penTooltip(QwtPlotCanvas *canvas):
|
||||
QwtPlotZoomer(canvas), tip("")
|
||||
{
|
||||
// With some versions of Qt/Qwt, setting this to AlwaysOn
|
||||
// causes an infinite recursion.
|
||||
//setTrackerMode(AlwaysOn);
|
||||
setTrackerMode(AlwaysOn);
|
||||
penTooltip(QwtPlotCanvas *canvas): QwtPlotZoomer(canvas), tip("") {
|
||||
// With some versions of Qt/Qwt, setting this to AlwaysOn
|
||||
// causes an infinite recursion.
|
||||
//setTrackerMode(AlwaysOn);
|
||||
setTrackerMode(AlwaysOn);
|
||||
}
|
||||
|
||||
virtual QwtText trackerText(const QwtDoublePoint &/*pos*/) const
|
||||
{
|
||||
QColor bg = QColor(255,255, 170); // toolyip yellow
|
||||
virtual QwtText trackerText(const QwtDoublePoint &/*pos*/) const {
|
||||
QColor bg = QColor(255,255, 170); // toolyip yellow
|
||||
#if QT_VERSION >= 0x040300
|
||||
bg.setAlpha(200);
|
||||
bg.setAlpha(200);
|
||||
#endif
|
||||
QwtText text;
|
||||
QFont def;
|
||||
//def.setPointSize(8); // too small on low res displays (Mac)
|
||||
//double val = ceil(pos.y()*100) / 100; // round to 2 decimal place
|
||||
//text.setText(QString("%1 %2").arg(val).arg(format), QwtText::PlainText);
|
||||
text.setText(tip);
|
||||
text.setFont(def);
|
||||
text.setBackgroundBrush( QBrush( bg ));
|
||||
text.setRenderFlags(Qt::AlignLeft | Qt::AlignTop);
|
||||
return text;
|
||||
}
|
||||
void setFormat(QString fmt) { format = fmt; }
|
||||
void setText(QString txt) { tip = txt; }
|
||||
QwtText text;
|
||||
QFont def;
|
||||
//def.setPointSize(8); // too small on low res displays (Mac)
|
||||
//double val = ceil(pos.y()*100) / 100; // round to 2 decimal place
|
||||
//text.setText(QString("%1 %2").arg(val).arg(format), QwtText::PlainText);
|
||||
text.setText(tip);
|
||||
text.setFont(def);
|
||||
text.setBackgroundBrush( QBrush( bg ));
|
||||
text.setRenderFlags(Qt::AlignLeft | Qt::AlignTop);
|
||||
return text;
|
||||
}
|
||||
|
||||
void setFormat(QString fmt) { format = fmt; }
|
||||
void setText(QString txt) { tip = txt; }
|
||||
|
||||
private:
|
||||
QString format;
|
||||
QString tip;
|
||||
};
|
||||
QString format;
|
||||
QString tip;
|
||||
};
|
||||
|
||||
class PowerHist : public QwtPlot
|
||||
{
|
||||
Q_OBJECT
|
||||
G_OBJECT
|
||||
|
||||
friend class ::HrHistBackground;
|
||||
friend class ::HrHistZoneLabel;
|
||||
friend class ::PowerHistBackground;
|
||||
friend class ::PowerHistZoneLabel;
|
||||
friend class ::HistogramWindow;
|
||||
|
||||
public:
|
||||
|
||||
QwtPlotCurve *curve, *curveSelected;
|
||||
QList <PowerHistZoneLabel *> zoneLabels;
|
||||
QList <HrHistZoneLabel *> hrzoneLabels;
|
||||
|
||||
PowerHist(MainWindow *mainWindow);
|
||||
~PowerHist();
|
||||
~PowerHist();
|
||||
|
||||
int binWidth() const { return binw; }
|
||||
inline bool islnY() const { return lny; }
|
||||
inline bool withZeros() const { return withz; }
|
||||
bool shadeZones() const;
|
||||
bool shadeHRZones() const;
|
||||
|
||||
enum Selection {
|
||||
watts,
|
||||
wattsZone,
|
||||
nm,
|
||||
hr,
|
||||
hrZone,
|
||||
kph,
|
||||
cad
|
||||
} selected;
|
||||
inline Selection selection() { return selected; }
|
||||
|
||||
bool shade;
|
||||
inline bool shaded() const { return shade; }
|
||||
|
||||
|
||||
void setData(RideItem *_rideItem);
|
||||
|
||||
void setSelection(Selection selection);
|
||||
void fixSelection();
|
||||
|
||||
void setShading(bool x) { shade=x; }
|
||||
|
||||
void setBinWidth(int value);
|
||||
double getDelta();
|
||||
int getDigits();
|
||||
double getBinWidthRealUnits();
|
||||
int setBinWidthRealUnits(double value);
|
||||
|
||||
void refreshZoneLabels();
|
||||
void refreshHRZoneLabels();
|
||||
|
||||
RideItem *rideItem;
|
||||
MainWindow *mainWindow;
|
||||
|
||||
public slots:
|
||||
|
||||
// public setters
|
||||
void setShading(bool x) { shade=x; }
|
||||
void setSeries(RideFile::SeriesType series);
|
||||
void setData(RideItem *_rideItem, bool force=false);
|
||||
void setData(RideFileCache *source);
|
||||
void setlnY(bool value);
|
||||
void setWithZeros(bool value);
|
||||
void setZoned(bool value);
|
||||
void setSumY(bool value);
|
||||
void pointHover(QwtPlotCurve *curve, int index);
|
||||
void configChanged();
|
||||
void setAxisTitle(int axis, QString label);
|
||||
void setYMax();
|
||||
void setBinWidth(int value);
|
||||
int setBinWidthRealUnits(double value);
|
||||
|
||||
// public getters
|
||||
double getDelta();
|
||||
double getBinWidthRealUnits();
|
||||
int getDigits();
|
||||
inline bool islnY() const { return lny; }
|
||||
inline bool withZeros() const { return withz; }
|
||||
inline int binWidth() const { return binw; }
|
||||
|
||||
// react to plot signals
|
||||
void pointHover(QwtPlotCurve *curve, int index);
|
||||
|
||||
// get told to refresh
|
||||
void recalc(bool force=false);
|
||||
void refreshZoneLabels();
|
||||
|
||||
protected:
|
||||
|
||||
QwtPlotGrid *grid;
|
||||
void refreshHRZoneLabels();
|
||||
void setParameterAxisTitle();
|
||||
bool isSelected(const RideFilePoint *p, double);
|
||||
void percentify(QVector<double> &, double factor); // and a function to convert
|
||||
|
||||
// storage for data counts
|
||||
QVector<unsigned int>
|
||||
wattsArray,
|
||||
wattsZoneArray,
|
||||
nmArray,
|
||||
hrArray,
|
||||
hrZoneArray,
|
||||
kphArray,
|
||||
cadArray;
|
||||
|
||||
// storage for data counts in interval selected
|
||||
QVector<unsigned int>
|
||||
wattsSelectedArray,
|
||||
wattsZoneSelectedArray,
|
||||
nmSelectedArray,
|
||||
hrSelectedArray,
|
||||
hrZoneSelectedArray,
|
||||
kphSelectedArray,
|
||||
cadSelectedArray;
|
||||
bool shadeZones() const; // check if zone shading is both wanted and possible
|
||||
bool shadeHRZones() const; // check if zone shading is both wanted and possible
|
||||
|
||||
// plot settings
|
||||
RideItem *rideItem;
|
||||
MainWindow *mainWindow;
|
||||
RideFile::SeriesType series;
|
||||
bool useMetricUnits; // whether metric units are used (or imperial)
|
||||
QVariant unit;
|
||||
bool lny;
|
||||
bool shade;
|
||||
bool zoned; // show in zones
|
||||
int binw;
|
||||
|
||||
bool withz; // whether zeros are included in histogram
|
||||
double dt; // length of sample
|
||||
double dt; // length of sample
|
||||
bool absolutetime; // do we sum absolute or percentage?
|
||||
|
||||
void recalc();
|
||||
void setYMax();
|
||||
penTooltip *zoomer;
|
||||
|
||||
private:
|
||||
QVariant unit;
|
||||
|
||||
PowerHistBackground *bg;
|
||||
HrHistBackground *hrbg;
|
||||
// plot objects
|
||||
QwtPlotGrid *grid;
|
||||
PowerHistBackground *bg;
|
||||
HrHistBackground *hrbg;
|
||||
penTooltip *zoomer;
|
||||
LTMCanvasPicker *canvasPicker;
|
||||
QwtPlotCurve *curve, *curveSelected;
|
||||
QList <PowerHistZoneLabel *> zoneLabels;
|
||||
QList <HrHistZoneLabel *> hrzoneLabels;
|
||||
|
||||
bool lny;
|
||||
// source cache
|
||||
RideFileCache *cache;
|
||||
|
||||
// discritized unit for smoothing
|
||||
// discritized unit for smoothing
|
||||
static const double wattsDelta = 1.0;
|
||||
static const double nmDelta = 0.1;
|
||||
static const double hrDelta = 1.0;
|
||||
static const double kphDelta = 0.1;
|
||||
static const double cadDelta = 1.0;
|
||||
static const double nmDelta = 0.1;
|
||||
static const double hrDelta = 1.0;
|
||||
static const double kphDelta = 0.1;
|
||||
static const double cadDelta = 1.0;
|
||||
|
||||
// digits for text entry validator
|
||||
// digits for text entry validator
|
||||
static const int wattsDigits = 0;
|
||||
static const int nmDigits = 1;
|
||||
static const int hrDigits = 0;
|
||||
static const int kphDigits = 1;
|
||||
static const int cadDigits = 0;
|
||||
static const int nmDigits = 1;
|
||||
static const int hrDigits = 0;
|
||||
static const int kphDigits = 1;
|
||||
static const int cadDigits = 0;
|
||||
|
||||
void setParameterAxisTitle();
|
||||
bool isSelected(const RideFilePoint *p, double);
|
||||
// storage for data counts
|
||||
QVector<unsigned int> wattsArray, wattsZoneArray, nmArray, hrArray,
|
||||
hrZoneArray, kphArray, cadArray;
|
||||
|
||||
bool useMetricUnits; // whether metric units are used (or imperial)
|
||||
// storage for data counts in interval selected
|
||||
QVector<unsigned int> wattsSelectedArray, wattsZoneSelectedArray,
|
||||
nmSelectedArray, hrSelectedArray,
|
||||
hrZoneSelectedArray, kphSelectedArray,
|
||||
cadSelectedArray;
|
||||
|
||||
bool absolutetime; // do we sum absolute or percentage?
|
||||
void percentify(QVector<double> &, double factor); // and a function to convert
|
||||
enum Source { Ride, Cache } source, LASTsource;
|
||||
|
||||
LTMCanvasPicker *canvasPicker;
|
||||
// last plot settings - to avoid lots of uneeded recalcs
|
||||
RideItem *LASTrideItem;
|
||||
RideFileCache *LASTcache;
|
||||
RideFile::SeriesType LASTseries;
|
||||
bool LASTshade;
|
||||
bool LASTuseMetricUnits; // whether metric units are used (or imperial)
|
||||
bool LASTlny;
|
||||
bool LASTzoned; // show in zones
|
||||
int LASTbinw;
|
||||
bool LASTwithz; // whether zeros are included in histogram
|
||||
double LASTdt; // length of sample
|
||||
bool LASTabsolutetime; // do we sum absolute or percentage?
|
||||
};
|
||||
|
||||
/*----------------------------------------------------------------------
|
||||
* From here to the end of source file the routines for zone shading
|
||||
*--------------------------------------------------------------------*/
|
||||
|
||||
// define a background class to handle shading of power zones
|
||||
// draws power zone bands IF zones are defined and the option
|
||||
// to draw bonds has been selected
|
||||
class PowerHistBackground: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
PowerHist *parent;
|
||||
|
||||
public:
|
||||
PowerHistBackground(PowerHist *_parent)
|
||||
{
|
||||
setZ(0.0);
|
||||
parent = _parent;
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
virtual void draw(QPainter *painter,
|
||||
const QwtScaleMap &xMap, const QwtScaleMap &,
|
||||
const QRect &rect) const
|
||||
{
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
const Zones *zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
if (parent->shadeZones() && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = zones->getZoneLows(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
if (num_zones > 0) {
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
QRect r = rect;
|
||||
|
||||
QColor shading_color =
|
||||
zoneColor(z, num_zones);
|
||||
shading_color.setHsv(
|
||||
shading_color.hue(),
|
||||
shading_color.saturation() / 4,
|
||||
shading_color.value()
|
||||
);
|
||||
r.setLeft(xMap.transform(zone_lows[z]));
|
||||
if (z + 1 < num_zones)
|
||||
r.setRight(xMap.transform(zone_lows[z + 1]));
|
||||
if (r.right() >= r.left())
|
||||
painter->fillRect(r, shading_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Zone labels are drawn if power zone bands are enabled, automatically
|
||||
// at the center of the plot
|
||||
class PowerHistZoneLabel: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
PowerHist *parent;
|
||||
int zone_number;
|
||||
double watts;
|
||||
QwtText text;
|
||||
|
||||
public:
|
||||
PowerHistZoneLabel(PowerHist *_parent, int _zone_number)
|
||||
{
|
||||
parent = _parent;
|
||||
zone_number = _zone_number;
|
||||
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
const Zones *zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
setZ(1.0 + zone_number / 100.0);
|
||||
|
||||
// create new zone labels if we're shading
|
||||
if (parent->shadeZones() && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = zones->getZoneLows(zone_range);
|
||||
QList <QString> zone_names = zones->getZoneNames(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
assert(zone_names.size() == num_zones);
|
||||
if (zone_number < num_zones) {
|
||||
watts =
|
||||
(
|
||||
(zone_number + 1 < num_zones) ?
|
||||
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
|
||||
(
|
||||
(zone_number > 0) ?
|
||||
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
|
||||
2.0 * zone_lows[zone_number]
|
||||
)
|
||||
);
|
||||
|
||||
text = QwtText(zone_names[zone_number]);
|
||||
text.setFont(QFont("Helvetica",24, QFont::Bold));
|
||||
QColor text_color = zoneColor(zone_number, num_zones);
|
||||
text_color.setAlpha(64);
|
||||
text.setColor(text_color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
void draw(QPainter *painter,
|
||||
const QwtScaleMap &xMap, const QwtScaleMap &,
|
||||
const QRect &rect) const
|
||||
{
|
||||
if (parent->shadeZones()) {
|
||||
int x = xMap.transform(watts);
|
||||
int y = (rect.bottom() + rect.top()) / 2;
|
||||
|
||||
// the following code based on source for QwtPlotMarker::draw()
|
||||
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
|
||||
tr.moveCenter(QPoint(y, -x));
|
||||
painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed
|
||||
text.draw(painter, tr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// define a background class to handle shading of HR zones
|
||||
// draws power zone bands IF zones are defined and the option
|
||||
// to draw bonds has been selected
|
||||
class HrHistBackground: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
PowerHist *parent;
|
||||
|
||||
public:
|
||||
HrHistBackground(PowerHist *_parent)
|
||||
{
|
||||
setZ(0.0);
|
||||
parent = _parent;
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
virtual void draw(QPainter *painter,
|
||||
const QwtScaleMap &xMap, const QwtScaleMap &,
|
||||
const QRect &rect) const
|
||||
{
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
const HrZones *zones = parent->mainWindow->hrZones();
|
||||
int zone_range = rideItem->hrZoneRange();
|
||||
|
||||
if (parent->shadeHRZones() && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = zones->getZoneLows(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
if (num_zones > 0) {
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
QRect r = rect;
|
||||
|
||||
QColor shading_color =
|
||||
hrZoneColor(z, num_zones);
|
||||
shading_color.setHsv(
|
||||
shading_color.hue(),
|
||||
shading_color.saturation() / 4,
|
||||
shading_color.value()
|
||||
);
|
||||
r.setLeft(xMap.transform(zone_lows[z]));
|
||||
if (z + 1 < num_zones)
|
||||
r.setRight(xMap.transform(zone_lows[z + 1]));
|
||||
if (r.right() >= r.left())
|
||||
painter->fillRect(r, shading_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Zone labels are drawn if power zone bands are enabled, automatically
|
||||
// at the center of the plot
|
||||
class HrHistZoneLabel: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
PowerHist *parent;
|
||||
int zone_number;
|
||||
double watts;
|
||||
QwtText text;
|
||||
|
||||
public:
|
||||
HrHistZoneLabel(PowerHist *_parent, int _zone_number)
|
||||
{
|
||||
parent = _parent;
|
||||
zone_number = _zone_number;
|
||||
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
const HrZones *zones = parent->mainWindow->hrZones();
|
||||
int zone_range = rideItem->hrZoneRange();
|
||||
|
||||
setZ(1.0 + zone_number / 100.0);
|
||||
|
||||
// create new zone labels if we're shading
|
||||
if (parent->shadeHRZones() && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = zones->getZoneLows(zone_range);
|
||||
QList <QString> zone_names = zones->getZoneNames(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
assert(zone_names.size() == num_zones);
|
||||
if (zone_number < num_zones) {
|
||||
watts =
|
||||
(
|
||||
(zone_number + 1 < num_zones) ?
|
||||
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
|
||||
(
|
||||
(zone_number > 0) ?
|
||||
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
|
||||
2.0 * zone_lows[zone_number]
|
||||
)
|
||||
);
|
||||
|
||||
text = QwtText(zone_names[zone_number]);
|
||||
text.setFont(QFont("Helvetica",24, QFont::Bold));
|
||||
QColor text_color = hrZoneColor(zone_number, num_zones);
|
||||
text_color.setAlpha(64);
|
||||
text.setColor(text_color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
void draw(QPainter *painter,
|
||||
const QwtScaleMap &xMap, const QwtScaleMap &,
|
||||
const QRect &rect) const
|
||||
{
|
||||
if (parent->shadeHRZones()) {
|
||||
int x = xMap.transform(watts);
|
||||
int y = (rect.bottom() + rect.top()) / 2;
|
||||
|
||||
// the following code based on source for QwtPlotMarker::draw()
|
||||
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
|
||||
tr.moveCenter(QPoint(y, -x));
|
||||
painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed
|
||||
text.draw(painter, tr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // _GC_PowerHist_h
|
||||
|
||||
@@ -161,6 +161,37 @@ RideFileCache::meanMaxArray(RideFile::SeriesType series)
|
||||
}
|
||||
}
|
||||
|
||||
QVector<double> &
|
||||
RideFileCache::distributionArray(RideFile::SeriesType series)
|
||||
{
|
||||
switch (series) {
|
||||
|
||||
case RideFile::watts:
|
||||
return wattsDistributionDouble;
|
||||
break;
|
||||
|
||||
case RideFile::cad:
|
||||
return cadDistributionDouble;
|
||||
break;
|
||||
|
||||
case RideFile::hr:
|
||||
return hrDistributionDouble;
|
||||
break;
|
||||
|
||||
case RideFile::nm:
|
||||
return nmDistributionDouble;
|
||||
break;
|
||||
|
||||
case RideFile::kph:
|
||||
return kphDistributionDouble;
|
||||
break;
|
||||
|
||||
default:
|
||||
//? dunno give em power anyway
|
||||
return wattsMeanMaxDouble;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// COMPUTATION
|
||||
@@ -550,7 +581,7 @@ RideFileCache::computeDistribution(QVector<unsigned long> &array, RideFile::Seri
|
||||
unsigned long lvalue = value * pow(10, decimals);
|
||||
|
||||
int offset = lvalue - min;
|
||||
if (offset >= 0 && offset < array.size()) array[offset]++; // XXX recintsecs != 1
|
||||
if (offset >= 0 && offset < array.size()) array[offset] += ride->recIntSecs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,7 +615,8 @@ static void meanMaxAggregate(QVector<double> &into, QVector<double> &other, QVec
|
||||
// resize into and then sum the arrays
|
||||
static void distAggregate(QVector<double> &into, QVector<double> &other)
|
||||
{
|
||||
for (int i=0; i<into.size(); i++) into[i] += other[i];
|
||||
if (into.size() < other.size()) into.resize(other.size());
|
||||
for (int i=0; i<other.size(); i++) into[i] += other[i];
|
||||
}
|
||||
|
||||
RideFileCache::RideFileCache(MainWindow *main, QDate start, QDate end)
|
||||
|
||||
Reference in New Issue
Block a user