mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-15 00:49:55 +00:00
642 lines
18 KiB
C++
642 lines
18 KiB
C++
/*
|
|
* Copyright (c) 2014 Mark Liversedge (liversedge@gmail.com)
|
|
* Copyright (c) 2014 Andy Froncioni
|
|
*
|
|
* 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 "MUPlot.h"
|
|
#include "MUWidget.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "Zones.h"
|
|
#include "Colors.h"
|
|
|
|
#include <unistd.h>
|
|
#include <QDebug>
|
|
#include <qwt_series_data.h>
|
|
#include <qwt_legend.h>
|
|
#include <qwt_plot_curve.h>
|
|
#include <qwt_plot_grid.h>
|
|
#include <qwt_plot_layout.h>
|
|
#include <qwt_plot_marker.h>
|
|
#include <qwt_symbol.h>
|
|
#include <qwt_scale_engine.h>
|
|
#include <qwt_scale_widget.h>
|
|
#include <qwt_color_map.h>
|
|
#include <algorithm> // for std::lower_bound
|
|
|
|
#include "CriticalPowerWindow.h"
|
|
#include "CPPlot.h"
|
|
#include "GcOverlayWidget.h"
|
|
#include "Settings.h"
|
|
#include "TimeUtils.h"
|
|
|
|
double PI = 3.14159265359f;
|
|
|
|
MUPlot::MUPlot(MUWidget *muw, CriticalPowerWindow *parent, Context *context)
|
|
: QwtPlot(parent), QwtSyntheticPointData(MUN),
|
|
context(context), muw(muw), parent(parent), modelCurve(NULL), slowCurve(NULL), fastCurve(NULL), mmpCurve(NULL)
|
|
{
|
|
|
|
// initalise all the model stufff
|
|
// probably abstract this out later
|
|
muSet = NULL;
|
|
slowHandle = NULL;
|
|
slowLine = NULL;
|
|
slowDrag = false;
|
|
fastHandle = NULL;
|
|
fastLine = NULL;
|
|
fastDrag = false;
|
|
|
|
setAutoDelete(false);
|
|
setAutoFillBackground(true);
|
|
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
|
|
|
|
// left yAxis scale prettify
|
|
QwtScaleDraw *sd = new QwtScaleDraw;
|
|
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
sd->enableComponent(QwtScaleDraw::Ticks, false);
|
|
sd->enableComponent(QwtScaleDraw::Backbone, false);
|
|
setAxisScaleDraw(yLeft, sd);
|
|
setAxisTitle(yLeft, tr("w(x)"));
|
|
setAxisMaxMinor(yLeft, 0);
|
|
plotLayout()->setAlignCanvasToScales(true);
|
|
|
|
// 0.5 increments on bottom axis
|
|
QwtValueList ytick[QwtScaleDiv::NTickTypes];
|
|
for (double i=0.0f; i<=10.0f; i+= 1.0)
|
|
ytick[QwtScaleDiv::MajorTick]<<i;
|
|
setAxisScaleDiv(yLeft,QwtScaleDiv(0.0f,10.0f,ytick));
|
|
|
|
// bottom xAxis scale prettify
|
|
sd = new QwtScaleDraw;
|
|
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
sd->enableComponent(QwtScaleDraw::Ticks, false);
|
|
sd->enableComponent(QwtScaleDraw::Backbone, false);
|
|
setAxisScaleDraw(xBottom, sd);
|
|
setAxisTitle(xBottom, tr("Motor Unit, x"));
|
|
setAxisMaxMinor(xBottom, 0);
|
|
|
|
// 0.2 increments on bottom axis
|
|
QwtValueList xtick[QwtScaleDiv::NTickTypes];
|
|
for (double i=0.0f; i<=1.0f; i+= 0.2)
|
|
xtick[QwtScaleDiv::MajorTick]<<i;
|
|
setAxisScaleDiv(xBottom,QwtScaleDiv(0.0f,1.0f,xtick));
|
|
|
|
// now color everything we created
|
|
configChanged(CONFIG_APPEARANCE);
|
|
|
|
// set to a 2 Normal Model
|
|
setModel(2);
|
|
|
|
// set mouse tracker
|
|
parent->setMouseTracking(true);
|
|
installEventFilter(parent);
|
|
new muMouseTracker(this);
|
|
}
|
|
|
|
// set colours mostly
|
|
void
|
|
MUPlot::configChanged(qint32)
|
|
{
|
|
QPalette palette;
|
|
palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
|
|
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
|
|
setPalette(palette);
|
|
|
|
axisWidget(QwtPlot::xBottom)->setPalette(palette);
|
|
axisWidget(QwtPlot::yLeft)->setPalette(palette);
|
|
|
|
setCanvasBackground(GColor(CPLOTBACKGROUND));
|
|
}
|
|
|
|
// get the fonts and colors right for the axis scales
|
|
void
|
|
MUPlot::setAxisTitle(int axis, QString label)
|
|
{
|
|
// setup the default fonts
|
|
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
|
|
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
|
|
|
QwtText title(label);
|
|
title.setColor(GColor(CPLOTMARKER));
|
|
title.setFont(stGiles);
|
|
QwtPlot::setAxisFont(axis, stGiles);
|
|
QwtPlot::setAxisTitle(axis, title);
|
|
}
|
|
|
|
|
|
double
|
|
MUPlot::y(double x) const
|
|
{
|
|
switch(model) {
|
|
case 0 : return 0;
|
|
case 1 : return slowNormal->y(x);
|
|
default:
|
|
case 2 : return slowNormal->y(x) + fastNormal->y(x); // additive
|
|
}
|
|
}
|
|
|
|
double
|
|
MUPlot::totalY()
|
|
{
|
|
double tot = 0;
|
|
|
|
// sum all points -- only really needed for 2 normal model
|
|
for (int x=0; x<MUN; x++) {
|
|
tot += y(double(x)/MUN);
|
|
}
|
|
return tot;
|
|
}
|
|
|
|
// We are going to support multiple models; normal, normal-skew, linear
|
|
// So for now lets make it a parameter for the model curve creation
|
|
// The model may end up being comprised of 'sub models' for each fibre type
|
|
void
|
|
MUPlot::setModel(int model)
|
|
{
|
|
this->model = model;
|
|
|
|
// first lets clear any curves we shouldn't be displaying
|
|
// no model curve if not power !
|
|
if (modelCurve) {
|
|
modelCurve->detach();
|
|
delete modelCurve;
|
|
modelCurve = NULL;
|
|
}
|
|
|
|
if (slowCurve) {
|
|
slowCurve->detach();
|
|
delete slowCurve;
|
|
slowCurve = NULL;
|
|
}
|
|
|
|
if (slowHandle) {
|
|
slowHandle->detach();
|
|
delete slowHandle;
|
|
slowHandle = NULL;
|
|
}
|
|
|
|
if (slowLine) {
|
|
slowLine->detach();
|
|
delete slowLine;
|
|
slowLine = NULL;
|
|
}
|
|
|
|
if (fastCurve) {
|
|
fastCurve->detach();
|
|
delete fastCurve;
|
|
fastCurve = NULL;
|
|
}
|
|
|
|
if (fastHandle) {
|
|
fastHandle->detach();
|
|
delete fastHandle;
|
|
fastHandle = NULL;
|
|
}
|
|
|
|
if (fastLine) {
|
|
fastLine->detach();
|
|
delete fastLine;
|
|
fastLine = NULL;
|
|
}
|
|
|
|
if (mmpCurve) {
|
|
mmpCurve->detach();
|
|
delete mmpCurve;
|
|
mmpCurve = NULL;
|
|
}
|
|
|
|
switch (model) {
|
|
|
|
case 0 : // no model - do nothing
|
|
{
|
|
}
|
|
break;
|
|
|
|
case 2 : // 2 Normal distribution -- drops through to case 1 below
|
|
{
|
|
fastCurve = new QwtPlotCurve( "MU Distribution" );
|
|
fastCurve->setData(fastNormal = new MUNormal(MU_FASTMEAN, 0.05f));
|
|
fastCurve->attach(this);
|
|
|
|
QColor handleColor = QColor(Qt::magenta).darker(30); // customise (?)
|
|
handleColor.setAlpha(64);
|
|
|
|
// now a mean line
|
|
fastLine = new QwtPlotMarker();
|
|
fastLine->setLineStyle(QwtPlotMarker::VLine);
|
|
fastLine->setLinePen(QPen(QColor(handleColor), 0, Qt::SolidLine));
|
|
fastLine->setValue(MU_FASTMEAN, 0); // mean
|
|
fastLine->setZ(50);
|
|
fastLine->attach(this);
|
|
|
|
// now add a handle
|
|
QColor color = GColor(CPLOTBACKGROUND);
|
|
double x = MU_FASTMEAN; // mean is ok
|
|
double y = 0.05f * 40.0f; // variance scaled to w(x)
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::Rect);
|
|
sym->setSize(8);
|
|
sym->setPen(QPen(handleColor)); // customise ?
|
|
sym->setBrush(QBrush(color));
|
|
|
|
fastHandle = new QwtPlotMarker();
|
|
fastHandle->setValue(x, y);
|
|
fastHandle->setYAxis(yLeft);
|
|
fastHandle->setZ(100);
|
|
fastHandle->setSymbol(sym);
|
|
fastHandle->attach(this);
|
|
|
|
}
|
|
|
|
case 1 : // Normal distribution
|
|
{
|
|
slowCurve = new QwtPlotCurve( "MU Distribution" );
|
|
slowCurve->setData(slowNormal = new MUNormal(MU_SLOWMEAN, 0.05f));
|
|
slowCurve->attach(this);
|
|
|
|
QColor handleColor = GColor(CCP).darker(30);
|
|
handleColor.setAlpha(64);
|
|
|
|
// now a mean line
|
|
slowLine = new QwtPlotMarker();
|
|
slowLine->setLineStyle(QwtPlotMarker::VLine);
|
|
slowLine->setLinePen(QPen(QColor(handleColor), 0, Qt::SolidLine));
|
|
slowLine->setValue(MU_SLOWMEAN, 0); // mean
|
|
slowLine->setZ(50);
|
|
slowLine->attach(this);
|
|
|
|
// now add a handle
|
|
QColor color = GColor(CPLOTBACKGROUND);
|
|
double x = MU_SLOWMEAN; // mean is ok
|
|
double y = 0.05f * 40.0f; // variance scaled to w(x)
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::Rect);
|
|
sym->setSize(8);
|
|
sym->setPen(QPen(handleColor)); // customise ?
|
|
sym->setBrush(QBrush(color));
|
|
|
|
slowHandle = new QwtPlotMarker();
|
|
slowHandle->setValue(x, y);
|
|
slowHandle->setYAxis(yLeft);
|
|
slowHandle->setZ(100);
|
|
slowHandle->setSymbol(sym);
|
|
slowHandle->attach(this);
|
|
|
|
// mmp curve
|
|
mmpCurve = new QwtPlotCurve( "MU Distribution" );
|
|
mmpCurve->attach(parent->cpPlot);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// set the colors etc for our curve(s)
|
|
if (fastCurve) {
|
|
|
|
// antialias
|
|
bool antialias = appsettings->value(this, GC_ANTIALIAS, false).toBool();
|
|
if (antialias) fastCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
|
|
// color and brush
|
|
QColor color = QColor(Qt::magenta); //XXX customise
|
|
QPen pen(color);
|
|
pen.setWidth(1.0);
|
|
|
|
color.setAlpha(80);
|
|
QBrush brush(color);
|
|
|
|
fastCurve->setPen(pen);
|
|
fastCurve->setBrush(brush);
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::Ellipse);
|
|
sym->setSize(4);
|
|
sym->setPen(QPen(Qt::yellow)); // customise ?
|
|
sym->setBrush(QBrush(Qt::yellow));
|
|
}
|
|
|
|
if (slowCurve) {
|
|
|
|
// antialias
|
|
bool antialias = appsettings->value(this, GC_ANTIALIAS, false).toBool();
|
|
if (antialias) slowCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
|
|
// color and brush
|
|
QColor color = GColor(CCP); // customise ?
|
|
QPen pen(color);
|
|
pen.setWidth(1.0);
|
|
|
|
color.setAlpha(80);
|
|
QBrush brush(color);
|
|
|
|
slowCurve->setPen(pen);
|
|
slowCurve->setBrush(brush);
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::Ellipse);
|
|
sym->setSize(4);
|
|
sym->setPen(QPen(Qt::yellow)); // customise ?
|
|
sym->setBrush(QBrush(Qt::yellow));
|
|
|
|
mmpCurve->setPen(QPen(Qt::yellow));
|
|
mmpCurve->setSymbol(sym);
|
|
mmpCurve->setBrush(Qt::NoBrush);
|
|
|
|
// and refresh the MMP
|
|
setMUSet();
|
|
|
|
}
|
|
|
|
if (model) {
|
|
modelCurve = new QwtPlotCurve( "Total Distribution" );
|
|
modelCurve->setData(this); // we provide the data too!
|
|
|
|
QColor color = QColor(Qt::yellow); //XXX customise
|
|
QPen pen(color);
|
|
pen.setWidth(1.0);
|
|
color.setAlpha(80);
|
|
modelCurve->setPen(pen);
|
|
modelCurve->setBrush(Qt::NoBrush);
|
|
|
|
modelCurve->attach(this);
|
|
}
|
|
|
|
|
|
replot();
|
|
}
|
|
|
|
bool
|
|
MUPlot::eventFilter(QObject *, QEvent *e)
|
|
{
|
|
switch (e->type()) {
|
|
|
|
case QEvent::MouseButtonPress:
|
|
{
|
|
if (!slowDrag && !fastDrag) {
|
|
|
|
// where is the mouse ?
|
|
QPoint pos = QCursor::pos();
|
|
|
|
// slow handle ?
|
|
QPoint mpos = canvas()->mapToGlobal(QPoint(transform(xBottom, slowHandle->value().x()),
|
|
transform(yLeft, slowHandle->value().y())));
|
|
|
|
int dx = mpos.x() - pos.x();
|
|
int dy = mpos.y() - pos.y();
|
|
|
|
if ((dx > -6 && dx < 6) && (dy > -6 && dy < 6)) {
|
|
|
|
// close enough
|
|
slowDrag = true;
|
|
return false;
|
|
}
|
|
|
|
// fast handle ?
|
|
mpos = canvas()->mapToGlobal(QPoint(transform(xBottom, fastHandle->value().x()),
|
|
transform(yLeft, fastHandle->value().y())));
|
|
|
|
dx = mpos.x() - pos.x();
|
|
dy = mpos.y() - pos.y();
|
|
|
|
if ((dx > -6 && dx < 6) && (dy > -6 && dy < 6)) {
|
|
|
|
// close enough
|
|
fastDrag = true;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case QEvent::MouseButtonRelease:
|
|
{
|
|
if (fastDrag) {
|
|
fastDrag = false;
|
|
}
|
|
if (slowDrag) {
|
|
slowDrag = false;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MUPlot::mouseMoved()
|
|
{
|
|
if ((!slowDrag && !fastDrag)) return;
|
|
|
|
// where is the mouse ?
|
|
QPoint pos = QCursor::pos();
|
|
|
|
// cursor position
|
|
QPoint cpos = canvas()->mapFromGlobal(pos);
|
|
|
|
double mean = invTransform(xBottom,cpos.x());
|
|
double variance = invTransform(yLeft, cpos.y()) / 40.0f;
|
|
|
|
// dragging slow
|
|
if (fastDrag) {
|
|
|
|
fastNormal->setMean(mean);
|
|
fastNormal->setVariance(variance);
|
|
|
|
// move the handle
|
|
fastHandle->setValue(mean, variance * 40.0f);
|
|
|
|
// move the line
|
|
fastLine->setValue(mean, 0);
|
|
}
|
|
|
|
// dragging slow
|
|
if (slowDrag) {
|
|
|
|
slowNormal->setMean(mean);
|
|
slowNormal->setVariance(variance);
|
|
|
|
// move the handle
|
|
slowHandle->setValue(mean, variance * 40.0f);
|
|
|
|
// move the line
|
|
slowLine->setValue(mean, 0);
|
|
}
|
|
|
|
// redo MMP
|
|
setMUSet();
|
|
|
|
replot();
|
|
}
|
|
|
|
void
|
|
MUPlot::setMUSet()
|
|
{
|
|
// set the MU distribution, starting conditions
|
|
// prior to determining the MMP etc from those
|
|
// starting conditions.
|
|
|
|
// this is a TEMPLATE and is used in assignment and never
|
|
// changed -- this is to speed up creation of worker threads
|
|
// all using the same starting conditions
|
|
|
|
if (muSet == NULL) {
|
|
// allocate the array
|
|
muSet = new QVector<MUPool>(MUN); // MUN is usually set to 1000
|
|
}
|
|
|
|
// we do this every time as the parameters are editable
|
|
// by the user, so they may change from the UI
|
|
|
|
double totaly = totalY(); // add up distribution
|
|
for (int x=0; x<MUN; x++) {
|
|
|
|
(*muSet)[x].twitch = double(x) / double(MUN);
|
|
(*muSet)[x].pmax = muw->pmax0->value() + ((muw->pmax1->value() - muw->pmax0->value()) * (double(x) / double(MUN)));
|
|
(*muSet)[x].pmin = muw->pmin0->value() + ((muw->pmin1->value() - muw->pmin0->value()) * (double(x) / double(MUN)));
|
|
(*muSet)[x].tau = muw->tau0->value() + ((muw->tau1->value() - muw->tau0->value()) * (double(x) / double(MUN)));
|
|
(*muSet)[x].alpha = muw->alpha->value();
|
|
|
|
// get mass from the model
|
|
(*muSet)[x].mass = y(double(x)/double(MUN)) / totaly * muw->mass->value();
|
|
|
|
}
|
|
|
|
// now refresh MMP Curve using the new MU set
|
|
QVector<double> xseries;
|
|
QVector<double> yseries;
|
|
|
|
// for now we use the same worker in serial
|
|
// we might reiplement this as an OpenCL kernel or similar
|
|
// to perform in parallel for performance
|
|
MUSet worker(muSet);
|
|
|
|
xseries << 1.0/60.00f;
|
|
yseries << worker.pMax;
|
|
|
|
int inc = (worker.pMax-worker.pMin) / MUSAMPLES;
|
|
|
|
for (int i=worker.pMax - inc; i > worker.pMin; i -= inc) {
|
|
|
|
double duration = worker.applyLoad(i);
|
|
if (duration) {
|
|
xseries << double(duration) / 60.0f;
|
|
yseries << double(i);
|
|
}
|
|
}
|
|
|
|
xseries << double(3 * 3600)/60.00f;
|
|
yseries << worker.pMin;
|
|
|
|
mmpCurve->setSamples(xseries, yseries);
|
|
|
|
parent->cpPlot->replot();
|
|
}
|
|
|
|
//
|
|
// Working with a set of motor units
|
|
//
|
|
MUSet::MUSet(QVector<MUPool>* set) : muSet(*set)
|
|
{
|
|
setMinMax();
|
|
}
|
|
|
|
// reset the pool
|
|
void
|
|
MUSet::reset(QVector<MUPool>* set)
|
|
{
|
|
muSet = *set;
|
|
setMinMax();
|
|
}
|
|
|
|
void
|
|
MUSet::setMinMax()
|
|
{
|
|
pMax = 0; // whats the max power this distribution can muster?
|
|
pMin = 0; // whats the max power this distribution can muster?
|
|
for (int x=0; x<muSet.size(); x++) {
|
|
pMax += muSet[x].mass * muSet[x].pmax;
|
|
pMin += muSet[x].mass * muSet[x].pmin;
|
|
}
|
|
}
|
|
|
|
// apply load and return maximum duration
|
|
// this load can be applied
|
|
int
|
|
MUSet::applyLoad(int watts)
|
|
{
|
|
// lets not waste our time with out of bound questions
|
|
if (watts > pMax || watts < pMin) return 0;
|
|
|
|
// initialise starting conditions
|
|
QVector<double> pMu(MUN);
|
|
QVector<double> act(MUN);
|
|
for(int x=0; x<MUN; x++) {
|
|
pMu[x] = muSet[x].pmax;
|
|
act[x] = -1;
|
|
}
|
|
|
|
int duration = 0;
|
|
while (duration < (3 * 3600)) { // anything more than 3 hours is too long dude
|
|
|
|
// lets get a second of that power
|
|
double ptot = 0; // how much power did we generate ?
|
|
int x = 0; // how far across the pool did we get ?
|
|
|
|
// lets recruite some fibers!
|
|
while (ptot < watts && x < MUN) {
|
|
|
|
// is this bin fired yet ?
|
|
if (act[x] < 0) { // first time!
|
|
act[x] = duration; // we just fired it!
|
|
}
|
|
|
|
// how long since we fired this sucker ?
|
|
int wasfired = duration - act[x];
|
|
|
|
// if we've passed tau seconds we need to apply some fatigue
|
|
if (wasfired > muSet[x].tau) {
|
|
|
|
// apply fatigue to pmax
|
|
pMu[x] = (pMu[x] - muSet[x].pmin) * exp(-1.0 * muSet[x].alpha * (double(x)/MUN) * (wasfired - muSet[x].tau)) + muSet[x].pmin;
|
|
}
|
|
|
|
// take the power
|
|
ptot += muSet[x].mass * pMu[x];
|
|
|
|
// next bin
|
|
x ++;
|
|
|
|
}
|
|
|
|
// we exhausted fibers
|
|
if (x == MUN) return duration; // we're cooked
|
|
|
|
// well we managed to hang in there for another second !
|
|
duration++;
|
|
}
|
|
|
|
// too long .. return 3 hours
|
|
return duration;
|
|
}
|