/* * 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 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&charts) { // Map default (english) chart name to external (Localized) name // New default charts need to be added to this list to be translated QMap 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; icvalue(cyclist, GC_UNIT); useMetricUnits = (unit.toString() == GC_UNIT_METRIC); } // invalidate PMC data if (state & (CONFIG_PMC | CONFIG_SEASONS)) { QMapIterator 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&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; }