Files
GoldenCheetah/src/IntervalSummaryWindow.cpp
Mark Liversedge 148064ed81 Add Core Temperature
.. New derived data series representing an estimate of core temperature
   on the basis of HR changes.

   * shown on AllPlot and RideSummary
   * 2 new metrics; max and avg core temperature

.. This has been based upon "Estimation of human core temperature from
   sequential heart rate observations" Mark J Buller, William J Tharion,
   Samuel N Cheuvront, Scott J Montain, Robert W Kenefick, John
   Castellani, William A Latzka, Warren S Roberts, Mark Richter,
   Odest Chadwicke Jenkins and Reed W Hoyt. (2013). Physiological
   Measurement. IOP Publishing 34 (2013) 781–798.
2015-06-18 18:41:04 +01:00

321 lines
11 KiB
C++

/*
* Copyright (c) 2011 Eric Brandt (eric.l.brandt@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 "Context.h"
#include "Athlete.h"
#include "RideFile.h"
#include "RideItem.h"
#include "RideMetric.h"
#include "IntervalItem.h"
#include "IntervalTreeView.h"
#include "IntervalSummaryWindow.h"
#include "Settings.h"
#include "TimeUtils.h"
#include "Colors.h"
#include <QStyle>
#include <QStyleFactory>
#include <QScrollBar>
IntervalSummaryWindow::IntervalSummaryWindow(Context *context) : context(context)
{
setWindowTitle(tr("Interval Summary"));
setReadOnly(true);
setFrameStyle(QFrame::NoFrame);
#ifdef Q_OS_WIN
QStyle *cde = QStyleFactory::create(OS_STYLE);
verticalScrollBar()->setStyle(cde);
#endif
#ifdef Q_OS_MAC
setAttribute(Qt::WA_MacShowFocusRect, 0);
#endif
connect(context, SIGNAL(intervalsChanged()), this, SLOT(intervalSelected()));
connect(context, SIGNAL(intervalSelected()), this, SLOT(intervalSelected()));
connect(context, SIGNAL(intervalHover(IntervalItem*)), this, SLOT(intervalHover(IntervalItem*)));
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(intervalSelected()));
setHtml(GCColor::css() + "<body></body>");
}
IntervalSummaryWindow::~IntervalSummaryWindow() {
}
void IntervalSummaryWindow::intervalSelected()
{
// if no ride available don't bother - just reset for color changes
RideItem *rideItem = const_cast<RideItem*>(context->currentRideItem());
if (rideItem == NULL || rideItem->intervalsSelected().count() == 0 || rideItem->ride() == NULL) {
// no ride just update the colors
QString html = GCColor::css();
html += "<body></body>";
setHtml(html);
return;
}
// summary is html
QString html = GCColor::css();
html += "<body>";
// summarise all the intervals selected - this is painful!
// now also summarises for entire ride EXCLUDING the intervals selected
QString notincluding;
if (rideItem->intervalsSelected().count()>1) html += summary(rideItem->intervalsSelected(), notincluding);
// summary for each of the currently selected intervals
foreach(IntervalItem *interval, rideItem->intervalsSelected()) html += summary(interval);
// now add the excluding text
html += notincluding;
if (html == GCColor::css()+"<body>") html += "<i>" + tr("select an interval for summary info") + "</i>";
html += "</body>";
setHtml(html);
return;
}
void
IntervalSummaryWindow::intervalHover(IntervalItem* x)
{
// if we're not visible don't bother
if (!isVisible()) return;
// we already have summaries!
if (x && x->rideItem()->intervalsSelected().count()) return;
// its to clear, but if the current ride has selected intervals then we will ignore it
RideItem *rideItem = const_cast<RideItem*>(context->currentRideItem());
if (!x && rideItem && rideItem->intervalsSelected().count()) return;
QString html = GCColor::css();
html += "<body>";
if (x == NULL) {
html += "<i>" + tr("select an interval for summary info") + "</i>";
} else {
html += summary(x);
}
html += "</body>";
setHtml(html);
return;
}
static bool contains(const RideFile*ride, QList<IntervalItem*> intervals, int index)
{
foreach(IntervalItem *item, intervals) {
int start = ride->timeIndex(item->start);
int end = ride->timeIndex(item->stop);
if (index >= start && index <= end) return true;
}
return false;
}
QString IntervalSummaryWindow::summary(QList<IntervalItem*> intervals, QString &notincluding)
{
// We need to create a special ridefile just for the selected intervals
// to calculate the aggregated metrics because intervals can OVERLAP!
// so we can't just aggregate the pre-computed metrics as this will lead
// to overstated totals and skewed averages.
const RideFile* ride = context->ride ? context->ride->ride() : NULL;
RideFile f(const_cast<RideFile*>(ride));
RideFile notf(const_cast<RideFile*>(ride));
// for concatenating intervals
RideFilePoint *last = NULL;
double timeOff=0;
double distOff=0;
RideFilePoint *notlast = NULL;
double nottimeOff=0;
double notdistOff=0;
for (int i = 0; i < ride->dataPoints().count(); ++i) {
// append points for selected intervals
const RideFilePoint *p = ride->dataPoints()[i];
if (contains(ride, intervals, i)) {
// drag back time/distance for data not included below
if (notlast) {
nottimeOff = p->secs - notlast->secs;
notdistOff = p->km - notlast->km;
} else {
nottimeOff = p->secs;
notdistOff = p->km;
}
f.appendPoint(p->secs-timeOff, p->cad, p->hr, p->km-distOff, p->kph, p->nm,
p->watts, p->alt, p->lon, p->lat, p->headwind, p->slope, p->temp, p->lrbalance,
p->lte, p->rte, p->lps, p->rps,
p->lpco, p->rpco,
p->lppb, p->rppb, p->lppe, p->rppe,
p->lpppb, p->rpppb, p->lpppe, p->rpppe,
p->smo2, p->thb,
p->rvert, p->rcad, p->rcontact, p->tcore, 0);
// derived data
last = f.dataPoints().last();
last->np = p->np;
last->xp = p->xp;
last->apower = p->apower;
} else {
// drag back time/distance for data not included above
if (last) {
timeOff = p->secs - last->secs;
distOff = p->km - last->km;
} else {
timeOff = p->secs;
distOff = p->km;
}
notf.appendPoint(p->secs-nottimeOff, p->cad, p->hr, p->km-notdistOff, p->kph, p->nm,
p->watts, p->alt, p->lon, p->lat, p->headwind, p->slope, p->temp, p->lrbalance,
p->lte, p->rte, p->lps, p->rps,
p->lpco, p->rpco,
p->lppb, p->rppb, p->lppe, p->rppe,
p->lpppb, p->rpppb, p->lpppe, p->rpppe,
p->smo2, p->thb,
p->rvert, p->rcad, p->rcontact, p->tcore, 0);
// derived data
notlast = notf.dataPoints().last();
notlast->np = p->np;
notlast->xp = p->xp;
notlast->apower = p->apower;
}
}
QString s;
if (appsettings->contains(GC_SETTINGS_INTERVAL_METRICS))
s = appsettings->value(this, GC_SETTINGS_INTERVAL_METRICS).toString();
else
s = GC_SETTINGS_INTERVAL_METRICS_DEFAULT;
QStringList intervalMetrics = s.split(",");
const RideMetricFactory &factory = RideMetricFactory::instance();
QHash<QString,RideMetricPtr> metrics =
RideMetric::computeMetrics(context, &f, context->athlete->zones(), context->athlete->hrZones(), intervalMetrics);
QHash<QString,RideMetricPtr> notmetrics =
RideMetric::computeMetrics(context, &notf, context->athlete->zones(), context->athlete->hrZones(), intervalMetrics);
// create temp interval item to use by interval summary
IntervalItem temp;
// pack the metrics away and clean up if needed
temp.metrics().fill(0, factory.metricCount());
// NOTE INCLUDED
// snaffle away all the computed values into the array
QHashIterator<QString, RideMetricPtr> i(metrics);
while (i.hasNext()) {
i.next();
temp.metrics()[i.value()->index()] = i.value()->value();
}
// clean any bad values
for(int j=0; j<factory.metricCount(); j++)
if (std::isinf(temp.metrics()[j]) || std::isnan(temp.metrics()[j]))
temp.metrics()[j] = 0.00f;
// set name
temp.name = QString(tr("%1 selected intervals")).arg(intervals.count());
temp.rideItem_ = intervals.at(0)->rideItem_;
QString returning = summary(&temp);
if (notf.dataPoints().count()) {
// EXCLUDING
// snaffle away all the computed values into the array
// pack the metrics away and clean up if needed
temp.metrics().fill(0, factory.metricCount());
QHashIterator<QString, RideMetricPtr> i(notmetrics);
while (i.hasNext()) {
i.next();
temp.metrics()[i.value()->index()] = i.value()->value();
}
// clean any bad values
for(int j=0; j<factory.metricCount(); j++)
if (std::isinf(temp.metrics()[j]) || std::isnan(temp.metrics()[j]))
temp.metrics()[j] = 0.00f;
// set name
temp.name = QString(tr("Excluding %1 selected")).arg(intervals.count());
temp.rideItem_ = intervals.at(0)->rideItem_;
// use standard method from above
notincluding = summary(&temp);
}
return returning;
}
QString IntervalSummaryWindow::summary(IntervalItem *interval)
{
QString html;
bool useMetricUnits = context->athlete->useMetricUnits;
QString s;
if (appsettings->contains(GC_SETTINGS_INTERVAL_METRICS))
s = appsettings->value(this, GC_SETTINGS_INTERVAL_METRICS).toString();
else
s = GC_SETTINGS_INTERVAL_METRICS_DEFAULT;
QStringList intervalMetrics = s.split(",");
html += "<b>" + interval->name + "</b>";
html += "<table align=\"center\" width=\"90%\" ";
html += "cellspacing=0 border=0>";
RideMetricFactory &factory = RideMetricFactory::instance();
foreach (QString symbol, intervalMetrics) {
const RideMetric *m = factory.rideMetric(symbol);
if (!m) continue;
// skip metrics that are not relevant for this ride
if (!interval->rideItem() || m->isRelevantForRide(interval->rideItem()) == false) continue;
html += "<tr>";
// left column (names)
html += "<td align=\"right\" valign=\"bottom\">" + m->name() + "</td>";
// right column (values)
QString s("<td align=\"center\">%1</td>");
html += s.arg(interval->getStringForSymbol(symbol, useMetricUnits));
html += "<td align=\"left\" valign=\"bottom\">";
if (m->units(useMetricUnits) == "seconds" ||
m->units(useMetricUnits) == tr("seconds"))
; // don't do anything
else if (m->units(useMetricUnits).size() > 0)
html += m->units(useMetricUnits);
html += "</td>";
html += "</tr>";
}
html += "</table>";
return html;
}