diff --git a/src/RideSummaryWindow.cpp b/src/RideSummaryWindow.cpp index 47531028a..2b39bd8cc 100644 --- a/src/RideSummaryWindow.cpp +++ b/src/RideSummaryWindow.cpp @@ -85,7 +85,8 @@ RideSummaryWindow::RideSummaryWindow(Context *context, bool ridesummary) : connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideItemChanged())); connect(context->athlete, SIGNAL(zonesChanged()), this, SLOT(refresh())); connect(context, SIGNAL(intervalsChanged()), this, SLOT(refresh())); - connect(context, SIGNAL(compareIntervalsStateChanged(bool)), this, SLOT(compareChanged(bool))); + connect(context, SIGNAL(compareIntervalsStateChanged(bool)), this, SLOT(compareChanged())); + connect(context, SIGNAL(compareIntervalsChanged()), this, SLOT(compareChanged())); } else { @@ -94,7 +95,8 @@ RideSummaryWindow::RideSummaryWindow(Context *context, bool ridesummary) : connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(refresh())); connect(context, SIGNAL(filterChanged()), this, SLOT(refresh())); connect(context, SIGNAL(homeFilterChanged()), this, SLOT(refresh())); - connect(context, SIGNAL(compareDateRangesStateChanged(bool)), this, SLOT(compareChanged(bool))); + connect(context, SIGNAL(compareDateRangesStateChanged(bool)), this, SLOT(compareChanged())); + connect(context, SIGNAL(compareDateRangesChanged()), this, SLOT(compareChanged())); // date settings connect(dateSetting, SIGNAL(useCustomRange(DateRange)), this, SLOT(useCustomRange(DateRange))); @@ -124,16 +126,20 @@ RideSummaryWindow::setFilter(QStringList list) #endif void -RideSummaryWindow::compareChanged(bool state) +RideSummaryWindow::compareChanged() { - // don't do much for now - repaint(); + // don't do much for now -- just refresh + // we get called if the items to compare changed + // or if compare mode is switched off / on + refresh(); + GcWindow::repaint(); } void RideSummaryWindow::rideSelected() { - refresh(); + // so long as you can see me and I'm not in compare mode... + if (!isCompare() && isVisible()) refresh(); } void @@ -158,6 +164,7 @@ RideSummaryWindow::rideItemChanged() void RideSummaryWindow::metadataChanged() { + if (!isCompare() && isVisible()) refresh(); refresh(); } @@ -166,25 +173,52 @@ RideSummaryWindow::refresh() { if (!amVisible()) return; // only if you can see me! - // if we're summarising a ride but have no ride to summarise - if (ridesummary && !myRideItem) { - rideSummary->page()->mainFrame()->setHtml(""); - return; - } + if (isCompare()) { - if (ridesummary) { - RideItem *rideItem = myRideItem; - setSubTitle(rideItem->dateTime.toString(tr("dddd MMMM d, yyyy, h:mm AP"))); - } else { + setSubTitle(tr("Compare")); // fallback to this + if (ridesummary) { - if (myDateRange.name != "") setSubTitle(myDateRange.name); - else { - setSubTitle(myDateRange.from.toString("dddd MMMM d yyyy") + - " - " + - myDateRange.to.toString("dddd MMMM d yyyy")); + if (context->compareIntervals.count() == 2) { + + setSubTitle(QString("%2 on %1 vs %4 on %3") + .arg(context->compareIntervals.at(0).data->startTime().toString("dd MMM yy")) + .arg(context->compareIntervals.at(0).name) + .arg(context->compareIntervals.at(1).data->startTime().toString("dd MMM yy")) + .arg(context->compareIntervals.at(1).name)); + } else if (context->compareIntervals.count() > 2) { + setSubTitle(QString("%2 on %1 vs %3 others") + .arg(context->compareIntervals.at(0).data->startTime().toString("dd MMM yy")) + .arg(context->compareIntervals.at(0).name) + .arg(context->compareIntervals.count()-1)); + } + } else { + + // summary of seasons } + rideSummary->page()->mainFrame()->setHtml(htmlCompareSummary()); + + } else { + // if we're summarising a ride but have no ride to summarise + if (ridesummary && !myRideItem) { + setSubTitle(tr("Summary")); + rideSummary->page()->mainFrame()->setHtml(""); + return; + } + + if (ridesummary) { + RideItem *rideItem = myRideItem; + setSubTitle(rideItem->dateTime.toString(tr("dddd MMMM d, yyyy, h:mm AP"))); + } else { + + if (myDateRange.name != "") setSubTitle(myDateRange.name); + else { + setSubTitle(myDateRange.from.toString("dddd MMMM d yyyy") + + " - " + + myDateRange.to.toString("dddd MMMM d yyyy")); + } + } + rideSummary->page()->mainFrame()->setHtml(htmlSummary()); } - rideSummary->page()->mainFrame()->setHtml(htmlSummary()); } QString @@ -807,6 +841,641 @@ RideSummaryWindow::htmlSummary() const return summary; } +QString +RideSummaryWindow::htmlCompareSummary() const +{ + QString summary; + + // SETUP ALL THE METRICS WE WILL SHOW + + // All the metrics we will display -- same as non compare mode FOR NOW + static const QStringList columnNames = QStringList() << tr("Totals") << tr("Averages") << tr("Maximums") << tr("Metrics*"); + static QStringList totalColumn = QStringList() + << "workout_time" + << "time_riding" + << "total_distance" + << "total_work" + << "elevation_gain"; + + if (!ridesummary) totalColumn << "ride_count"; // number of rides + + static const QStringList rtotalColumn = QStringList() + << "workout_time" + << "total_distance" + << "total_work" + << "elevation_gain"; + + QStringList averageColumn = QStringList() // not const as modified below.. + << "average_speed" + << "average_power" + << "average_hr" + << "average_cad"; + + QStringList maximumColumn = QStringList() // not const as modified below.. + << "max_speed" + << "max_power" + << "max_heartrate" + << "max_cadence"; + +#if 0 // XXX do /any/ of them have temperature -or- do they /all/ need to ??? + // show average and max temp if it is available (in ride summary mode) + if (ridesummary && (ride->areDataPresent()->temp || ride->getTag("Temperature", "-") != "-")) { + averageColumn << "average_temp"; + maximumColumn << "max_temp"; + } +#endif + + // users determine the metrics to display + QString s = appsettings->value(this, GC_SETTINGS_SUMMARY_METRICS, GC_SETTINGS_SUMMARY_METRICS_DEFAULT).toString(); + if (s == "") s = GC_SETTINGS_SUMMARY_METRICS_DEFAULT; + QStringList metricColumn = s.split(","); + + s = appsettings->value(this, GC_SETTINGS_BESTS_METRICS, GC_SETTINGS_BESTS_METRICS_DEFAULT).toString(); + if (s == "") s = GC_SETTINGS_BESTS_METRICS_DEFAULT; + QStringList bestsColumn = s.split(","); + + static const QStringList timeInZones = QStringList() + << "time_in_zone_L1" + << "time_in_zone_L2" + << "time_in_zone_L3" + << "time_in_zone_L4" + << "time_in_zone_L5" + << "time_in_zone_L6" + << "time_in_zone_L7" + << "time_in_zone_L8" + << "time_in_zone_L9" + << "time_in_zone_L10"; + + static const QStringList timeInZonesHR = QStringList() + << "time_in_zone_H1" + << "time_in_zone_H2" + << "time_in_zone_H3" + << "time_in_zone_H4" + << "time_in_zone_H5" + << "time_in_zone_H6" + << "time_in_zone_H7" + << "time_in_zone_H8"; + + if (ridesummary) { + + // + // SUMMARISING INTERVALS SO ALWAYS COMPUTE METRICS ON DEMAND + // + QList intervalMetrics; + + QStringList worklist; + worklist += totalColumn; + worklist += averageColumn; + worklist += maximumColumn; + worklist += metricColumn; + worklist += timeInZones; + worklist += timeInZonesHR; + + // go calculate them then... + RideMetricFactory &factory = RideMetricFactory::instance(); + for (int j=0; jcompareIntervals.count(); j++) { + + SummaryMetrics metrics; + + // calculate using the source context of course! + QHash computed = RideMetric::computeMetrics( + context->compareIntervals.at(j).sourceContext, + context->compareIntervals.at(j).data, + context->compareIntervals.at(j).sourceContext->athlete->zones(), + context->compareIntervals.at(j).sourceContext->athlete->hrZones(), + worklist); + + for(int i = 0; i < worklist.count(); ++i) { + if (worklist[i] != "") { + RideMetricPtr m = computed.value(worklist[i]); + if (m) metrics.setForSymbol(worklist[i], m->value(true)); + else metrics.setForSymbol(worklist[i], 0.00); + } + } + intervalMetrics << metrics; + } + + // LETS FORMAT THE HTML + summary = "
"; + + // used for alternate shding + QColor color = QApplication::palette().alternateBase().color(); + color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); + + // + // TOTALS, AVERAGES, MAX, METRICS + // + for (int v = 0; v < columnNames.count(); ++v) { + + QString columnName; + QStringList metricsList; + + switch (v) { // slightly different order when summarising for compare... + // we want the metrics towards the top of the screen as they + // are more important than the maximums (generally anyway) + + case 0: metricsList = totalColumn; columnName = columnNames[0]; break; + case 1: metricsList = metricColumn; columnName = columnNames[3]; break; + case 2: metricsList = averageColumn; columnName = columnNames[1]; break; + default: + case 3: metricsList = maximumColumn; columnName = columnNames[2]; break; + } + + // + // Repeat for each 'column' (now separate paragraphs) + // + summary += "

" + columnName + "

"; + + // table of metrics + summary += ""; + + // first row is a row of headings + summary += ""; + summary += ""; // removed the text as its blinking obvious.. but left code in + // case we ever come back here or use it for other things. + summary += ""; // spacing + + foreach (QString symbol, metricsList) { + const RideMetric *m = factory.rideMetric(symbol); + + QString name, units; + if (m->units(context->athlete->useMetricUnits) != "seconds") units = m->units(context->athlete->useMetricUnits); + if (units != "") name = QString("%1 (%2)").arg(m->name()).arg(units); + else name = QString("%1").arg(m->name()); + + name = name.replace(QRegExp(tr("^(Average|Max) ")), ""); // average/max on average/max is dumb + summary += ""; + summary += ""; // spacing + } + summary += ""; + + // then one row for each interval + int counter = 0; + foreach (SummaryMetrics metrics, intervalMetrics) { + + // alternating shading + if (counter%2) summary += ""; + else summary += ""; + + summary += ""; + summary += ""; // spacing + + foreach (QString symbol, metricsList) { + + // the values ... + const RideMetric *m = factory.rideMetric(symbol); + + // get value and convert if needed (use local context for units) + double value = metrics.getForSymbol(symbol) + * (context->athlete->useMetricUnits ? 1 : m->conversion()) + + (context->athlete->useMetricUnits ? 0 : m->conversionSum()); + + // use right precision + QString strValue = QString("%1").arg(value, 0, 'f', m->precision()); + + // or maybe its a duration (worry about local lang or translated) + if (m->units(true) == "seconds" || m->units(true) == tr("seconds")) + strValue = time_to_string((int)value); + + summary += ""; + + // delta to first entry + if (counter) { + + // calculate me vs the original + double value0 = intervalMetrics[0].getForSymbol(symbol) + * (context->athlete->useMetricUnits ? 1 : m->conversion()) + + (context->athlete->useMetricUnits ? 0 : m->conversionSum()); + + value -= value0; // delta + + // use right precision + QString strValue = QString("%1%2").arg(value >= 0 ? "+" : "") // - sign added anyway + .arg(value, 0, 'f', m->precision()); + + // or maybe its a duration (worry about local lang or translated) + if (m->units(true) == "seconds" || m->units(true) == tr("seconds")) + strValue = QString(value >= 0 ? "+" : "-" ) + time_to_string(fabs(value)); + + summary += ""; + } else { + summary += ""; + } + summary += ""; // spacing + + } + summary += ""; + counter++; + } + summary += "
 " + name + " 
" + context->compareIntervals[counter].name + " " + strValue + "" + strValue + " 
"; + } + + // + // TIME IN POWER ZONES + // + if (context->athlete->zones()) { // use my zones + + // get from end if period + int rangeidx = context->athlete->zones()->whichRange(QDate::currentDate()); // use current zone names et al + if (rangeidx > -1) { + + // get the list of zones + ZoneRange range = const_cast(context->athlete->zones())->getZoneRange(rangeidx); + QList zones = range.zones; + + // we've got a range and a count of zones so all is well + // we need to throw up a table of time in zone for each interval + summary += tr("

Power Zones

"); + summary += ""; + + // lets get some headings + summary += ""; // ne need to have a heading for the interval name + summary += ""; // spacing + + foreach (ZoneInfo zone, zones) { + summary += QString("").arg(zone.desc).arg(zone.name); + summary += ""; // spacing + } + summary += ""; + + // now the sumamry + int counter = 0; + foreach (SummaryMetrics metrics, intervalMetrics) { + + if (counter%2) summary += ""; + else summary += ""; + + summary += ""; + summary += ""; // spacing + + int idx=0; + foreach (ZoneInfo zone, zones) { + + int timeZone = metrics.getForSymbol(timeInZones[idx]); + int dt = timeZone - intervalMetrics[0].getForSymbol(timeInZones[idx]); + idx++; + + // time and then +time + summary += QString("").arg(time_to_string(timeZone)); + + if (counter) summary += QString("") + .arg(dt>0 ? "+" : "-") + .arg(time_to_string(fabs(dt))); + + else summary += ""; + + summary += ""; // spacing + + } + summary += ""; + counter++; + } + + // done + summary += "
 %1 (%2) 
" + context->compareIntervals[counter].name + " %1%1%2 
"; + } + } + + // + // TIME IN HR ZONES + // + if (context->athlete->hrZones()) { // use my zones + + // get from end if period + int rangeidx = context->athlete->hrZones()->whichRange(QDate::currentDate()); // use current zone names et al + if (rangeidx > -1) { + + // get the list of zones + HrZoneRange range = const_cast(context->athlete->hrZones())->getHrZoneRange(rangeidx); + QList zones = range.zones; + + // we've got a range and a count of zones so all is well + // we need to throw up a table of time in zone for each interval + summary += tr("

Heartrate Zones

"); + summary += ""; + + // lets get some headings + summary += ""; // ne need to have a heading for the interval name + summary += ""; // spacing + + foreach (HrZoneInfo zone, zones) { + summary += QString("").arg(zone.desc).arg(zone.name); + summary += ""; // spacing + } + summary += ""; + + // now the sumamry + int counter = 0; + foreach (SummaryMetrics metrics, intervalMetrics) { + + if (counter%2) summary += ""; + else summary += ""; + + summary += ""; + summary += ""; // spacing + + int idx=0; + foreach (HrZoneInfo zone, zones) { + + int timeZone = metrics.getForSymbol(timeInZonesHR[idx]); + int dt = timeZone - intervalMetrics[0].getForSymbol(timeInZonesHR[idx]); + idx++; + + // time and then +time + summary += QString("").arg(time_to_string(timeZone)); + + if (counter) summary += QString("") + .arg(dt>0 ? "+" : "-") + .arg(time_to_string(fabs(dt))); + + else summary += ""; + + summary += ""; // spacing + + } + summary += ""; + counter++; + } + + // done + summary += "
 %1 (%2) 
" + context->compareIntervals[counter].name + " %1%1%2 
"; + } + } + + + } else { // DATE RANGE COMPARE + + // LETS FORMAT THE HTML + summary = "
"; + + // get metric details here ... + RideMetricFactory &factory = RideMetricFactory::instance(); + + // used for alternate shding + QColor color = QApplication::palette().alternateBase().color(); + color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); + + // + // TOTALS, AVERAGES, MAX, METRICS + // + for (int v = 0; v < columnNames.count(); ++v) { + + QString columnName; + QStringList metricsList; + + switch (v) { // slightly different order when summarising for compare... + // we want the metrics towards the top of the screen as they + // are more important than the maximums (generally anyway) + + case 0: metricsList = totalColumn; columnName = columnNames[0]; break; + case 1: metricsList = metricColumn; columnName = columnNames[3]; break; + case 2: metricsList = averageColumn; columnName = columnNames[1]; break; + default: + case 3: metricsList = maximumColumn; columnName = columnNames[2]; break; + } + + // + // Repeat for each 'column' (now separate paragraphs) + // + summary += "

" + columnName + "

"; + + // table of metrics + summary += ""; + + // first row is a row of headings + summary += ""; + summary += ""; // removed the text as its blinking obvious.. but left code in + // case we ever come back here or use it for other things. + summary += ""; // spacing + + foreach (QString symbol, metricsList) { + const RideMetric *m = factory.rideMetric(symbol); + + QString name, units; + if (m->units(context->athlete->useMetricUnits) != "seconds") units = m->units(context->athlete->useMetricUnits); + if (units != "") name = QString("%1 (%2)").arg(m->name()).arg(units); + else name = QString("%1").arg(m->name()); + + name = name.replace(QRegExp(tr("^(Average|Max) ")), ""); // average/max on average/max is dumb + summary += ""; + summary += ""; // spacing + } + summary += ""; + + // then one row for each interval + int counter = 0; + foreach (CompareDateRange dr, context->compareDateRanges) { + + // alternating shading + if (counter%2) summary += ""; + else summary += ""; + + summary += ""; + summary += ""; // spacing + + foreach (QString symbol, metricsList) { + + // the values ... + const RideMetric *m = factory.rideMetric(symbol); + + // get value and convert if needed (use local context for units) + double value = SummaryMetrics::getAggregated(context, symbol, dr.metrics, QStringList(), false, + context->athlete->useMetricUnits, true).toDouble(); + + // use right precision + QString strValue = QString("%1").arg(value, 0, 'f', m->precision()); + + // or maybe its a duration (worry about local lang or translated) + if (m->units(true) == "seconds" || m->units(true) == tr("seconds")) + strValue = time_to_string((int)value); + + summary += ""; + + // delta to first entry + if (counter) { + + // calculate me vs the original + double value0 = SummaryMetrics::getAggregated(context, symbol, + context->compareDateRanges[0].metrics, QStringList(), false, + context->athlete->useMetricUnits, true).toDouble(); + + value -= value0; // delta + + // use right precision + QString strValue = QString("%1%2").arg(value >= 0 ? "+" : "") // - sign added anyway + .arg(value, 0, 'f', m->precision()); + + // or maybe its a duration (worry about local lang or translated) + if (m->units(true) == "seconds" || m->units(true) == tr("seconds")) + strValue = QString(value >= 0 ? "+" : "-" ) + time_to_string(fabs(value)); + + summary += ""; + } else { + summary += ""; + } + summary += ""; // spacing + + } + summary += ""; + counter++; + } + summary += "
 " + name + " 
" + dr.name + " " + strValue + "" + strValue + " 
"; + } + + // + // TIME IN POWER ZONES + // + if (context->athlete->zones()) { // use my zones + + // get from end if period + int rangeidx = context->athlete->zones()->whichRange(QDate::currentDate()); // use current zone names et al + if (rangeidx > -1) { + + // get the list of zones + ZoneRange range = const_cast(context->athlete->zones())->getZoneRange(rangeidx); + QList zones = range.zones; + + // we've got a range and a count of zones so all is well + // we need to throw up a table of time in zone for each interval + summary += tr("

Power Zones

"); + summary += ""; + + // lets get some headings + summary += ""; // ne need to have a heading for the interval name + summary += ""; // spacing + + foreach (ZoneInfo zone, zones) { + summary += QString("").arg(zone.desc).arg(zone.name); + summary += ""; // spacing + } + summary += ""; + + // now the sumamry + int counter = 0; + foreach (CompareDateRange dr, context->compareDateRanges) { + + if (counter%2) summary += ""; + else summary += ""; + + summary += ""; + summary += ""; // spacing + + int idx=0; + foreach (ZoneInfo zone, zones) { + + int timeZone = SummaryMetrics::getAggregated(context, timeInZones[idx], dr.metrics, QStringList(), false, + context->athlete->useMetricUnits, true).toInt(); + + int dt = timeZone - SummaryMetrics::getAggregated(context, timeInZones[idx], + context->compareDateRanges[0].metrics, QStringList(), false, + context->athlete->useMetricUnits, true).toInt(); + idx++; + + // time and then +time + summary += QString("").arg(time_to_string(timeZone)); + + if (counter) summary += QString("") + .arg(dt>0 ? "+" : "-") + .arg(time_to_string(fabs(dt))); + + else summary += ""; + + summary += ""; // spacing + + } + summary += ""; + counter++; + } + + // done + summary += "
 %1 (%2) 
" + dr.name + " %1%1%2 
"; + } + } + + // + // TIME IN HR ZONES + // + if (context->athlete->hrZones()) { // use my zones + + // get from end if period + int rangeidx = context->athlete->hrZones()->whichRange(QDate::currentDate()); // use current zone names et al + if (rangeidx > -1) { + + // get the list of zones + HrZoneRange range = const_cast(context->athlete->hrZones())->getHrZoneRange(rangeidx); + QList zones = range.zones; + + // we've got a range and a count of zones so all is well + // we need to throw up a table of time in zone for each interval + summary += tr("

Heartrate Zones

"); + summary += ""; + + // lets get some headings + summary += ""; // ne need to have a heading for the interval name + summary += ""; // spacing + + foreach (HrZoneInfo zone, zones) { + summary += QString("").arg(zone.desc).arg(zone.name); + summary += ""; // spacing + } + summary += ""; + + // now the sumamry + int counter = 0; + foreach (CompareDateRange dr, context->compareDateRanges) { + + if (counter%2) summary += ""; + else summary += ""; + + summary += ""; + summary += ""; // spacing + + int idx=0; + foreach (HrZoneInfo zone, zones) { + + int timeZone = SummaryMetrics::getAggregated(context, timeInZonesHR[idx], dr.metrics, QStringList(), false, + context->athlete->useMetricUnits, true).toInt(); + + int dt = timeZone - SummaryMetrics::getAggregated(context, timeInZonesHR[idx], + context->compareDateRanges[0].metrics, QStringList(), false, + context->athlete->useMetricUnits, true).toInt(); + idx++; + + // time and then +time + summary += QString("").arg(time_to_string(timeZone)); + + if (counter) summary += QString("") + .arg(dt>0 ? "+" : "-") + .arg(time_to_string(fabs(dt))); + + else summary += ""; + + summary += ""; // spacing + + } + summary += ""; + counter++; + } + + // done + summary += "
 %1 (%2) 
" + dr.name + " %1%1%2 
"; + } + } + + } + + // add the usual disclaimers etc at the bottom + summary += "

"; + + // The extra
works around a bug in QT 4.3.1, + // which will otherwise put the following above the
. + summary += tr("
BikeScore is a trademark of Dr. Philip " + "Friere Skiba, PhysFarm Training Systems LLC"); + + summary += tr("
TSS, NP and IF are trademarks of Peaksware LLC
"); + return summary; +} + void RideSummaryWindow::useCustomRange(DateRange range) { diff --git a/src/RideSummaryWindow.h b/src/RideSummaryWindow.h index 79aba163e..90cb6d0d4 100644 --- a/src/RideSummaryWindow.h +++ b/src/RideSummaryWindow.h @@ -99,12 +99,13 @@ class RideSummaryWindow : public GcChartWindow void setFilter(QStringList); #endif - // compare mode started - void compareChanged(bool); + // compare mode started or items to compare changed + void compareChanged(); protected: - QString htmlSummary() const; + QString htmlSummary() const; // summary of a ride or a date range + QString htmlCompareSummary() const; // comparing intervals or seasons Context *context; QWebView *rideSummary;