mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
... sync handling of (TM) in all places and Do Not allow translation for these terms any more, since it's a fixed term anyway - and there are many dependencies (which outside EN create unexpected results). - in all Texts/Fields use (TM) in HTML use the TM (special characters). -- more testing in language other then EN still required - but so far working fine ... and more tr()
511 lines
18 KiB
C++
511 lines
18 KiB
C++
/*
|
|
* Copyright (c) 2009 Eric Murray (ericm@lne.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 "ManualRideDialog.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "Settings.h"
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <QtGui>
|
|
#include <math.h>
|
|
#include "Units.h"
|
|
|
|
#include "MetricAggregator.h"
|
|
|
|
//
|
|
// Get the estimate factors
|
|
//
|
|
void
|
|
ManualRideDialog::deriveFactors()
|
|
{
|
|
// we already calculated for that day range
|
|
if (days->value() == daysago) return;
|
|
else daysago = days->value();
|
|
|
|
// working variables
|
|
timeKJ = distanceKJ = timeTSS = distanceTSS = timeBS = distanceBS = timeDP = distanceDP = 0.0;
|
|
|
|
// whats the most recent ride?
|
|
QList<SummaryMetrics> metrics = context->athlete->metricDB->getAllMetricsFor(QDateTime(), QDateTime());
|
|
|
|
// do we have any rides?
|
|
if (metrics.count()) {
|
|
|
|
// last 'n' days calculation
|
|
double seconds, distance, bs, dp, tss, kj;
|
|
seconds = distance = tss = kj = bs = dp = 0;
|
|
int rides = 0;
|
|
|
|
// fall back to 'all time' calculation
|
|
double totalseconds, totaldistance, totalbs, totaldp, totaltss, totalkj;
|
|
totalseconds = totaldistance = totaltss = totalkj = totalbs = totaldp = 0;
|
|
|
|
// just use the metricDB versions, nice 'n fast
|
|
foreach (SummaryMetrics metric, context->athlete->metricDB->getAllMetricsFor(QDateTime() , QDateTime())) {
|
|
|
|
// skip those with no time or distance values (not comparing doubles)
|
|
if (metric.getForSymbol("time_riding") == 0 || metric.getForSymbol("total_distance") == 0) continue;
|
|
|
|
// how many days ago was it?
|
|
int days = metric.getRideDate().daysTo(QDateTime::currentDateTime());
|
|
|
|
// only use rides in last 'n' days
|
|
if (days >= 0 && days < daysago) {
|
|
|
|
bs += metric.getForSymbol("skiba_bike_score");
|
|
seconds += metric.getForSymbol("time_riding");
|
|
distance += metric.getForSymbol("total_distance");
|
|
dp += metric.getForSymbol("daniels_points");
|
|
tss += metric.getForSymbol("coggan_tss");
|
|
kj += metric.getForSymbol("total_work");
|
|
|
|
rides++;
|
|
}
|
|
totalbs += metric.getForSymbol("skiba_bike_score");
|
|
totalseconds += metric.getForSymbol("time_riding");
|
|
totaldistance += metric.getForSymbol("total_distance");
|
|
totaldp += metric.getForSymbol("daniels_points");
|
|
totaltss += metric.getForSymbol("coggan_tss");
|
|
totalkj += metric.getForSymbol("total_work");
|
|
|
|
}
|
|
|
|
// total values, not just last 'n' days -- but avoid divide by zero
|
|
if (totalseconds && totaldistance) {
|
|
if (!context->athlete->useMetricUnits) totaldistance *= MILES_PER_KM;
|
|
timeBS = (totalbs * 3600) / totalseconds; // BS per hour
|
|
distanceBS = totalbs / totaldistance; // BS per mile or km
|
|
timeDP = (totaldp * 3600) / totalseconds; // DP per hour
|
|
distanceDP = totaldp / totaldistance; // DP per mile or km
|
|
timeTSS = (totaltss * 3600) / totalseconds; // DP per hour
|
|
distanceTSS = totaltss / totaldistance; // DP per mile or km
|
|
timeKJ = (totalkj * 3600) / totalseconds; // DP per hour
|
|
distanceKJ = totalkj / totaldistance; // DP per mile or km
|
|
}
|
|
|
|
// don't use defaults if we have rides in last 'n' days
|
|
if (rides) {
|
|
if (!context->athlete->useMetricUnits) distance *= MILES_PER_KM;
|
|
timeBS = (bs * 3600) / seconds; // BS per hour
|
|
distanceBS = bs / distance; // BS per mile or km
|
|
timeDP = (dp * 3600) / seconds; // DP per hour
|
|
distanceDP = dp / distance; // DP per mile or km
|
|
timeTSS = (tss * 3600) / seconds; // DP per hour
|
|
distanceTSS = tss / distance; // DP per mile or km
|
|
timeKJ = (kj * 3600) / seconds; // DP per hour
|
|
distanceKJ = kj / distance; // DP per mile or km
|
|
}
|
|
}
|
|
}
|
|
|
|
ManualRideDialog::ManualRideDialog(Context *context) : context(context)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
setWindowTitle(tr("Manual Ride Entry"));
|
|
#ifdef Q_OS_MAC
|
|
setFixedSize(610,415);
|
|
#else
|
|
setFixedSize(615,360);
|
|
#endif
|
|
|
|
// we haven't derived factors yet
|
|
daysago = -1;
|
|
|
|
//
|
|
// Create the GUI widgets
|
|
//
|
|
|
|
// BASIC DATA
|
|
|
|
// ride start date and start time
|
|
QLabel *dateLabel = new QLabel(tr("Ride date:"), this);
|
|
dateEdit = new QDateEdit(this);
|
|
dateEdit->setDate(QDate::currentDate());
|
|
|
|
QLabel *timeLabel = new QLabel(tr("Start time:"), this);
|
|
timeEdit = new QTimeEdit(this);
|
|
timeEdit->setDisplayFormat("hh:mm:ss");
|
|
timeEdit->setTime(QTime::currentTime().addSecs(-4 * 3600)); // 4 hours ago by default
|
|
|
|
// ride duration
|
|
QLabel *durationLabel = new QLabel(tr("Duration:"), this);
|
|
duration = new QTimeEdit(this);
|
|
duration->setDisplayFormat("hh:mm:ss");
|
|
|
|
// ride distance
|
|
QString distanceString = QString(tr("Distance (%1):")).arg(context->athlete->useMetricUnits ? "km" : "miles");
|
|
QLabel *distanceLabel = new QLabel(distanceString, this);
|
|
distance = new QDoubleSpinBox(this);
|
|
distance->setSingleStep(1.0);
|
|
distance->setDecimals(2);
|
|
distance->setMinimum(0);
|
|
distance->setMaximum(999);
|
|
|
|
QLabel *sportLabel = new QLabel(tr("Sport:"), this);
|
|
sport = new QLineEdit(this);
|
|
QLabel *wcodeLabel = new QLabel(tr("Workout Code:"), this);
|
|
wcode = new QLineEdit(this);
|
|
QLabel *notesLabel = new QLabel(tr("Notes:"), this);
|
|
notes = new QTextEdit(this);
|
|
|
|
// METRICS
|
|
|
|
// average heartrate
|
|
QLabel *avgBPMLabel = new QLabel(tr("Average HR:"), this);
|
|
avgBPM = new QDoubleSpinBox(this);
|
|
avgBPM->setSingleStep(1.0);
|
|
avgBPM->setDecimals(0);
|
|
avgBPM->setMinimum(0);
|
|
avgBPM->setMaximum(250);
|
|
|
|
// average power
|
|
QLabel *avgWattsLabel = new QLabel(tr("Average Watts:"), this);
|
|
avgWatts = new QDoubleSpinBox(this);
|
|
avgWatts->setSingleStep(1.0);
|
|
avgWatts->setDecimals(0);
|
|
avgWatts->setMinimum(0);
|
|
avgWatts->setMaximum(2500);
|
|
|
|
// average cadence
|
|
QLabel *avgRPMLabel = new QLabel(tr("Average Cadence:"), this);
|
|
avgRPM = new QDoubleSpinBox(this);
|
|
avgRPM->setSingleStep(1.0);
|
|
avgRPM->setDecimals(0);
|
|
avgRPM->setMinimum(0);
|
|
avgRPM->setMaximum(250);
|
|
|
|
// average speed
|
|
QLabel *avgKPHLabel = new QLabel(tr("Average Speed:"), this);
|
|
avgKPH = new QDoubleSpinBox(this);
|
|
avgKPH->setSingleStep(1.0);
|
|
avgKPH->setDecimals(0);
|
|
avgKPH->setMinimum(0);
|
|
avgKPH->setMaximum(100);
|
|
|
|
// how to estimate stress
|
|
QLabel *modeLabel = new QLabel(tr("Estmate Stress by:"), this);
|
|
byDuration = new QRadioButton(tr("Duration"));
|
|
byDistance = new QRadioButton(tr("Distance"));
|
|
byManual = new QRadioButton(tr("Manually"));
|
|
|
|
days = new QDoubleSpinBox(this);
|
|
days->setSingleStep(1.0);
|
|
days->setDecimals(0);
|
|
days->setMinimum(0);
|
|
days->setMaximum(999);
|
|
days->setValue(appsettings->value(this, GC_BIKESCOREDAYS, "30").toInt());
|
|
QLabel *dayLabel = new QLabel(tr("Estimate Stress days:"), this);
|
|
|
|
// Restore from last time
|
|
QVariant BSmode = appsettings->value(this, GC_BIKESCOREMODE); // remember from before
|
|
if (BSmode.toString() == "time") byDuration->setChecked(true);
|
|
else byDistance->setChecked(true);
|
|
|
|
// Derived metrics
|
|
QLabel *BSLabel = new QLabel("BikeScore (TM): ", this);
|
|
BS = new QDoubleSpinBox(this);
|
|
BS->setSingleStep(1.0);
|
|
BS->setDecimals(0);
|
|
BS->setMinimum(0);
|
|
BS->setMaximum(999);
|
|
|
|
QLabel *DPLabel = new QLabel(tr("Daniel Points: "), this);
|
|
DP = new QDoubleSpinBox(this);
|
|
DP->setSingleStep(1.0);
|
|
DP->setDecimals(0);
|
|
DP->setMinimum(0);
|
|
DP->setMaximum(999);
|
|
|
|
QLabel *TSSLabel = new QLabel(tr("TSS: "), this);
|
|
TSS = new QDoubleSpinBox(this);
|
|
TSS->setSingleStep(1.0);
|
|
TSS->setDecimals(0);
|
|
TSS->setMinimum(0);
|
|
TSS->setMaximum(999);
|
|
|
|
QLabel *KJLabel = new QLabel(tr("Work (KJ):"), this);
|
|
KJ = new QDoubleSpinBox(this);
|
|
KJ->setSingleStep(1.0);
|
|
KJ->setDecimals(0);
|
|
KJ->setMinimum(0);
|
|
KJ->setMaximum(9999);
|
|
|
|
// buttons
|
|
okButton = new QPushButton(tr("&OK"), this);
|
|
cancelButton = new QPushButton(tr("&Cancel"), this);
|
|
|
|
// save by default -- we don't overwrite and
|
|
// the user will expect enter to save file
|
|
okButton->setDefault(true);
|
|
cancelButton->setDefault(false);
|
|
|
|
//
|
|
// LAY OUT THE GUI
|
|
//
|
|
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
|
QHBoxLayout *squeeze = new QHBoxLayout;
|
|
mainLayout->addLayout(squeeze);
|
|
QGridLayout *basicLayout = new QGridLayout;
|
|
squeeze->addLayout(basicLayout);
|
|
QGridLayout *notesLayout = new QGridLayout;
|
|
squeeze->addLayout(notesLayout);
|
|
|
|
notesLayout->addWidget(wcodeLabel, 0,0, Qt::AlignLeft);
|
|
notesLayout->addWidget(wcode, 0,1, Qt::AlignLeft);
|
|
notesLayout->addWidget(sportLabel, 1,0, Qt::AlignLeft);
|
|
notesLayout->addWidget(sport, 1,1, Qt::AlignLeft);
|
|
notesLayout->addWidget(notesLabel, 2,0, Qt::AlignTop|Qt::AlignLeft);
|
|
notesLayout->addWidget(notes, 2,1, Qt::AlignLeft);
|
|
|
|
basicLayout->addWidget(dateLabel, 0,0,Qt::AlignLeft);
|
|
basicLayout->addWidget(dateEdit, 0,1,Qt::AlignLeft);
|
|
basicLayout->addWidget(timeLabel, 1,0,Qt::AlignLeft);
|
|
basicLayout->addWidget(timeEdit, 1,1,Qt::AlignLeft);
|
|
basicLayout->addWidget(durationLabel, 2,0,Qt::AlignLeft);
|
|
basicLayout->addWidget(duration, 2,1, Qt::AlignLeft);
|
|
basicLayout->addWidget(distanceLabel, 3,0, Qt::AlignLeft);
|
|
basicLayout->addWidget(distance, 3,1, Qt::AlignLeft);
|
|
|
|
QGroupBox *metricBox = new QGroupBox(tr("Metrics"),this);
|
|
QGridLayout *metricLayout = new QGridLayout;
|
|
metricBox->setLayout(metricLayout);
|
|
mainLayout->addWidget(metricBox);
|
|
|
|
metricLayout->addWidget(avgBPMLabel, 0,0,Qt::AlignLeft);
|
|
metricLayout->addWidget(avgBPM, 0,1,Qt::AlignLeft);
|
|
metricLayout->addWidget(avgWattsLabel, 1,0,Qt::AlignLeft);
|
|
metricLayout->addWidget(avgWatts, 1,1,Qt::AlignLeft);
|
|
metricLayout->addWidget(avgRPMLabel, 2,0,Qt::AlignLeft);
|
|
metricLayout->addWidget(avgRPM, 2,1,Qt::AlignLeft);
|
|
metricLayout->addWidget(avgKPHLabel, 3,0,Qt::AlignLeft);
|
|
metricLayout->addWidget(avgKPH, 3,1,Qt::AlignLeft);
|
|
|
|
QHBoxLayout *estimateby = new QHBoxLayout;
|
|
estimateby->addWidget(modeLabel);
|
|
estimateby->addWidget(byDuration);
|
|
estimateby->addWidget(byDistance);
|
|
estimateby->addWidget(byManual);
|
|
metricLayout->addLayout(estimateby, 0,2,1,-1,Qt::AlignLeft);
|
|
|
|
|
|
QHBoxLayout *daysLayout = new QHBoxLayout;
|
|
daysLayout->addWidget(dayLabel);
|
|
daysLayout->addWidget(days);
|
|
metricLayout->addLayout(daysLayout, 1,2,1,-1,Qt::AlignLeft);
|
|
|
|
metricLayout->addWidget(TSSLabel, 2,2, Qt::AlignLeft);
|
|
metricLayout->addWidget(TSS, 2,3, Qt::AlignLeft);
|
|
metricLayout->addWidget(KJLabel, 2,4, Qt::AlignLeft);
|
|
metricLayout->addWidget(KJ, 2,5, Qt::AlignLeft);
|
|
metricLayout->addWidget(BSLabel, 3,2, Qt::AlignLeft);
|
|
metricLayout->addWidget(BS, 3,3, Qt::AlignLeft);
|
|
metricLayout->addWidget(DPLabel, 3,4, Qt::AlignLeft);
|
|
metricLayout->addWidget(DP, 3,5, Qt::AlignLeft);
|
|
|
|
QHBoxLayout *buttons = new QHBoxLayout;
|
|
mainLayout->addLayout(buttons);
|
|
buttons->addStretch();
|
|
buttons->addWidget(okButton);
|
|
buttons->addWidget(cancelButton);
|
|
|
|
// if any of the fields used to determine estimation are changed then
|
|
// lets re-calculate and apply
|
|
connect(distance, SIGNAL(valueChanged(double)), this, SLOT(estimate()));
|
|
connect(duration, SIGNAL(timeChanged(QTime)), this, SLOT(estimate()));
|
|
connect(byDistance, SIGNAL(toggled(bool)), this, SLOT(estimate()));
|
|
connect(byDuration, SIGNAL(toggled(bool)), this, SLOT(estimate()));
|
|
connect(byManual, SIGNAL(toggled(bool)), this, SLOT(estimate()));
|
|
connect(days, SIGNAL(valueChanged(double)), this, SLOT(estimate()));
|
|
|
|
// dialog buttons
|
|
connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked()));
|
|
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
|
|
|
|
// initialise estimates / widgets enabled
|
|
estimate();
|
|
}
|
|
|
|
void
|
|
ManualRideDialog::estimate()
|
|
{
|
|
if (byManual->isChecked()) {
|
|
BS->setEnabled(true);
|
|
DP->setEnabled(true);
|
|
TSS->setEnabled(true);
|
|
KJ->setEnabled(true);
|
|
|
|
return; // no calculation - manually entered by user
|
|
} else {
|
|
|
|
BS->setEnabled(false);
|
|
DP->setEnabled(false);
|
|
TSS->setEnabled(false);
|
|
KJ->setEnabled(false);
|
|
}
|
|
|
|
deriveFactors(); // if days changed..
|
|
|
|
if (byDuration->isChecked()) {
|
|
// by time
|
|
QTime time = duration->time();
|
|
double hours = (time.hour()) + (time.minute() / 60.00) + (time.second() / 3600.00);
|
|
|
|
BS->setValue(hours * timeBS);
|
|
DP->setValue(hours * timeDP);
|
|
TSS->setValue(hours * timeTSS);
|
|
KJ->setValue(hours * timeKJ);
|
|
|
|
} else {
|
|
// by distance
|
|
double dist = distance->value();
|
|
|
|
BS->setValue(dist * distanceBS);
|
|
DP->setValue(dist * distanceDP);
|
|
TSS->setValue(dist * distanceTSS);
|
|
KJ->setValue(dist * distanceKJ);
|
|
}
|
|
}
|
|
|
|
void
|
|
ManualRideDialog::cancelClicked()
|
|
{
|
|
reject();
|
|
}
|
|
|
|
void
|
|
ManualRideDialog::okClicked()
|
|
{
|
|
// remember parameters
|
|
appsettings->setValue(GC_BIKESCOREDAYS, days->value());
|
|
appsettings->setValue(GC_BIKESCOREMODE,
|
|
byDistance->isChecked() ? "dist" :
|
|
(byDuration->isChecked() ? "time" : "manual"));
|
|
|
|
// create a new ridefile
|
|
RideFile *rideFile = new RideFile();
|
|
|
|
// set the first class variables
|
|
QDateTime rideDateTime = QDateTime(dateEdit->date(), timeEdit->time());
|
|
rideFile->setStartTime(rideDateTime);
|
|
rideFile->setRecIntSecs(0.00);
|
|
rideFile->setDeviceType("Manual");
|
|
rideFile->setFileFormat("GoldenCheetah Json");
|
|
|
|
// basic data
|
|
if (distance->value()) {
|
|
QMap<QString,QString> override;
|
|
if (!context->athlete->useMetricUnits) {
|
|
override.insert("value", QString("%1").arg(distance->value() * KM_PER_MILE));
|
|
}else{
|
|
override.insert("value", QString("%1").arg(distance->value()));
|
|
}
|
|
rideFile->metricOverrides.insert("total_distance", override);
|
|
}
|
|
|
|
QTime time = duration->time();
|
|
double seconds = (time.hour() * 3600) + (time.minute() * 60.00) + (time.second());
|
|
if (seconds) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(seconds));
|
|
rideFile->metricOverrides.insert("workout_time", override);
|
|
rideFile->metricOverrides.insert("time_riding", override);
|
|
}
|
|
|
|
// basic metadata
|
|
rideFile->setTag("Sport", sport->text());
|
|
rideFile->setTag("Workout Code", wcode->text());
|
|
rideFile->setTag("Notes", notes->toPlainText());
|
|
|
|
// average metrics
|
|
if (avgBPM->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(avgBPM->value()));
|
|
rideFile->metricOverrides.insert("average_hr", override);
|
|
}
|
|
if (avgRPM->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(avgRPM->value()));
|
|
rideFile->metricOverrides.insert("average_cad", override);
|
|
}
|
|
if (avgKPH->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(avgKPH->value()));
|
|
rideFile->metricOverrides.insert("average_speed", override);
|
|
}
|
|
if (avgWatts->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(avgWatts->value()));
|
|
rideFile->metricOverrides.insert("average_power", override);
|
|
}
|
|
|
|
// stress metrics
|
|
if (BS->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(BS->value()));
|
|
rideFile->metricOverrides.insert("skiba_bike_score", override);
|
|
}
|
|
if (DP->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(DP->value()));
|
|
rideFile->metricOverrides.insert("daniels_points", override);
|
|
}
|
|
if (TSS->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(TSS->value()));
|
|
rideFile->metricOverrides.insert("coggan_tss", override);
|
|
}
|
|
if (KJ->value()) {
|
|
QMap<QString,QString> override;
|
|
override.insert("value", QString("%1").arg(KJ->value()));
|
|
rideFile->metricOverrides.insert("total_work", override);
|
|
}
|
|
|
|
// what should the filename be?
|
|
QChar zero = QLatin1Char ('0');
|
|
QString basename = QString ("%1_%2_%3_%4_%5_%6")
|
|
.arg (rideDateTime.date().year(), 4, 10, zero)
|
|
.arg (rideDateTime.date().month(), 2, 10, zero)
|
|
.arg (rideDateTime.date().day(), 2, 10, zero)
|
|
.arg (rideDateTime.time().hour(), 2, 10, zero)
|
|
.arg (rideDateTime.time().minute(), 2, 10, zero)
|
|
.arg (rideDateTime.time().second(), 2, 10, zero);
|
|
|
|
QString filename = context->athlete->home.absolutePath() + "/" + basename + ".json";
|
|
|
|
QFile out(filename);
|
|
bool success = RideFileFactory::instance().writeRideFile(context, rideFile, out, "json");
|
|
delete rideFile;
|
|
|
|
if (success) {
|
|
|
|
// refresh metric db etc
|
|
context->athlete->addRide(basename + ".json");
|
|
accept();
|
|
|
|
} else {
|
|
|
|
// rather than dance around renaming existing rides, this time we will let the user
|
|
// work it out -- they may actually want to keep an existing ride, so we shouldn't
|
|
// rename it silently.
|
|
QMessageBox oops(QMessageBox::Critical, tr("Unable to save"),
|
|
tr("There is already an ride with the same start time or you do not have permissions to save a file."));
|
|
oops.exec();
|
|
}
|
|
}
|