/* * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) * * 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 "RideItem.h" #include "RideMetric.h" #include "RideFile.h" #include "Settings.h" #include "TimeUtils.h" #include "Units.h" #include "Zones.h" #include #include #include RideItem::RideItem(int type, QString path, QString fileName, const QDateTime &dateTime, Zones **zones, QString notesFileName) : QTreeWidgetItem(type), path(path), fileName(fileName), dateTime(dateTime), ride(NULL), zones(zones), notesFileName(notesFileName) { setText(0, dateTime.toString("ddd")); setText(1, dateTime.toString("MMM d, yyyy")); setText(2, dateTime.toString("h:mm AP")); setTextAlignment(1, Qt::AlignRight); setTextAlignment(2, Qt::AlignRight); } RideItem::~RideItem() { MetricIter i(metrics); while (i.hasNext()) { i.next(); delete i.value(); } } static void summarize(QString &intervals, unsigned last_interval, double km_start, double km_end, double &int_watts_sum, double &int_hr_sum, QVector &int_hrs, double &int_cad_sum, double &int_kph_sum, double &int_secs_hr, double &int_max_power, double int_dur) { double dur = int_dur; double mile_len = (km_end - km_start) * MILES_PER_KM; double minutes = (int) (dur/60.0); double seconds = dur - (60 * minutes); double watts_avg = int_watts_sum / dur; double hr_avg = int_hr_sum / int_secs_hr; double cad_avg = int_cad_sum / dur; double mph_avg = int_kph_sum * MILES_PER_KM / dur; double energy = int_watts_sum / 1000.0; // watts_avg / 1000.0 * dur; std::sort(int_hrs.begin(), int_hrs.end()); double top5hr = int_hrs.size() > 0 ? int_hrs[int_hrs.size() * 0.95] : 0; intervals += "%1"; intervals += "%2:%3"; intervals += "%4"; intervals += "%5"; intervals += "%6"; intervals += "%7"; intervals += "%8"; intervals += "%9"; intervals += "%10"; intervals += "%11"; intervals = intervals.arg(last_interval); intervals = intervals.arg(minutes, 0, 'f', 0); intervals = intervals.arg(seconds, 2, 'f', 0, QLatin1Char('0')); intervals = intervals.arg(mile_len, 0, 'f', 1); intervals = intervals.arg(energy, 0, 'f', 0); intervals = intervals.arg(int_max_power, 0, 'f', 0); intervals = intervals.arg(watts_avg, 0, 'f', 0); intervals = intervals.arg(top5hr, 0, 'f', 0); intervals = intervals.arg(hr_avg, 0, 'f', 0); intervals = intervals.arg(cad_avg, 0, 'f', 0); boost::shared_ptr settings = GetApplicationSettings(); QVariant unit = settings->value(GC_UNIT); if(unit.toString() == "Metric") intervals = intervals.arg(mph_avg * 1.60934, 0, 'f', 1); else intervals = intervals.arg(mph_avg, 0, 'f', 1); int_watts_sum = 0.0; int_hr_sum = 0.0; int_cad_sum = 0.0; int_kph_sum = 0.0; int_max_power = 0.0; int_hrs.clear(); } int RideItem::zoneRange() { return ( (zones && *zones) ? (*zones)->whichRange(dateTime.date()) : -1 ); } int RideItem::numZones() { if (zones && *zones) { int zone_range = zoneRange(); return ((zone_range >= 0) ? (*zones)->numZones(zone_range) : 0 ); } else return 0; } double RideItem::timeInZone(int zone) { htmlSummary(); if (!ride) return 0.0; assert(zone < numZones()); return time_in_zone[zone]; } static const char *metricsXml = "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; void RideItem::freeMemory() { if (ride) { delete ride; ride = NULL; } } void RideItem::computeMetrics() { const QDateTime nilTime; if ((computeMetricsTime != nilTime) && (!zones || !*zones || (computeMetricsTime >= (*zones)->modificationTime))) { return; } if (!ride) { QFile file(path + "/" + fileName); QStringList errors; ride = RideFileFactory::instance().openRideFile(file, errors); if (!ride) return; } computeMetricsTime = QDateTime::currentDateTime(); int zone_range = -1; int num_zones = 0; if (zones && *zones && ((zone_range = (*zones)->whichRange(dateTime.date())) >= 0)) { num_zones = (*zones)->numZones(zone_range); } const RideMetricFactory &factory = RideMetricFactory::instance(); QSet todo; // hack djconnel: do the metrics TWICE, to catch dependencies // on displayed variables. Presently if a variable depends on zones, // for example, and zones change, the value may be considered still // value even though it will change. This is presently happening // where bikescore depends on relative intensity. // note metrics are only calculated if zones are defined for (int metriciteration = 0; metriciteration < 2; metriciteration ++) { for (int i = 0; i < factory.metricCount(); ++i) { todo.insert(factory.metricName(i)); while (!todo.empty()) { QMutableSetIterator i(todo); later: while (i.hasNext()) { const QString &name = i.next(); const QVector &deps = factory.dependencies(name); for (int j = 0; j < deps.size(); ++j) if (!metrics.contains(deps[j])) goto later; RideMetric *metric = factory.newMetric(name); metric->compute(ride, *zones, zone_range, metrics); metrics.insert(name, metric); i.remove(); } } } } } QString RideItem::htmlSummary() { if (summary.isEmpty() || (zones && *zones && (summaryGenerationTime < (*zones)->modificationTime))) { // set defaults for zone range and number of zones int zone_range = -1; int num_zones = 0; summaryGenerationTime = QDateTime::currentDateTime(); QFile file(path + "/" + fileName); QStringList errors; ride = RideFileFactory::instance().openRideFile(file, errors); if (!ride) { summary = "

Couldn't read file \"" + file.fileName() + "\":"; QListIterator i(errors); while (i.hasNext()) summary += "
" + i.next(); return summary; } summary = ("

" + dateTime.toString("dddd MMMM d, yyyy, h:mm AP") + "

Device Type: " + ride->deviceType() + "

"); computeMetrics(); boost::shared_ptr settings = GetApplicationSettings(); QVariant unit = settings->value(GC_UNIT); if (zones && *zones && ((zone_range = (*zones)->whichRange(dateTime.date())) >= 0) && ((num_zones = (*zones)->numZones(zone_range)) > 0) ) { time_in_zone.clear(); time_in_zone.resize(num_zones); } double secs_watts = 0.0; QString intervals = ""; int interval_count = 0; int last_interval = INT_MAX; double int_watts_sum = 0.0; double int_hr_sum = 0.0; QVector int_hrs; double int_cad_sum = 0.0; double int_kph_sum = 0.0; double int_secs_hr = 0.0; double int_max_power = 0.0; double time_start, time_end, km_start, km_end, int_dur; QListIterator i(ride->dataPoints()); while (i.hasNext()) { RideFilePoint *point = i.next(); double secs_delta = ride->recIntSecs(); if (point->interval != last_interval) { if (last_interval != INT_MAX) { summarize(intervals, last_interval, km_start, km_end, int_watts_sum, int_hr_sum, int_hrs, int_cad_sum, int_kph_sum, int_secs_hr, int_max_power, int_dur); } interval_count++; last_interval = point->interval; time_start = point->secs; km_start = point->km; int_secs_hr = secs_delta; int_dur = 0.0; } if ((point->kph > 0.0) || (point->cad > 0.0)) { int_dur += secs_delta; } if (point->watts >= 0.0) { secs_watts += secs_delta; int_watts_sum += point->watts * secs_delta; if (point->watts > int_max_power) int_max_power = point->watts; if (num_zones > 0) { int zone = (*zones)->whichZone(zone_range, point->watts); if (zone >= 0) time_in_zone[zone] += secs_delta; } } if (point->hr > 0) { int_hr_sum += point->hr * secs_delta; int_secs_hr += secs_delta; } if (point->hr >= 0) int_hrs.push_back(point->hr); if (point->cad > 0) int_cad_sum += point->cad * secs_delta; if (point->kph >= 0) int_kph_sum += point->kph * secs_delta; km_end = point->km; time_end = point->secs + secs_delta; } summarize(intervals, last_interval, km_start, km_end, int_watts_sum, int_hr_sum, int_hrs, int_cad_sum, int_kph_sum, int_secs_hr, int_max_power, int_dur); summary += "

"; bool metricUnits = (unit.toString() == "Metric"); QDomDocument doc; { QString err; int errLine, errCol; if (!doc.setContent(QString(metricsXml), &err, &errLine, &errCol)){ fprintf(stderr, "error: %s, line %d, col %d\n", err.toAscii().constData(), errLine, errCol); assert(false); } } QString noteString = ""; QString stars; QDomNodeList groups = doc.elementsByTagName("metric_group"); const int columns = 3; for (int groupNum = 0; groupNum < groups.size(); ++groupNum) { QDomElement group = groups.at(groupNum).toElement(); assert(!group.isNull()); QString groupName = group.attribute("name"); QString groupNote = group.attribute("note"); assert(groupName.length() > 0); if (groupNum % columns == 0) summary += ""; summary += ""; if ((groupNum % columns == (columns - 1)) || (groupNum == groups.size() - 1)) summary += "
" ""; summary = summary.arg(90 / columns); if (groupNote.length() > 0) { stars += "*"; summary = summary.arg(groupName + stars); noteString += "
" + stars + " " + groupNote; } else { summary = summary.arg(groupName); } QDomNodeList metricsList = group.childNodes(); for (int i = 0; i < metricsList.size(); ++i) { QDomElement metric = metricsList.at(i).toElement(); QString name = metric.attribute("name"); QString displayName = metric.attribute("display_name"); int precision = metric.attribute("precision", "0").toInt(); assert(name.length() > 0); assert(displayName.length() > 0); const RideMetric *m = metrics.value(name); assert(m); if (m->units(metricUnits) == "seconds") { QString s(""); s = s.arg(displayName); s = s.arg(time_to_string(m->value(metricUnits))); summary += s; } else { QString s = ""; if (precision == 0) s = s.arg((unsigned) round(m->value(metricUnits))); else s = s.arg(m->value(metricUnits), 0, 'f', precision); summary += s; } } summary += "

%2

%1:%2
" + displayName; if (m->units(metricUnits) != "") s += " (" + m->units(metricUnits) + ")"; s += ":%1
"; } if (num_zones > 0) { summary += "

Power Zones

"; summary += (*zones)->summarize(zone_range, time_in_zone); } // TODO: Ergomo uses non-consecutive interval numbers. // Seems to use 0 when not in an interval // and an integer < 30 when in an interval. // We'll need to create a counter for the intervals // rather than relying on the final data point's interval number. if (interval_count > 1) { summary += "

Intervals

\n

\n"; summary += "Interval"; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; if(unit.toString() == "Metric") summary += ""; else summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; summary += ""; if(unit.toString() == "Metric") summary += ""; else summary += ""; summary += ""; summary += intervals; summary += "
DistanceWorkMax PowerAvg Power95% HRAvg HRAvg CadenceAvg Speed
NumberDuration(km)(miles)(kJ)(watts)(watts)(bpm)(rpm)(km/h)(mph)
"; } if (!errors.empty()) { summary += "

Errors reading file:

    "; QStringListIterator i(errors); while(i.hasNext()) summary += "
  • " + i.next(); summary += "
"; } if (noteString.length() > 0) { // The extra
works around a bug in QT 4.3.1, // which will otherwise put the noteString above the
. summary += "

" + noteString; } summary += "
"; } return summary; }