From ca4278f90429a1aaeef80f04893a64d8058864f9 Mon Sep 17 00:00:00 2001 From: Justin Knotzke Date: Fri, 7 Aug 2009 21:25:22 -0400 Subject: [PATCH] Manual patch entry by Eric Murray. Users can now enter in a manual entry based on distance or time. --- src/ManualRideDialog.cpp | 356 +++++++++++++++++++++++++++++++++++++++ src/ManualRideDialog.h | 67 ++++++++ src/ManualRideFile.cpp | 139 +++++++++++++++ src/ManualRideFile.h | 29 ++++ 4 files changed, 591 insertions(+) create mode 100644 src/ManualRideDialog.cpp create mode 100644 src/ManualRideDialog.h create mode 100644 src/ManualRideFile.cpp create mode 100644 src/ManualRideFile.h diff --git a/src/ManualRideDialog.cpp b/src/ManualRideDialog.cpp new file mode 100644 index 000000000..c8986edfb --- /dev/null +++ b/src/ManualRideDialog.cpp @@ -0,0 +1,356 @@ +/* + * $Id:$ + * + * 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 "MainWindow.h" +#include "Settings.h" +#include +#include +#include +#include +#include +#include + + +ManualRideDialog::ManualRideDialog(MainWindow *mainWindow, + const QDir &home, bool useMetric) : + mainWindow(mainWindow), home(home) +{ + useMetricUnits = useMetric; + int row; + + + mainWindow->getBSFactors(timeBS,distanceBS); + + + setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle(tr("Manually Enter Ride Data")); + + // ride date + QLabel *manualDateLabel = new QLabel(tr("Ride date: "), this); + dateTimeEdit = new QDateTimeEdit( QDateTime::currentDateTime(), this ); + // Wed 6/24/09 6:55 AM + dateTimeEdit->setDisplayFormat(tr("ddd MMM d, yyyy h:mm AP")); + + // ride length + QLabel *manualLengthLabel = new QLabel(tr("Ride length: "), this); + QHBoxLayout *manualLengthLayout = new QHBoxLayout; + hrslbl = new QLabel(tr("hours"),this); + hrslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken); + hrsentry = new QLineEdit(this); + hrsentry->setInputMask("00"); + manualLengthLayout->addWidget(hrslbl); + manualLengthLayout->addWidget(hrsentry); + + minslbl = new QLabel(tr("mins"),this); + minslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken); + minsentry = new QLineEdit(this); + minsentry->setInputMask("00"); + manualLengthLayout->addWidget(minslbl); + manualLengthLayout->addWidget(minsentry); + + secslbl = new QLabel(tr("secs"),this); + secslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken); + secsentry = new QLineEdit(this); + secsentry->setInputMask("00"); + manualLengthLayout->addWidget(secslbl); + manualLengthLayout->addWidget(secsentry); + + // ride distance + QString *DistanceString = new QString(tr("Distance ")); + if (useMetricUnits) + DistanceString->append("(" + tr("km") + "):"); + else + DistanceString->append("(" + tr("miles") + "):"); + + QLabel *DistanceLabel = new QLabel(*DistanceString, this); + distanceentry = new QLineEdit(this); + distanceentry->setInputMask("009.00"); + + // AvgHR + QLabel *HRLabel = new QLabel(tr("Average HR: "), this); + HRentry = new QLineEdit(this); + HRentry->setInputMask("099"); + + // how to estimate BikeScore: + QLabel *BSEstLabel; + QSettings settings(GC_SETTINGS_CO, GC_SETTINGS_APP); + QVariant BSmode = settings.value(GC_BIKESCOREMODE); + + + estBSbyTimeButton = NULL; + estBSbyDistButton = NULL; + if (timeBS || distanceBS) { + BSEstLabel = new QLabel(tr("Estimate BikeScore by: ")); + if (timeBS) { + estBSbyTimeButton = new QRadioButton(tr("Time")); + // default to time based unless no timeBS factor + if (BSmode.toString() != "distance") + estBSbyTimeButton->setDown(true); + } + if (distanceBS) { + estBSbyDistButton = new QRadioButton(tr("Distance")); + if (BSmode.toString() == "distance" || ! timeBS) + estBSbyDistButton->setDown(true); + } + } + + // BikeScore + QLabel *ManualBSLabel = new QLabel(tr("BikeScore: "), this); + BSentry = new QLineEdit(this); + BSentry->setInputMask("009"); + + // buttons + enterButton = new QPushButton(tr("&OK"), this); + cancelButton = new QPushButton(tr("&Cancel"), this); + + // Set up the layout: + QGridLayout *glayout = new QGridLayout(this); + row = 0; + glayout->addWidget(manualDateLabel, row, 0); + glayout->addWidget(dateTimeEdit, row, 1, 1, -1); + row++; + + glayout->addWidget(manualLengthLabel, row, 0); + glayout->addLayout(manualLengthLayout,row,1,1,-1); + row++; + + glayout->addWidget(DistanceLabel,row,0); + glayout->addWidget(distanceentry,row,1,1,-1); + row++; + + glayout->addWidget(HRLabel,row,0); + glayout->addWidget(HRentry,row,1,1,-1); + row++; + + if (timeBS || distanceBS) { + glayout->addWidget(BSEstLabel,row,0); + if (estBSbyTimeButton) + glayout->addWidget(estBSbyTimeButton,row,1,1,-1); + if (estBSbyDistButton) + glayout->addWidget(estBSbyDistButton,row,2,1,-1); + row++; + } + + glayout->addWidget(ManualBSLabel,row,0); + glayout->addWidget(BSentry,row,1,1,-1); + row++; + + glayout->addWidget(enterButton,row,1); + glayout->addWidget(cancelButton,row,2); + + this->resize(QSize(400,275)); + + connect(enterButton, SIGNAL(clicked()), this, SLOT(enterClicked())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); + connect(hrsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst())); + //connect(secsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst())); + connect(minsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst())); + connect(distanceentry, SIGNAL(editingFinished()), this, SLOT(setBsEst())); + if (estBSbyTimeButton) + connect(estBSbyTimeButton,SIGNAL(clicked()),this, SLOT(bsEstChanged())); + if (estBSbyDistButton) + connect(estBSbyDistButton,SIGNAL(clicked()),this, SLOT(bsEstChanged())); +} + +void +ManualRideDialog::estBSFromDistance() +{ + // calculate distance-based BS estimate + double bs = 0; + bs = distanceentry->text().toFloat() * distanceBS; + QString text = QString("%1").arg((int)bs); + // cast to int so QLineEdit doesn't interpret "51.3" as "513" + BSentry->clear(); + BSentry->insert(text); +} + + +void +ManualRideDialog::estBSFromTime() +{ + // calculate time-based BS estimate + double bs = 0; + bs = ((hrsentry->text().toInt() ) + + (minsentry->text().toInt() / 60) + + (secsentry->text().toInt()/ 3600)) * timeBS; + QString text = QString("%1").arg((int)bs); + BSentry->clear(); + BSentry->insert(text); + +} + + +void +ManualRideDialog::bsEstChanged() +{ + if (estBSbyDistButton->isChecked()) { + estBSFromDistance(); + } + else { + estBSFromTime(); + } +} + +void +ManualRideDialog::setBsEst() +{ + if (estBSbyDistButton->isChecked()) { + estBSFromDistance(); + } + else { + estBSFromTime(); + } +} + + +void +ManualRideDialog::cancelClicked() +{ + reject(); +} + +void +ManualRideDialog::enterClicked() +{ + // write data to manual entry file + + if (filename == "") { + char tmp[32]; + + // use user's time for file: + QDateTime lt = dateTimeEdit->dateTime().toLocalTime(); + + sprintf(tmp, "%04d_%02d_%02d_%02d_%02d_%02d.man", + lt.date().year(), lt.date().month(), + lt.date().day(), lt.time().hour(), + lt.time().minute(), + lt.time().second()); + + filename = tmp; + filepath = home.absolutePath() + "/" + filename; + FILE *out = fopen(filepath.toAscii().constData(), "r"); + if (out) { + fclose(out); + if (QMessageBox::warning( + this, + tr("Ride Already Downloaded"), + tr("This ride appears to have already ") + + tr("been downloaded. Do you want to ") + + tr("download it again and overwrite ") + + tr("the previous download?"), + tr("&Overwrite"), tr("&Cancel"), + QString(), 1, 1) == 1) { + reject(); + return ; + } + } + } + + QString tmpname; + { + // QTemporaryFile doesn't actually close the file on .close(); it + // closes the file when in its destructor. On Windows, we can't + // rename an open file. So let tmp go out of scope before calling + // rename. + + QString tmpl = home.absoluteFilePath(".ptdl.XXXXXX"); + QTemporaryFile tmp(tmpl); + tmp.setAutoRemove(false); + if (!tmp.open()) { + QMessageBox::critical(this, tr("Error"), + tr("Failed to create temporary file ") + + tmpl + ": " + tmp.error()); + reject(); + return; + } + QTextStream out(&tmp); + + tmpname = tmp.fileName(); // after close(), tmp.fileName() is "" + + /* + * File format: + * "manual" + * "minutes,mph,watts,miles,hr,bikescore" # header (metric or imperial) + * minutes,mph,watts,miles,hr,bikeScore # data + */ + + out << "manual\n"; + if (useMetricUnits) + out << "minutes,kmh,watts,km,hr,bikescore\n"; + else + out << "minutes,mph,watts,miles,hr,bikescore\n"; + + // data + double secs = (hrsentry->text().toInt() * 3600) + + (minsentry->text().toInt() * 60) + + (secsentry->text().toInt()); + out << secs/60.0; + out << ","; + out << distanceentry->text().toFloat() / (secs / 3600.0); + out << ","; + out << 0.0; // watts + out << ","; + out << distanceentry->text().toFloat(); + out << ","; + out << HRentry->text().toInt(); + out << ","; + out << BSentry->text().toInt(); + out << "\n"; + + + tmp.close(); + + // QTemporaryFile initially has permissions set to 0600. + // Make it readable by everyone. + tmp.setPermissions(tmp.permissions() + | QFile::ReadOwner | QFile::ReadUser + | QFile::ReadGroup | QFile::ReadOther); + } + +#ifdef __WIN32__ + // Windows ::rename won't overwrite an existing file. + if (QFile::exists(filepath)) { + QFile old(filepath); + if (!old.remove()) { + QMessageBox::critical(this, tr("Error"), + tr("Failed to remove existing file ") + + filepath + ": " + old.error()); + QFile::remove(tmpname); + reject(); + } + } +#endif + + // Use ::rename() instead of QFile::rename() to get forced overwrite. + if (rename(QFile::encodeName(tmpname), QFile::encodeName(filepath)) < 0) { + QMessageBox::critical(this, tr("Error"), + tr("Failed to rename ") + tmpname + tr(" to ") + + filepath + ": " + strerror(errno)); + QFile::remove(tmpname); + reject(); + return; + } + + mainWindow->addRide(filename); + accept(); +} + + diff --git a/src/ManualRideDialog.h b/src/ManualRideDialog.h new file mode 100644 index 000000000..f56603a4c --- /dev/null +++ b/src/ManualRideDialog.h @@ -0,0 +1,67 @@ +/* + * 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 + */ + +#ifndef _GC_ManualRideDialog_h +#define _GC_ManualRideDialog_h 1 + +#include +#include +#include "PowerTap.h" + +class MainWindow; + +class ManualRideDialog : public QDialog +{ + Q_OBJECT + + public: + ManualRideDialog(MainWindow *mainWindow, const QDir &home, + bool useMetric); + + private slots: + void enterClicked(); + void cancelClicked(); + void bsEstChanged(); + void setBsEst(); + + private: + + void estBSFromDistance(); + void estBSFromTime(); + + bool useMetricUnits; + float timeBS, distanceBS; + MainWindow *mainWindow; + QDir home; + QPushButton *enterButton, *cancelButton; + QLabel *label; + + QLabel *hrslbl, *minslbl, *secslbl; + QLineEdit *hrsentry, *minsentry, *secsentry; + QLabel * distancelbl; + QLineEdit *distanceentry; + QLineEdit *HRentry, *BSentry; + QDateTimeEdit *dateTimeEdit; + QRadioButton *estBSbyTimeButton, *estBSbyDistButton; + + QVector records; + QString filename, filepath; +}; + +#endif // _GC_ManualRideDialog_h + diff --git a/src/ManualRideFile.cpp b/src/ManualRideFile.cpp new file mode 100644 index 000000000..3bc410351 --- /dev/null +++ b/src/ManualRideFile.cpp @@ -0,0 +1,139 @@ +/* + * 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 "ManualRideFile.h" +#include +#include +#include // for std::sort +#include +#include "math.h" + +#define MILES_TO_KM 1.609344 +#define FEET_TO_METERS 0.3048 + +static int manualFileReaderRegistered = + RideFileFactory::instance().registerReader("man", new ManualFileReader()); + +RideFile *ManualFileReader::openRideFile(QFile &file, QStringList &errors) const +{ + QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive); + QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive); + bool metric; + + int unitsHeader = 2; + + /* + * File format: + * "manual" + * "minutes,mph,watts,miles,hr,bikescore" # header (metric or imperial) + * minutes,mph,watts,miles,hr,bikeScore # data + */ + QRegExp manualCSV("manual", Qt::CaseInsensitive); + bool manual = false; + + double rideSec; + + if (!file.open(QFile::ReadOnly)) { + errors << ("Could not open ride file: \"" + + file.fileName() + "\""); + return NULL; + } + int lineno = 1; + QTextStream is(&file); + RideFile *rideFile = new RideFile(); + while (!is.atEnd()) { + // the readLine() method doesn't handle old Macintosh CR line endings + // this workaround will load the the entire file if it has CR endings + // then split and loop through each line + // otherwise, there will be nothing to split and it will read each line as expected. + QString linesIn = is.readLine(); + QStringList lines = linesIn.split('\r'); + // workaround for empty lines + if(lines.isEmpty()) { + lineno++; + continue; + } + for (int li = 0; li < lines.size(); ++li) { + QString line = lines[li]; + + if (lineno == 1) { + if (manualCSV.indexIn(line) != -1) { + manual = true; + rideFile->setDeviceType("Manual CSV"); + ++lineno; + continue; + } + } + else if (lineno == unitsHeader) { + if (metricUnits.indexIn(line) != -1) + metric = true; + else if (englishUnits.indexIn(line) != -1) + metric = false; + else { + errors << ("Can't find units in first line: \"" + line + "\""); + delete rideFile; + file.close(); + return NULL; + } + ++lineno; + continue; + } + // minutes,kph,watts,km,hr,bikeScore + else if (lineno > unitsHeader) { + double minutes,kph,watts,km,hr,bs; + double cad, nm; + int interval; + minutes = line.section(',', 0, 0).toDouble(); + kph = line.section(',', 1, 1).toDouble(); + watts = line.section(',', 2, 2).toDouble(); + km = line.section(',', 3, 3).toDouble(); + hr = line.section(',', 4, 4).toDouble(); + bs = line.section(',', 5, 5).toDouble(); + if (!metric) { + km *= MILES_TO_KM; + kph *= MILES_TO_KM; + } + cad = nm = 0.0; + interval = 0; + + rideFile->appendPoint(minutes * 60.0, cad, hr, km, + kph, nm, watts, interval, bs); + + rideSec = minutes * 60.0; + } + ++lineno; + } + } + // fix recording interval at ride length: + rideFile->setRecIntSecs(rideSec); + + QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_" + "(\\d\\d)_(\\d\\d)_(\\d\\d)\\.csv$"); + if (rideTime.indexIn(file.fileName()) >= 0) { + QDateTime datetime(QDate(rideTime.cap(1).toInt(), + rideTime.cap(2).toInt(), + rideTime.cap(3).toInt()), + QTime(rideTime.cap(4).toInt(), + rideTime.cap(5).toInt(), + rideTime.cap(6).toInt())); + rideFile->setStartTime(datetime); + } + file.close(); + return rideFile; +} + diff --git a/src/ManualRideFile.h b/src/ManualRideFile.h new file mode 100644 index 000000000..209d05551 --- /dev/null +++ b/src/ManualRideFile.h @@ -0,0 +1,29 @@ +/* + * 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 + */ + +#ifndef _ManualRideFile_h +#define _ManualRideFile_h + +#include "RideFile.h" + +struct ManualFileReader : public RideFileReader { + virtual RideFile *openRideFile(QFile &file, QStringList &errors) const; +}; + +#endif // _ManualRideFile_h +