mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
.. rather than check if lucene index contains every ride upon startup just force a rebuild of the index is missing. .. if the index gets out of sync its because people are copying data and so they should delete the index when they do so to make sure it stays in sync
502 lines
14 KiB
C++
502 lines
14 KiB
C++
/*
|
|
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
|
*
|
|
* 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 "RideItem.h"
|
|
#include "RideMetric.h"
|
|
#include "RideFile.h"
|
|
#include "RideFileCache.h"
|
|
#include "RideMetadata.h"
|
|
#include "Context.h"
|
|
#include "Zones.h"
|
|
#include "HrZones.h"
|
|
#include "PaceZones.h"
|
|
#include "Settings.h"
|
|
#include "Colors.h" // for ColorEngine
|
|
|
|
#include <cmath>
|
|
#include <QtAlgorithms>
|
|
#include <QMap>
|
|
#include <QMapIterator>
|
|
#include <QByteArray>
|
|
|
|
#ifdef GC_HAVE_LUCENE
|
|
#include "Lucene.h"
|
|
#endif
|
|
|
|
// used to create a temporary ride item that is not in the cache and just
|
|
// used to enable using the same calling semantics in things like the
|
|
// merge wizard and interval navigator
|
|
RideItem::RideItem()
|
|
:
|
|
ride_(NULL), fileCache_(NULL), context(NULL), isdirty(false), isstale(true), isedit(false), path(""), fileName(""),
|
|
color(QColor(1,1,1)), isRun(false), fingerprint(0), metacrc(0), crc(0), timestamp(0), dbversion(0), weight(0) {
|
|
metrics_.fill(0, RideMetricFactory::instance().metricCount());
|
|
}
|
|
|
|
RideItem::RideItem(RideFile *ride, Context *context)
|
|
:
|
|
ride_(ride), fileCache_(NULL), context(context), isdirty(false), isstale(true), isedit(false), path(""), fileName(""),
|
|
color(QColor(1,1,1)), isRun(false), fingerprint(0), metacrc(0), crc(0), timestamp(0), dbversion(0), weight(0)
|
|
{
|
|
metrics_.fill(0, RideMetricFactory::instance().metricCount());
|
|
}
|
|
|
|
RideItem::RideItem(QString path, QString fileName, QDateTime &dateTime, Context *context)
|
|
:
|
|
ride_(NULL), fileCache_(NULL), context(context), isdirty(false), isstale(true), isedit(false), path(path),
|
|
fileName(fileName), dateTime(dateTime), color(QColor(1,1,1)), isRun(false), fingerprint(0),
|
|
metacrc(0), crc(0), timestamp(0), dbversion(0), weight(0)
|
|
{
|
|
metrics_.fill(0, RideMetricFactory::instance().metricCount());
|
|
}
|
|
|
|
// Create a new RideItem destined for the ride cache and used for caching
|
|
// pre-computed metrics and storing ride metadata
|
|
RideItem::RideItem(RideFile *ride, QDateTime &dateTime, Context *context)
|
|
:
|
|
ride_(ride), fileCache_(NULL), context(context), isdirty(true), isstale(true), isedit(false), dateTime(dateTime),
|
|
fingerprint(0), metacrc(0), crc(0), timestamp(0), dbversion(0), weight(0)
|
|
{
|
|
metrics_.fill(0, RideMetricFactory::instance().metricCount());
|
|
}
|
|
|
|
// clone a ride item
|
|
void
|
|
RideItem::setFrom(RideItem&here) // used when loading cache/rideDB.json
|
|
{
|
|
ride_ = NULL;
|
|
fileCache_ = NULL;
|
|
metrics_ = here.metrics_;
|
|
metadata_ = here.metadata_;
|
|
errors_ = here.errors_;
|
|
context = here.context;
|
|
isdirty = here.isdirty;
|
|
isstale = here.isstale;
|
|
isedit = here.isedit;
|
|
path = here.path;
|
|
fileName = here.fileName;
|
|
dateTime = here.dateTime;
|
|
fingerprint = here.fingerprint;
|
|
metacrc = here.metacrc;
|
|
crc = here.crc;
|
|
timestamp = here.timestamp;
|
|
dbversion = here.dbversion;
|
|
color = here.color;
|
|
present = here.present;
|
|
isRun = here.isRun;
|
|
weight = here.weight;
|
|
}
|
|
|
|
// set the metric array
|
|
void
|
|
RideItem::setFrom(QHash<QString, RideMetricPtr> computed)
|
|
{
|
|
QHashIterator<QString, RideMetricPtr> i(computed);
|
|
while (i.hasNext()) {
|
|
i.next();
|
|
metrics_[i.value()->index()] = i.value()->value(true);
|
|
}
|
|
}
|
|
|
|
// calculate metadata crc
|
|
unsigned long
|
|
RideItem::metaCRC()
|
|
{
|
|
QMapIterator<QString,QString> i(metadata_);
|
|
QByteArray ba;
|
|
i.toFront();
|
|
while(i.hasNext()) {
|
|
i.next();
|
|
ba.append(i.key());
|
|
ba.append(i.value());
|
|
}
|
|
return qChecksum(ba, ba.length());
|
|
}
|
|
|
|
RideFile *RideItem::ride(bool open)
|
|
{
|
|
if (!open || ride_) return ride_;
|
|
|
|
// open the ride file
|
|
QFile file(path + "/" + fileName);
|
|
ride_ = RideFileFactory::instance().openRideFile(context, file, errors_);
|
|
if (ride_ == NULL) return NULL; // failed to read ride
|
|
|
|
// refresh if stale..
|
|
refresh();
|
|
|
|
setDirty(false); // we're gonna use on-disk so by
|
|
// definition it is clean - but do it *after*
|
|
// we read the file since it will almost
|
|
// certainly be referenced by consuming widgets
|
|
|
|
// stay aware of state changes to our ride
|
|
// Context saves and RideFileCommand modifies
|
|
connect(ride_, SIGNAL(modified()), this, SLOT(modified()));
|
|
connect(ride_, SIGNAL(saved()), this, SLOT(saved()));
|
|
connect(ride_, SIGNAL(reverted()), this, SLOT(reverted()));
|
|
|
|
return ride_;
|
|
}
|
|
|
|
RideItem::~RideItem()
|
|
{
|
|
//qDebug()<<"deleting:"<<fileName;
|
|
if (isOpen()) close();
|
|
if (fileCache_) delete fileCache_;
|
|
}
|
|
|
|
RideFileCache *
|
|
RideItem::fileCache()
|
|
{
|
|
if (!fileCache_) {
|
|
fileCache_ = new RideFileCache(context, fileName, ride());
|
|
if (isDirty()) fileCache_->refresh(ride()); // refresh from what we have now !
|
|
}
|
|
return fileCache_;
|
|
}
|
|
|
|
void
|
|
RideItem::setRide(RideFile *overwrite)
|
|
{
|
|
RideFile *old = ride_;
|
|
ride_ = overwrite; // overwrite
|
|
|
|
// connect up to new one
|
|
connect(ride_, SIGNAL(modified()), this, SLOT(modified()));
|
|
connect(ride_, SIGNAL(saved()), this, SLOT(saved()));
|
|
connect(ride_, SIGNAL(reverted()), this, SLOT(reverted()));
|
|
|
|
// don't bother with the old one any more
|
|
disconnect(old);
|
|
|
|
// update status
|
|
setDirty(true);
|
|
notifyRideDataChanged();
|
|
|
|
//XXX SORRY ! memory leak XXX
|
|
//XXX delete old; // now wipe it once referrers had chance to change
|
|
//XXX this is only used by MergeActivityWizard and causes issues
|
|
//XXX because the data is accessed in separate threads (Wizard is a dialog)
|
|
//XXX because it is such an edge case (Merge) we will leave it for now
|
|
}
|
|
|
|
void
|
|
RideItem::notifyRideDataChanged()
|
|
{
|
|
// refresh the cache
|
|
if (fileCache_) fileCache_->refresh(ride());
|
|
|
|
// refresh the metrics
|
|
isstale=true;
|
|
refresh();
|
|
|
|
emit rideDataChanged();
|
|
}
|
|
|
|
void
|
|
RideItem::notifyRideMetadataChanged()
|
|
{
|
|
// refresh the metrics
|
|
isstale=true;
|
|
refresh();
|
|
|
|
emit rideMetadataChanged();
|
|
}
|
|
|
|
void
|
|
RideItem::modified()
|
|
{
|
|
setDirty(true);
|
|
}
|
|
|
|
void
|
|
RideItem::saved()
|
|
{
|
|
setDirty(false);
|
|
}
|
|
|
|
void
|
|
RideItem::reverted()
|
|
{
|
|
setDirty(false);
|
|
}
|
|
|
|
void
|
|
RideItem::setDirty(bool val)
|
|
{
|
|
if (isdirty == val) return; // np change
|
|
|
|
isdirty = val;
|
|
|
|
if (isdirty == true) {
|
|
|
|
context->notifyRideDirty();
|
|
|
|
} else {
|
|
|
|
context->notifyRideClean();
|
|
}
|
|
}
|
|
|
|
// name gets changed when file is converted in save
|
|
void
|
|
RideItem::setFileName(QString path, QString fileName)
|
|
{
|
|
this->path = path;
|
|
this->fileName = fileName;
|
|
}
|
|
|
|
bool
|
|
RideItem::isOpen()
|
|
{
|
|
return ride_ != NULL;
|
|
}
|
|
|
|
void
|
|
RideItem::close()
|
|
{
|
|
if (ride_) {
|
|
delete ride_;
|
|
ride_ = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
RideItem::setStartTime(QDateTime newDateTime)
|
|
{
|
|
dateTime = newDateTime;
|
|
ride()->setStartTime(newDateTime);
|
|
}
|
|
|
|
// check if we need to be refreshed
|
|
bool
|
|
RideItem::checkStale()
|
|
{
|
|
// if we're marked stale already then just return that !
|
|
if (isstale) return true;
|
|
|
|
// upgraded metrics
|
|
if (dbversion != DBSchemaVersion) {
|
|
|
|
isstale = true;
|
|
|
|
} else {
|
|
|
|
// has weight changed?
|
|
unsigned long prior = 1000.0f * weight;
|
|
unsigned long now = 1000.0f * getWeight();
|
|
|
|
if (prior != now) {
|
|
|
|
weight = getWeight();
|
|
isstale = true;
|
|
|
|
} else {
|
|
|
|
// or have cp / zones have changed ?
|
|
// note we now get the fingerprint from the zone range
|
|
// and not the entire config so that if you add a new
|
|
// range (e.g. set CP from today) but none of the other
|
|
// ranges change then there is no need to recompute the
|
|
// metrics for older rides !
|
|
|
|
// get the new zone configuration fingerprint that applies for the ride date
|
|
unsigned long rfingerprint = static_cast<unsigned long>(context->athlete->zones()->getFingerprint(context, dateTime.date()))
|
|
+ static_cast<unsigned long>(context->athlete->paceZones()->getFingerprint(dateTime.date()))
|
|
+ static_cast<unsigned long>(context->athlete->hrZones()->getFingerprint(dateTime.date()));
|
|
|
|
if (fingerprint != rfingerprint) {
|
|
|
|
isstale = true;
|
|
|
|
} else {
|
|
|
|
// or has file content changed ?
|
|
QString fullPath = QString(context->athlete->home->activities().absolutePath()) + "/" + fileName;
|
|
QFile file(fullPath);
|
|
|
|
// has timestamp changed ?
|
|
if (timestamp < QFileInfo(file).lastModified().toTime_t()) {
|
|
|
|
// if timestamp has changed then check crc
|
|
unsigned long fcrc = RideFile::computeFileCRC(fullPath);
|
|
|
|
if (crc == 0 || crc != fcrc) {
|
|
crc = fcrc; // update as expensive to calculate
|
|
isstale = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// still reckon its clean? what about the cache ?
|
|
if (isstale == false) isstale = RideFileCache::checkStale(context, this);
|
|
|
|
#ifdef GC_HAVE_LUCENE
|
|
// lucene metadata value ?
|
|
if (isstale == false) {
|
|
|
|
if (context->athlete->emptyindex == true) {
|
|
metacrc = 0; // reset as index missing
|
|
isstale = true;
|
|
|
|
// metadata has changed
|
|
} else if (metacrc != metaCRC()) isstale = true;
|
|
|
|
}
|
|
#endif
|
|
|
|
return isstale;
|
|
}
|
|
|
|
void
|
|
RideItem::refresh()
|
|
{
|
|
if (!isstale) return;
|
|
|
|
// if already open no need to close
|
|
bool doclose = false;
|
|
if (!isOpen()) doclose = true;
|
|
|
|
// open ride file will extract details too, but only if not
|
|
// already open, so we refresh anyway
|
|
RideFile *f = ride();
|
|
if (f) {
|
|
|
|
// get the metadata & metric overrides
|
|
metadata_ = f->tags();
|
|
|
|
// get weight that applies to the date
|
|
getWeight();
|
|
|
|
// first class stuff
|
|
isRun = f->isRun();
|
|
color = context->athlete->colorEngine->colorFor(f->getTag(context->athlete->rideMetadata()->getColorField(), ""));
|
|
present = f->getTag("Data", "");
|
|
|
|
// refresh metrics etc
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
QHash<QString,RideMetricPtr> computed= RideMetric::computeMetrics(context, f, context->athlete->zones(),
|
|
context->athlete->hrZones(), factory.allMetrics());
|
|
|
|
|
|
// ressize and initialize so we can store metric values at
|
|
// RideMetric::index offsets into the metrics_ qvector
|
|
metrics_.fill(0, factory.metricCount());
|
|
|
|
// snaffle away all the computed values into the array
|
|
QHashIterator<QString, RideMetricPtr> i(computed);
|
|
while (i.hasNext()) {
|
|
i.next();
|
|
metrics_[i.value()->index()] = i.value()->value(true);
|
|
}
|
|
|
|
// update current state
|
|
isstale = false;
|
|
|
|
// update fingerprints etc, crc done above
|
|
fingerprint = static_cast<unsigned long>(context->athlete->zones()->getFingerprint(context, dateTime.date()))
|
|
+ static_cast<unsigned long>(context->athlete->paceZones()->getFingerprint(dateTime.date()))
|
|
+ static_cast<unsigned long>(context->athlete->hrZones()->getFingerprint(dateTime.date()));
|
|
|
|
dbversion = DBSchemaVersion;
|
|
timestamp = QDateTime::currentDateTime().toTime_t();
|
|
|
|
// RideFile cache needs refreshing possibly
|
|
RideFileCache updater(context, context->athlete->home->activities().canonicalPath() + "/" + fileName, ride_, true);
|
|
|
|
// and Lucene search texts if they have changed since we
|
|
// last updated lucene for this ride item
|
|
#ifdef GC_HAVE_LUCENE
|
|
unsigned long mc = metaCRC();
|
|
if (metacrc != mc) {
|
|
context->athlete->lucene->importRide(ride_);
|
|
metacrc = mc;
|
|
}
|
|
#endif
|
|
|
|
// close if we opened it
|
|
if (doclose) {
|
|
close();
|
|
}
|
|
|
|
} else {
|
|
qDebug()<<"** FILE READ ERROR: "<<fileName;
|
|
isstale = false;
|
|
}
|
|
}
|
|
|
|
double
|
|
RideItem::getWeight()
|
|
{
|
|
// withings first
|
|
weight = context->athlete->getWithingsWeight(dateTime.date());
|
|
|
|
// from metadata
|
|
if (!weight) weight = metadata_.value("Weight", "0.0").toDouble();
|
|
|
|
// global options
|
|
if (!weight) weight = appsettings->cvalue(context->athlete->cyclist, GC_WEIGHT, "75.0").toString().toDouble(); // default to 75kg
|
|
|
|
// No weight default is weird, we'll set to 80kg
|
|
if (weight <= 0.00) weight = 80.00;
|
|
|
|
return weight;
|
|
}
|
|
|
|
double
|
|
RideItem::getForSymbol(QString name, bool useMetricUnits)
|
|
{
|
|
if (metrics_.size()) {
|
|
// return the precomputed metric value
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
const RideMetric *m = factory.rideMetric(name);
|
|
if (m) {
|
|
if (useMetricUnits) return metrics_[m->index()];
|
|
else {
|
|
// little hack to set/get for conversion
|
|
const_cast<RideMetric*>(m)->setValue(metrics_[m->index()]);
|
|
return m->value(useMetricUnits);
|
|
}
|
|
}
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
QString
|
|
RideItem::getStringForSymbol(QString name, bool useMetricUnits)
|
|
{
|
|
QString returning("-");
|
|
|
|
if (metrics_.size()) {
|
|
// return the precomputed metric value
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
const RideMetric *m = factory.rideMetric(name);
|
|
if (m) {
|
|
|
|
double value = metrics_[m->index()];
|
|
if (std::isinf(value) || std::isnan(value)) value=0;
|
|
const_cast<RideMetric*>(m)->setValue(value);
|
|
returning = m->toString(useMetricUnits);
|
|
}
|
|
}
|
|
return returning;
|
|
}
|