mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
.. 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.
321 lines
11 KiB
C++
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 ¬including)
|
|
{
|
|
// 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, ¬f, 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;
|
|
}
|