/* * Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net) * 2009 Justin F. Knotzke (jknotzke@shampoo.ca) * * 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 "RideFile.h" #include "DataProcessor.h" #include "RideMetadata.h" #include "Settings.h" #include "Units.h" #include #include // for std::lower_bound #include #define mark() \ { \ addInterval(start, previous->secs + recIntSecs_, \ QString("%1").arg(interval)); \ interval = point->interval; \ start = point->secs; \ } #define tr(s) QObject::tr(s) RideFile::RideFile(const QDateTime &startTime, double recIntSecs) : startTime_(startTime), recIntSecs_(recIntSecs), deviceType_("unknown"), data(NULL) { command = new RideFileCommand(this); } RideFile::RideFile() : recIntSecs_(0.0), deviceType_("unknown"), data(NULL) { command = new RideFileCommand(this); } RideFile::~RideFile() { emit deleted(); foreach(RideFilePoint *point, dataPoints_) delete point; delete command; } QString RideFile::seriesName(SeriesType series) { switch (series) { case RideFile::secs: return QString(tr("Time")); case RideFile::cad: return QString(tr("Cadence")); case RideFile::hr: return QString(tr("Heartrate")); case RideFile::km: return QString(tr("Distance")); case RideFile::kph: return QString(tr("Speed")); case RideFile::nm: return QString(tr("Torque")); case RideFile::watts: return QString(tr("Power")); case RideFile::alt: return QString(tr("Altitude")); case RideFile::lon: return QString(tr("Longitude")); case RideFile::lat: return QString(tr("Latitude")); case RideFile::headwind: return QString(tr("Headwind")); case RideFile::interval: return QString(tr("Interval")); default: return QString(tr("Unknown")); } } void RideFile::clearIntervals() { intervals_.clear(); } void RideFile::fillInIntervals() { if (dataPoints_.empty()) return; intervals_.clear(); double start = 0.0; int interval = dataPoints().first()->interval; const RideFilePoint *point, *previous; foreach (point, dataPoints()) { if (point->interval != interval) mark(); previous = point; } if (interval > 0) mark(); } struct ComparePointKm { bool operator()(const RideFilePoint *p1, const RideFilePoint *p2) { return p1->km < p2->km; } }; struct ComparePointSecs { bool operator()(const RideFilePoint *p1, const RideFilePoint *p2) { return p1->secs < p2->secs; } }; int RideFile::intervalBegin(const RideFileInterval &interval) const { RideFilePoint p; p.secs = interval.start; QVector::const_iterator i = std::lower_bound( dataPoints_.begin(), dataPoints_.end(), &p, ComparePointSecs()); if (i == dataPoints_.end()) return dataPoints_.size(); return i - dataPoints_.begin(); } double RideFile::timeToDistance(double secs) const { RideFilePoint p; p.secs = secs; // Check we have some data and the secs is in bounds if (dataPoints_.isEmpty()) return 0; if (secs < dataPoints_.first()->secs) return dataPoints_.first()->km; if (secs > dataPoints_.last()->secs) return dataPoints_.last()->km; QVector::const_iterator i = std::lower_bound(dataPoints_.begin(), dataPoints_.end(), &p, ComparePointSecs()); return (*i)->km; } int RideFile::timeIndex(double secs) const { // return index offset for specified time RideFilePoint p; p.secs = secs; QVector::const_iterator i = std::lower_bound( dataPoints_.begin(), dataPoints_.end(), &p, ComparePointSecs()); if (i == dataPoints_.end()) return dataPoints_.size(); return i - dataPoints_.begin(); } int RideFile::distanceIndex(double km) const { // return index offset for specified distance in km RideFilePoint p; p.km = km; QVector::const_iterator i = std::lower_bound( dataPoints_.begin(), dataPoints_.end(), &p, ComparePointKm()); if (i == dataPoints_.end()) return dataPoints_.size(); return i - dataPoints_.begin(); } void RideFile::writeAsCsv(QFile &file, bool bIsMetric) const { // Use the column headers that make WKO+ happy. double convertUnit; QTextStream out(&file); if (!bIsMetric) { out << "Minutes,Torq (N-m),MPH,Watts,Miles,Cadence,Hrate,ID,Altitude (feet)\n"; convertUnit = MILES_PER_KM; } else { out << "Minutes,Torq (N-m),Km/h,Watts,Km,Cadence,Hrate,ID,Altitude (m)\n"; convertUnit = 1.0; } foreach (const RideFilePoint *point, dataPoints()) { if (point->secs == 0.0) continue; out << point->secs/60.0; out << ","; out << ((point->nm >= 0) ? point->nm : 0.0); out << ","; out << ((point->kph >= 0) ? (point->kph * convertUnit) : 0.0); out << ","; out << ((point->watts >= 0) ? point->watts : 0.0); out << ","; out << point->km * convertUnit; out << ","; out << point->cad; out << ","; out << point->hr; out << ","; out << point->interval; out << ","; out << point->alt; out << "\n"; } file.close(); } RideFileFactory *RideFileFactory::instance_; RideFileFactory &RideFileFactory::instance() { if (!instance_) instance_ = new RideFileFactory(); return *instance_; } int RideFileFactory::registerReader(const QString &suffix, const QString &description, RideFileReader *reader) { assert(!readFuncs_.contains(suffix)); readFuncs_.insert(suffix, reader); descriptions_.insert(suffix, description); return 1; } QStringList RideFileFactory::suffixes() const { return readFuncs_.keys(); } QRegExp RideFileFactory::rideFileRegExp() const { QStringList suffixList = RideFileFactory::instance().suffixes(); QString s("^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.(%1)$"); return QRegExp(s.arg(suffixList.join("|")), Qt::CaseInsensitive); } RideFile *RideFileFactory::openRideFile(MainWindow *main, QFile &file, QStringList &errors) const { QString suffix = file.fileName(); int dot = suffix.lastIndexOf("."); assert(dot >= 0); suffix.remove(0, dot + 1); RideFileReader *reader = readFuncs_.value(suffix.toLower()); assert(reader); //qDebug()<<"open"<openRideFile(file, errors); //qDebug()<<"open"<intervals().empty()) result->fillInIntervals(); // legacy support for .notes file QFileInfo fileInfo(file.fileName()); QString notesFileName = fileInfo.absolutePath() + '/' + fileInfo.baseName() + ".notes"; QFile notesFile(notesFileName); // read it in if it exists and "Notes" is not already set if (result->getTag("Notes", "") == "" && notesFile.exists() && notesFile.open(QFile::ReadOnly | QFile::Text)) { QTextStream in(¬esFile); result->setTag("Notes", in.readAll()); notesFile.close(); } // Construct the summary text used on the calendar QString calendarText; foreach (FieldDefinition field, main->rideMetadata()->getFields()) { if (field.diary == true && result->getTag(field.name, "") != "") { calendarText += QString("%1\n") .arg(result->getTag(field.name, "")); } } result->setTag("Calendar Text", calendarText); // set other "special" fields result->setTag("Filename", file.fileName()); result->setTag("Device", result->deviceType()); result->setTag("Athlete", QFileInfo(file).dir().dirName()); result->setTag("Year", result->startTime().toString("yyyy")); result->setTag("Month", result->startTime().toString("MMMM")); result->setTag("Weekday", result->startTime().toString("ddd")); DataProcessorFactory::instance().autoProcess(result); } return result; } QStringList RideFileFactory::listRideFiles(const QDir &dir) const { QStringList filters; QMapIterator i(readFuncs_); while (i.hasNext()) { i.next(); filters << ("*." + i.key()); } // This will read the user preferences and change the file list order as necessary: QVariant isAscending = appsettings->value(NULL, GC_ALLRIDES_ASCENDING,Qt::Checked); QFlags spec = QDir::Files; #ifdef Q_OS_WIN32 spec |= QDir::Hidden; #endif if(isAscending.toInt()>0){ return dir.entryList(filters, spec, QDir::Name); } return dir.entryList(filters, spec, QDir::Name|QDir::Reversed); } void RideFile::appendPoint(double secs, double cad, double hr, double km, double kph, double nm, double watts, double alt, double lon, double lat, double headwind, int interval) { dataPoints_.append(new RideFilePoint(secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, interval)); dataPresent.secs |= (secs != 0); dataPresent.cad |= (cad != 0); dataPresent.hr |= (hr != 0); dataPresent.km |= (km != 0); dataPresent.kph |= (kph != 0); dataPresent.nm |= (nm != 0); dataPresent.watts |= (watts != 0); dataPresent.alt |= (alt != 0); dataPresent.lon |= (lon != 0); dataPresent.lat |= (lat != 0); dataPresent.headwind |= (headwind != 0); dataPresent.interval |= (interval != 0); } void RideFile::appendPoint(const RideFilePoint &point) { dataPoints_.append(new RideFilePoint(point.secs,point.cad,point.hr,point.km,point.kph,point.nm,point.watts,point.alt,point.lon,point.lat, point.headwind,point.interval)); } void RideFile::setDataPresent(SeriesType series, bool value) { switch (series) { case secs : dataPresent.secs = value; break; case cad : dataPresent.cad = value; break; case hr : dataPresent.hr = value; break; case km : dataPresent.km = value; break; case kph : dataPresent.kph = value; break; case nm : dataPresent.nm = value; break; case watts : dataPresent.watts = value; break; case alt : dataPresent.alt = value; break; case lon : dataPresent.lon = value; break; case lat : dataPresent.lat = value; break; case headwind : dataPresent.headwind = value; break; case interval : dataPresent.interval = value; break; case none : break; } } bool RideFile::isDataPresent(SeriesType series) { switch (series) { case secs : return dataPresent.secs; break; case cad : return dataPresent.cad; break; case hr : return dataPresent.hr; break; case km : return dataPresent.km; break; case kph : return dataPresent.kph; break; case nm : return dataPresent.nm; break; case watts : return dataPresent.watts; break; case alt : return dataPresent.alt; break; case lon : return dataPresent.lon; break; case lat : return dataPresent.lat; break; case headwind : return dataPresent.headwind; break; case interval : return dataPresent.interval; break; case none : break; } return false; } void RideFile::setPointValue(int index, SeriesType series, double value) { switch (series) { case secs : dataPoints_[index]->secs = value; break; case cad : dataPoints_[index]->cad = value; break; case hr : dataPoints_[index]->hr = value; break; case km : dataPoints_[index]->km = value; break; case kph : dataPoints_[index]->kph = value; break; case nm : dataPoints_[index]->nm = value; break; case watts : dataPoints_[index]->watts = value; break; case alt : dataPoints_[index]->alt = value; break; case lon : dataPoints_[index]->lon = value; break; case lat : dataPoints_[index]->lat = value; break; case headwind : dataPoints_[index]->headwind = value; break; case interval : dataPoints_[index]->interval = value; break; case none : break; } } double RideFile::getPointValue(int index, SeriesType series) { switch (series) { case secs : return dataPoints_[index]->secs; break; case cad : return dataPoints_[index]->cad; break; case hr : return dataPoints_[index]->hr; break; case km : return dataPoints_[index]->km; break; case kph : return dataPoints_[index]->kph; break; case nm : return dataPoints_[index]->nm; break; case watts : return dataPoints_[index]->watts; break; case alt : return dataPoints_[index]->alt; break; case lon : return dataPoints_[index]->lon; break; case lat : return dataPoints_[index]->lat; break; case headwind : return dataPoints_[index]->headwind; break; case interval : return dataPoints_[index]->interval; break; case none : break; } return 0.0; // shutup the compiler } int RideFile::decimalsFor(SeriesType series) { switch (series) { case secs : return 3; break; case cad : return 0; break; case hr : return 0; break; case km : return 6; break; case kph : return 4; break; case nm : return 2; break; case watts : return 0; break; case alt : return 3; break; case lon : return 6; break; case lat : return 6; break; case headwind : return 4; break; case interval : return 0; break; case none : break; } return 2; // default } double RideFile::maximumFor(SeriesType series) { switch (series) { case secs : return 999999; break; case cad : return 300; break; case hr : return 300; break; case km : return 999999; break; case kph : return 999; break; case nm : return 999; break; case watts : return 4000; break; case alt : return 8850; break; // mt everest is highest point above sea level case lon : return 180; break; case lat : return 90; break; case headwind : return 999; break; case interval : return 999; break; case none : break; } return 9999; // default } double RideFile::minimumFor(SeriesType series) { switch (series) { case secs : return 0; break; case cad : return 0; break; case hr : return 0; break; case km : return 0; break; case kph : return 0; break; case nm : return 0; break; case watts : return 0; break; case alt : return -413; break; // the Red Sea is lowest land point on earth case lon : return -180; break; case lat : return -90; break; case headwind : return -999; break; case interval : return 0; break; case none : break; } return 0; // default } void RideFile::deletePoint(int index) { delete dataPoints_[index]; dataPoints_.remove(index); } void RideFile::deletePoints(int index, int count) { for(int i=index; i<(index+count); i++) delete dataPoints_[i]; dataPoints_.remove(index, count); } void RideFile::insertPoint(int index, RideFilePoint *point) { dataPoints_.insert(index, point); } void RideFile::appendPoints(QVector newRows) { dataPoints_ += newRows; } void RideFile::emitSaved() { emit saved(); } void RideFile::emitReverted() { emit reverted(); } void RideFile::emitModified() { emit modified(); }