mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
Activities: CV Trends: CV and Speed histogram Library: PMC (TriScore), Time in Pace Zones, Run and Swim Pace
513 lines
16 KiB
C++
513 lines
16 KiB
C++
/*
|
|
* Copyright (c) 2013 Mark Liversedge (liversedge@gmail.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 "Athlete.h"
|
|
|
|
#include "MainWindow.h"
|
|
#include "Context.h"
|
|
#include "Season.h"
|
|
#include "Colors.h"
|
|
#include "RideMetadata.h"
|
|
#include "RideCache.h"
|
|
#include "RideFileCache.h"
|
|
#include "RideMetric.h"
|
|
#include "Settings.h"
|
|
#include "TimeUtils.h"
|
|
#include "Units.h"
|
|
#include "Zones.h"
|
|
#include "HrZones.h"
|
|
#include "PaceZones.h"
|
|
#include "WithingsDownload.h"
|
|
#include "CalendarDownload.h"
|
|
#include "PMCData.h"
|
|
#include "ErgDB.h"
|
|
#ifdef GC_HAVE_ICAL
|
|
#include "ICalendar.h"
|
|
#include "CalDAV.h"
|
|
#endif
|
|
#include "NamedSearch.h"
|
|
#include "IntervalItem.h"
|
|
#include "IntervalTreeView.h"
|
|
#include "LTMSettings.h"
|
|
#include "RideImportWizard.h"
|
|
#include "RideAutoImportConfig.h"
|
|
|
|
#include "Route.h"
|
|
|
|
#include "GcUpgrade.h" // upgrade wizard
|
|
#include "GcCrashDialog.h" // recovering from a crash?
|
|
|
|
Athlete::Athlete(Context *context, const QDir &homeDir)
|
|
{
|
|
// athlete name / structured directory
|
|
this->home = new AthleteDirectoryStructure(homeDir);
|
|
this->context = context;
|
|
context->athlete = this;
|
|
cyclist = this->home->root().dirName();
|
|
|
|
// Recovering from a crash?
|
|
if(!appsettings->cvalue(cyclist, GC_SAFEEXIT, true).toBool()) {
|
|
GcCrashDialog *crashed = new GcCrashDialog(homeDir);
|
|
crashed->exec();
|
|
}
|
|
appsettings->setCValue(cyclist, GC_SAFEEXIT, false); // will be set to true on exit
|
|
|
|
// make sure that the latest folder structure exists in Athlete Directory -
|
|
// e.g. Cache could be deleted by mistake or empty folders are not copied
|
|
// later GC expects the folders are available
|
|
|
|
if (!this->home->subDirsExist()) this->home->createAllSubdirs();
|
|
|
|
// Before we initialise we need to run the upgrade wizard for this athlete
|
|
GcUpgrade v3;
|
|
int returnCode = v3.upgrade(home->root());
|
|
if (returnCode != 0) return;
|
|
|
|
// metric / non-metric
|
|
QVariant unit = appsettings->cvalue(cyclist, GC_UNIT);
|
|
if (unit == 0) {
|
|
// Default to system locale
|
|
unit = appsettings->value(this, GC_UNIT,
|
|
QLocale::system().measurementSystem() == QLocale::MetricSystem ? GC_UNIT_METRIC : GC_UNIT_IMPERIAL);
|
|
appsettings->setCValue(cyclist, GC_UNIT, unit);
|
|
}
|
|
useMetricUnits = (unit.toString() == GC_UNIT_METRIC);
|
|
|
|
// Power Zones
|
|
zones_ = new Zones;
|
|
QFile zonesFile(home->config().canonicalPath() + "/power.zones");
|
|
if (zonesFile.exists()) {
|
|
if (!zones_->read(zonesFile)) {
|
|
QMessageBox::critical(context->mainWindow, tr("Zones File Error"),
|
|
zones_->errorString());
|
|
} else if (! zones_->warningString().isEmpty())
|
|
QMessageBox::warning(context->mainWindow, tr("Reading Zones File"), zones_->warningString());
|
|
}
|
|
|
|
// Heartrate Zones
|
|
hrzones_ = new HrZones;
|
|
QFile hrzonesFile(home->config().canonicalPath() + "/hr.zones");
|
|
if (hrzonesFile.exists()) {
|
|
if (!hrzones_->read(hrzonesFile)) {
|
|
QMessageBox::critical(context->mainWindow, tr("HR Zones File Error"),
|
|
hrzones_->errorString());
|
|
} else if (! hrzones_->warningString().isEmpty())
|
|
QMessageBox::warning(context->mainWindow, tr("Reading HR Zones File"), hrzones_->warningString());
|
|
}
|
|
|
|
// Pace Zones for Run & Swim
|
|
for (int i=0; i < 2; i++) {
|
|
pacezones_[i] = new PaceZones(i>0);
|
|
QFile pacezonesFile(home->config().canonicalPath() + "/" + pacezones_[i]->fileName());
|
|
if (pacezonesFile.exists()) {
|
|
if (!pacezones_[i]->read(pacezonesFile)) {
|
|
QMessageBox::critical(context->mainWindow, tr("Pace Zones File %1 Error").arg(pacezones_[i]->fileName()), pacezones_[i]->errorString());
|
|
}
|
|
}
|
|
}
|
|
|
|
// read athlete's autoimport configuration
|
|
autoImportConfig = new RideAutoImportConfig(home->config());
|
|
|
|
// read athlete's charts.xml and translate etc
|
|
LTMSettings reader;
|
|
reader.readChartXML(context->athlete->home->config(), context->athlete->useMetricUnits, presets);
|
|
translateDefaultCharts(presets);
|
|
|
|
// Search / filter
|
|
namedSearches = new NamedSearches(this); // must be before navigator
|
|
|
|
// Metadata
|
|
rideCache = NULL; // let metadata know we don't have a ridecache yet
|
|
rideMetadata_ = new RideMetadata(context,true);
|
|
rideMetadata_->hide();
|
|
colorEngine = new ColorEngine(context);
|
|
|
|
// Date Ranges
|
|
seasons = new Seasons(home->config());
|
|
|
|
// seconds step of the upgrade - now everything of configuration needed should be in place in Context
|
|
v3.upgradeLate(context);
|
|
|
|
// Routes
|
|
routes = new Routes(context, home->config());
|
|
|
|
// get withings in if there is a cache
|
|
QFile withingsJSON(QString("%1/withings.json").arg(context->athlete->home->cache().canonicalPath()));
|
|
if (withingsJSON.exists() && withingsJSON.open(QFile::ReadOnly)) {
|
|
|
|
QString text;
|
|
QStringList errors;
|
|
QTextStream stream(&withingsJSON);
|
|
text = stream.readAll();
|
|
withingsJSON.close();
|
|
|
|
WithingsParser parser;
|
|
parser.parse(text, errors);
|
|
if (errors.count() == 0) setWithings(parser.readings());
|
|
}
|
|
|
|
// now most dependencies are in get cache
|
|
rideCache = new RideCache(context);
|
|
|
|
// Downloaders
|
|
withingsDownload = new WithingsDownload(context);
|
|
calendarDownload = new CalendarDownload(context);
|
|
|
|
// Calendar
|
|
#ifdef GC_HAVE_ICAL
|
|
rideCalendar = new ICalendar(context); // my local/remote calendar entries
|
|
davCalendar = new CalDAV(context); // remote caldav
|
|
davCalendar->download(true); // refresh the diary window but do not show any error messages
|
|
#endif
|
|
|
|
// trap signals
|
|
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
|
connect(context,SIGNAL(rideAdded(RideItem*)),this,SLOT(checkCPX(RideItem*)));
|
|
connect(context,SIGNAL(rideDeleted(RideItem*)),this,SLOT(checkCPX(RideItem*)));
|
|
}
|
|
|
|
void
|
|
Athlete::close()
|
|
{
|
|
// set to latest so we don't repeat
|
|
appsettings->setCValue(context->athlete->home->root().dirName(), GC_VERSION_USED, VERSION_LATEST);
|
|
appsettings->setCValue(context->athlete->home->root().dirName(), GC_SAFEEXIT, true);
|
|
}
|
|
|
|
Athlete::~Athlete()
|
|
{
|
|
// close the ride cache down first
|
|
delete rideCache;
|
|
|
|
// save those preset charts
|
|
LTMSettings reader;
|
|
reader.writeChartXML(home->config(), presets); // don't write it until we fix the code
|
|
// all the changes to LTM settings and chart config
|
|
// have not been reflected in the charts.xml file
|
|
|
|
delete withingsDownload;
|
|
delete calendarDownload;
|
|
|
|
#ifdef GC_HAVE_ICAL
|
|
delete rideCalendar;
|
|
delete davCalendar;
|
|
#endif
|
|
|
|
delete namedSearches;
|
|
delete routes;
|
|
delete seasons;
|
|
|
|
delete rideMetadata_;
|
|
delete colorEngine;
|
|
delete zones_;
|
|
delete hrzones_;
|
|
for (int i=0; i<2; i++) delete pacezones_[i];
|
|
}
|
|
|
|
void Athlete::selectRideFile(QString fileName)
|
|
{
|
|
// it already is ...
|
|
if (context->ride && context->ride->fileName == fileName) return;
|
|
|
|
// lets find it
|
|
foreach (RideItem *rideItem, rideCache->rides()) {
|
|
|
|
context->ride = (RideItem*) rideItem;
|
|
if (context->ride->fileName == fileName)
|
|
break;
|
|
}
|
|
context->notifyRideSelected(context->ride);
|
|
}
|
|
|
|
void
|
|
Athlete::addRide(QString name, bool dosignal, bool useTempActivities)
|
|
{
|
|
rideCache->addRide(name, dosignal, useTempActivities);
|
|
}
|
|
|
|
void
|
|
Athlete::removeCurrentRide()
|
|
{
|
|
rideCache->removeCurrentRide();
|
|
}
|
|
|
|
|
|
void
|
|
Athlete::checkCPX(RideItem*ride)
|
|
{
|
|
QList<RideFileCache*> newList;
|
|
|
|
foreach(RideFileCache *p, cpxCache) {
|
|
if (ride->dateTime.date() < p->start || ride->dateTime.date() > p->end)
|
|
newList.append(p);
|
|
}
|
|
cpxCache = newList;
|
|
}
|
|
|
|
void
|
|
Athlete::translateDefaultCharts(QList<LTMSettings>&charts)
|
|
{
|
|
// Map default (english) chart name to external (Localized) name
|
|
// New default charts need to be added to this list to be translated
|
|
QMap<QString, QString> chartNameMap;
|
|
chartNameMap.insert("PMC", tr("PMC"));
|
|
chartNameMap.insert("Track Weight", tr("Track Weight"));
|
|
chartNameMap.insert("Time In Power Zone (Stacked)", tr("Time In Power Zone (Stacked)"));
|
|
chartNameMap.insert("Time In Power Zone (Bar)", tr("Time In Power Zone (Bar)"));
|
|
chartNameMap.insert("Time In HR Zone", tr("Time In HR Zone"));
|
|
chartNameMap.insert("Power Distribution", tr("Power Distribution"));
|
|
chartNameMap.insert("KPI Tracker", tr("KPI Tracker"));
|
|
chartNameMap.insert("Critical Power Trend", tr("Critical Power Trend"));
|
|
chartNameMap.insert("Aerobic Power", tr("Aerobic Power"));
|
|
chartNameMap.insert("Aerobic WPK", tr("Aerobic WPK"));
|
|
chartNameMap.insert("Power Variance", tr("Power Variance"));
|
|
chartNameMap.insert("Power Profile", tr("Power Profile"));
|
|
chartNameMap.insert("Anaerobic Power", tr("Anaerobic Power"));
|
|
chartNameMap.insert("Anaerobic WPK", tr("Anaerobic WPK"));
|
|
chartNameMap.insert("Power & Speed Trend", tr("Power & Speed Trend"));
|
|
chartNameMap.insert("Cardiovascular Response", tr("Cardiovascular Response"));
|
|
chartNameMap.insert("Tempo & Threshold Time", tr("Tempo & Threshold Time"));
|
|
chartNameMap.insert("Training Mix", tr("Training Mix"));
|
|
chartNameMap.insert("Time & Distance", tr("Time & Distance"));
|
|
chartNameMap.insert("Skiba Power", tr("Skiba Power"));
|
|
chartNameMap.insert("Daniels Power", tr("Daniels Power"));
|
|
chartNameMap.insert("PM Ramp & Peak", tr("PM Ramp & Peak"));
|
|
chartNameMap.insert("Skiba PM", tr("Skiba PM"));
|
|
chartNameMap.insert("Daniels PM", tr("Daniels PM"));
|
|
chartNameMap.insert("Device Reliability", tr("Device Reliability"));
|
|
chartNameMap.insert("Withings Weight", tr("Withings Weight"));
|
|
chartNameMap.insert("Stress and Distance", tr("Stress and Distance"));
|
|
chartNameMap.insert("Calories vs Duration", tr("Calories vs Duration"));
|
|
chartNameMap.insert("Stress (TISS)", tr("Stress (TISS)"));
|
|
chartNameMap.insert("PMC (Coggan)", tr("PMC (Coggan)"));
|
|
chartNameMap.insert("PMC (Skiba)", tr("PMC (Skiba)"));
|
|
chartNameMap.insert("PMC (TRIMP)", tr("PMC (TRIMP)"));
|
|
chartNameMap.insert("CP History", tr("CP History"));
|
|
chartNameMap.insert("PMC (TriScore)", tr("PMC (TriScore)"));
|
|
chartNameMap.insert("Time in Pace Zones", tr("Time in Pace Zones"));
|
|
chartNameMap.insert("Run Pace", tr("Run Pace"));
|
|
chartNameMap.insert("Swim Pace", tr("Swim Pace"));
|
|
|
|
for(int i=0; i<charts.count(); i++) {
|
|
// Replace chart name for localized version, default to english name
|
|
charts[i].name = chartNameMap.value(charts[i].name, charts[i].name);
|
|
}
|
|
}
|
|
|
|
void
|
|
Athlete::configChanged(qint32 state)
|
|
{
|
|
// change units
|
|
if (state & CONFIG_UNITS) {
|
|
QVariant unit = appsettings->cvalue(cyclist, GC_UNIT);
|
|
useMetricUnits = (unit.toString() == GC_UNIT_METRIC);
|
|
}
|
|
|
|
// invalidate PMC data
|
|
if (state & (CONFIG_PMC | CONFIG_SEASONS)) {
|
|
QMapIterator<QString, PMCData *> pmcs(pmcData);
|
|
pmcs.toFront();
|
|
while(pmcs.hasNext()) {
|
|
pmcs.next();
|
|
pmcs.value()->invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
Athlete::importFilesWhenOpeningAthlete() {
|
|
|
|
// just do it if something is configured
|
|
if (autoImportConfig->hasRules()) {
|
|
|
|
RideImportWizard *import = new RideImportWizard(autoImportConfig, context);
|
|
|
|
// only process the popup if we have any files available at all
|
|
if ( import->getNumberOfFiles() > 0) {
|
|
import->process();
|
|
} else {
|
|
delete import;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
AthleteDirectoryStructure::AthleteDirectoryStructure(const QDir home){
|
|
|
|
myhome = home;
|
|
|
|
athlete_activities = "activities";
|
|
athlete_tmp_activities = "tempActivities";
|
|
athlete_imports = "imports";
|
|
athlete_records = "records";
|
|
athlete_downloads = "downloads";
|
|
athlete_fileBackup = "bak";
|
|
athlete_config = "config";
|
|
athlete_cache = "cache";
|
|
athlete_calendar = "calendar";
|
|
athlete_workouts = "workouts";
|
|
athlete_temp = "temp";
|
|
athlete_logs = "logs";
|
|
athlete_quarantine = "quarantine";
|
|
|
|
|
|
}
|
|
|
|
AthleteDirectoryStructure::~AthleteDirectoryStructure() {
|
|
|
|
myhome = NULL;
|
|
|
|
}
|
|
|
|
void
|
|
AthleteDirectoryStructure::createAllSubdirs() {
|
|
|
|
myhome.mkdir(athlete_activities);
|
|
myhome.mkdir(athlete_tmp_activities);
|
|
myhome.mkdir(athlete_imports);
|
|
myhome.mkdir(athlete_records);
|
|
myhome.mkdir(athlete_downloads);
|
|
myhome.mkdir(athlete_fileBackup);
|
|
myhome.mkdir(athlete_config);
|
|
myhome.mkdir(athlete_cache);
|
|
myhome.mkdir(athlete_calendar);
|
|
myhome.mkdir(athlete_workouts);
|
|
myhome.mkdir(athlete_logs);
|
|
myhome.mkdir(athlete_temp);
|
|
myhome.mkdir(athlete_quarantine);
|
|
|
|
|
|
|
|
}
|
|
|
|
bool
|
|
AthleteDirectoryStructure::subDirsExist() {
|
|
|
|
return (activities().exists() &&
|
|
tmpActivities().exists() &&
|
|
imports().exists() &&
|
|
records().exists() &&
|
|
downloads().exists() &&
|
|
fileBackup().exists() &&
|
|
config().exists() &&
|
|
cache().exists() &&
|
|
calendar().exists() &&
|
|
workouts().exists() &&
|
|
logs().exists() &&
|
|
temp().exists() &&
|
|
quarantine().exists()
|
|
);
|
|
}
|
|
|
|
bool
|
|
AthleteDirectoryStructure::upgradedDirectoriesHaveData() {
|
|
|
|
if ( activities().exists() && config().exists()) {
|
|
QStringList activityFiles = activities().entryList(QDir::Files);
|
|
if (!activityFiles.isEmpty()) { return true; }
|
|
QStringList configFiles = config().entryList(QDir::Files);
|
|
if (!configFiles.isEmpty()) { return true; }
|
|
}
|
|
return false;
|
|
|
|
}
|
|
|
|
// working with withings data
|
|
void
|
|
Athlete::setWithings(QList<WithingsReading>&x)
|
|
{
|
|
withings_ = x;
|
|
qSort(withings_); // date order
|
|
}
|
|
|
|
double
|
|
Athlete::getWithingsWeight(QDate date)
|
|
{
|
|
if (withings_.count() == 0) return 0;
|
|
|
|
double lastWeight=0.0f;
|
|
foreach(WithingsReading x, withings_) {
|
|
if (x.when.date() <= date) lastWeight = x.weightkg;
|
|
if (x.when.date() > date) break;
|
|
}
|
|
return lastWeight;
|
|
}
|
|
|
|
double
|
|
Athlete::getWeight(QDate date, RideFile *ride)
|
|
{
|
|
double weight;
|
|
|
|
// withings first
|
|
weight = getWithingsWeight(date);
|
|
|
|
// ride (if available)
|
|
if (!weight && ride)
|
|
weight = ride->getTag("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
|
|
Athlete::getHeight(RideFile *ride)
|
|
{
|
|
double height = 0;
|
|
|
|
// ride if present?
|
|
if (ride) height = ride->getTag("Height", "0.0").toDouble();
|
|
|
|
// global options ?
|
|
if (!height) height = appsettings->cvalue(context->athlete->cyclist, GC_HEIGHT, 0.0f).toString().toDouble();
|
|
|
|
// from weight via Stillman Average?
|
|
if (!height && ride) height = (getWeight(ride->startTime().date(), ride)+100.0)/98.43;
|
|
|
|
// it must not be zero!!!
|
|
if (!height) height = 1.7526f; // 5'9" is average male height
|
|
|
|
return height;
|
|
}
|
|
|
|
// working with PMC data series
|
|
PMCData *
|
|
Athlete::getPMCFor(QString metricName, int stsdays, int ltsdays)
|
|
{
|
|
PMCData *returning = NULL;
|
|
|
|
// if we don't already have one, create it
|
|
returning = pmcData.value(metricName, NULL);
|
|
if (!returning) {
|
|
|
|
// specification is blank and passes for all
|
|
returning = new PMCData(context, Specification(), metricName, stsdays, ltsdays);
|
|
|
|
// add to our collection
|
|
pmcData.insert(metricName, returning);
|
|
}
|
|
|
|
return returning;
|
|
}
|