mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
Instead of passing the CP value for the rider when opening an ErgFile it now looks up the CP value itself. Simplifies the API.
750 lines
23 KiB
C++
750 lines
23 KiB
C++
/*
|
|
* Copyright (c) 2009 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 "ErgFile.h"
|
|
|
|
#include <stdint.h>
|
|
#include "Units.h"
|
|
|
|
// Supported file types
|
|
static QStringList supported;
|
|
static bool setSupported()
|
|
{
|
|
::supported << ".erg";
|
|
::supported << ".mrc";
|
|
::supported << ".crs";
|
|
::supported << ".pgmf";
|
|
return true;
|
|
}
|
|
static bool isinit = setSupported();
|
|
bool ErgFile::isWorkout(QString name)
|
|
{
|
|
foreach(QString extension, supported) {
|
|
if (name.endsWith(extension, Qt::CaseInsensitive))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
ErgFile::ErgFile(QString filename, int &mode, MainWindow *main) :
|
|
filename(filename), main(main), mode(mode)
|
|
{
|
|
if (main->zones()) {
|
|
int zonerange = main->zones()->whichRange(QDateTime::currentDateTime().date());
|
|
if (zonerange >= 0) CP = main->zones()->getCP(zonerange);
|
|
}
|
|
reload();
|
|
}
|
|
|
|
ErgFile::ErgFile(MainWindow *main) : main(main), mode(nomode)
|
|
{
|
|
if (main->zones()) {
|
|
int zonerange = main->zones()->whichRange(QDateTime::currentDateTime().date());
|
|
if (zonerange >= 0) CP = main->zones()->getCP(zonerange);
|
|
} else {
|
|
CP = 300;
|
|
}
|
|
filename ="";
|
|
}
|
|
|
|
ErgFile *
|
|
ErgFile::fromContent(QString contents, MainWindow *main)
|
|
{
|
|
ErgFile *p = new ErgFile(main);
|
|
p->parseComputrainer(contents);
|
|
return p;
|
|
}
|
|
|
|
void ErgFile::reload()
|
|
{
|
|
// which parser to call? XXX should look at moving to an ergfile factory
|
|
// like we do with ride files if we end up with lots of different formats
|
|
if (filename.endsWith(".pgmf", Qt::CaseInsensitive)) parseTacx();
|
|
else parseComputrainer();
|
|
|
|
}
|
|
|
|
void ErgFile::parseTacx()
|
|
{
|
|
// Initialise
|
|
Version = "";
|
|
Units = "";
|
|
Filename = "";
|
|
Name = "";
|
|
Duration = -1;
|
|
Ftp = 0; // FTP this file was targetted at
|
|
MaxWatts = 0; // maxWatts in this ergfile (scaling)
|
|
valid = false; // did it parse ok?
|
|
rightPoint = leftPoint = 0;
|
|
format = CRS; // default to couse until we know
|
|
Points.clear();
|
|
Laps.clear();
|
|
|
|
// running totals
|
|
double rdist = 0; // running total for distance
|
|
double ralt = 200; // always start at 200 meters just to prettify the graph
|
|
|
|
// open the file for binary reading and open a datastream
|
|
QFile pgmfFile(filename);
|
|
if (pgmfFile.open(QIODevice::ReadOnly) == false) return;
|
|
QDataStream input(&pgmfFile);
|
|
input.setByteOrder(QDataStream::LittleEndian);
|
|
input.setVersion(QDataStream::Qt_4_0); // 32 bit floats not 64 bit.
|
|
|
|
bool happy = true; // are we ok to continue reading?
|
|
|
|
//
|
|
// BASIC DATA STRUCTURES
|
|
//
|
|
struct {
|
|
uint16_t fingerprint;
|
|
uint16_t version;
|
|
uint32_t blocks;
|
|
} header; // file header
|
|
|
|
struct {
|
|
uint16_t type;
|
|
uint16_t version;
|
|
uint32_t records;
|
|
uint32_t recordSize;
|
|
} info; // tells us what to read
|
|
|
|
struct {
|
|
quint32 checksum; // 4
|
|
// we don't use an array for the filename since C++ arrays are prepended by a 16bit size
|
|
quint8 name[34];
|
|
qint32 wattSlopePulse; // 42
|
|
qint32 timeDist; // 46
|
|
double totalTimeDist; // 54
|
|
double energyCons; // 62
|
|
float altStart; // 66
|
|
qint32 brakeCategory; // 70
|
|
} general; // type 1010
|
|
|
|
struct {
|
|
float distance;
|
|
float slope;
|
|
float friction;
|
|
} program; // type 1020
|
|
|
|
//
|
|
// FILE HEADER
|
|
//
|
|
int rc = input.readRawData((char*)&header, sizeof(header));
|
|
if (rc == sizeof(header)) {
|
|
if (header.fingerprint == 1000 && header.version == 100) happy = true;
|
|
else happy = false;
|
|
} else happy = false;
|
|
|
|
unsigned int block = 0; // keep track of how many blocks we have read
|
|
|
|
//
|
|
// READ THE BLOCKS INSIDE THE FILE
|
|
//
|
|
while (happy && block < header.blocks) {
|
|
|
|
// read the info for this block
|
|
rc = input.readRawData((char*)&info, sizeof(info));
|
|
if (rc == sizeof(info)) {
|
|
|
|
// okay now read tha block
|
|
switch (info.type) {
|
|
|
|
case 1010 : // general
|
|
{
|
|
// read it but mostly ignore -- for now
|
|
// we read member by member to avoid struct word alignment problem caused
|
|
// by the filename being 34 bytes long (why didn't they use 32 or 36?)
|
|
input>>general.checksum;
|
|
input.readRawData((char*)&general.name[0], 34);
|
|
input>>general.wattSlopePulse;
|
|
input>>general.timeDist;
|
|
input>>general.totalTimeDist;
|
|
input>>general.energyCons;
|
|
input>>general.altStart;
|
|
input>>general.brakeCategory;
|
|
switch (general.wattSlopePulse) {
|
|
case 0 :
|
|
mode = format = ERG;
|
|
break;
|
|
case 1 :
|
|
mode = format = CRS;
|
|
break;
|
|
default:
|
|
happy = false;
|
|
break;
|
|
}
|
|
ralt = general.altStart;
|
|
}
|
|
break;
|
|
|
|
case 1020 : // program
|
|
{
|
|
// read in the program records
|
|
for (unsigned int record=0; record < info.records; record++) {
|
|
|
|
// get the next record
|
|
if (sizeof(program) != input.readRawData((char*)&program, sizeof(program))) {
|
|
happy = false;
|
|
break;
|
|
}
|
|
|
|
ErgFilePoint add;
|
|
|
|
if (format == CRS) {
|
|
// distance guff
|
|
add.x = rdist;
|
|
double distance = program.distance; // in meters
|
|
rdist += distance;
|
|
|
|
// gradient and altitude
|
|
add.val = program.slope;
|
|
add.y = ralt;
|
|
ralt += (distance * add.val) / 100;
|
|
|
|
} else {
|
|
|
|
add.x = rdist;
|
|
rdist += program.distance * 1000; // 1000ths of a second
|
|
add.val = add.y = program.slope; // its watts now
|
|
}
|
|
|
|
Points.append(add);
|
|
if (add.y > MaxWatts) MaxWatts=add.y;
|
|
|
|
}
|
|
}
|
|
break;
|
|
|
|
default: // unexpected block type
|
|
happy = false;
|
|
break;
|
|
}
|
|
|
|
block++;
|
|
|
|
} else happy = false;
|
|
}
|
|
|
|
// done
|
|
pgmfFile.close();
|
|
|
|
// if we got here and are still happy then it
|
|
// must have been a valid file.
|
|
if (happy) {
|
|
valid = true;
|
|
|
|
// set ErgFile duration
|
|
Duration = Points.last().x; // last is the end point in msecs
|
|
leftPoint = 0;
|
|
rightPoint = 1;
|
|
|
|
// calculate climbing etc
|
|
calculateMetrics();
|
|
}
|
|
}
|
|
|
|
void ErgFile::parseComputrainer(QString p)
|
|
{
|
|
QFile ergFile(filename);
|
|
int section = NOMANSLAND; // section 0=init, 1=header data, 2=course data
|
|
leftPoint=rightPoint=0;
|
|
MaxWatts = Ftp = 0;
|
|
int lapcounter = 0;
|
|
format = ERG; // either ERG or MRC
|
|
Points.clear();
|
|
|
|
// start by assuming the input file is Metric
|
|
bool bIsMetric = true;
|
|
|
|
// running totals for CRS file format
|
|
long rdist = 0; // running total for distance
|
|
long ralt = 200; // always start at 200 meters just to prettify the graph
|
|
|
|
// open the file
|
|
if (p == "" && ergFile.open(QIODevice::ReadOnly | QIODevice::Text) == false) {
|
|
valid = false;
|
|
return;
|
|
}
|
|
// Section markers
|
|
QRegExp startHeader("^.*\\[COURSE HEADER\\].*$", Qt::CaseInsensitive);
|
|
QRegExp endHeader("^.*\\[END COURSE HEADER\\].*$", Qt::CaseInsensitive);
|
|
QRegExp startData("^.*\\[COURSE DATA\\].*$", Qt::CaseInsensitive);
|
|
QRegExp endData("^.*\\[END COURSE DATA\\].*$", Qt::CaseInsensitive);
|
|
// ignore whitespace and support for ';' comments (a GC extension)
|
|
QRegExp ignore("^(;.*|[ \\t\\n]*)$", Qt::CaseInsensitive);
|
|
// workout settings
|
|
QRegExp settings("^([^=]*)=[ \\t]*([^=\\n\\r\\t]*).*$", Qt::CaseInsensitive);
|
|
|
|
// format setting for ergformat
|
|
QRegExp ergformat("^[;]*(MINUTES[ \\t]+WATTS).*$", Qt::CaseInsensitive);
|
|
QRegExp mrcformat("^[;]*(MINUTES[ \\t]+PERCENT).*$", Qt::CaseInsensitive);
|
|
QRegExp crsformat("^[;]*(DISTANCE[ \\t]+GRADE[ \\t]+WIND).*$", Qt::CaseInsensitive);
|
|
|
|
// time watts records
|
|
QRegExp absoluteWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)[ \\t\\n]*$", Qt::CaseInsensitive);
|
|
QRegExp relativeWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)%[ \\t\\n]*$", Qt::CaseInsensitive);
|
|
|
|
// distance slope wind records
|
|
QRegExp absoluteSlope("^[ \\t]*([0-9\\.]+)[ \\t]*([-0-9\\.]+)[ \\t\\n]*([-0-9\\.]+)[ \\t\\n]*$",
|
|
Qt::CaseInsensitive);
|
|
|
|
// Lap marker in an ERG/MRC file
|
|
QRegExp lapmarker("^[ \\t]*([0-9\\.]+)[ \\t]*LAP[ \\t\\n]*(.*)$", Qt::CaseInsensitive);
|
|
QRegExp crslapmarker("^[ \\t]*LAP[ \\t\\n]*(.*)$", Qt::CaseInsensitive);
|
|
|
|
// ok. opened ok lets parse.
|
|
QTextStream inputStream(&ergFile);
|
|
QTextStream stringStream(&p);
|
|
if (p != "") inputStream.setString(&p); // use a string not a file!
|
|
while ((p=="" && !inputStream.atEnd()) || (p!="" && !stringStream.atEnd())) {
|
|
|
|
// Code plagiarised from CsvRideFile.
|
|
// the readLine() method doesn't handle old Macintosh CR line endings
|
|
// this workaround will load the the entire file if it has CR endings
|
|
// then split and loop through each line
|
|
// otherwise, there will be nothing to split and it will read each line as expected.
|
|
QString linesIn = (p != "" ? stringStream.readLine() : ergFile.readLine());
|
|
QStringList lines = linesIn.split('\r');
|
|
|
|
// workaround for empty lines
|
|
if(lines.isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
for (int li = 0; li < lines.size(); ++li) {
|
|
QString line = lines[li];
|
|
|
|
// so what we go then?
|
|
if (startHeader.exactMatch(line)) {
|
|
section = SETTINGS;
|
|
} else if (endHeader.exactMatch(line)) {
|
|
section = NOMANSLAND;
|
|
} else if (startData.exactMatch(line)) {
|
|
section = DATA;
|
|
} else if (endData.exactMatch(line)) {
|
|
section = END;
|
|
} else if (ergformat.exactMatch(line)) {
|
|
// save away the format
|
|
mode = format = ERG;
|
|
} else if (mrcformat.exactMatch(line)) {
|
|
// save away the format
|
|
mode = format = MRC;
|
|
} else if (crsformat.exactMatch(line)) {
|
|
// save away the format
|
|
mode = format = CRS;
|
|
} else if (lapmarker.exactMatch(line)) {
|
|
// lap marker found
|
|
ErgFileLap add;
|
|
|
|
add.x = lapmarker.cap(1).toDouble() * 60000; // from mins to 1000ths of a second
|
|
add.LapNum = ++lapcounter;
|
|
add.name = lapmarker.cap(2).simplified();
|
|
Laps.append(add);
|
|
|
|
} else if (crslapmarker.exactMatch(line)) {
|
|
// new distance lapmarker
|
|
ErgFileLap add;
|
|
|
|
add.x = rdist;
|
|
add.LapNum = ++lapcounter;
|
|
add.name = lapmarker.cap(2).simplified();
|
|
Laps.append(add);
|
|
|
|
} else if (settings.exactMatch(line)) {
|
|
// we have name = value setting
|
|
QRegExp pversion("^VERSION *", Qt::CaseInsensitive);
|
|
if (pversion.exactMatch(settings.cap(1))) Version = settings.cap(2);
|
|
|
|
QRegExp pfilename("^FILE NAME *", Qt::CaseInsensitive);
|
|
if (pfilename.exactMatch(settings.cap(1))) Filename = settings.cap(2);
|
|
|
|
QRegExp pname("^DESCRIPTION *", Qt::CaseInsensitive);
|
|
if (pname.exactMatch(settings.cap(1))) Name = settings.cap(2);
|
|
|
|
QRegExp sname("^SOURCE *", Qt::CaseInsensitive);
|
|
if (sname.exactMatch(settings.cap(1))) Source = settings.cap(2);
|
|
|
|
QRegExp punit("^UNITS *", Qt::CaseInsensitive);
|
|
if (punit.exactMatch(settings.cap(1))) {
|
|
Units = settings.cap(2);
|
|
// UNITS can be ENGLISH or METRIC (miles/km)
|
|
QRegExp penglish(" ENGLISH$", Qt::CaseInsensitive);
|
|
if (penglish.exactMatch(Units)) { // Units <> METRIC
|
|
//qDebug("Setting conversion to ENGLISH");
|
|
bIsMetric = false;
|
|
}
|
|
}
|
|
|
|
QRegExp pftp("^FTP *", Qt::CaseInsensitive);
|
|
if (pftp.exactMatch(settings.cap(1))) Ftp = settings.cap(2).toInt();
|
|
|
|
} else if (absoluteWatts.exactMatch(line)) {
|
|
// we have mins watts line
|
|
ErgFilePoint add;
|
|
|
|
add.x = absoluteWatts.cap(1).toDouble() * 60000; // from mins to 1000ths of a second
|
|
add.val = add.y = round(absoluteWatts.cap(2).toDouble()); // plain watts
|
|
|
|
switch (format) {
|
|
|
|
case ERG: // its an absolute wattage
|
|
if (Ftp) { // adjust if target FTP is set.
|
|
// if ftp is set then convert to the users CP
|
|
|
|
double watts = add.y;
|
|
double ftp = Ftp;
|
|
watts *= CP/ftp;
|
|
add.y = add.val = watts;
|
|
}
|
|
break;
|
|
case MRC: // its a percent relative to CP (mrc file)
|
|
add.y *= CP;
|
|
add.y /= 100.00;
|
|
add.val = add.y;
|
|
break;
|
|
}
|
|
Points.append(add);
|
|
if (add.y > MaxWatts) MaxWatts=add.y;
|
|
|
|
} else if (relativeWatts.exactMatch(line)) {
|
|
|
|
// we have a relative watts match
|
|
ErgFilePoint add;
|
|
add.x = relativeWatts.cap(1).toDouble() * 60000; // from mins to 1000ths of a second
|
|
add.val = add.y = (relativeWatts.cap(2).toDouble() /100.00) * CP;
|
|
Points.append(add);
|
|
if (add.y > MaxWatts) MaxWatts=add.y;
|
|
|
|
} else if (absoluteSlope.exactMatch(line)) {
|
|
// dist, grade, wind strength
|
|
ErgFilePoint add;
|
|
|
|
// distance guff
|
|
add.x = rdist;
|
|
int distance = absoluteSlope.cap(1).toDouble() * 1000; // convert to meters
|
|
|
|
if (!bIsMetric) distance *= KM_PER_MILE;
|
|
rdist += distance;
|
|
|
|
// gradient and altitude
|
|
add.val = absoluteSlope.cap(2).toDouble();
|
|
add.y = ralt;
|
|
ralt += distance * add.val / 100; /* paused */
|
|
|
|
Points.append(add);
|
|
if (add.y > MaxWatts) MaxWatts=add.y;
|
|
|
|
|
|
} else if (ignore.exactMatch(line)) {
|
|
// do nothing for this line
|
|
} else {
|
|
// ignore bad lines for now. just bark.
|
|
//qDebug()<<"huh?" << line;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// done.
|
|
if (p=="") ergFile.close();
|
|
|
|
if (section == END && Points.count() > 0) {
|
|
valid = true;
|
|
|
|
// add the last point for a crs file
|
|
if (mode == CRS) {
|
|
ErgFilePoint add;
|
|
add.x = rdist;
|
|
add.val = 0.0;
|
|
add.y = ralt;
|
|
Points.append(add);
|
|
if (add.y > MaxWatts) MaxWatts=add.y;
|
|
}
|
|
|
|
// add a start point if it doesn't exist
|
|
if (Points.at(0).x > 0) {
|
|
ErgFilePoint add;
|
|
add.x = 0;
|
|
add.y = Points.at(0).y;
|
|
add.val = Points.at(0).val;
|
|
Points.insert(0, add);
|
|
}
|
|
|
|
// set ErgFile duration
|
|
Duration = Points.last().x; // last is the end point in msecs
|
|
|
|
leftPoint = 0;
|
|
rightPoint = 1;
|
|
|
|
calculateMetrics();
|
|
|
|
} else {
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
ErgFile::~ErgFile()
|
|
{
|
|
Points.clear();
|
|
}
|
|
|
|
bool
|
|
ErgFile::isValid()
|
|
{
|
|
return valid;
|
|
}
|
|
|
|
int
|
|
ErgFile::wattsAt(long x, int &lapnum)
|
|
{
|
|
// workout what wattage load should be set for any given
|
|
// point in time in msecs.
|
|
if (!isValid()) return -100; // not a valuid ergfile
|
|
|
|
// is it in bounds?
|
|
if (x < 0 || x > Duration) return -100; // out of bounds!!!
|
|
|
|
// do we need to return the Lap marker?
|
|
if (Laps.count() > 0) {
|
|
int lap=0;
|
|
for (int i=0; i<Laps.count(); i++) {
|
|
if (x>=Laps.at(i).x) lap += 1;
|
|
}
|
|
lapnum = lap;
|
|
|
|
} else lapnum = 0;
|
|
|
|
// find right section of the file
|
|
while (x < Points.at(leftPoint).x || x > Points.at(rightPoint).x) {
|
|
if (x < Points.at(leftPoint).x) {
|
|
leftPoint--;
|
|
rightPoint--;
|
|
} else if (x > Points.at(rightPoint).x) {
|
|
leftPoint++;
|
|
rightPoint++;
|
|
}
|
|
}
|
|
|
|
// two different points in time but the same watts
|
|
// at both, it doesn't really matter which value
|
|
// we use
|
|
if (Points.at(leftPoint).val == Points.at(rightPoint).val)
|
|
return Points.at(rightPoint).val;
|
|
|
|
// the erg file will list the point in time twice
|
|
// to show a jump from one wattage to another
|
|
// at this point in ime (i.e x=100 watts=100 followed
|
|
// by x=100 watts=200)
|
|
if (Points.at(leftPoint).x == Points.at(rightPoint).x)
|
|
return Points.at(rightPoint).val;
|
|
|
|
// so this point in time between two points and
|
|
// we are ramping from one point and another
|
|
// the steps in the calculation have been explicitly
|
|
// listed for code clarity
|
|
double deltaW = Points.at(rightPoint).val - Points.at(leftPoint).val;
|
|
double deltaT = Points.at(rightPoint).x - Points.at(leftPoint).x;
|
|
double offT = x - Points.at(leftPoint).x;
|
|
double factor = offT / deltaT;
|
|
|
|
double nowW = Points.at(leftPoint).val + (deltaW * factor);
|
|
|
|
return nowW;
|
|
}
|
|
|
|
double
|
|
ErgFile::gradientAt(long x, int &lapnum)
|
|
{
|
|
// workout what wattage load should be set for any given
|
|
// point in time in msecs.
|
|
if (!isValid()) return -100; // not a valid ergfile
|
|
|
|
// is it in bounds?
|
|
if (x < 0 || x > Duration) return -100; // out of bounds!!! (-10 through +15 are valid return vals)
|
|
|
|
// do we need to return the Lap marker?
|
|
if (Laps.count() > 0) {
|
|
int lap=0;
|
|
for (int i=0; i<Laps.count(); i++) {
|
|
if (x>=Laps.at(i).x) lap += 1;
|
|
}
|
|
lapnum = lap;
|
|
}
|
|
|
|
// find right section of the file
|
|
while (x < Points.at(leftPoint).x || x > Points.at(rightPoint).x) {
|
|
if (x < Points.at(leftPoint).x) {
|
|
leftPoint--;
|
|
rightPoint--;
|
|
} else if (x > Points.at(rightPoint).x) {
|
|
leftPoint++;
|
|
rightPoint++;
|
|
}
|
|
}
|
|
return Points.at(leftPoint).val;
|
|
}
|
|
|
|
int ErgFile::nextLap(long x)
|
|
{
|
|
if (!isValid()) return -1; // not a valid ergfile
|
|
|
|
// do we need to return the Lap marker?
|
|
if (Laps.count() > 0) {
|
|
for (int i=0; i<Laps.count(); i++) {
|
|
if (x<Laps.at(i).x) return Laps.at(i).x;
|
|
}
|
|
}
|
|
return -1; // nope, no marker ahead of there
|
|
}
|
|
|
|
void
|
|
ErgFile::calculateMetrics()
|
|
{
|
|
|
|
// reset metrics
|
|
XP = CP = AP = NP = IF = RI = TSS = BS = SVI = VI = 0;
|
|
ELE = ELEDIST = GRADE = 0;
|
|
|
|
maxY = 0; // we need to reset it
|
|
|
|
// is it valid?
|
|
if (!isValid()) return;
|
|
|
|
if (format == CRS) {
|
|
|
|
ErgFilePoint last;
|
|
bool first = true;
|
|
foreach (ErgFilePoint p, Points) {
|
|
|
|
// set the maximum Y value
|
|
if (p.y > maxY) maxY= p.y;
|
|
|
|
if (first == true) {
|
|
first = false;
|
|
} else if (p.y > last.y) {
|
|
|
|
ELEDIST += p.x - last.x;
|
|
ELE += p.y - last.y;
|
|
}
|
|
last = p;
|
|
}
|
|
if (ELE == 0 || ELEDIST == 0) GRADE = 0;
|
|
else GRADE = ELE/ELEDIST * 100;
|
|
|
|
} else {
|
|
|
|
QVector<double> rolling(30);
|
|
rolling.fill(0.0f);
|
|
int index = 0;
|
|
double sum = 0; // 30s rolling average
|
|
double apsum = 0; // average power used in VI calculation
|
|
|
|
double total = 0;
|
|
double count = 0;
|
|
long nextSecs = 0;
|
|
|
|
static const double EPSILON = 0.1;
|
|
static const double NEGLIGIBLE = 0.1;
|
|
|
|
double secsDelta = 1;
|
|
double sampsPerWindow = 25.0;
|
|
double attenuation = sampsPerWindow / (sampsPerWindow + secsDelta);
|
|
double sampleWeight = secsDelta / (sampsPerWindow + secsDelta);
|
|
|
|
double lastSecs = 0.0;
|
|
double weighted = 0.0;
|
|
|
|
double sktotal = 0.0;
|
|
int skcount = 0;
|
|
|
|
ErgFilePoint last;
|
|
foreach (ErgFilePoint p, Points) {
|
|
|
|
// set the maximum Y value
|
|
if (p.y > maxY) maxY= p.y;
|
|
|
|
while (nextSecs < p.x) {
|
|
|
|
// CALCULATE NP
|
|
apsum += last.y;
|
|
sum += last.y;
|
|
sum -= rolling[index];
|
|
|
|
// update 30s circular buffer
|
|
rolling[index] = last.y;
|
|
if (index == 29) index=0;
|
|
else index++;
|
|
|
|
total += pow(sum/30, 4);
|
|
count ++;
|
|
|
|
// CALCULATE XPOWER
|
|
while ((weighted > NEGLIGIBLE) && ((nextSecs / 1000) > (lastSecs/1000) + 1000 + EPSILON)) {
|
|
weighted *= attenuation;
|
|
lastSecs += 1000;
|
|
sktotal += pow(weighted, 4.0);
|
|
skcount++;
|
|
}
|
|
weighted *= attenuation;
|
|
weighted += sampleWeight * last.y;
|
|
lastSecs = p.x;
|
|
sktotal += pow(weighted, 4.0);
|
|
skcount++;
|
|
|
|
nextSecs += 1000;
|
|
}
|
|
last = p;
|
|
}
|
|
|
|
// XP, NP and AP
|
|
XP = pow(sktotal / skcount, 0.25);
|
|
NP = pow(total / count, 0.25);
|
|
AP = apsum / count;
|
|
|
|
// CP
|
|
if (main->zones()) {
|
|
int zonerange = main->zones()->whichRange(QDateTime::currentDateTime().date());
|
|
if (zonerange >= 0) CP = main->zones()->getCP(zonerange);
|
|
}
|
|
|
|
// IF
|
|
if (CP) {
|
|
IF = NP / CP;
|
|
RI = XP / CP;
|
|
}
|
|
|
|
// TSS
|
|
double normWork = NP * (Duration / 1000); // msecs
|
|
double rawTSS = normWork * IF;
|
|
double workInAnHourAtCP = CP * 3600;
|
|
TSS = rawTSS / workInAnHourAtCP * 100.0;
|
|
|
|
// BS
|
|
double xWork = XP * (Duration / 1000); // msecs
|
|
double rawBS = xWork * RI;
|
|
BS = rawBS / workInAnHourAtCP * 100.0;
|
|
|
|
// VI and RI
|
|
if (AP) {
|
|
VI = NP / AP;
|
|
SVI = XP / AP;
|
|
}
|
|
}
|
|
}
|