diff --git a/src/CpintPlot.cpp b/src/CpintPlot.cpp index 704349ceb..8570b146f 100644 --- a/src/CpintPlot.cpp +++ b/src/CpintPlot.cpp @@ -41,13 +41,15 @@ #define USE_T0_IN_CP_MODEL 0 // added djconnel 08Apr2009: allow 3-parameter CP model CpintPlot::CpintPlot(MainWindow *main, QString p, const Zones *zones) : - needToScanRides(true), path(p), thisCurve(NULL), CPCurve(NULL), + allCurve(NULL), zones(zones), + series(RideFile::watts), mainWindow(main), - energyMode_(false) + current(NULL), + bests(NULL) { setInstanceName("CP Plot"); assert(!USE_T0_IN_CP_MODEL); // doesn't work with energyMode=true @@ -90,229 +92,73 @@ CpintPlot::setAxisTitle(int axis, QString label) QwtPlot::setAxisTitle(axis, title); } -struct cpi_file_info { - QString file, inname, outname; -}; - -QString -ride_filename_to_cpi_filename(const QString filename) -{ - return (QFileInfo(filename).completeBaseName() + ".cpi"); -} - -static void -cpi_files_to_update(const QDir &dir, QList &result) -{ - QStringList filenames = RideFileFactory::instance().listRideFiles(dir); - foreach (const QString &filename, filenames) { - if (RideFileFactory::instance().rideFileRegExp().exactMatch(filename)) { - QString inname = dir.absoluteFilePath(filename); - QString outname = - dir.absoluteFilePath(ride_filename_to_cpi_filename(filename)); - QFileInfo ifi(inname), ofi(outname); - if (!ofi.exists() || (ofi.lastModified() < ifi.lastModified())) { - cpi_file_info info; - info.file = filename; - info.inname = inname; - info.outname = outname; - result.append(info); - } - } - } -} - -struct cpint_point -{ - double secs; - int watts; - cpint_point() : secs(0.0), watts(0) {} - cpint_point(double s, int w) : secs(s), watts(w) {} -}; - -struct cpint_data { - QStringList errors; - QVector points; - int rec_int_ms; - cpint_data() : rec_int_ms(0) {} -}; - -static void -update_cpi_file(MainWindow *mainWindow, const cpi_file_info *info, QProgressDialog *progress, - double &progress_sum, double progress_max) -{ - QFile file(info->inname); - QStringList errors; - boost::scoped_ptr rideFile( - RideFileFactory::instance().openRideFile(mainWindow, file, errors)); - if (!rideFile || rideFile->dataPoints().isEmpty()) - return; - cpint_data data; - data.rec_int_ms = (int) round(rideFile->recIntSecs() * 1000.0); - foreach (const RideFilePoint *p, rideFile->dataPoints()) { - double secs = round(p->secs * 1000.0) / 1000; - if (secs > 0) - data.points.append(cpint_point(secs, (int) round(p->watts))); - } - - FILE *out = fopen(info->outname.toAscii().constData(), "w"); - assert(out); - - int total_secs = (int) ceil(data.points.back().secs); - - // don't allow data more than one week - #define SECONDS_PER_WEEK 7 * 24 * 60 * 60 - if (total_secs > SECONDS_PER_WEEK) { - fclose(out); - return; - } - - QVector ride_bests(total_secs + 1); - bool canceled = false; - int progress_count = 0; - for (int i = 0; i < data.points.size() - 1; ++i) { - cpint_point *p = &data.points[i]; - double sum = 0.0; - double prev_secs = p->secs; - for (int j = i + 1; j < data.points.size(); ++j) { - cpint_point *q = &data.points[j]; - if (++progress_count % 1000 == 0) { - double x = (progress_count + progress_sum) - / progress_max * 1000.0; - // Use min() just in case math is wrong... - int n = qMin((int) round(x), 1000); - progress->setValue(n); - QCoreApplication::processEvents(); - if (progress->wasCanceled()) { - canceled = true; - goto done; - } - } - sum += data.rec_int_ms / 1000.0 * q->watts; - double dur_secs = q->secs - p->secs; - double avg = sum / dur_secs; - int dur_secs_top = (int) floor(dur_secs); - int dur_secs_bot = - qMax((int) floor(dur_secs - data.rec_int_ms / 1000.0), 0); - for (int k = dur_secs_top; k > dur_secs_bot; --k) { - if (ride_bests[k] < avg) - ride_bests[k] = avg; - } - prev_secs = q->secs; - } - } - - // avoid decreasing work with increasing duration - { - double maxwork = 0.0; - for (int i = 1; i <= total_secs; ++i) { - // note index is being used here in lieu of time, as the index - // is assumed to be proportional to time - double work = ride_bests[i] * i; - if (maxwork > work) - ride_bests[i] = round(maxwork / i); - else - maxwork = work; - if (ride_bests[i] != 0) - fprintf(out, "%6.3f %3.0f\n", i / 60.0, round(ride_bests[i])); - } - } - -done: - fclose(out); - if (canceled) - unlink(info->outname.toAscii().constData()); - progress_sum += progress_count; -} - -static QDate -cpi_filename_to_date(const QString filename) { - QRegExp rx("^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_\\d\\d_\\d\\d_\\d\\d\\.cpi$"); - if (rx.exactMatch(filename)) { - assert(rx.numCaptures() == 3); - QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(), rx.cap(3).toInt()); - if (date.isValid()) - return date; - } - return QDate(); // nil date -} - -static int -read_one(const QDir& dir, const QString &filename, QVector &bests, - QVector *bestDates, QHash *cpiDataInBests) -{ - QString inname = dir.absoluteFilePath(filename); - FILE *in = fopen(inname.toAscii().constData(), "r"); - if (!in) - return -1; - int lineno = 1; - char line[40]; - - while (fgets(line, sizeof(line), in) != NULL) { - double mins; - int watts; - if (sscanf(line, "%lf %d\n", &mins, &watts) != 2) { - QMessageBox::warning( - NULL, "Warning", - QString("Error reading %1, line %2").arg(inname).arg(line), - QMessageBox::Ok, - QMessageBox::NoButton); - fclose(in); - return -1; - } - int secs = (int) round(mins * 60.0); - if (secs >= bests.size()) { - bests.resize(secs + 1); - if (bestDates) - bestDates->resize(secs + 1); - } - if (bests[secs] < watts){ - bests[secs] = watts; - if (bestDates) - (*bestDates)[secs] = cpi_filename_to_date(filename); - - // mark the filename as having contributed to the bests - // Note this contribution may subsequently be over-written, so - // for example the first file scanned will always be tagged. - if (cpiDataInBests) - (*cpiDataInBests)[inname] = true; - } - ++lineno; - } - fclose(in); - - return 0; -} - void CpintPlot::changeSeason(const QDate &start, const QDate &end) { - startDate = start; - endDate = end; - needToScanRides = true; - delete CPCurve; - CPCurve = NULL; - clear_CP_Curves(); + // wipe out current - calculate will reinstate + startDate = (start == QDate()) ? QDate(1900, 1, 1) : start; + endDate = (end == QDate()) ? QDate(3000, 12, 31) : end; + + if (CPCurve) { + delete CPCurve; + CPCurve = NULL; + clear_CP_Curves(); + } + if (bests) { + delete bests; + bests = NULL; + } } void -CpintPlot::setEnergyMode(bool value) +CpintPlot::setSeries(RideFile::SeriesType x) { - energyMode_ = value; - if (energyMode_) { - setAxisTitle(yLeft, tr("Total work (kJ)")); - setAxisScaleEngine(xBottom, new QwtLinearScaleEngine); - setAxisScaleDraw(xBottom, new QwtScaleDraw); - setAxisTitle(xBottom, tr("Interval Length (minutes)")); - } - else { - setAxisTitle(yLeft, tr("Average Power (watts)")); - setAxisScaleEngine(xBottom, new LogTimeScaleEngine); - setAxisScaleDraw(xBottom, new LogTimeScaleDraw); - setAxisTitle(xBottom, tr("Interval Length")); + series = x; + + // Log scale for all bar Energy + setAxisScaleEngine(xBottom, new LogTimeScaleEngine); + setAxisScaleDraw(xBottom, new LogTimeScaleDraw); + setAxisTitle(xBottom, tr("Interval Length")); + + switch (series) { + + case RideFile::none: + setAxisTitle(yLeft, tr("Total work (kJ)")); + setAxisScaleEngine(xBottom, new QwtLinearScaleEngine); + setAxisScaleDraw(xBottom, new QwtScaleDraw); + setAxisTitle(xBottom, tr("Interval Length (minutes)")); + break; + + case RideFile::cad: + setAxisTitle(yLeft, tr("Average Cadence (rpm)")); + break; + + case RideFile::hr: + setAxisTitle(yLeft, tr("Average Heartrate (bpm)")); + break; + + case RideFile::kph: + setAxisTitle(yLeft, tr("Average Speed (kph)")); + break; + + case RideFile::nm: + setAxisTitle(yLeft, tr("Average Pedal Force (nm)")); + break; + + default: + case RideFile::watts: + setAxisTitle(yLeft, tr("Average Power (watts)")); + break; + } + delete CPCurve; CPCurve = NULL; clear_CP_Curves(); + if (allCurve) { + delete allCurve; + allCurve = NULL; + } } // extract critical power parameters which match the given curve @@ -338,18 +184,18 @@ CpintPlot::deriveCPParameters() // find the indexes associated with the bounds // the first point must be at least the minimum for the anaerobic interval, or quit for (i1 = 0; i1 < 60 * t1; i1++) - if (i1 + 1 >= bests.size()) + if (i1 + 1 >= bests->meanMaxArray(series).size()) return; // the second point is the maximum point suitable for anaerobicly dominated efforts. for (i2 = i1; i2 + 1 <= 60 * t2; i2++) - if (i2 + 1 >= bests.size()) + if (i2 + 1 >= bests->meanMaxArray(series).size()) return; // the third point is the beginning of the minimum duration for aerobic efforts for (i3 = i2; i3 < 60 * t3; i3++) - if (i3 + 1 >= bests.size()) + if (i3 + 1 >= bests->meanMaxArray(series).size()) return; for (i4 = i3; i4 + 1 <= 60 * t4; i4++) - if (i4 + 1 >= bests.size()) + if (i4 + 1 >= bests->meanMaxArray(series).size()) break; // initial estimate of tau @@ -398,7 +244,7 @@ CpintPlot::deriveCPParameters() int i; cp = 0; for (i = i3; i <= i4; i++) { - double cpn = bests[i] / (1 + tau / (t0 + i / 60.0)); + double cpn = bests->meanMaxArray(series)[i] / (1 + tau / (t0 + i / 60.0)); if (cp < cpn) cp = cpn; } @@ -410,14 +256,14 @@ CpintPlot::deriveCPParameters() // estimate tau, given cp tau = tau_min; for (i = i1; i <= i2; i++) { - double taun = (bests[i] / cp - 1) * (i / 60.0 + t0) - t0; + double taun = (bests->meanMaxArray(series)[i] / cp - 1) * (i / 60.0 + t0) - t0; if (tau < taun) tau = taun; } // update t0 if we're using that model #if USE_T0_IN_CP_MODEL - t0 = tau / (bests[1] / cp - 1) - 1 / 60.0; + t0 = tau / (bests->meanMaxArray(series)[1] / cp - 1) - 1 / 60.0; #endif } while ((fabs(tau - tau_prev) > tau_delta_max) || @@ -452,7 +298,7 @@ CpintPlot::plot_CP_curve(CpintPlot *thisPlot, // the plot we're currently di double x = (double) i / (curve_points - 1); double t = pow(tmax, x) * pow(tmin, 1-x); cp_curve_time[i] = t; - if (energyMode_) + if (series == RideFile::none) //XXX this is ENERGY cp_curve_power[i] = (cp * t + cp * tau) * 60.0 / 1000.0; else cp_curve_power[i] = cp * (1 + tau / (t + t0)); @@ -537,17 +383,16 @@ CpintPlot::plot_allCurve(CpintPlot *thisPlot, curve->attach(thisPlot); color.setAlpha(64); curve->setBrush(color); // brush fills below the line - if (energyMode_) { + if (series == RideFile::none) { // this is Energy mode curve->setData(time_values.data() + low, energyBests.data() + low, high - low + 1); - } - else { + } else { curve->setData(time_values.data() + low, power_values + low, high - low + 1); } allCurves.append(curve); - if (!energyMode_ || energyBests[high] > 100.0) { + if (series != RideFile::none || energyBests[high] > 100.0) { QwtText text(name); text.setFont(QFont("Helvetica", 20, QFont::Bold)); color.setAlpha(255); @@ -555,7 +400,7 @@ CpintPlot::plot_allCurve(CpintPlot *thisPlot, QwtPlotMarker *label_mark = new QwtPlotMarker(); // place the text in the geometric mean in time, at a decent power double x, y; - if (energyMode_) { + if (series == RideFile::none) { x = (time_values[low] + time_values[high]) / 2; y = (energyBests[low] + energyBests[high]) / 5; } @@ -584,7 +429,7 @@ CpintPlot::plot_allCurve(CpintPlot *thisPlot, QColor brush_color = GColor(CCP); brush_color.setAlpha(200); curve->setBrush(brush_color); // brush fills below the line - if (energyMode_) + if (series == RideFile::none) curve->setData(time_values.data(), energyBests.data(), n_values); else curve->setData(time_values.data(), power_values, n_values); @@ -594,11 +439,11 @@ CpintPlot::plot_allCurve(CpintPlot *thisPlot, // Energy mode is really only interesting in the range where energy is // linear in interval duration--up to about 1 hour. - double xmax = energyMode_ ? 60.0 : time_values[n_values - 1]; + double xmax = (series == RideFile::none) ? 60.0 : time_values[n_values - 1]; thisPlot->setAxisScale(thisPlot->xBottom, (double) 0.017, (double)xmax); double ymax; - if (energyMode_) { + if (series == RideFile::none) { int i = std::lower_bound(time_values.begin(), time_values.end(), 60.0) - time_values.begin(); ymax = 10 * ceil(energyBests[i] / 10); } @@ -618,104 +463,22 @@ CpintPlot::calculate(RideItem *rideItem) QDir dir(path); QFileInfo file(fileName); - if (needToScanRides) { - bests.clear(); - bestDates.clear(); - cpiDataInBests.clear(); - bool aborted = false; - QList to_update; - cpi_files_to_update(dir, to_update); - double progress_max = 0.0; - if (!to_update.empty()) { - foreach (const cpi_file_info &info, to_update) { - QFile file(info.inname); - QStringList errors; - boost::scoped_ptr rideFile( - RideFileFactory::instance().openRideFile(mainWindow, file, errors)); - if (rideFile) { - double x = rideFile->dataPoints().size(); - progress_max += x * (x + 1.0) / 2.0; - } - } - } - QProgressDialog progress( - QString(tr("Computing critical power intervals.\n" - "This may take a while.\n")), - tr("Abort"), 0, 1000, this); - double progress_sum = 0.0; - int endingOffset = progress.labelText().size(); - if (!to_update.empty()) { - int count = 1; - foreach (const cpi_file_info &info, to_update) { - QString existing = progress.labelText(); - existing.chop(progress.labelText().size() - endingOffset); - progress.setLabelText( - existing + QString(tr("Processing %1...")).arg(info.file)); - progress.setValue(count++); - update_cpi_file(mainWindow, &info, &progress, progress_sum, progress_max); - QCoreApplication::processEvents(); - if (progress.wasCanceled()) { - aborted = true; - break; - } - } - } - if (!aborted) { - QString existing = progress.labelText(); - existing.chop(progress.labelText().size() - endingOffset); - QStringList filters; - filters << "*.cpi"; - QStringList list = dir.entryList(filters, QDir::Files, QDir::Name); - list = filterForSeason(list, startDate, endDate); - progress.setLabelText( - existing + tr("Aggregating over all files.")); - progress.setRange(0, list.size()); - progress.setValue(0); - progress.show(); - foreach (const QString &filename, list) { - QString path = dir.absoluteFilePath(filename); - read_one(dir, filename, bests, &bestDates, &cpiDataInBests); - progress.setValue(progress.value() + 1); - QCoreApplication::processEvents(); - if (progress.wasCanceled()) { - aborted = true; - break; - } - } - } - if (!aborted && bests.size()) { - // check that total work doesn't decrease with time - double maxwork = 0.0; + // get current ride statistics + current = new RideFileCache(mainWindow, mainWindow->home.absolutePath() + "/" + fileName); - for (int i = 0; i < bests.size(); ++i) { - // note index is being used here in lieu of time, as the index - // is assumed to be proportional to time - double work = bests[i] * i; - if ((i > 0) && (maxwork > work)) { - bests[i] = round(maxwork / i); - bestDates[i] = bestDates[i - 1]; - } - else - maxwork = work; - } + // get aggregates - incase not initialised from date change + if (bests == NULL) bests = new RideFileCache(mainWindow, startDate, endDate); - // derive CP model - if (bests.size() > 1) { - // cp model parameters - cp = 0; - tau = 0; - t0 = 0; - - // calculate CP model from all-time best data - deriveCPParameters(); - } - needToScanRides = false; + // + // PLOT MODEL CURVE (DERIVED) + // + if (series == RideFile::watts || series == RideFile::none) { + if (bests->meanMaxArray(series).size() > 1) { + // calculate CP model from all-time best data + cp = tau = t0 = 0; + deriveCPParameters(); } - } - - if (!needToScanRides) { - if (!CPCurve) - plot_CP_curve(this, cp, tau, t0); + if (!CPCurve) plot_CP_curve(this, cp, tau, t0); else { // make sure color reflects latest config QPen pen(GColor(CCP)); @@ -723,46 +486,138 @@ CpintPlot::calculate(RideItem *rideItem) pen.setStyle(Qt::DashLine); CPCurve->setPen(pen); } - if (allCurves.empty()) { + + // + // PLOT ZONE (RAINBOW) AGGREGATED CURVE + // + if (bests->meanMaxArray(series).size()) { int maxNonZero = 0; - for (int i = 0; i < bests.size(); ++i) { - if (bests[i] > 0) - maxNonZero = i; + for (int i = 0; i < bests->meanMaxArray(series).size(); ++i) { + if (bests->meanMaxArray(series)[i] > 0) maxNonZero = i; } - plot_allCurve(this, maxNonZero - 1, bests.constData() + 1); + plot_allCurve(this, maxNonZero, bests->meanMaxArray(series).constData() + 1); + } + } else { + + // + // PLOT BESTS IN SERIES COLOR + // + if (allCurve) { + delete allCurve; + allCurve = NULL; + } + if (bests->meanMaxArray(series).size()) { + + int maxNonZero = 0; + QVector timeArray(bests->meanMaxArray(series).size()); + for (int i = 0; i < bests->meanMaxArray(series).size(); ++i) { + timeArray[i] = i / 60.0; + if (bests->meanMaxArray(series)[i] > 0) maxNonZero = i; + } + + if (maxNonZero > 1) { + + allCurve = new QwtPlotCurve(dateTime.toString(tr("ddd MMM d, yyyy h:mm AP"))); + allCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + + QPen line; + QColor fill; + switch (series) { + + case RideFile::kph: + line.setColor(GColor(CSPEED).darker(200)); + fill = (GColor(CSPEED)); + break; + + case RideFile::cad: + line.setColor(GColor(CCADENCE).darker(200)); + fill = (GColor(CCADENCE)); + break; + + case RideFile::nm: + line.setColor(GColor(CTORQUE).darker(200)); + fill = (GColor(CTORQUE)); + break; + + default: + case RideFile::hr: + line.setColor(GColor(CHEARTRATE).darker(200)); + fill = (GColor(CHEARTRATE)); + break; + } + + // wow, QVector really doesn't have a max/min method! + double ymax = 0; + double ymin = 100000; + foreach(double v, current->meanMaxArray(series)) { + if (v > ymax) ymax = v; + if (v && v < ymin) ymin = v; + } + foreach(double v, bests->meanMaxArray(series)) { + if (v > ymax) ymax = v; + if (v&& v < ymin) ymin = v; + } + if (ymin == 100000) ymin = 0; + + ymax *= 1.1; // bit of headroom + ymin *= 0.9; + + // xmax is directly related to the size of the arrays + double xmax = current->meanMaxArray(series).size(); + if (bests->meanMaxArray(series).size() > xmax) + xmax = bests->meanMaxArray(series).size(); + xmax /= 60; // its in minutes not seconds + + setAxisScale(yLeft, ymin, ymax); + setAxisScale(xBottom, 0.017, xmax); + allCurve->setPen(line); + fill.setAlpha(64); + allCurve->setBrush(fill); + allCurve->attach(this); + allCurve->setData(timeArray.data() + 1, bests->meanMaxArray(series).constData() + 1, maxNonZero - 1); + } + } + } + + // + // PLOT THIS RIDE CURVE + // + if (thisCurve) { + delete thisCurve; + thisCurve = NULL; + } + + if (current->meanMaxArray(series).size()) { + int maxNonZero = 0; + QVector timeArray(current->meanMaxArray(series).size()); + for (int i = 0; i < current->meanMaxArray(series).size(); ++i) { + timeArray[i] = i / 60.0; + if (current->meanMaxArray(series)[i] > 0) maxNonZero = i; } - if (thisCurve) { - delete thisCurve; - thisCurve = NULL; - } - QVector bests; - QString filename = file.completeBaseName() + ".cpi"; - if ((read_one(dir, filename, bests, NULL, NULL) == 0) && bests.size()) { - QVector energyArray(bests.size()); - QVector timeArray(bests.size()); - int maxNonZero = 0; - for (int i = 0; i < bests.size(); ++i) { - timeArray[i] = i / 60.0; - energyArray[i] = timeArray[i] * bests[i] * 60.0 / 1000.0; - if (bests[i] > 0) maxNonZero = i; - } - if (maxNonZero > 1) { - thisCurve = new QwtPlotCurve( - dateTime.toString(tr("ddd MMM d, yyyy h:mm AP"))); - thisCurve->setRenderHint(QwtPlotItem::RenderAntialiased); - thisCurve->setPen(QPen(Qt::black)); - thisCurve->attach(this); - if (energyMode_) { - thisCurve->setData(timeArray.data() + 1, - energyArray.constData() + 1, - maxNonZero - 1); - } - else { - thisCurve->setData(timeArray.data() + 1, - bests.constData() + 1, - maxNonZero - 1); + if (maxNonZero > 1) { + + thisCurve = new QwtPlotCurve(dateTime.toString(tr("ddd MMM d, yyyy h:mm AP"))); + thisCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + thisCurve->setPen(QPen(Qt::black)); + thisCurve->attach(this); + + if (series == RideFile::none) { + + // Calculate Energy + QVector energyArray(current->meanMaxArray(RideFile::watts).size()); + for (int i = 0; i <= maxNonZero; ++i) { + energyArray[i] = + timeArray[i] * + current->meanMaxArray(RideFile::watts)[i] * 60.0 / 1000.0; } + thisCurve->setData(timeArray.data() + 1, energyArray.constData() + 1, maxNonZero - 1); + + } else { + + // normal + thisCurve->setData(timeArray.data() + 1, + current->meanMaxArray(series).constData() + 1, maxNonZero - 1); } } } @@ -770,27 +625,6 @@ CpintPlot::calculate(RideItem *rideItem) replot(); } -// delete a CPI file -bool -CpintPlot::deleteCpiFile(QString filename) -{ - // first, get ride of the file - if (! QFile::remove(filename)) - return false; - - // now check to see if this file contributed to the bests - // in the current implementation a false means it does - // not contribute, but a true only means it at one time - // contributed (may not in the end). - if (cpiDataInBests.contains(filename)) { - if (cpiDataInBests[filename]) - needToScanRides = true; - cpiDataInBests.remove(filename); - } - - return true; -} - void CpintPlot::showGrid(int state) { @@ -798,21 +632,3 @@ CpintPlot::showGrid(int state) grid->setVisible(state == Qt::Checked); replot(); } - -QStringList -CpintPlot::filterForSeason(QStringList cpints, QDate startDate, QDate endDate) -{ - //Check to see if no date was assigned. - QDate nilDate; - if(startDate == nilDate) - return cpints; - QStringList returnList; - foreach (const QString &cpi, cpints) { - QDate cpiDate = cpi_filename_to_date(cpi); - if(cpiDate > startDate && cpiDate < endDate) - returnList << cpi; - - } - return returnList; -} - diff --git a/src/CpintPlot.h b/src/CpintPlot.h index 8b07be598..12f1daf89 100644 --- a/src/CpintPlot.h +++ b/src/CpintPlot.h @@ -20,6 +20,8 @@ #define _GC_CpintPlot_h 1 #include "GoldenCheetah.h" +#include "RideFileCache.h" + #include #include @@ -30,8 +32,6 @@ class RideItem; class Zones; class MainWindow; -QString ride_filename_to_cpi_filename(const QString filename); - class CpintPlot : public QwtPlot { Q_OBJECT @@ -41,20 +41,18 @@ class CpintPlot : public QwtPlot public: CpintPlot(MainWindow *, QString path, const Zones *zones); - bool needToScanRides; const QwtPlotCurve *getThisCurve() const { return thisCurve; } const QwtPlotCurve *getCPCurve() const { return CPCurve; } - QVector getBestDates() { return bestDates; } - QVector getBests() { return bests; } double cp, tau, t0; // CP model parameters void deriveCPParameters(); - bool deleteCpiFile(QString filename); void changeSeason(const QDate &start, const QDate &end); - void setEnergyMode(bool value); - bool energyMode() const { return energyMode_; } void setAxisTitle(int axis, QString label); + void setSeries(RideFile::SeriesType); + + QVector getBests() { return bests->meanMaxArray(series); } + QVector getBestDates() { return bests->meanMaxDates(series); } public slots: @@ -70,19 +68,18 @@ class CpintPlot : public QwtPlot QwtPlotCurve *thisCurve; QwtPlotCurve *CPCurve; QList allCurves; + QwtPlotCurve *allCurve; // bests but not zoned QList allZoneLabels; void clear_CP_Curves(); QStringList filterForSeason(QStringList cpints, QDate startDate, QDate endDate); QwtPlotGrid *grid; - QVector bests; - QVector bestDates; QDate startDate; QDate endDate; const Zones *zones; - // keys are CPI files contributing to bests (at least originally) - QHash cpiDataInBests; - bool energyMode_; + RideFile::SeriesType series; MainWindow *mainWindow; + + RideFileCache *current, *bests; }; #endif // _GC_CpintPlot_h diff --git a/src/CriticalPowerWindow.cpp b/src/CriticalPowerWindow.cpp index 7e4436d65..61b3840c8 100644 --- a/src/CriticalPowerWindow.cpp +++ b/src/CriticalPowerWindow.cpp @@ -73,16 +73,15 @@ CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent) : cl->addLayout(cpintPickerLayout); // tools /properties + seriesCombo = new QComboBox(this); + addSeries(); cComboSeason = new QComboBox(this); addSeasons(); cpintSetCPButton = new QPushButton(tr("&Save CP value"), this); cpintSetCPButton->setEnabled(false); cl->addWidget(cpintSetCPButton); cl->addWidget(cComboSeason); - yAxisCombo = new QComboBox(this); - yAxisCombo->addItem(tr("Y Axis Shows Power")); - yAxisCombo->addItem(tr("Y Axis Shows Energy")); - cl->addWidget(yAxisCombo); + cl->addWidget(seriesCombo); cl->addStretch(); picker = new QwtPlotPicker(QwtPlot::xBottom, QwtPlot::yLeft, @@ -91,16 +90,11 @@ CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent) : QwtPicker::AlwaysOff, cpintPlot->canvas()); picker->setRubberBandPen(GColor(CPLOTTRACKER)); - connect(picker, SIGNAL(moved(const QPoint &)), - SLOT(pickerMoved(const QPoint &))); - connect(cpintTimeValue, SIGNAL(editingFinished()), - this, SLOT(cpintTimeValueEntered())); - connect(cpintSetCPButton, SIGNAL(clicked()), - this, SLOT(cpintSetCPButtonClicked())); - connect(cComboSeason, SIGNAL(currentIndexChanged(int)), - this, SLOT(seasonSelected(int))); - connect(yAxisCombo, SIGNAL(currentIndexChanged(int)), - this, SLOT(setEnergyMode(int))); + connect(picker, SIGNAL(moved(const QPoint &)), SLOT(pickerMoved(const QPoint &))); + connect(cpintTimeValue, SIGNAL(editingFinished()), this, SLOT(cpintTimeValueEntered())); + connect(cpintSetCPButton, SIGNAL(clicked()), this, SLOT(cpintSetCPButtonClicked())); + connect(cComboSeason, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int))); + connect(seriesCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setSeries(int))); //connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected())); connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected())); connect(mainWindow, SIGNAL(configChanged()), cpintPlot, SLOT(configChanged())); @@ -112,14 +106,7 @@ CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent) : void CriticalPowerWindow::newRideAdded() { - cpintPlot->needToScanRides = true; -} - -void -CriticalPowerWindow::deleteCpiFile(QString rideFilename) -{ - cpintPlot->deleteCpiFile(home.absolutePath() + "/" + - ride_filename_to_cpi_filename(rideFilename)); + // XXX } void @@ -138,10 +125,12 @@ CriticalPowerWindow::rideSelected() } void -CriticalPowerWindow::setEnergyMode(int index) +CriticalPowerWindow::setSeries(int index) { - cpintPlot->setEnergyMode(index != 0); - cpintPlot->calculate(currentRide); + if (index >= 0) { + cpintPlot->setSeries(static_cast(seriesCombo->itemData(index).toInt())); + cpintPlot->calculate(currentRide); + } } void @@ -187,12 +176,43 @@ curve_to_point(double x, const QwtPlotCurve *curve) void CriticalPowerWindow::updateCpint(double minutes) { + QString units; + + switch (series()) { + + case RideFile::none: + units = "kJ"; + break; + + case RideFile::cad: + units = "rpm"; + break; + + case RideFile::kph: + units = "kph"; + break; + + case RideFile::hr: + units = "bpm"; + break; + + case RideFile::nm: + units = "nm"; + break; + + default: + case RideFile::watts: + units = "Watts"; + break; + + } + // current ride { - unsigned watts = curve_to_point(minutes, cpintPlot->getThisCurve()); + unsigned value = curve_to_point(minutes, cpintPlot->getThisCurve()); QString label; - if (watts > 0) - label = QString(cpintPlot->energyMode() ? "%1 kJ" : "%1 watts").arg(watts); + if (value > 0) + label = QString("%1 %2").arg(value).arg(units); else label = tr("no data"); cpintTodayValue->setText(label); @@ -200,10 +220,10 @@ CriticalPowerWindow::updateCpint(double minutes) // cp line if (cpintPlot->getCPCurve()) { - unsigned watts = curve_to_point(minutes, cpintPlot->getCPCurve()); + unsigned value = curve_to_point(minutes, cpintPlot->getCPCurve()); QString label; - if (watts > 0) - label = QString(cpintPlot->energyMode() ? "%1 kJ" : "%1 watts").arg(watts); + if (value > 0) + label = QString("%1 %2").arg(value).arg(units); else label = tr("no data"); cpintCPValue->setText(label); @@ -213,14 +233,14 @@ CriticalPowerWindow::updateCpint(double minutes) { QString label; int index = (int) ceil(minutes * 60); - if (cpintPlot->getBests().count() > index) { + if (index >= 0 && cpintPlot->getBests().count() > index) { QDate date = cpintPlot->getBestDates()[index]; - unsigned watts = cpintPlot->getBests()[index]; - if (cpintPlot->energyMode()) + unsigned value = cpintPlot->getBests()[index]; +#if 0 label = QString("%1 kJ (%2)").arg(watts * minutes * 60.0 / 1000.0, 0, 'f', 0); - else - label = QString("%1 watts (%2)").arg(watts); - label = label.arg(date.isValid() ? date.toString(tr("MM/dd/yyyy")) : tr("no date")); +#endif + label = QString("%1 %2 (%3)").arg(value).arg(units) + .arg(date.isValid() ? date.toString(tr("MM/dd/yyyy")) : tr("no date")); } else { label = tr("no data"); @@ -243,6 +263,27 @@ CriticalPowerWindow::pickerMoved(const QPoint &pos) cpintTimeValue->setText(interval_to_str(60.0*minutes)); updateCpint(minutes); } +void CriticalPowerWindow::addSeries() +{ + // setup series list + seriesList << RideFile::watts +#if 0 // need to work out the best algorithm + << RideFile::xPower + << RideFile::NP +#endif + << RideFile::hr + << RideFile::kph + << RideFile::cad + << RideFile::nm + << RideFile::none; // XXX actually this shows energy (hack) + + foreach (RideFile::SeriesType x, seriesList) { + if (x==RideFile::none) + seriesCombo->addItem(tr("Energy"), static_cast(x)); + else + seriesCombo->addItem(RideFile::seriesName(x), static_cast(x)); + } +} void CriticalPowerWindow::addSeasons() { diff --git a/src/CriticalPowerWindow.h b/src/CriticalPowerWindow.h index 74f21e5aa..ec225162b 100644 --- a/src/CriticalPowerWindow.h +++ b/src/CriticalPowerWindow.h @@ -47,8 +47,13 @@ class CriticalPowerWindow : public GcWindow // set/get properties int season() const { return cComboSeason->currentIndex(); } void setSeason(int x) { cComboSeason->setCurrentIndex(x); } - int mode() const { return yAxisCombo->currentIndex(); } - void setMode(int x) { yAxisCombo->setCurrentIndex(x); } + int mode() const { return seriesCombo->currentIndex(); } + void setMode(int x) { seriesCombo->setCurrentIndex(x); } + + RideFile::SeriesType series() { + return static_cast + (seriesCombo->itemData(seriesCombo->currentIndex()).toInt()); + } protected slots: void cpintTimeValueEntered(); @@ -56,7 +61,7 @@ class CriticalPowerWindow : public GcWindow void pickerMoved(const QPoint &pos); void rideSelected(); void seasonSelected(int season); - void setEnergyMode(int index); + void setSeries(int index); private: void updateCpint(double minutes); @@ -70,13 +75,15 @@ class CriticalPowerWindow : public GcWindow QLineEdit *cpintTodayValue; QLineEdit *cpintAllValue; QLineEdit *cpintCPValue; + QComboBox *seriesCombo; QComboBox *cComboSeason; - QComboBox *yAxisCombo; QPushButton *cpintSetCPButton; QwtPlotPicker *picker; void addSeasons(); + void addSeries(); QList seasons; RideItem *currentRide; + QList seriesList; }; #endif // _GC_CriticalPowerWindow_h diff --git a/src/HistogramWindow.cpp b/src/HistogramWindow.cpp index 39415fc0c..70f45fd92 100644 --- a/src/HistogramWindow.cpp +++ b/src/HistogramWindow.cpp @@ -20,6 +20,7 @@ #include "MainWindow.h" #include "PowerHist.h" #include "RideFile.h" +#include "RideFileCache.h" #include "RideItem.h" #include "Settings.h" #include @@ -85,7 +86,10 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) : histSumY = new QComboBox(); histSumY->addItem(tr("Absolute Time")); histSumY->addItem(tr("Percentage Time")); + cComboSeason = new QComboBox(this); + addSeasons(); cl->addWidget(histSumY); + cl->addWidget(cComboSeason); cl->addStretch(); // sort out default values @@ -114,6 +118,7 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) : connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged())); connect(mainWindow, SIGNAL(configChanged()), powerHist, SLOT(configChanged())); + connect(cComboSeason, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int))); } void @@ -154,6 +159,47 @@ HistogramWindow::zonesChanged() powerHist->replot(); } +void HistogramWindow::seasonSelected(int index) +{ + if (index > 0) { + index--; // it is now an index into the season array + + // Set data from BESTS + Season season = seasons.at(index); + + } else if (!index) { + // Set data from RIDE + + } +} + +void HistogramWindow::addSeasons() +{ + QFile seasonFile(mainWindow->home.absolutePath() + "/seasons.xml"); + QXmlInputSource source( &seasonFile ); + QXmlSimpleReader xmlReader; + SeasonParser( handler ); + xmlReader.setContentHandler(&handler); + xmlReader.setErrorHandler(&handler); + bool ok = xmlReader.parse( source ); + if(!ok) + qWarning("Failed to parse seasons.xml"); + + seasons = handler.getSeasons(); + Season season; + season.setName(tr("All Seasons")); + seasons.insert(0,season); + + cComboSeason->addItem("Selected Ride"); + foreach (Season season, seasons) + cComboSeason->addItem(season.getName()); + if (!seasons.empty()) { + cComboSeason->setCurrentIndex(cComboSeason->count() - 1); + Season season = seasons.last(); + // set default parameters here + // XXX todo + } +} void HistogramWindow::setBinWidthFromSlider() { diff --git a/src/HistogramWindow.h b/src/HistogramWindow.h index 06f993e55..b4db0388c 100644 --- a/src/HistogramWindow.h +++ b/src/HistogramWindow.h @@ -20,6 +20,9 @@ #define _GC_HistogramWindow_h 1 #include "GoldenCheetah.h" +#include "Season.h" +#include "SeasonParser.h" + #include class MainWindow; @@ -70,9 +73,11 @@ class HistogramWindow : public GcWindow void setWithZerosFromCheckBox(); void setHistSelection(int id); void setSumY(int); + void seasonSelected(int season); protected: + QList seasons; void setHistTextValidator(); void setHistBinWidthText(); @@ -85,6 +90,8 @@ class HistogramWindow : public GcWindow QCheckBox *histShadeZones; QComboBox *histParameterCombo; QComboBox *histSumY; + QComboBox *cComboSeason; + void addSeasons(); int powerRange, hrRange; }; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 8a9fe2d26..814ecdfe0 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -444,7 +444,7 @@ MainWindow::MainWindow(const QDir &home) : ////////////////////// Critical Power Plot Tab ////////////////////// criticalPowerWindow = new CriticalPowerWindow(home, this); - tabs.append(TabInfo(criticalPowerWindow, tr("Critical Power"))); + tabs.append(TabInfo(criticalPowerWindow, tr("Critical Durations"))); //////////////////////// Power Histogram Tab //////////////////////// @@ -905,7 +905,7 @@ MainWindow::removeCurrentRide() } // added djconnel: remove old cpi file, then update bests which are associated with the file - criticalPowerWindow->deleteCpiFile(strOldFileName); + //XXX need to clean up in metricaggregator criticalPowerWindow->deleteCpiFile(strOldFileName); treeWidget->setCurrentItem(itemToSelect); rideTreeWidgetSelectionChanged(); diff --git a/src/MetricAggregator.cpp b/src/MetricAggregator.cpp index c009bdb10..383bd15a3 100644 --- a/src/MetricAggregator.cpp +++ b/src/MetricAggregator.cpp @@ -19,6 +19,7 @@ #include "MetricAggregator.h" #include "DBAccess.h" #include "RideFile.h" +#include "RideFileCache.h" #include "Zones.h" #include "HrZones.h" #include "Settings.h" @@ -92,9 +93,16 @@ void MetricAggregator::refreshMetrics() // update statistics for ride files which are out of date // showing a progress bar as we go - QProgressDialog bar(tr("Refreshing Metrics Database..."), tr("Abort"), 0, filenames.count(), main); + QTime elapsed; + elapsed.start(); + QString title = tr("Refreshing Ride Statistics...\nStarted"); + QProgressDialog bar(title, tr("Abort"), 0, filenames.count(), main); bar.setWindowModality(Qt::WindowModal); + bar.setMinimumDuration(0); + bar.show(); + int processed=0; + QApplication::processEvents(); // get that dialog up! // log of progress QFile log(home.absolutePath() + "/" + "metric.log"); @@ -111,14 +119,17 @@ void MetricAggregator::refreshMetrics() status current = dbStatus.value(name); unsigned long dbTimeStamp = current.timestamp; unsigned long fingerprint = current.fingerprint; + + RideFile *ride = NULL; + if (dbTimeStamp < QFileInfo(file).lastModified().toTime_t() || zoneFingerPrint != fingerprint) { // log out << "Opening ride: " << name << "\r\n"; - // read file and process it - RideFile *ride = RideFileFactory::instance().openRideFile(main, file, errors); + // read file and process it if we didn't already... + if (ride == NULL) ride = RideFileFactory::instance().openRideFile(main, file, errors); out << "File open completed: " << name << "\r\n"; @@ -126,10 +137,26 @@ void MetricAggregator::refreshMetrics() out << "Updating statistics: " << name << "\r\n"; importRide(home, ride, name, zoneFingerPrint, (dbTimeStamp > 0)); - delete ride; + } } + + // update cache (will check timestamps itself) + // if ride wasn't opened it will do it itself + // we only want to check so passing check=true + // because we don't actually want the results now + RideFileCache updater(main, home.absolutePath() + "/" + name, ride, true); + + // free memory - if needed + if (ride) delete ride; + // update progress bar + long elapsedtime = elapsed.elapsed(); + QString elapsedString = QString("%1:%2:%3").arg(elapsedtime/3600000,2) + .arg((elapsedtime%3600000)/60000,2,10,QLatin1Char('0')) + .arg((elapsedtime%60000)/1000,2,10,QLatin1Char('0')); + QString title = tr("Refreshing Ride Statistics...\nElapsed: %1").arg(elapsedString); + bar.setLabelText(title); bar.setValue(++processed); QApplication::processEvents(); diff --git a/src/PowerHist.cpp b/src/PowerHist.cpp index c1d00913f..480cda5d4 100644 --- a/src/PowerHist.cpp +++ b/src/PowerHist.cpp @@ -21,6 +21,7 @@ #include "RideItem.h" #include "IntervalItem.h" #include "RideFile.h" +#include "RideFileCache.h" #include "Settings.h" #include "Zones.h" #include "HrZones.h" @@ -771,6 +772,8 @@ PowerHist::setData(RideItem *_rideItem) RideFile *ride = rideItem->ride(); + RideFileCache updater(mainWindow, mainWindow->home.absolutePath() + "/" + rideItem->fileName, ride); + bool hasData = (selected == watts && ride->areDataPresent()->watts) || (selected == nm && ride->areDataPresent()->nm) || (selected == kph && ride->areDataPresent()->kph) || diff --git a/src/RideFile.cpp b/src/RideFile.cpp index 05c3881fd..b94af90de 100644 --- a/src/RideFile.cpp +++ b/src/RideFile.cpp @@ -67,6 +67,8 @@ RideFile::seriesName(SeriesType series) case RideFile::kph: return QString(tr("Speed")); case RideFile::nm: return QString(tr("Torque")); case RideFile::watts: return QString(tr("Power")); + case RideFile::xPower: return QString(tr("xPower")); + case RideFile::NP: return QString(tr("Normalised Power")); case RideFile::alt: return QString(tr("Altitude")); case RideFile::lon: return QString(tr("Longitude")); case RideFile::lat: return QString(tr("Latitude")); @@ -358,6 +360,7 @@ RideFile::setDataPresent(SeriesType series, bool value) case lat : dataPresent.lat = value; break; case headwind : dataPresent.headwind = value; break; case interval : dataPresent.interval = value; break; + default: case none : break; } } @@ -378,7 +381,8 @@ RideFile::isDataPresent(SeriesType series) case lat : return dataPresent.lat; break; case headwind : return dataPresent.headwind; break; case interval : return dataPresent.interval; break; - case none : break; + default: + case none : return false; break; } return false; } @@ -398,29 +402,37 @@ RideFile::setPointValue(int index, SeriesType series, double value) case lat : dataPoints_[index]->lat = value; break; case headwind : dataPoints_[index]->headwind = value; break; case interval : dataPoints_[index]->interval = value; break; + default: case none : break; } } double -RideFile::getPointValue(int index, SeriesType series) +RideFilePoint::value(RideFile::SeriesType series) const { switch (series) { - case secs : return dataPoints_[index]->secs; break; - case cad : return dataPoints_[index]->cad; break; - case hr : return dataPoints_[index]->hr; break; - case km : return dataPoints_[index]->km; break; - case kph : return dataPoints_[index]->kph; break; - case nm : return dataPoints_[index]->nm; break; - case watts : return dataPoints_[index]->watts; break; - case alt : return dataPoints_[index]->alt; break; - case lon : return dataPoints_[index]->lon; break; - case lat : return dataPoints_[index]->lat; break; - case headwind : return dataPoints_[index]->headwind; break; - case interval : return dataPoints_[index]->interval; break; - case none : break; + case RideFile::secs : return secs; break; + case RideFile::cad : return cad; break; + case RideFile::hr : return hr; break; + case RideFile::km : return km; break; + case RideFile::kph : return kph; break; + case RideFile::nm : return nm; break; + case RideFile::watts : return watts; break; + case RideFile::alt : return alt; break; + case RideFile::lon : return lon; break; + case RideFile::lat : return lat; break; + case RideFile::headwind : return headwind; break; + case RideFile::interval : return interval; break; + default: + case RideFile::none : break; } - return 0.0; // shutup the compiler + return 0.0; +} + +double +RideFile::getPointValue(int index, SeriesType series) const +{ + return dataPoints_[index]->value(series); } int @@ -434,6 +446,8 @@ RideFile::decimalsFor(SeriesType series) case kph : return 4; break; case nm : return 2; break; case watts : return 0; break; + case xPower : return 0; break; + case NP : return 0; break; case alt : return 3; break; case lon : return 6; break; case lat : return 6; break; @@ -449,12 +463,14 @@ RideFile::maximumFor(SeriesType series) { switch (series) { case secs : return 999999; break; - case cad : return 300; break; - case hr : return 300; break; + case cad : return 255; break; + case hr : return 255; break; case km : return 999999; break; - case kph : return 999; break; - case nm : return 999; break; - case watts : return 4000; break; + case kph : return 150; break; + case nm : return 100; break; + case watts : return 2500; break; + case NP : return 2500; break; + case xPower : return 2500; break; case alt : return 8850; break; // mt everest is highest point above sea level case lon : return 180; break; case lat : return 90; break; @@ -476,6 +492,8 @@ RideFile::minimumFor(SeriesType series) case kph : return 0; break; case nm : return 0; break; case watts : return 0; break; + case xPower : return 0; break; + case NP : return 0; break; case alt : return -413; break; // the Red Sea is lowest land point on earth case lon : return -180; break; case lat : return -90; break; diff --git a/src/RideFile.h b/src/RideFile.h index ad93ce909..4b5a9f12b 100644 --- a/src/RideFile.h +++ b/src/RideFile.h @@ -29,6 +29,10 @@ #include class RideItem; +class RideFile; +struct RideFilePoint; +struct RideFileDataPresent; +struct RideFileInterval; class EditorData; // attached to a RideFile class RideFileCommand; // for manipulating ride data class MainWindow; // for context; cyclist, homedir @@ -48,18 +52,6 @@ class MainWindow; // for context; cyclist, homedir // suffixes to the RideFileReader objects capable of converting those files // into RideFile objects. -struct RideFilePoint -{ - double secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind; - int interval; - RideFilePoint() : secs(0.0), cad(0.0), hr(0.0), km(0.0), kph(0.0), - nm(0.0), watts(0.0), alt(0.0), lon(0.0), lat(0.0), headwind(0.0), interval(0) {} - RideFilePoint(double secs, double cad, double hr, double km, double kph, - double nm, double watts, double alt, double lon, double lat, double headwind, int interval) : - secs(secs), cad(cad), hr(hr), km(km), kph(kph), nm(nm), - watts(watts), alt(alt), lon(lon), lat(lat), headwind(headwind), interval(interval) {} -}; - struct RideFileDataPresent { bool secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, interval; @@ -95,7 +87,7 @@ class RideFile : public QObject // QObject to emit signals virtual ~RideFile(); // Working with DATASERIES - enum seriestype { secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, interval, none }; + enum seriestype { secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, interval, NP, xPower, none }; typedef enum seriestype SeriesType; static QString seriesName(SeriesType); static int decimalsFor(SeriesType series); @@ -104,7 +96,7 @@ class RideFile : public QObject // QObject to emit signals // Working with DATAPOINTS -- ***use command to modify*** RideFileCommand *command; - double getPointValue(int index, SeriesType series); + double getPointValue(int index, SeriesType series) const; void appendPoint(double secs, double cad, double hr, double km, double kph, double nm, double watts, double alt, double lon, double lat, double headwind, int interval); @@ -195,6 +187,19 @@ class RideFile : public QObject // QObject to emit signals }; +struct RideFilePoint +{ + double secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind; + int interval; + RideFilePoint() : secs(0.0), cad(0.0), hr(0.0), km(0.0), kph(0.0), + nm(0.0), watts(0.0), alt(0.0), lon(0.0), lat(0.0), headwind(0.0), interval(0) {} + RideFilePoint(double secs, double cad, double hr, double km, double kph, + double nm, double watts, double alt, double lon, double lat, double headwind, int interval) : + secs(secs), cad(cad), hr(hr), km(km), kph(kph), nm(nm), + watts(watts), alt(alt), lon(lon), lat(lat), headwind(headwind), interval(interval) {} + double value(RideFile::SeriesType series) const; +}; + struct RideFileReader { virtual ~RideFileReader() {} virtual RideFile *openRideFile(QFile &file, QStringList &errors) const = 0; diff --git a/src/RideFileCache.cpp b/src/RideFileCache.cpp new file mode 100644 index 000000000..81d55b3a7 --- /dev/null +++ b/src/RideFileCache.cpp @@ -0,0 +1,697 @@ +/* + * Copyright (c) 2011 Mark Liversedge (liversedge@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 "RideFileCache.h" +#include "MainWindow.h" +#include // for pow() +#include +#include +#include +#include // for qStableSort + +// cache from ride +RideFileCache::RideFileCache(MainWindow *main, QString fileName, RideFile *passedride, bool check) : + main(main), rideFileName(fileName), ride(passedride) +{ + // resize all the arrays to zero + wattsMeanMax.resize(0); + hrMeanMax.resize(0); + cadMeanMax.resize(0); + nmMeanMax.resize(0); + kphMeanMax.resize(0); + xPowerMeanMax.resize(0); + npMeanMax.resize(0); + wattsDistribution.resize(0); + hrDistribution.resize(0); + cadDistribution.resize(0); + nmDistribution.resize(0); + kphDistribution.resize(0); + xPowerDistribution.resize(0); + npDistribution.resize(0); + + // Get info for ride file and cache file + QFileInfo rideFileInfo(rideFileName); + cacheFileName = rideFileInfo.path() + "/" + rideFileInfo.baseName() + ".cpx"; + QFileInfo cacheFileInfo(cacheFileName); + + // is it up-to-date? + if (cacheFileInfo.exists() && rideFileInfo.lastModified() < cacheFileInfo.lastModified() && + cacheFileInfo.size() != 0) { + if (check == false) readCache(); // if check is false we aren't just checking + return; + } + + // not up-to-date we need to refresh from the ridefile + if (ride) { + + // we got passed the ride - so update from that + refreshCache(); + + } else { + + // we need to open it to update since we were not passed one + QStringList errors; + QFile file(rideFileName); + + ride = RideFileFactory::instance().openRideFile(main, file, errors); + + if (ride) { + refreshCache(); + delete ride; + } + ride = 0; + } +} + +// +// DATA ACCESS +// +QVector & +RideFileCache::meanMaxDates(RideFile::SeriesType series) +{ + switch (series) { + + case RideFile::watts: + return wattsMeanMaxDate; + break; + + case RideFile::cad: + return cadMeanMaxDate; + break; + + case RideFile::hr: + return hrMeanMaxDate; + break; + + case RideFile::nm: + return nmMeanMaxDate; + break; + + case RideFile::kph: + return kphMeanMaxDate; + break; + + case RideFile::xPower: + return xPowerMeanMaxDate; + break; + + case RideFile::NP: + return npMeanMaxDate; + break; + + default: + //? dunno give em power anyway + return wattsMeanMaxDate; + break; + } +} + +QVector & +RideFileCache::meanMaxArray(RideFile::SeriesType series) +{ + switch (series) { + + case RideFile::watts: + return wattsMeanMaxDouble; + break; + + case RideFile::cad: + return cadMeanMaxDouble; + break; + + case RideFile::hr: + return hrMeanMaxDouble; + break; + + case RideFile::nm: + return nmMeanMaxDouble; + break; + + case RideFile::kph: + return kphMeanMaxDouble; + break; + + case RideFile::xPower: + return xPowerMeanMaxDouble; + break; + + case RideFile::NP: + return npMeanMaxDouble; + break; + + default: + //? dunno give em power anyway + return wattsMeanMaxDouble; + break; + } +} + + +// +// COMPUTATION +// +void +RideFileCache::refreshCache() +{ + static bool writeerror=false; + + // update cache! + QFile cacheFile(cacheFileName); + + if (cacheFile.open(QIODevice::WriteOnly) == true) { + + // ok so we are going to be able to write this stuff + // so lets go recalculate it all + compute(); + + QDataStream outFile(&cacheFile); + + // go write it out + serialize(&outFile); + + // all done now, phew + cacheFile.close(); + + } else if (writeerror == false) { + + // popup the first time... + writeerror = true; + QMessageBox err; + QString errMessage = QString("Cannot create cache file %1.").arg(cacheFileName); + err.setText(errMessage); + err.setIcon(QMessageBox::Warning); + err.exec(); + return; + + } else { + + // send a console message instead... + qDebug()<<"cannot create cache file"<isDataPresent(series) == false) return; + + // if we want decimal places only keep to 1 dp max + // this is a factor that is applied at the end to + // convert from high-precision double to long + // e.g. 145.456 becomes 1455 if we want decimals + // and becomes 145 if we don't + double decimals = RideFile::decimalsFor(series) ? 10 : 1; + + // decritize the data series - seems wrong, since it just + // rounds to the nearest second - what if the recIntSecs + // is less than a second? Has been used for a long while + // so going to leave in tact for now + cpintdata data; + data.rec_int_ms = (int) round(ride->recIntSecs() * 1000.0); + foreach (const RideFilePoint *p, ride->dataPoints()) { + double secs = round(p->secs * 1000.0) / 1000; + if (secs > 0) data.points.append(cpintpoint(secs, (int) round(p->value(series)))); + } + int total_secs = (int) ceil(data.points.back().secs); + + // don't allow data more than two days + // was one week, but no single ride is longer + // than 2 days, even if you are doing RAAM + if (total_secs > 2*24*60*60) return; + + // ride_bests is used to temporarily hold + // the computed best intervals since I do + // not want to disturb the code at this stage + QVector ride_bests(total_secs + 1); + + // loop through the decritized data from top + // FIRST 5 MINUTES DO BESTS FOR EVERY SECOND + for (int i = 0; i < data.points.size() - 1; ++i) { + + cpintpoint *p = &data.points[i]; + + double sum = 0.0; + double prev_secs = p->secs; + + // from current point to end loop over remaining points + // look at every duration int seconds up to 300 seconds (5 minutes) + for (int j = i + 1; j < data.points.size() && data.points[j].secs - data.points[i].secs <= 360 ; ++j) { + + cpintpoint *q = &data.points[j]; + + sum += data.rec_int_ms / 1000.0 * q->value; + double dur_secs = q->secs - p->secs; + double avg = sum / dur_secs; + int dur_secs_top = (int) floor(dur_secs); + int dur_secs_bot = qMax((int) floor(dur_secs - data.rec_int_ms / 1000.0), 0); + + // loop over our bests (1 for every second of the ride) + // to see if we have a new best + for (int k = dur_secs_top; k > dur_secs_bot; --k) { + if (ride_bests[k] < avg) ride_bests[k] = avg; + } + prev_secs = q->secs; + } + } + + // NOW DO BESTS FOR EVERY 60s + // BETWEEN 6mins and the rest of the ride + // + // because we are dealing with longer durations we + // can afford to be less sensitive to missed data + // and sample rates - so we can downsample the sample + // data now to 5s samples - but if the recording interval + // is > 5s we won't bother, just set the interval used to + // whatever the sample rate is for the device. + QVector downsampled(0); + double samplerate; + + // moving to 5s samples would INCREASE the work... + if (ride->recIntSecs() >= 5) { + + samplerate = ride->recIntSecs(); + for (int i=0; i sums(downsampled.size()); + int windowsize = slice / samplerate; + int index=0; + double sum=0; + + for (int i=0; iwindowsize) { + sum -= downsampled[i-windowsize]; + sums[index++] = sum; + } + } + qSort(sums.begin(), sums.end()); + ride_bests[slice] = sums.last() / windowsize; + } + + // XXX Commented out since it just 'smooths' the drop + // off in CPs which is actually quite enlightening + // LEFT HERE IN CASE IT IS IMPORTANT! +#if 0 + // avoid decreasing work with increasing duration + { + double maxwork = 0.0; + for (int i = 1; i <= total_secs; ++i) { + // note index is being used here in lieu of time, as the index + // is assumed to be proportional to time + double work = ride_bests[i] * i; + if (maxwork > work) + ride_bests[i] = round(maxwork / i); + else + maxwork = work; + } + } +#endif + + // + // FILL IN THE GAPS + // + // We want to present a full set of bests for + // every duration so the data interface for this + // cache can remain the same, but the level of + // accuracy/granularity can change in here in the + // future if some fancy new algorithm arrives + // + double last = 0; + for (int i=ride_bests.size()-1; i; i--) { + if (ride_bests[i] == 0) ride_bests[i]=last; + else last = ride_bests[i]; + } + + // + // Now copy across into the array passed + // encoding decimal places as we go + array.resize(ride_bests.count()); + for (int i=0; i &array, RideFile::SeriesType series) +{ + // Derived data series are a special case + if (series == RideFile::xPower) { + computeDistributionXPower(); + return; + } + + if (series == RideFile::NP) { + computeDistributionNP(); + return; + } + + // only bother if the data series is actually present + if (ride->isDataPresent(series) == false) return; + + // setup the array based upon the ride + int decimals = RideFile::decimalsFor(series) ? 1 : 0; + double min = RideFile::minimumFor(series) * pow(10, decimals); + double max = RideFile::maximumFor(series) * pow(10, decimals); + + // lets resize the array to the right size + // it will also initialise with a default value + // which for longs is handily zero + array.resize(max-min); + + foreach(RideFilePoint *dp, ride->dataPoints()) { + double value = dp->value(series); + unsigned long lvalue = value * pow(10, decimals); + + int offset = lvalue - min; + if (offset >= 0 && offset < array.size()) array[offset]++; // XXX recintsecs != 1 + } +} + +void +RideFileCache::computeDistributionNP() {} + +void +RideFileCache::computeDistributionXPower() {} + +void +RideFileCache::computeMeanMaxNP() {} + +void +RideFileCache::computeMeanMaxXPower() {} + +// +// AGGREGATE FOR A GIVEN DATE RANGE +// +static QDate dateFromFileName(const QString filename) { + QRegExp rx("^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_\\d\\d_\\d\\d_\\d\\d\\..*$"); + if (rx.exactMatch(filename)) { + QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(), rx.cap(3).toInt()); + if (date.isValid()) return date; + } + return QDate(); // nil date +} + +// select and update bests +static void meanMaxAggregate(QVector &into, QVector &other, QVector&dates, QDate rideDate) +{ + if (into.size() < other.size()) { + into.resize(other.size()); + dates.resize(other.size()); + } + + for (int i=0; i into[i]) { + into[i] = other[i]; + dates[i] = rideDate; + } +} + +// resize into and then sum the arrays +static void distAggregate(QVector &into, QVector &other) +{ + for (int i=0; ihome)) { + QDate rideDate = dateFromFileName(rideFileName); + if (rideDate >= start && rideDate <= end) { + // get its cached values (will refresh if needed...) + RideFileCache rideCache(main, main->home.absolutePath() + "/" + rideFileName); + + // lets aggregate + meanMaxAggregate(wattsMeanMaxDouble, rideCache.wattsMeanMaxDouble, wattsMeanMaxDate, rideDate); + meanMaxAggregate(hrMeanMaxDouble, rideCache.hrMeanMaxDouble, hrMeanMaxDate, rideDate); + meanMaxAggregate(cadMeanMaxDouble, rideCache.cadMeanMaxDouble, cadMeanMaxDate, rideDate); + meanMaxAggregate(nmMeanMaxDouble, rideCache.nmMeanMaxDouble, nmMeanMaxDate, rideDate); + meanMaxAggregate(kphMeanMaxDouble, rideCache.kphMeanMaxDouble, kphMeanMaxDate, rideDate); + meanMaxAggregate(xPowerMeanMaxDouble, rideCache.xPowerMeanMaxDouble, xPowerMeanMaxDate, rideDate); + meanMaxAggregate(npMeanMaxDouble, rideCache.npMeanMaxDouble, npMeanMaxDate, rideDate); + + distAggregate(wattsDistributionDouble, rideCache.wattsDistributionDouble); + distAggregate(hrDistributionDouble, rideCache.hrDistributionDouble); + distAggregate(cadDistributionDouble, rideCache.cadDistributionDouble); + distAggregate(nmDistributionDouble, rideCache.nmDistributionDouble); + distAggregate(kphDistributionDouble, rideCache.kphDistributionDouble); + distAggregate(xPowerDistributionDouble, rideCache.xPowerDistributionDouble); + distAggregate(npDistributionDouble, rideCache.npDistributionDouble); + } + } +} + +// +// PERSISTANCE +// +void +RideFileCache::serialize(QDataStream *out) +{ + RideFileCacheHeader head; + + // write header + head.version = RideFileCacheVersion; + head.wattsMeanMaxCount = wattsMeanMax.size(); + head.hrMeanMaxCount = hrMeanMax.size(); + head.cadMeanMaxCount = cadMeanMax.size(); + head.nmMeanMaxCount = nmMeanMax.size(); + head.kphMeanMaxCount = kphMeanMax.size(); + head.xPowerMeanMaxCount = xPowerMeanMax.size(); + head.npMeanMaxCount = npMeanMax.size(); + + head.wattsDistCount = wattsDistribution.size(); + head.xPowerDistCount = xPowerDistribution.size(); + head.npDistCount = xPowerDistribution.size(); + head.hrDistCount = hrDistribution.size(); + head.cadDistCount = cadDistribution.size(); + head.nmDistrCount = nmDistribution.size(); + head.kphDistCount = kphDistribution.size(); + out->writeRawData((const char *) &head, sizeof(head)); + + // write meanmax + out->writeRawData((const char *) wattsMeanMax.data(), sizeof(unsigned long) * wattsMeanMax.size()); + out->writeRawData((const char *) hrMeanMax.data(), sizeof(unsigned long) * hrMeanMax.size()); + out->writeRawData((const char *) cadMeanMax.data(), sizeof(unsigned long) * cadMeanMax.size()); + out->writeRawData((const char *) nmMeanMax.data(), sizeof(unsigned long) * nmMeanMax.size()); + out->writeRawData((const char *) kphMeanMax.data(), sizeof(unsigned long) * kphMeanMax.size()); + out->writeRawData((const char *) xPowerMeanMax.data(), sizeof(unsigned long) * xPowerMeanMax.size()); + out->writeRawData((const char *) npMeanMax.data(), sizeof(unsigned long) * npMeanMax.size()); + + // write dist + out->writeRawData((const char *) wattsDistribution.data(), sizeof(unsigned long) * wattsDistribution.size()); + out->writeRawData((const char *) hrDistribution.data(), sizeof(unsigned long) * hrDistribution.size()); + out->writeRawData((const char *) cadDistribution.data(), sizeof(unsigned long) * cadDistribution.size()); + out->writeRawData((const char *) nmDistribution.data(), sizeof(unsigned long) * nmDistribution.size()); + out->writeRawData((const char *) kphDistribution.data(), sizeof(unsigned long) * kphDistribution.size()); + out->writeRawData((const char *) xPowerDistribution.data(), sizeof(unsigned long) * xPowerDistribution.size()); + out->writeRawData((const char *) npDistribution.data(), sizeof(unsigned long) * npDistribution.size()); +} + +void +RideFileCache::readCache() +{ + RideFileCacheHeader head; + QFile cacheFile(cacheFileName); + + if (cacheFile.open(QIODevice::ReadOnly) == true) { + QDataStream inFile(&cacheFile); + + inFile.readRawData((char *) &head, sizeof(head)); + + // resize all the arrays to fit + wattsMeanMax.resize(head.wattsMeanMaxCount); + hrMeanMax.resize(head.hrMeanMaxCount); + cadMeanMax.resize(head.cadMeanMaxCount); + nmMeanMax.resize(head.nmMeanMaxCount); + kphMeanMax.resize(head.kphMeanMaxCount); + wattsDistribution.resize(head.wattsDistCount); + hrDistribution.resize(head.hrDistCount); + cadDistribution.resize(head.cadDistCount); + nmDistribution.resize(head.nmDistrCount); + kphDistribution.resize(head.kphDistCount); + xPowerDistribution.resize(head.xPowerDistCount); + npDistribution.resize(head.npDistCount); + + // read in the arrays + inFile.readRawData((char *) wattsMeanMax.data(), sizeof(unsigned long) * wattsMeanMax.size()); + inFile.readRawData((char *) hrMeanMax.data(), sizeof(unsigned long) * hrMeanMax.size()); + inFile.readRawData((char *) cadMeanMax.data(), sizeof(unsigned long) * cadMeanMax.size()); + inFile.readRawData((char *) nmMeanMax.data(), sizeof(unsigned long) * nmMeanMax.size()); + inFile.readRawData((char *) kphMeanMax.data(), sizeof(unsigned long) * kphMeanMax.size()); + inFile.readRawData((char *) xPowerMeanMax.data(), sizeof(unsigned long) * xPowerMeanMax.size()); + inFile.readRawData((char *) npMeanMax.data(), sizeof(unsigned long) * npMeanMax.size()); + + // write dist + inFile.readRawData((char *) wattsDistribution.data(), sizeof(unsigned long) * wattsDistribution.size()); + inFile.readRawData((char *) hrDistribution.data(), sizeof(unsigned long) * hrDistribution.size()); + inFile.readRawData((char *) cadDistribution.data(), sizeof(unsigned long) * cadDistribution.size()); + inFile.readRawData((char *) nmDistribution.data(), sizeof(unsigned long) * nmDistribution.size()); + inFile.readRawData((char *) kphDistribution.data(), sizeof(unsigned long) * kphDistribution.size()); + inFile.readRawData((char *) xPowerDistribution.data(), sizeof(unsigned long) * xPowerDistribution.size()); + inFile.readRawData((char *) npDistribution.data(), sizeof(unsigned long) * npDistribution.size()); + + // setup the doubles the users use + doubleArray(wattsMeanMaxDouble, wattsMeanMax, RideFile::watts); + doubleArray(hrMeanMaxDouble, hrMeanMax, RideFile::hr); + doubleArray(cadMeanMaxDouble, cadMeanMax, RideFile::cad); + doubleArray(nmMeanMaxDouble, nmMeanMax, RideFile::nm); + doubleArray(kphMeanMaxDouble, kphMeanMax, RideFile::kph); + doubleArray(wattsDistributionDouble, wattsDistribution, RideFile::watts); + doubleArray(hrDistributionDouble, hrDistribution, RideFile::hr); + doubleArray(cadDistributionDouble, cadDistribution, RideFile::cad); + doubleArray(nmDistributionDouble, nmDistribution, RideFile::nm); + doubleArray(kphDistributionDouble, kphDistribution, RideFile::kph); + doubleArray(xPowerDistributionDouble, xPowerDistribution, RideFile::xPower); + doubleArray(npDistributionDouble, npDistribution, RideFile::NP); + + cacheFile.close(); + } +} + +// unpack the longs into a double array +void RideFileCache::doubleArray(QVector &into, QVector &from, RideFile::SeriesType series) +{ + double divisor = RideFile::decimalsFor(series) ? 10 : 1; + into.resize(from.size()); + for(int i=0; i +#include +#include +#include + +class MainWindow; +class RideFile; + +#include "GoldenCheetah.h" + +// RideFileCache is used to get meanmax and sample distribution +// arrays when plotting CP curves and histograms. It is precoputed +// to save time and cached in a file .cpx +// +// The contents of the cache reflect the data that is available within +// the source file. +static const unsigned int RideFileCacheVersion = 1; + +// The current version of the file has a binary format: +// 1 x Header data - describing the version and contents of the cache +// n x Blocks - meanmax or distribution arrays + +// The header is written directly to disk, the only +// field which is endian sensitive is the count field +// which will always be written in local format since these +// files are local caches we do not worry about endianness +struct RideFileCacheHeader { + + unsigned int version; + unsigned int wattsMeanMaxCount, + hrMeanMaxCount, + cadMeanMaxCount, + nmMeanMaxCount, + kphMeanMaxCount, + xPowerMeanMaxCount, + npMeanMaxCount, + wattsDistCount, + hrDistCount, + cadDistCount, + nmDistrCount, + kphDistCount, + xPowerDistCount, + npDistCount; + +}; + +// Each block of data is an array of uint32_t (32-bit "local-endian") +// integers so the "count" setting within the block definition tells +// us how long it is so we can read in one instruction and reference +// it directly. Of course, this means that for data series that require +// decimal places (e.g. speed) they are stored multiplied by 10^dp. +// so 27.1 is stored as 271, 27.454 is stored as 27454, 100.0001 is +// stored as 1000001. + +// So that none of the plots need to understand the format of this +// cache file this class is repsonsible for supplying the pre-computed +// values they desire. If the values have not been computed or are +// out of date then they are computed as needed. +// +// This cache is also updated by the metricaggregator to ensure it +// is updated alongside the metrics. So, in theory, at runtime, once +// the arrays have been computed they can be retrieved quickly. +// +// This is the main user entry to the ridefile cached data. +class RideFileCache +{ + public: + enum cachetype { meanmax, distribution, none }; + typedef enum cachetype CacheType; + + // Construct from a ridefile or its filename + // will reference cache if it exists, and create it + // if it doesn't. We allow to create from ridefile to + // save on ridefile reading if it is already opened by + // the calling class. + // to save time you can pass the ride file if you already have it open + // and if you don't want the data and just want to check pass check=true + RideFileCache(MainWindow *main, QString filename, RideFile *ride =0, bool check = false); + + // Construct a ridefile cache that represents the data + // across a date range. This is used to provide aggregated data. + RideFileCache(MainWindow *main, QDate start, QDate end); + + // get data + QVector &meanMaxArray(RideFile::SeriesType); // return meanmax array for the given series + QVector &meanMaxDates(RideFile::SeriesType series); // the dates of the bests + QVector &distributionArray(RideFile::SeriesType); // return distribution array for the given series + + // explain the array binning / sampling + double &distBinSize(RideFile::SeriesType); // return distribution bin size + double &meanMaxBinSize(RideFile::SeriesType); // return distribution bin size + + protected: + + void refreshCache(); // compute arrays and update cache + void readCache(); // just read from saved file and setup arrays + void serialize(QDataStream *out); // write to file + + void compute(); // compute all arrays + //void computeMeanMax(QVector&, RideFile::SeriesType); // compute mean max arrays + void computeDistribution(QVector&, RideFile::SeriesType); // compute the distributions + + // derived values are processed slightly differently + void computeDistributionNP(); + void computeDistributionXPower(); + void computeMeanMaxNP(); + void computeMeanMaxXPower(); + + + private: + + MainWindow *main; + QString rideFileName; // filename of ride + QString cacheFileName; // filename of cache file + RideFile *ride; + + // Should be 1 regardless of the rideFile::recIntSecs + // this might change in the future - but at the moment + // means that the data is "smoothed" to 1s samples + static const double _meanMaxBinSize = 1.0; + + // + // MEAN MAXIMAL VALUES + // + // each array has a best for duration 0 - RideDuration seconds + QVector wattsMeanMax; // RideFile::watts + QVector hrMeanMax; // RideFile::hr + QVector cadMeanMax; // RideFile::cad + QVector nmMeanMax; // RideFile::nm + QVector kphMeanMax; // RideFile::kph + QVector xPowerMeanMax; // RideFile::kph + QVector npMeanMax; // RideFile::kph + QVector wattsMeanMaxDouble; // RideFile::watts + QVector hrMeanMaxDouble; // RideFile::hr + QVector cadMeanMaxDouble; // RideFile::cad + QVector nmMeanMaxDouble; // RideFile::nm + QVector kphMeanMaxDouble; // RideFile::kph + QVector xPowerMeanMaxDouble; // RideFile::kph + QVector npMeanMaxDouble; // RideFile::kph + QVector wattsMeanMaxDate; // RideFile::watts + QVector hrMeanMaxDate; // RideFile::hr + QVector cadMeanMaxDate; // RideFile::cad + QVector nmMeanMaxDate; // RideFile::nm + QVector kphMeanMaxDate; // RideFile::kph + QVector xPowerMeanMaxDate; // RideFile::kph + QVector npMeanMaxDate; // RideFile::kph + + // + // SAMPLE DISTRIBUTION + // + // the distribution matches RideFile::decimalsFor(SeriesType series); + // each array contains a count (duration in recIntSecs) for each distrbution + // from RideFile::minimumFor() to RideFile::maximumFor(). The steps (binsize) + // is 1.0 or if the dataseries in question does have a nonZero value for + // RideFile::decimalsFor() then it will be distributed in 0.1 of a unit + QVector wattsDistribution; // RideFile::watts + QVector hrDistribution; // RideFile::hr + QVector cadDistribution; // RideFile::cad + QVector nmDistribution; // RideFile::nm + QVector kphDistribution; // RideFile::kph + QVector xPowerDistribution; // RideFile::kph + QVector npDistribution; // RideFile::kph + QVector wattsDistributionDouble; // RideFile::watts + QVector hrDistributionDouble; // RideFile::hr + QVector cadDistributionDouble; // RideFile::cad + QVector nmDistributionDouble; // RideFile::nm + QVector kphDistributionDouble; // RideFile::kph + QVector xPowerDistributionDouble; // RideFile::kph + QVector npDistributionDouble; // RideFile::kph + + // we need to return doubles not longs, we just use longs + // to reduce disk storage + void doubleArray(QVector &into, QVector &from, RideFile::SeriesType series); +}; + +// Working structured inherited from CpintPlot.cpp +// could probably be factored out and just use the +// ridefile structures, but this keeps well tested +// and stable legacy code intact +struct cpintpoint { + double secs; + int value; + cpintpoint() : secs(0.0), value(0) {} + cpintpoint(double s, int w) : secs(s), value(w) {} +}; + +struct cpintdata { + QStringList errors; + QVector points; + int rec_int_ms; + cpintdata() : rec_int_ms(0) {} +}; + +// the mean-max computer ... runs in a thread +class MeanMaxComputer : public QThread +{ + public: + MeanMaxComputer(RideFile *ride, QVector&array, RideFile::SeriesType series) + : ride(ride), array(array), series(series) {} + void run(); + + private: + RideFile *ride; + QVector &array; + RideFile::SeriesType series; +}; +#endif // _GC_RideFileCache_h diff --git a/src/src.pro b/src/src.pro index 914bf4457..b60be6f4c 100644 --- a/src/src.pro +++ b/src/src.pro @@ -249,6 +249,7 @@ HEADERS += \ RideCalendar.h \ RideEditor.h \ RideFile.h \ + RideFileCache.h \ RideFileCommand.h \ RideFileTableModel.h \ RideImportWizard.h \ @@ -406,6 +407,7 @@ SOURCES += \ RideCalendar.cpp \ RideEditor.cpp \ RideFile.cpp \ + RideFileCache.cpp \ RideFileCommand.cpp \ RideFileTableModel.cpp \ RideImportWizard.cpp \