Files
GoldenCheetah/src/MetricAggregator.cpp
Mark Liversedge d22777ed74 UI Nits: Daily/Weekly/Monthly Summary
You can now add the summary chart to the diary
view to get a summary of the date range currently
being summarised on that view.

Once the Home view has its own sidebar that selects
date ranges you will be able to add it there too
and summarise seasons etc.
2012-11-13 13:27:36 +00:00

391 lines
13 KiB
C++

/*
* Copyright (c) 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 "MetricAggregator.h"
#include "DBAccess.h"
#include "RideFile.h"
#include "RideFileCache.h"
#ifdef GC_HAVE_LUCENE
#include "Lucene.h"
#endif
#include "Zones.h"
#include "HrZones.h"
#include "Settings.h"
#include "RideItem.h"
#include "RideMetric.h"
#include "TimeUtils.h"
#include <assert.h>
#include <math.h>
#include <QtXml/QtXml>
#include <QProgressDialog>
MetricAggregator::MetricAggregator(MainWindow *main, QDir home, const Zones *zones, const HrZones *hrzones) : QObject(main), main(main), home(home), zones(zones), hrzones(hrzones)
{
colorEngine = new ColorEngine(main);
dbaccess = new DBAccess(main, home);
connect(main, SIGNAL(configChanged()), this, SLOT(update()));
connect(main, SIGNAL(rideAdded(RideItem*)), this, SLOT(addRide(RideItem*)));
connect(main, SIGNAL(rideDeleted(RideItem*)), this, SLOT(update(void)));
connect(main, SIGNAL(rideClean()), this, SLOT(update(void)));
}
MetricAggregator::~MetricAggregator()
{
delete colorEngine;
delete dbaccess;
}
/*----------------------------------------------------------------------
* Refresh the database -- only updates metrics when they are out
* of date or missing altogether or where
* the ride file no longer exists
*----------------------------------------------------------------------*/
// used to store timestamp and fingerprint used in database
struct status { unsigned long timestamp, fingerprint; };
void MetricAggregator::refreshMetrics()
{
// 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 = zones->getFingerprint() + hrzones->getFingerprint(); // crc 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) {
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 << "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;
}
}
// stop logging
out << "METRIC REFRESH ENDS: " << QDateTime::currentDateTime().toString() + "\r\n";
log.close();
// end LUW -- now syncs DB
dbaccess->connection().commit();
#ifdef GC_HAVE_LUCENE
main->lucene->optimise();
#endif
main->isclean = true;
dataChanged(); // notify models/views
}
/*----------------------------------------------------------------------
* Calculate the metrics for a ride file using the metrics factory
*----------------------------------------------------------------------*/
// add a ride (after import / download)
void MetricAggregator::addRide(RideItem*ride)
{
if (ride && ride->ride()) {
importRide(main->home, ride->ride(), ride->fileName, main->zones()->getFingerprint(), true);
RideFileCache updater(main, home.absolutePath() + "/" + ride->fileName, ride->ride(), true); // update cpx etc
dataChanged(); // notify models/views
}
}
void MetricAggregator::update() {
main->isclean = false;
if (!main->ismultisave) {
refreshMetrics();
}
}
bool MetricAggregator::importRide(QDir path, RideFile *ride, QString fileName, unsigned long fingerprint, bool modify)
{
SummaryMetrics *summaryMetric = new SummaryMetrics();
QFile file(path.absolutePath() + "/" + fileName);
QRegExp rx = RideFileFactory::instance().rideFileRegExp();
if (!rx.exactMatch(fileName)) {
return false; // not a ridefile!
}
summaryMetric->setFileName(fileName);
assert(rx.numCaptures() == 7);
QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt());
QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt());
QDateTime dateTime(date, time);
summaryMetric->setRideDate(dateTime);
summaryMetric->setId(ride->id());
const RideMetricFactory &factory = RideMetricFactory::instance();
QStringList metrics;
for (int i = 0; i < factory.metricCount(); ++i)
metrics << factory.metricName(i);
// compute all the metrics
QHash<QString, RideMetricPtr> computed = RideMetric::computeMetrics(main, ride, zones, hrzones, metrics);
// get metrics into summaryMetric QMap
for(int i = 0; i < factory.metricCount(); ++i) {
// check for override
summaryMetric->setForSymbol(factory.metricName(i), computed.value(factory.metricName(i))->value(true));
}
// what color will this ride be?
QColor color = colorEngine->colorFor(ride->getTag("Calendar Text", ""));
dbaccess->importRide(summaryMetric, ride, color, fingerprint, modify);
#ifdef GC_HAVE_LUCENE
main->lucene->importRide(summaryMetric, ride, color, fingerprint, modify);
#endif
delete summaryMetric;
return true;
}
void
MetricAggregator::importMeasure(SummaryMetrics *sm)
{
dbaccess->importMeasure(sm);
}
/*----------------------------------------------------------------------
* Query functions are wrappers around DBAccess functions
*----------------------------------------------------------------------*/
void
MetricAggregator::writeAsCSV(QString filename)
{
// write all metrics as a CSV file
QList<SummaryMetrics> all = getAllMetricsFor(QDateTime(), QDateTime());
// write headings
if (!all.count()) return; // no dice
// open file.. truncate if exists already
QFile file(filename);
file.open(QFile::WriteOnly);
file.resize(0);
QTextStream out(&file);
// write headings
out<<"date, time, filename,";
QMapIterator<QString, double>i(all[0].values());
while (i.hasNext()) {
i.next();
out<<i.key()<<",";
}
out<<"\n";
// write values
foreach(SummaryMetrics x, all) {
out<<x.getRideDate().date().toString("MM/dd/yy")<<","
<<x.getRideDate().time().toString()<<","
<<x.getFileName()<<",";
QMapIterator<QString, double>i(x.values());
while (i.hasNext()) {
i.next();
out<<i.value()<<",";
}
out<<"\n";
}
file.close();
}
QList<SummaryMetrics>
MetricAggregator::getAllMetricsFor(DateRange dr)
{
return getAllMetricsFor(QDateTime(dr.from, QTime(0,0,0)), QDateTime(dr.to, QTime(23,59,59)));
}
QList<SummaryMetrics>
MetricAggregator::getAllMetricsFor(QDateTime start, QDateTime end)
{
if (main->isclean == false) refreshMetrics(); // get them up-to-date
QList<SummaryMetrics> empty;
// only if we have established a connection to the database
if (dbaccess == NULL) {
qDebug()<<"lost db connection?";
return empty;
}
// apparently using transactions for queries
// can improve performance!
dbaccess->connection().transaction();
QList<SummaryMetrics> results = dbaccess->getAllMetricsFor(start, end);
dbaccess->connection().commit();
return results;
}
SummaryMetrics
MetricAggregator::getAllMetricsFor(QString filename)
{
if (main->isclean == false) refreshMetrics(); // get them up-to-date
SummaryMetrics results;
QColor color; // ignored for now...
// only if we have established a connection to the database
if (dbaccess == NULL) {
qDebug()<<"lost db connection?";
return results;
}
// apparently using transactions for queries
// can improve performance!
dbaccess->connection().transaction();
dbaccess->getRide(filename, results, color);
dbaccess->connection().commit();
return results;
}
QList<SummaryMetrics>
MetricAggregator::getAllMeasuresFor(DateRange dr)
{
return getAllMeasuresFor(QDateTime(dr.from, QTime(0,0,0)), QDateTime(dr.to, QTime(23,59,59)));
}
QList<SummaryMetrics>
MetricAggregator::getAllMeasuresFor(QDateTime start, QDateTime end)
{
QList<SummaryMetrics> empty;
// only if we have established a connection to the database
if (dbaccess == NULL) {
qDebug()<<"lost db connection?";
return empty;
}
return dbaccess->getAllMeasuresFor(start, end);
}
SummaryMetrics
MetricAggregator::getRideMetrics(QString filename)
{
if (main->isclean == false) refreshMetrics(); // get them up-to-date
SummaryMetrics empty;
// only if we have established a connection to the database
if (dbaccess == NULL) {
qDebug()<<"lost db connection?";
return empty;
}
return dbaccess->getRideMetrics(filename);
}