Updating power values of planned activities with linked workouts (#4799)

* Updating power values of workout based planned activities when they
  fall into a timerange with different CP
* Triggers:
  * CP configuration changes (future activities only)
  * planned activities are moved (calendar)
  * schedules are repeated (calendar)
  * rest days are inserted or removed (calendar)
* Additional: Typo in Calendar (show in train _n_ode -> mode)
This commit is contained in:
Joachim Kohlhammer
2026-01-13 05:42:43 +01:00
committed by GitHub
parent 1c69798463
commit 3e8c6fe047
7 changed files with 163 additions and 34 deletions

View File

@@ -31,6 +31,8 @@
#include "HrZones.h"
#include "PaceZones.h"
#include "ErgFile.h"
#include "JsonRideFile.h" // for DATETIME_FORMAT
#ifdef SLOW_REFRESH
@@ -1225,6 +1227,10 @@ RideCache::moveActivity
result.affectedCount = 1;
}
if (item->planned) {
updateFromWorkout(item, false);
}
item->refresh();
context->notifyRideChanged(item);
if (context->ride == item) {
@@ -1540,6 +1546,7 @@ RideCache::shiftPlannedActivities
continue;
}
item->setFileName(plannedDirectory.canonicalPath(), newFileName);
updateFromWorkout(item, true);
item->isstale = true;
RideItem *linkedItem = getLinkedActivity(item);
@@ -1683,6 +1690,99 @@ RideCache::findSuggestion
}
bool
RideCache::updateFromWorkout
(RideItem *item, bool autoSave)
{
if (item == nullptr || ! item->planned) {
return false;
}
QString workoutFilename = item->getText("WorkoutFilename", item->ride()->getTag("WorkoutFilename", "")).trimmed();
if (workoutFilename.isEmpty()) {
return false;
}
ErgFile ergFile(workoutFilename, ErgFileFormat::unknown, context, item->dateTime.date());
if (! ergFile.hasRelativeWatts()) {
return false;
}
bool changed = false;
for (const QString &name : item->overrides_) {
int value = static_cast<int>(item->getForSymbol(name));
// Operate only on the values overridden by ManualActivityWizard
if (name == "average_power") {
if (value != std::round(ergFile.AP())) {
QMap<QString, QString> values;
values.insert("value", QString::number(std::round(ergFile.AP())));
item->ride()->metricOverrides.insert(name, values);
changed = true;
}
} else if (name == "coggan_np") {
if (value != std::round(ergFile.IsoPower())) {
QMap<QString, QString> values;
values.insert("value", QString::number(std::round(ergFile.IsoPower())));
item->ride()->metricOverrides.insert(name, values);
changed = true;
}
} else if (name == "coggan_tss") {
if (value != std::round(ergFile.bikeStress())) {
QMap<QString, QString> values;
values.insert("value", QString::number(std::round(ergFile.bikeStress())));
item->ride()->metricOverrides.insert(name, values);
changed = true;
}
} else if (name == "skiba_bike_score") {
if (value != std::round(ergFile.BS())) {
QMap<QString, QString> values;
values.insert("value", QString::number(std::round(ergFile.BS())));
item->ride()->metricOverrides.insert(name, values);
changed = true;
}
} else if (name == "skiba_xpower") {
if (value != std::round(ergFile.XP())) {
QMap<QString, QString> values;
values.insert("value", QString::number(std::round(ergFile.XP())));
item->ride()->metricOverrides.insert(name, values);
changed = true;
}
}
}
if (changed) {
item->setDirty(true);
item->isstale = true;
if (autoSave) {
QString error;
saveActivity(item, error);
}
}
return changed;
}
bool
RideCache::updateFromWorkoutAfter
(const QDate &when, bool autoSave)
{
QList<RideItem*> changedItems;
for (RideItem *item : context->athlete->rideCache->rides()) {
if (item->planned && item->dateTime.date() >= when) {
if (context->athlete->rideCache->updateFromWorkout(item, false)) {
changedItems << item;
}
}
}
if (changedItems.count() > 0) {
if (autoSave) {
QString error;
saveActivities(changedItems, error);
}
cancel();
refresh();
estimator->refresh();
}
return changedItems.count() > 0;
}
bool
RideCache::isValidLink
(RideItem *item1, RideItem *item2, QString &error)
@@ -1747,6 +1847,7 @@ RideCache::copyPlannedRideFile
delete newRide;
RideItem *newItem = new RideItem(plannedDirectory.canonicalPath(), newFileName, newDateTime, context, true);
updateFromWorkout(newItem, true);
newItem->isstale = true;
return newItem;

View File

@@ -150,6 +150,9 @@ class RideCache : public QObject
RideItem *getLinkedActivity(RideItem *item);
RideItem *findSuggestion(RideItem *rideItem);
bool updateFromWorkout(RideItem *item, bool autoSave = false);
bool updateFromWorkoutAfter(const QDate &when, bool autoSave = false);
public slots:
// restore / dump cache to disk (json)

View File

@@ -205,7 +205,7 @@ CalendarBaseTable::buildContextMenu
}
if (entry.hasTrainMode) {
contextMenu->addSeparator();
contextMenu->addAction(tr("Show in train node..."), this, [this, entry]() { emit showInTrainMode(entry); });
contextMenu->addAction(tr("Show in train mode..."), this, [this, entry]() { emit showInTrainMode(entry); });
}
contextMenu->addSeparator();
contextMenu->addAction(tr("Delete planned activity"), this, [this, entry]() { emit delActivity(entry); });

View File

@@ -26,6 +26,7 @@
#include "Colors.h"
#include "Units.h"
#include "HelpWhatsThis.h"
#include "ErgFile.h"
#include "GcWindowRegistry.h" // for GcWinID types
#include "Perspective.h" // for GcWindowDialog
@@ -329,7 +330,7 @@ LTMSidebar::presetSelectionChanged()
}
void
LTMSidebar::configChanged(qint32)
LTMSidebar::configChanged(qint32 why)
{
seasonsWidget->setStyleSheet(GCColor::stylesheet());
eventsWidget->setStyleSheet(GCColor::stylesheet());
@@ -343,6 +344,9 @@ LTMSidebar::configChanged(qint32)
// let everyone know what date range we are starting with
dateRangeTreeWidgetSelectionChanged();
if (why & CONFIG_ZONES) {
context->athlete->rideCache->updateFromWorkoutAfter(QDate::currentDate(), true);
}
}
/*----------------------------------------------------------------------

View File

@@ -676,12 +676,33 @@ ManualActivityPageWorkout::selectionChanged
QString title = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::displayname), Qt::DisplayRole).toString();
QString type = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::type), Qt::DisplayRole).toString();
QString description = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::description), Qt::DisplayRole).toString();
int avgPower = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::avgPower), Qt::DisplayRole).toInt();
int bikeStress = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::bikestress), Qt::DisplayRole).toInt();
int bikeScore = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::bs), Qt::DisplayRole).toInt();
if (ergFile != nullptr) {
delete ergFile;
ergFile = nullptr;
}
if (type != "code") {
QDate when = field("activityDate").toDate();
ergFile = new ErgFile(filename, ErgFileFormat::unknown, context, when);
if (! ergFile->isValid()) {
delete ergFile;
ergFile = nullptr;
}
}
int avgPower = 0;
int bikeStress = 0;
int bikeScore = 0;
int isoPower = 0;
int xPower = 0;
if (ergFile != nullptr && type == "erg") {
avgPower = static_cast<int>(ergFile->AP());
bikeStress = static_cast<int>(ergFile->bikeStress());
bikeScore = static_cast<int>(ergFile->BS());
isoPower = static_cast<int>(ergFile->IsoPower());
xPower = static_cast<int>(ergFile->XP());
}
int elevationGain = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::elevation), Qt::DisplayRole).toInt();
int isoPower = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::isoPower), Qt::DisplayRole).toInt();
int xPower = workoutModel->data(workoutModel->index(target.row(), TdbWorkoutModelIdx::xp), Qt::DisplayRole).toInt();
setField("woFilename", filename);
setField("woTitle", title);
setField("woFileType", type);
@@ -703,18 +724,6 @@ ManualActivityPageWorkout::selectionChanged
setField("distance", distanceKM * (useMetricUnits ? 1.0 : MILES_PER_KM));
}
if (ergFile != nullptr) {
delete ergFile;
ergFile = nullptr;
}
if (type != "code") {
ergFile = new ErgFile(filename, ErgFileFormat::unknown, context);
if (! ergFile->isValid()) {
delete ergFile;
ergFile = nullptr;
}
}
contentStack->setCurrentIndex(ergFile != nullptr ? 1 : 2);
context->workout = ergFile;
ergFilePlot->setData(ergFile);

View File

@@ -62,28 +62,38 @@ bool ErgFile::isWorkout(QString name)
return false;
}
ErgFile::ErgFile(QString filename, ErgFileFormat mode, Context *context)
ErgFile::ErgFile(QString filename, ErgFileFormat mode, Context *context, QDate when)
: context(context)
{
if (when.isValid()) {
this->when = when;
} else {
this->when = QDate::currentDate();
}
this->filename(filename);
this->mode(mode);
strictGradient(true);
fHasGPS(false);
if (context->athlete->zones("Bike")) {
int zonerange = context->athlete->zones("Bike")->whichRange(QDateTime::currentDateTime().date());
int zonerange = context->athlete->zones("Bike")->whichRange(this->when);
if (zonerange >= 0) CP(context->athlete->zones("Bike")->getCP(zonerange));
}
reload();
}
ErgFile::ErgFile(Context *context)
ErgFile::ErgFile(Context *context, QDate when)
: context(context)
{
if (when.isValid()) {
this->when = when;
} else {
this->when = QDate::currentDate();
}
mode(ErgFileFormat::unknown);
strictGradient(true);
fHasGPS(false);
if (context->athlete->zones("Bike")) {
int zonerange = context->athlete->zones("Bike")->whichRange(QDateTime::currentDateTime().date());
int zonerange = context->athlete->zones("Bike")->whichRange(this->when);
if (zonerange >= 0) CP(context->athlete->zones("Bike")->getCP(zonerange));
} else {
CP(300);
@@ -107,9 +117,9 @@ ErgFile::setFrom(ErgFile *f)
}
ErgFile *
ErgFile::fromContent(QString contents, Context *context)
ErgFile::fromContent(QString contents, Context *context, QDate when)
{
ErgFile *p = new ErgFile(context);
ErgFile *p = new ErgFile(context, when);
p->parseComputrainer(contents);
p->finalize();
@@ -118,9 +128,9 @@ ErgFile::fromContent(QString contents, Context *context)
}
ErgFile *
ErgFile::fromContent2(QString contents, Context *context)
ErgFile::fromContent2(QString contents, Context *context, QDate when)
{
ErgFile *p = new ErgFile(context);
ErgFile *p = new ErgFile(context, when);
p->parseErg2(contents);
p->finalize();
@@ -1235,7 +1245,7 @@ ErgFile::ZoneSections()
QList<ErgFileZoneSection> ret;
const Zones *zones = context->athlete->zones("Bike");
int zoneRange = zones->whichRange(QDate::currentDate());
int zoneRange = zones->whichRange(when);
QList<QString> zoneNames = zones->getZoneNames(zoneRange);
if (hasWatts() && duration() > 0) {
for (int i = 0; i < Points.size() - 1; ++i) {
@@ -1302,7 +1312,7 @@ ErgFile::save(QStringList &errors)
// get CP so we can scale back etc
int lCP=0;
if (context->athlete->zones("Bike")) {
int zonerange = context->athlete->zones("Bike")->whichRange(QDateTime::currentDateTime().date());
int zonerange = context->athlete->zones("Bike")->whichRange(when);
if (zonerange >= 0) lCP = context->athlete->zones("Bike")->getCP(zonerange);
}
@@ -1934,7 +1944,7 @@ ErgFile::calculateMetrics()
bool first = true;
// CP
int zonerange = context->athlete->zones("Bike")->whichRange(QDateTime::currentDateTime().date());
int zonerange = context->athlete->zones("Bike")->whichRange(when);
if (context->athlete->zones("Bike")) {
if (zonerange >= 0) CP(context->athlete->zones("Bike")->getCP(zonerange));
}

View File

@@ -118,8 +118,8 @@ class ErgFileLap
class ErgFile : public ErgFileBase
{
public:
ErgFile(QString, ErgFileFormat, Context *context); // constructor uses filename
ErgFile(Context *context); // no filename, going to use a string
ErgFile(QString, ErgFileFormat, Context *context, QDate when = QDate()); // constructor uses filename
ErgFile(Context *context, QDate when = QDate()); // no filename, going to use a string
~ErgFile(); // delete the contents
@@ -128,8 +128,8 @@ class ErgFile : public ErgFileBase
void setFrom(ErgFile *f); // clone an existing workout
bool save(QStringList &errors); // save back, with changes
static ErgFile *fromContent(QString, Context *); // read from memory *.erg
static ErgFile *fromContent2(QString, Context *); // read from memory *.erg2
static ErgFile *fromContent(QString, Context *, QDate when = QDate()); // read from memory *.erg
static ErgFile *fromContent2(QString, Context *, QDate when = QDate()); // read from memory *.erg2
static bool isWorkout(QString); // is this a supported workout?
@@ -150,6 +150,8 @@ private:
bool coalescedSections = false;
QDate when;
public:
void coalesceSections();
bool hasCoalescedSections() const;