Workout Editor Show TSS/IF

.. we calculate for ourselves since its probably
   quite expensive to calculate every metric.

.. could look to use the metric factory in the future
   if we want to make these metrics more configurable
This commit is contained in:
Mark Liversedge
2015-12-31 21:10:54 +00:00
parent 135df4b3ab
commit dae514f586
4 changed files with 205 additions and 12 deletions

View File

@@ -183,6 +183,9 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event)
if (state == none) {
updateNeeded = createPoint(p);
// recompute metrics
recompute();
// but we may press and hold for a snip
// so lets set the timer and remember
// where we were
@@ -208,6 +211,9 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event)
state = none;
dragging = NULL;
updateNeeded = true;
// recompute metrics
recompute();
}
//
@@ -221,7 +227,7 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event)
}
//
// 5. MOUSE WHEEL
// 5. MOUSE WHEEL
//
if (event->type() == QEvent::Wheel) {
@@ -236,6 +242,9 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event)
returning = true;
}
// will need to ..
recompute();
}
// ALL DONE
@@ -322,24 +331,32 @@ WorkoutWidget::createPoint(QPoint p)
// add a point!
QPointF to = reverseTransform(p.x(), p.y());
// don't auto append, we are going to insert vvvvv
dragging = new WWPoint(this, to.x(), to.y(), false);
state = drag;
// don't auto append, we are going to insert vvvvv
WWPoint *add = new WWPoint(this, to.x(), to.y(), false);
onDrag = QPointF(dragging->x, dragging->y); // yuk, this should be done in the FSM (eventFilter)
// action at a distance ... yuk XXX TIDY THIS XXX
onDrag = QPointF(add->x, add->y); // yuk, this should be done in the FSM (eventFilter)
// action at a distance ... yuk XXX TIDY THIS XXX
// add into the points
for(int i=0; i<points_.count(); i++) {
if (points_[i]->x > to.x()) {
points_.insert(i, dragging);
points_.insert(i, add);
new CreatePointCommand(this, to.x(), to.y(), i);
// enter drag mode -- add command resets we
// are an edge case so handle it ourselves
state = drag;
dragging = add;
return true;
}
}
// after current
points_.append(dragging);
points_.append(add);
new CreatePointCommand(this, to.x(), to.y(), -1);
// enter drag mode - edge case as above
state = drag;
dragging = add;
return true;
}
@@ -361,7 +378,7 @@ WorkoutWidget::scale(QPoint p)
return true;
}
void
void
WorkoutWidget::ergFileSelected(ErgFile *ergFile)
{
// reset state and stack
@@ -393,9 +410,159 @@ WorkoutWidget::ergFileSelected(ErgFile *ergFile)
maxY_ *= 1.1f;
}
// reset metrics etc
recompute();
// repaint
repaint();
}
void
WorkoutWidget::recompute()
{
//QTime timer;
//timer.start();
int rnum=-1;
if (context->athlete->zones(false) == NULL ||
(rnum = context->athlete->zones(false)->whichRange(QDate::currentDate())) == -1) {
// no cp or ftp set
parent->TSSlabel->setText("- TSS");
parent->IFlabel->setText("- IF");
}
// get CP/FTP to use in calculation
int CP = context->athlete->zones(false)->getCP(rnum);
int FTP = context->athlete->zones(false)->getFTP(rnum);
bool useCPForFTP = (appsettings->cvalue(context->athlete->cyclist,
context->athlete->zones(false)->useCPforFTPSetting(), 0).toInt() == 0);
if (useCPForFTP) FTP=CP;
// compute the metrics based upon the data...
static const int SAMPLERATE=1000;// in ms
QVector<int> wattsArray;
// start from 0s
int lastsecs = 0;
// running time, in seconds
int time = 0;
// aggregating
int ssecs = 0;
int sjoules=0;
// resample the erg file into 1s samples
foreach(WWPoint *p, points_) {
// delta in ms
int dt = (p->x - lastsecs) * SAMPLERATE;
int watts = p->y;
//
// AGGREGATE INTO SAMPLES
//
while (dt > 0) {
// we keep track of how much time has been aggregated
// into sample, so 'need' is whats left to aggregate
// for the full sample
int need = SAMPLERATE - ssecs;
// aggregate
if (dt < need) {
// the entire sample read is less than we need
// so aggregate the whole lot and wait fore more
// data to be read. If there is no more data then
// this will be lost, we don't keep incomplete samples
ssecs += dt;
sjoules += dt * watts;
dt = 0;
} else {
// dt is more than we need to fill and entire sample
// so lets just take the fraction we need
dt -= need;
// accumulating time and distance
time += double(SAMPLERATE) / 1000.0f;
// averaging sample data
sjoules += need * watts;
sjoules /= double(SAMPLERATE);
// add to array
wattsArray.append(sjoules);
// the next sample
ssecs = 0;
sjoules = 0;
}
}
lastsecs = p->x;
}
//
// Now we have an array we can compute metrics from it
// we compute locally for speed rather than use the
// metric compute functions .. might change this if
// it can be done in a performant manner.
//
// The Workout Window has labels for TSS and IF.
double NP=0, TSS=0, IF=0;
// calculating NP
QVector<int> NProlling(30);
NProlling.fill(0,30);
double NPtotal=0;
double NPsum=0;
int NPindex=0;
int NPcount=0;
foreach(int watts, wattsArray) {
//
// Normalised Power
//
// sum last 30secs
NPsum += watts;
NPsum -= NProlling[NPindex];
NProlling[NPindex] = watts;
// running total and count
NPtotal += pow(NPsum/30,4); // raise rolling average to 4th power
NPcount ++;
// it moves up and down during the ride
if (NPcount > 30) {
NP = pow(double(NPtotal) / double(NPcount), 0.25f);
}
// move index on/round
NPindex = (NPindex >= 29) ? 0 : NPindex+1;
}
// IF.....
IF = double(NP) / double(FTP);
// TSS.....
double normWork = NP * NPcount;
double rawTSS = normWork * IF;
double workInAnHourAtCP = FTP * 3600;
TSS = rawTSS / workInAnHourAtCP * 100.0;
parent->IFlabel->setText(QString("%1 IF").arg(IF, 0, 'f', 2));
parent->TSSlabel->setText(QString("%1 TSS").arg(TSS, 0, 'f', 0));
//qDebug()<<"RECOMPUTE:"<<timer.elapsed()<<"ms"<<wattsArray.count()<<"samples";
}
void
WorkoutWidget::configChanged(qint32)
{
@@ -463,7 +630,7 @@ WorkoutWidget::paintEvent(QPaintEvent*)
if (XTICLENGTH) { // we can make the tics disappear
painter.drawLine(QPoint(x, bottom().topLeft().y()),
painter.drawLine(QPoint(x, bottom().topLeft().y()),
QPoint(x, bottom().topLeft().y()+XTICLENGTH));
}
@@ -471,7 +638,7 @@ WorkoutWidget::paintEvent(QPaintEvent*)
QString label = time_to_string(i);
QRect bound = fontMetrics.boundingRect(label);
painter.drawText(QPoint(x - (bound.width() / 2),
bottom().topLeft().y()+fontMetrics.ascent()+XTICLENGTH+(XTICLENGTH ? SPACING : 0)),
bottom().topLeft().y()+fontMetrics.ascent()+XTICLENGTH+(XTICLENGTH ? SPACING : 0)),
label);
}
@@ -620,7 +787,8 @@ WorkoutWidgetCommand::WorkoutWidgetCommand(WorkoutWidget *w) : workoutWidget_(w)
}
// add to the stack, don't execute since it was already executed
// and this is a memento to enable undo / redo
// and this is a memento to enable undo / redo - its a trigger
// to recompute metrics though since the model has been changed
void
WorkoutWidget::addCommand(WorkoutWidgetCommand *cmd)
{
@@ -660,6 +828,10 @@ WorkoutWidget::redo()
if (stackptr > 0) parent->undoAct->setEnabled(true);
if (stackptr >= stack.count()) parent->redoAct->setEnabled(false);
// recompute metrics
recompute();
// update
update();
}
@@ -677,5 +849,9 @@ WorkoutWidget::undo()
if (stackptr <= 0) parent->undoAct->setEnabled(false);
if (stackptr < stack.count()) parent->redoAct->setEnabled(true);
// recompute metrics
recompute();
// update
update();
}

View File

@@ -130,6 +130,9 @@ class WorkoutWidget : public QWidget
// and erg file was selected
void ergFileSelected(ErgFile *);
// recompute metrics etc
void recompute();
// trap signals
void configChanged(qint32);

View File

@@ -96,6 +96,12 @@ WorkoutWindow::WorkoutWindow(Context *context) :
ylabel = new QLabel("150w");
toolbar->addWidget(ylabel);
IFlabel = new QLabel("0 IF");
toolbar->addWidget(IFlabel);
TSSlabel = new QLabel("0 TSS");
toolbar->addWidget(TSSlabel);
#if 0 // not yet!
// get updates..
connect(context, SIGNAL(telemetryUpdate(RealtimeData)), this, SLOT(telemetryUpdate(RealtimeData)));
@@ -120,6 +126,10 @@ WorkoutWindow::configChanged(qint32)
QFontMetrics fm(workout->bigFont);
xlabel->setFont(workout->bigFont);
ylabel->setFont(workout->bigFont);
IFlabel->setFont(workout->bigFont);
TSSlabel->setFont(workout->bigFont);
IFlabel->setFixedWidth(fm.boundingRect(" 0.85 IF ").width());
TSSlabel->setFixedWidth(fm.boundingRect(" 100 TSS ").width());
xlabel->setFixedWidth(fm.boundingRect(" 00:00:00 ").width());
ylabel->setFixedWidth(fm.boundingRect(" 1000w ").width());
@@ -127,6 +137,8 @@ WorkoutWindow::configChanged(qint32)
.arg(GCColor::invertColor(GColor(CPLOTBACKGROUND)).name()));
xlabel->setStyleSheet("color: darkGray;");
ylabel->setStyleSheet("color: darkGray;");
TSSlabel->setStyleSheet("color: darkGray;");
IFlabel->setStyleSheet("color: darkGray;");
repaint();
}

View File

@@ -49,6 +49,8 @@ class WorkoutWindow : public GcWindow
// workout widget updates these
QLabel *xlabel, *ylabel;
QLabel *TSSlabel, *IFlabel;
QAction *saveAct, *undoAct, *redoAct,
*drawAct, *selectAct;