Files
GoldenCheetah/src/FileIO/FixRunningPower.cpp
Ale Martinez 28cbf3b943 Override existing watts on power estimators manual call
Similar to other DPs, it is what the users expect and it is easier
to try with different parameters.
When called automatically existing watts are preserved
to avoid accidental overwrite.
2020-09-20 20:58:07 -03:00

286 lines
12 KiB
C++

/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
* Copyright (c) 2016 Alejandro Martinez (amtriathlon@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 "DataProcessor.h"
#include "Settings.h"
#include "Units.h"
#include "HelpWhatsThis.h"
#include "LocationInterpolation.h"
#include <algorithm>
#include <QVector>
#ifndef MATHCONST_PI
#define MATHCONST_PI 3.141592653589793238462643383279502884L /* pi */
#endif
// Config widget used by the Preferences/Options config panes
class FixRunningPower;
class FixRunningPowerConfig : public DataProcessorConfig
{
Q_DECLARE_TR_FUNCTIONS(FixRunningPowerConfig)
friend class ::FixRunningPower;
protected:
QVBoxLayout *layoutV;
QHBoxLayout *layout;
QHBoxLayout *windLayout;
QLabel *equipWeightLabel;
QDoubleSpinBox *equipWeight;
QLabel *draftMLabel;
QDoubleSpinBox *draftM;
QLabel *windSpeedLabel;
QDoubleSpinBox *windSpeed;
QLabel *windHeadingLabel;
QDoubleSpinBox *windHeading;
public:
FixRunningPowerConfig(QWidget *parent) : DataProcessorConfig(parent) {
HelpWhatsThis *help = new HelpWhatsThis(parent);
parent->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::MenuBar_Edit_EstimatePowerValues));
layout = new QHBoxLayout(this);
layout->setContentsMargins(0,0,0,0);
setContentsMargins(0,0,0,0);
equipWeightLabel = new QLabel(tr("Equipment Weight (kg)"));
draftMLabel = new QLabel(tr("Draft mult."));
windSpeedLabel = new QLabel(tr("Wind (kph)"));
windHeadingLabel = new QLabel(tr(", direction"));
equipWeight = new QDoubleSpinBox();
equipWeight->setMaximum(9.9);
equipWeight->setMinimum(0);
equipWeight->setSingleStep(0.1);
equipWeight->setDecimals(1);
draftM = new QDoubleSpinBox();
draftM->setMaximum(1.0);
draftM->setMinimum(0.1);
draftM->setSingleStep(0.1);
draftM->setDecimals(1);
windSpeed = new QDoubleSpinBox();
windSpeed->setMaximum(99.9);
windSpeed->setMinimum(0);
windSpeed->setSingleStep(0.1);
windSpeed->setDecimals(1);
windHeading = new QDoubleSpinBox();
windHeading->setMaximum(180.0);
windHeading->setMinimum(-179.0);
windHeading->setSingleStep(1);
windHeading->setDecimals(0);
layout->addWidget(equipWeightLabel);
layout->addWidget(equipWeight);
layout->addWidget(draftMLabel);
layout->addWidget(draftM);
layout->addWidget(windSpeedLabel);
layout->addWidget(windSpeed);
layout->addWidget(windHeadingLabel);
layout->addWidget(windHeading);
layout->addStretch();
}
//~FixRunningPowerConfig() {} // deliberately not declared since Qt will delete
// the widget and its children when the config pane is deleted
QString explain() {
return(QString(tr("Derive estimated running power data based on "
"speed/elevation/weight etc using di Prampero "
"coefficcients\n\n"
"Equipment Weight parameter is added to athlete's"
" weight to compound total mass, it should "
"include apparel, shoes, etc\n\n"
"Draft Mult. parameter is the multiplier "
"to adjust for drafting, 1 is no drafting "
" and 0.7 seems legit for drafting in a group\n\n"
"wind speed shall be indicated in kph\n"
"wind direction (origin) unit is degrees "
"from -179 to +180 (-90=W, 0=N, 90=E, 180=S)\n"
"Note: when the file already contains wind data, "
"it will be overridden if wind speed is set\n\n"
"The activity has to be a Run with Speed and "
"Altitude.")));
}
void readConfig() {
double MEquip = appsettings->value(NULL, GC_DPRP_EQUIPWEIGHT, "0.7").toDouble();
equipWeight->setValue(MEquip);
double Draft = appsettings->value(NULL, GC_DPDR_DRAFTM, "1.0").toDouble();
draftM->setValue(Draft);
windSpeed->setValue(0.0);
windHeading->setValue(0.0);
}
void saveConfig() {
appsettings->setValue(GC_DPRP_EQUIPWEIGHT, equipWeight->value());
appsettings->setValue(GC_DPDR_DRAFTM, draftM->value());
}
};
// RideFile Dataprocessor -- Derive estimated running power data based on
// speed/elevation/weight with di Prampero coeff.
//
class FixRunningPower : public DataProcessor {
Q_DECLARE_TR_FUNCTIONS(FixRunningPower)
public:
FixRunningPower() {}
~FixRunningPower() {}
// the processor
bool postProcess(RideFile *, DataProcessorConfig* config, QString op);
// the config widget
DataProcessorConfig* processorConfig(QWidget *parent, const RideFile * ride = NULL) {
Q_UNUSED(ride);
return new FixRunningPowerConfig(parent);
}
// Localized Name
QString name() {
return (tr("Estimate Running Power"));
}
};
static bool FixRunningPowerAdded = DataProcessorFactory::instance().registerProcessor(QString("Estimate Running Power"), new FixRunningPower());
bool
FixRunningPower::postProcess(RideFile *ride, DataProcessorConfig *config=0, QString op="")
{
Q_UNUSED(op)
// get settings
double MEquip; // Equipment weight kg
double DraftM; // Drafting Multiplier
double windSpeed; // wind speed
double windHeading; //wind direction
if (config == NULL) { // being called automatically
MEquip = appsettings->value(NULL, GC_DPRP_EQUIPWEIGHT, "0.7").toDouble();
DraftM = appsettings->value(NULL, GC_DPDR_DRAFTM, "1.0").toDouble();
windSpeed = 0.0;
windHeading = 0.0;
} else { // being called manually
MEquip = ((FixRunningPowerConfig*)(config))->equipWeight->value();
DraftM = ((FixRunningPowerConfig*)(config))->draftM->value();
windSpeed = ((FixRunningPowerConfig*)(config))->windSpeed->value(); // kph
windHeading = ((FixRunningPowerConfig*)(config))->windHeading->value() / 180 * MATHCONST_PI; // rad
}
// if not a run do nothing !
if (!ride->isRun()) return false;
// if called automatically and power already present, do nothing !
if (!config && ride->areDataPresent()->watts) return false;
// no dice if we don't have alt and speed
if (!ride->areDataPresent()->alt || !ride->areDataPresent()->kph) return false;
// Power Estimation Constants (mostly constants...)
double H = ride->getHeight(); //Height in m
double M = ride->getWeight(); //Weight kg
double Mtotal = M + MEquip;
double T = 15; // Temp degC if not in ride data
double W = 0; // headwind (from records or based on wind parameters entered manually)
double bearing = 0.0; //runner direction used to compute headwind
double Cx = 0.9; // Axial force coefficient
double FA = 0.226 * 0.2025 * pow(H, 0.725) * pow(M, 0.425); // Frontal Area
double Cr = 3.72; // di Prampero cost of flat running
double eff = 0.228; // di Prampero efficiency (metabolic to external)
// apply the change
ride->command->startLUW("Estimate Running Power");
if (ride->areDataPresent()->slope) {
for (int i=0; i<ride->dataPoints().count(); i++) {
RideFilePoint *p = ride->dataPoints()[i];
// compute bearing in order to calculate headwind
if (i>=1)
{
RideFilePoint *prevPoint = ride->dataPoints()[i-1];
// ensure a movement occurred and valid lat/lon in order to compute cyclist direction
if (prevPoint->lat != p->lat || prevPoint->lon != p->lon)
{
geolocation prevLoc(prevPoint->lat, prevPoint->lon, prevPoint->alt);
geolocation loc(p->lat, p->lon, p->alt);
if (prevLoc.IsReasonableGeoLocation() && loc.IsReasonableGeoLocation())
{
bearing = prevLoc.BearingTo(loc);
}
}
}
// else keep previous bearing (or 0 at the beginning)
// wind parameters to be considered
if (windSpeed || windHeading) // if wind parameters were entered manually then use it and override rideFile headwind
W = cos(bearing - windHeading) * windSpeed * 0.27777777777778; // Absolute wind speed relative to cyclist orientation (m/s)
else if (ride->areDataPresent()->headwind) // otherwise use headwind from rideFile records (typ. weather forecast included in FIT files)
W = (p->headwind - p->kph) * 0.27777777777778; // Compute absolute wind speed relative to cyclist orientation (m/s)
else
W = 0.0; //otherwise assume no wind
double V = p->kph * 0.27777777777778; // Runner speed m/s
// if the file has temperature, use it
if (ride->areDataPresent()->temp) T = p->temp;
// Air Density based on altitude and temperature
double rho = 353 * exp(-p->alt * .0001253) / (273 + T);
double vw=V+W; // Wind speed against runner = speed + wind speed
double Slope = atan(p->slope * .01);
double forw = Cr * Mtotal * eff * V;
double aero = 0.5 * rho * Cx * FA * DraftM * (vw*vw) * V;
double grav = 9.81 * Mtotal * sin(Slope) * V;
double chgV = p->kphd > 1 ? 1 : p->kphd * Mtotal * V;
// Power = moving forward + aerodynamic + gravity + change of speed
double watts = forw + aero + grav + chgV;
ride->command->setPointValue(i, RideFile::watts, watts > 0 ? (watts > 1000 ? 1000 : watts) : 0);
}
int smoothPoints = 3;
// initialise rolling average
double rtot = 0;
for (int i=smoothPoints; i>0 && ride->dataPoints().count()-i >=0; i--) {
rtot += ride->dataPoints()[ride->dataPoints().count()-i]->watts;
}
// now run backwards setting the rolling average
for (int i=ride->dataPoints().count()-1; i>=smoothPoints; i--) {
double here = ride->dataPoints()[i]->watts;
ride->dataPoints()[i]->watts = rtot / smoothPoints;
if (ride->dataPoints()[i]->watts<0) ride->dataPoints()[i]->watts = 0;
rtot -= here;
rtot += ride->dataPoints()[i-smoothPoints]->watts;
}
ride->setDataPresent(ride->watts, true);
}
ride->command->endLUW();
return true;
}