void compute(const RideFile *ride, const Zones *, int, const HrZones *, int, const QHash<QString,RideMetric*> &, const Context *) { // LNP only makes sense for running and it needs recIntSecs > 0 if (!ride->isRun() || ride->recIntSecs() == 0) { setValue(0.0); setCount(0); return; } // unconst naughty boy, get athlete's data RideFile *uride = const_cast<RideFile*>(ride); double weight = uride->getWeight(); double height = uride->getHeight(); int rollingwindowsize120 = 120 / ride->recIntSecs(); int rollingwindowsize30 = 30 / ride->recIntSecs(); double total = 0.0; int count = 0; // no point doing a rolling average if the // sample rate is greater than the rolling average // window!! if (rollingwindowsize30 > 1) { QVector<double> rollingSpeed(rollingwindowsize120); QVector<double> rollingSlope(rollingwindowsize120); QVector<double> rollingPower(rollingwindowsize30); int index120 = 0; int index30 = 0; double sumSpeed = 0.0; double sumSlope = 0.0; double sumPower = 0.0; double initial_speed = 0.0; // loop over the data and convert to a rolling // average for the given windowsize for (int i=0; i<ride->dataPoints().size(); i++) { double speed = ride->dataPoints()[i]->kph/3.6; sumSpeed += speed; sumSpeed -= rollingSpeed[index120]; rollingSpeed[index120] = speed; double speed120 = sumSpeed/std::min(count+1, rollingwindowsize120); // speed rolling average double slope = ride->dataPoints()[i]->slope/100.0; sumSlope += slope; sumSlope -= rollingSlope[index120]; rollingSlope[index120] = slope; double slope120 = sumSlope/std::min(count+1, rollingwindowsize120); // slope rolling average // running power based on 120sec averages double watts = running_power(weight, height, speed120, slope120, speed120*ride->recIntSecs(), initial_speed); initial_speed = speed120; sumPower += watts; sumPower -= rollingPower[index30]; rollingPower[index30] = watts; total += pow(sumPower/std::min(count+1, rollingwindowsize30), 4); // raise rolling average to 4th power count ++; // move index on/round index120 = (index120 >= rollingwindowsize120-1) ? 0 : index120+1; index30 = (index30 >= rollingwindowsize30-1) ? 0 : index30+1; } } if (count) { lnp = pow(total/count, 0.25); secs = count * ride->recIntSecs(); } else { lnp = secs = 0; } setValue(lnp); setCount(secs); }
// Refresh not up to date metrics and metrics after date void MetricAggregator::refreshMetrics(QDateTime forceAfterThisDate) { // only if we have established a connection to the database if (dbaccess == NULL || main->isclean==true) return; // first check db structure is still up to date // this is because metadata.xml may add new fields dbaccess->checkDBVersion(); // Get a list of the ride files QRegExp rx = RideFileFactory::instance().rideFileRegExp(); QStringList filenames = RideFileFactory::instance().listRideFiles(home); QStringListIterator i(filenames); // get a Hash map of statistic records and timestamps QSqlQuery query(dbaccess->connection()); QHash <QString, status> dbStatus; bool rc = query.exec("SELECT filename, timestamp, fingerprint FROM metrics ORDER BY ride_date;"); while (rc && query.next()) { status add; QString filename = query.value(0).toString(); add.timestamp = query.value(1).toInt(); add.fingerprint = query.value(2).toInt(); dbStatus.insert(filename, add); } // begin LUW -- byproduct of turning off sync (nosync) dbaccess->connection().transaction(); // Delete statistics for non-existant ride files QHash<QString, status>::iterator d; for (d = dbStatus.begin(); d != dbStatus.end(); ++d) { if (QFile(home.absolutePath() + "/" + d.key()).exists() == false) { dbaccess->deleteRide(d.key()); #ifdef GC_HAVE_LUCENE main->lucene->deleteRide(d.key()); #endif } } unsigned long zoneFingerPrint = static_cast<unsigned long>(zones->getFingerprint()) + static_cast<unsigned long>(hrzones->getFingerprint()); // checksum of *all* zone data (HR and Power) // update statistics for ride files which are out of date // showing a progress bar as we go QTime elapsed; elapsed.start(); QString title = tr("Refreshing Ride Statistics...\nStarted"); QProgressDialog bar(title, tr("Abort"), 0, filenames.count(), main); bar.setWindowModality(Qt::WindowModal); bar.setMinimumDuration(0); bar.show(); int processed=0; QApplication::processEvents(); // get that dialog up! // log of progress QFile log(home.absolutePath() + "/" + "metric.log"); log.open(QIODevice::WriteOnly); log.resize(0); QTextStream out(&log); out << "METRIC REFRESH STARTS: " << QDateTime::currentDateTime().toString() + "\r\n"; while (i.hasNext()) { QString name = i.next(); QFile file(home.absolutePath() + "/" + name); // if it s missing or out of date then update it! status current = dbStatus.value(name); unsigned long dbTimeStamp = current.timestamp; unsigned long fingerprint = current.fingerprint; RideFile *ride = NULL; // update progress bar long elapsedtime = elapsed.elapsed(); QString elapsedString = QString("%1:%2:%3").arg(elapsedtime/3600000,2) .arg((elapsedtime%3600000)/60000,2,10,QLatin1Char('0')) .arg((elapsedtime%60000)/1000,2,10,QLatin1Char('0')); QString title = tr("Refreshing Ride Statistics...\nElapsed: %1\n%2").arg(elapsedString).arg(name); bar.setLabelText(title); bar.setValue(++processed); QApplication::processEvents(); if (dbTimeStamp < QFileInfo(file).lastModified().toTime_t() || zoneFingerPrint != fingerprint || (!forceAfterThisDate.isNull() && name >= forceAfterThisDate.toString("yyyy_MM_dd_hh_mm_ss"))) { QStringList errors; // log out << "Opening ride: " << name << "\r\n"; // read file and process it if we didn't already... if (ride == NULL) ride = RideFileFactory::instance().openRideFile(main, file, errors); out << "File open completed: " << name << "\r\n"; if (ride != NULL) { out << "Getting weight: " << name << "\r\n"; ride->getWeight(); out << "Updating statistics: " << name << "\r\n"; importRide(home, ride, name, zoneFingerPrint, (dbTimeStamp > 0)); } } // update cache (will check timestamps itself) // if ride wasn't opened it will do it itself // we only want to check so passing check=true // because we don't actually want the results now RideFileCache updater(main, home.absolutePath() + "/" + name, ride, true); // free memory - if needed if (ride) delete ride; if (bar.wasCanceled()) { out << "METRIC REFRESH CANCELLED\r\n"; break; } } // end LUW -- now syncs DB out << "COMMIT: " << QDateTime::currentDateTime().toString() + "\r\n"; dbaccess->connection().commit(); #ifdef GC_HAVE_LUCENE #ifndef WIN32 // windows crashes here.... out << "OPTIMISE: " << QDateTime::currentDateTime().toString() + "\r\n"; main->lucene->optimise(); #endif #endif main->isclean = true; // stop logging out << "SIGNAL DATA CHANGED: " << QDateTime::currentDateTime().toString() + "\r\n"; dataChanged(); // notify models/views out << "METRIC REFRESH ENDS: " << QDateTime::currentDateTime().toString() + "\r\n"; log.close(); }
void PowerHist::setData(RideItem *_rideItem, bool force) { // predefined deltas for each series static const double wattsDelta = 1.0; static const double wattsKgDelta = 0.01; static const double nmDelta = 0.1; static const double hrDelta = 1.0; static const double kphDelta = 0.1; static const double cadDelta = 1.0; source = Ride; // we set with this data already if (force == false && _rideItem == LASTrideItem && source == LASTsource) return; rideItem = _rideItem; if (!rideItem) return; RideFile *ride = rideItem->ride(); bool hasData = ((series == RideFile::watts || series == RideFile::wattsKg) && ride->areDataPresent()->watts) || (series == RideFile::nm && ride->areDataPresent()->nm) || (series == RideFile::kph && ride->areDataPresent()->kph) || (series == RideFile::cad && ride->areDataPresent()->cad) || (series == RideFile::aPower && ride->areDataPresent()->apower) || (series == RideFile::hr && ride->areDataPresent()->hr); if (ride && hasData) { //setTitle(ride->startTime().toString(GC_DATETIME_FORMAT)); static const int maxSize = 4096; // recording interval in minutes dt = ride->recIntSecs() / 60.0; wattsArray.resize(0); wattsZoneArray.resize(0); wattsKgArray.resize(0); aPowerArray.resize(0); nmArray.resize(0); hrArray.resize(0); hrZoneArray.resize(0); kphArray.resize(0); cadArray.resize(0); wattsSelectedArray.resize(0); wattsZoneSelectedArray.resize(0); wattsKgSelectedArray.resize(0); aPowerSelectedArray.resize(0); nmSelectedArray.resize(0); hrSelectedArray.resize(0); hrZoneSelectedArray.resize(0); kphSelectedArray.resize(0); cadSelectedArray.resize(0); // unit conversion factor for imperial units for selected parameters double torque_factor = (context->athlete->useMetricUnits ? 1.0 : 0.73756215); double speed_factor = (context->athlete->useMetricUnits ? 1.0 : 0.62137119); foreach(const RideFilePoint *p1, ride->dataPoints()) { bool selected = isSelected(p1, ride->recIntSecs()); // watts array int wattsIndex = int(floor(p1->watts / wattsDelta)); if (wattsIndex >= 0 && wattsIndex < maxSize) { if (wattsIndex >= wattsArray.size()) wattsArray.resize(wattsIndex + 1); wattsArray[wattsIndex]++; if (selected) { if (wattsIndex >= wattsSelectedArray.size()) wattsSelectedArray.resize(wattsIndex + 1); wattsSelectedArray[wattsIndex]++; } } // watts zoned array const Zones *zones = rideItem->zones; int zoneRange = zones ? zones->whichRange(ride->startTime().date()) : -1; // Only calculate zones if we have a valid range and check zeroes if (zoneRange > -1 && (withz || (!withz && p1->watts))) { wattsIndex = zones->whichZone(zoneRange, p1->watts); if (wattsIndex >= 0 && wattsIndex < maxSize) { if (wattsIndex >= wattsZoneArray.size()) wattsZoneArray.resize(wattsIndex + 1); wattsZoneArray[wattsIndex]++; if (selected) { if (wattsIndex >= wattsZoneSelectedArray.size()) wattsZoneSelectedArray.resize(wattsIndex + 1); wattsZoneSelectedArray[wattsIndex]++; } } } // aPower array int aPowerIndex = int(floor(p1->apower / wattsDelta)); if (aPowerIndex >= 0 && aPowerIndex < maxSize) { if (aPowerIndex >= aPowerArray.size()) aPowerArray.resize(aPowerIndex + 1); aPowerArray[aPowerIndex]++; if (selected) { if (aPowerIndex >= aPowerSelectedArray.size()) aPowerSelectedArray.resize(aPowerIndex + 1); aPowerSelectedArray[aPowerIndex]++; } } // wattsKg array int wattsKgIndex = int(floor(p1->watts / ride->getWeight() / wattsKgDelta)); if (wattsKgIndex >= 0 && wattsKgIndex < maxSize) { if (wattsKgIndex >= wattsKgArray.size()) wattsKgArray.resize(wattsKgIndex + 1); wattsKgArray[wattsKgIndex]++; if (selected) { if (wattsKgIndex >= wattsKgSelectedArray.size()) wattsKgSelectedArray.resize(wattsKgIndex + 1); wattsKgSelectedArray[wattsKgIndex]++; } } int nmIndex = int(floor(p1->nm * torque_factor / nmDelta)); if (nmIndex >= 0 && nmIndex < maxSize) { if (nmIndex >= nmArray.size()) nmArray.resize(nmIndex + 1); nmArray[nmIndex]++; if (selected) { if (nmIndex >= nmSelectedArray.size()) nmSelectedArray.resize(nmIndex + 1); nmSelectedArray[nmIndex]++; } } int hrIndex = int(floor(p1->hr / hrDelta)); if (hrIndex >= 0 && hrIndex < maxSize) { if (hrIndex >= hrArray.size()) hrArray.resize(hrIndex + 1); hrArray[hrIndex]++; if (selected) { if (hrIndex >= hrSelectedArray.size()) hrSelectedArray.resize(hrIndex + 1); hrSelectedArray[hrIndex]++; } } // hr zoned array int hrZoneRange = context->athlete->hrZones() ? context->athlete->hrZones()->whichRange(ride->startTime().date()) : -1; // Only calculate zones if we have a valid range if (hrZoneRange > -1 && (withz || (!withz && p1->hr))) { hrIndex = context->athlete->hrZones()->whichZone(hrZoneRange, p1->hr); if (hrIndex >= 0 && hrIndex < maxSize) { if (hrIndex >= hrZoneArray.size()) hrZoneArray.resize(hrIndex + 1); hrZoneArray[hrIndex]++; if (selected) { if (hrIndex >= hrZoneSelectedArray.size()) hrZoneSelectedArray.resize(hrIndex + 1); hrZoneSelectedArray[hrIndex]++; } } } int kphIndex = int(floor(p1->kph * speed_factor / kphDelta)); if (kphIndex >= 0 && kphIndex < maxSize) { if (kphIndex >= kphArray.size()) kphArray.resize(kphIndex + 1); kphArray[kphIndex]++; if (selected) { if (kphIndex >= kphSelectedArray.size()) kphSelectedArray.resize(kphIndex + 1); kphSelectedArray[kphIndex]++; } } int cadIndex = int(floor(p1->cad / cadDelta)); if (cadIndex >= 0 && cadIndex < maxSize) { if (cadIndex >= cadArray.size()) cadArray.resize(cadIndex + 1); cadArray[cadIndex]++; if (selected) { if (cadIndex >= cadSelectedArray.size()) cadSelectedArray.resize(cadIndex + 1); cadSelectedArray[cadIndex]++; } } } } else {
// Refresh not up to date metrics and metrics after date void MetricAggregator::refreshMetrics(QDateTime forceAfterThisDate) { // only if we have established a connection to the database if (dbaccess == NULL || context->athlete->isclean==true) return; // first check db structure is still up to date // this is because metadata.xml may add new fields dbaccess->checkDBVersion(); // Get a list of the ride files QRegExp rx = RideFileFactory::instance().rideFileRegExp(); QStringList filenames = RideFileFactory::instance().listRideFiles(context->athlete->home); QStringListIterator i(filenames); // get a Hash map of statistic records and timestamps QSqlQuery query(dbaccess->connection()); QHash <QString, status> dbStatus; bool rc = query.exec("SELECT filename, crc, timestamp, fingerprint FROM metrics ORDER BY ride_date;"); while (rc && query.next()) { status add; QString filename = query.value(0).toString(); add.crc = query.value(1).toInt(); add.timestamp = query.value(2).toInt(); add.fingerprint = query.value(3).toInt(); dbStatus.insert(filename, add); } // begin LUW -- byproduct of turning off sync (nosync) dbaccess->connection().transaction(); // Delete statistics for non-existant ride files QHash<QString, status>::iterator d; for (d = dbStatus.begin(); d != dbStatus.end(); ++d) { if (QFile(context->athlete->home.absolutePath() + "/" + d.key()).exists() == false) { dbaccess->deleteRide(d.key()); #ifdef GC_HAVE_LUCENE context->athlete->lucene->deleteRide(d.key()); #endif } } unsigned long zoneFingerPrint = static_cast<unsigned long>(context->athlete->zones()->getFingerprint(context)) + static_cast<unsigned long>(context->athlete->hrZones()->getFingerprint()); // checksum of *all* zone data (HR and Power) // update statistics for ride files which are out of date // showing a progress bar as we go QTime elapsed; elapsed.start(); QString title = context->athlete->cyclist; GProgressDialog *bar = NULL; int processed=0; int updates=0; QApplication::processEvents(); // get that dialog up! // log of progress QFile log(context->athlete->home.absolutePath() + "/" + "metric.log"); log.open(QIODevice::WriteOnly); log.resize(0); QTextStream out(&log); out << "METRIC REFRESH STARTS: " << QDateTime::currentDateTime().toString() + "\r\n"; while (i.hasNext()) { QString name = i.next(); QFile file(context->athlete->home.absolutePath() + "/" + name); // if it s missing or out of date then update it! status current = dbStatus.value(name); unsigned long dbTimeStamp = current.timestamp; unsigned long crc = current.crc; unsigned long fingerprint = current.fingerprint; RideFile *ride = NULL; processed++; // create the dialog if we need to show progress for long running uodate long elapsedtime = elapsed.elapsed(); if ((!forceAfterThisDate.isNull() || first || elapsedtime > 6000) && bar == NULL) { bar = new GProgressDialog(title, 0, filenames.count(), context->mainWindow->init, context->mainWindow); bar->show(); // lets hide until elapsed time is > 6 seconds // lets make sure it goes to the center! QApplication::processEvents(); } // update the dialog always after 6 seconds if (!forceAfterThisDate.isNull() || first || elapsedtime > 6000) { // update progress bar QString elapsedString = QString("%1:%2:%3").arg(elapsedtime/3600000,2) .arg((elapsedtime%3600000)/60000,2,10,QLatin1Char('0')) .arg((elapsedtime%60000)/1000,2,10,QLatin1Char('0')); QString title = tr("Update Statistics\nElapsed: %1\n\n%2").arg(elapsedString).arg(name); bar->setLabelText(title); bar->setValue(processed); } QApplication::processEvents(); if (dbTimeStamp < QFileInfo(file).lastModified().toTime_t() || zoneFingerPrint != fingerprint || (!forceAfterThisDate.isNull() && name >= forceAfterThisDate.toString("yyyy_MM_dd_hh_mm_ss"))) { QStringList errors; // ooh we have one to update -- lets check the CRC in case // its actually unchanged since last time and the timestamps // have been mucked up by dropbox / file copying / backups etc // but still update if we're doing this because settings changed not the ride! QString fullPath = QString(context->athlete->home.absolutePath()) + "/" + name; if ((crc == 0 || crc != DBAccess::computeFileCRC(fullPath)) || zoneFingerPrint != fingerprint || (!forceAfterThisDate.isNull() && name >= forceAfterThisDate.toString("yyyy_MM_dd_hh_mm_ss"))) { // log out << "Opening ride: " << name << "\r\n"; // read file and process it if we didn't already... if (ride == NULL) ride = RideFileFactory::instance().openRideFile(context, file, errors); out << "File open completed: " << name << "\r\n"; if (ride != NULL) { out << "Getting weight: " << name << "\r\n"; ride->getWeight(); out << "Updating statistics: " << name << "\r\n"; importRide(context->athlete->home, ride, name, zoneFingerPrint, (dbTimeStamp > 0)); } updates++; } } // update cache (will check timestamps itself) // if ride wasn't opened it will do it itself // we only want to check so passing check=true // because we don't actually want the results now // it will also check the file CRC as well as timestamps RideFileCache updater(context, context->athlete->home.absolutePath() + "/" + name, ride, true); // free memory - if needed if (ride) delete ride; // for model run... // RideFileCache::meanMaxPowerFor(context, context->athlete->home.absolutePath() + "/" + name); if (bar && bar->wasCanceled()) { out << "METRIC REFRESH CANCELLED\r\n"; break; } } // end LUW -- now syncs DB out << "COMMIT: " << QDateTime::currentDateTime().toString() + "\r\n"; dbaccess->connection().commit(); #ifdef GC_HAVE_LUCENE #ifndef WIN32 // windows crashes here.... out << "OPTIMISE: " << QDateTime::currentDateTime().toString() + "\r\n"; context->athlete->lucene->optimise(); #endif #endif context->athlete->isclean = true; // clear out the estimates if something changed! if (updates) context->athlete->PDEstimates.clear(); // now zap the progress bar if (bar) delete bar; // stop logging out << "SIGNAL DATA CHANGED: " << QDateTime::currentDateTime().toString() + "\r\n"; dataChanged(); // notify models/views out << "METRIC REFRESH ENDS: " << QDateTime::currentDateTime().toString() + "\r\n"; log.close(); first = false; }