From c1a8945a1189dc3784d65899118cadc4e02cbc7a Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Tue, 3 May 2011 16:26:40 +0100 Subject: [PATCH] Histogram plot by zone for seasons The recent update to plot histograms for seasons or other date ranges did not support displaying by zone since the cache did not contain zoned data. This patch fixes that with an update to RideFileCache to pre-compute and to the PowerHist class to retrieve and plot. There are some minor issues that need to be addressed: * Handling aggregation with different zone schemes * Deciding which zone scheme to use for the bar labels when multiple differing schemes have been used within the date range selected. * Showing a break down of time in zone by range i.e. how much time was spent at Threshold when CP was X as opposed to when it was Y (hint: do it like we currently display intervals when plotting a single ride). * Refreshing the Time In Zone data in the .cpx file when CP/LTHR changes is not implemented. The RideFileCache now checks the version of the cache to determine if it needs to be refreshed -- so no need to delete old .cpx files before running GC with this patch. --- src/PowerHist.cpp | 24 +++++++++++-- src/RideFileCache.cpp | 83 +++++++++++++++++++++++++++++++++++++++++-- src/RideFileCache.h | 26 +++++++++++--- 3 files changed, 124 insertions(+), 9 deletions(-) diff --git a/src/PowerHist.cpp b/src/PowerHist.cpp index 3fde6f0f2..79fa3d5c7 100644 --- a/src/PowerHist.cpp +++ b/src/PowerHist.cpp @@ -513,6 +513,20 @@ PowerHist::recalc(bool force) setAxisScale(QwtPlot::xBottom, -0.99, 0, 1); } + // watts zoned for a time range + if (source == Cache && zoned && series == RideFile::watts && mainWindow->zones()) { + setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(mainWindow->zones(), 0)); + if (mainWindow->zones()->getRangeSize()) + setAxisScale(QwtPlot::xBottom, -0.99, mainWindow->zones()->numZones(0), 1); // XXX use zones from first defined range + } + + // hr zoned for a time range + if (source == Cache && zoned && series == RideFile::hr && mainWindow->hrZones()) { + setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(mainWindow->hrZones(), 0)); + if (mainWindow->hrZones()->getRangeSize()) + setAxisScale(QwtPlot::xBottom, -0.99, mainWindow->hrZones()->numZones(0), 1); // XXX use zones from first defined range + } + setAxisMaxMinor(QwtPlot::xBottom, 0); } @@ -554,10 +568,10 @@ PowerHist::setData(RideFileCache *cache) // Now go set all those tedious arrays from // the ride cache wattsArray.resize(0); - wattsZoneArray.resize(0); + wattsZoneArray.resize(10); nmArray.resize(0); hrArray.resize(0); - hrZoneArray.resize(0); + hrZoneArray.resize(10); kphArray.resize(0); cadArray.resize(0); @@ -585,6 +599,12 @@ PowerHist::setData(RideFileCache *cache) for(int i=0; iwattsZoneArray()[i]; + hrZoneArray[i] = cache->hrZoneArray()[i]; + } } void diff --git a/src/RideFileCache.cpp b/src/RideFileCache.cpp index 56f2a4ce1..f07f7fc0c 100644 --- a/src/RideFileCache.cpp +++ b/src/RideFileCache.cpp @@ -18,6 +18,9 @@ #include "RideFileCache.h" #include "MainWindow.h" +#include "Zones.h" +#include "HrZones.h" + #include // for pow() #include #include @@ -44,6 +47,10 @@ RideFileCache::RideFileCache(MainWindow *main, QString fileName, RideFile *passe xPowerDistribution.resize(0); npDistribution.resize(0); + // time in zone are fixed to 10 zone max + wattsTimeInZone.resize(10); + hrTimeInZone.resize(10); + // Get info for ride file and cache file QFileInfo rideFileInfo(rideFileName); cacheFileName = rideFileInfo.path() + "/" + rideFileInfo.baseName() + ".cpx"; @@ -51,11 +58,34 @@ RideFileCache::RideFileCache(MainWindow *main, QString fileName, RideFile *passe // 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; + cacheFileInfo.size() >= (int)sizeof(struct RideFileCacheHeader)) { + + // we have a file, it is more recent than the ride file + // but is it the latest version? + RideFileCacheHeader head; + QFile cacheFile(cacheFileName); + if (cacheFile.open(QIODevice::ReadOnly) == true) { + + // read the header + QDataStream inFile(&cacheFile); + inFile.readRawData((char *) &head, sizeof(head)); + cacheFile.close(); + + // is it as recent as we are? + if (head.version == RideFileCacheVersion) { + + // Are the CP/LTHR values still correct + // XXX todo + + // WE'RE GOOD + if (check == false) readCache(); // if check is false we aren't just checking + return; + } + } } + // NEED TO UPDATE!! + // not up-to-date we need to refresh from the ridefile if (ride) { @@ -566,6 +596,16 @@ RideFileCache::computeDistribution(QVector &array, RideFile::Seri // only bother if the data series is actually present if (ride->isDataPresent(series) == false) return; + // get zones that apply, if any + int zoneRange = main->zones() ? main->zones()->whichRange(ride->startTime().date()) : -1; + int hrZoneRange = main->hrZones() ? main->hrZones()->whichRange(ride->startTime().date()) : -1; + + if (zoneRange != -1) CP=main->zones()->getCP(zoneRange); + else CP=0; + + if (hrZoneRange != -1) LTHR=main->hrZones()->getLT(hrZoneRange); + else LTHR=0; + // setup the array based upon the ride int decimals = RideFile::decimalsFor(series) ? 1 : 0; double min = RideFile::minimumFor(series) * pow(10, decimals); @@ -580,6 +620,14 @@ RideFileCache::computeDistribution(QVector &array, RideFile::Seri double value = dp->value(series); unsigned long lvalue = value * pow(10, decimals); + // watts time in zone + if (series == RideFile::watts && zoneRange != -1) + wattsTimeInZone[main->zones()->whichZone(zoneRange, dp->value(series))] += ride->recIntSecs(); + + // hr time in zone + if (series == RideFile::hr && hrZoneRange != -1) + hrTimeInZone[main->hrZones()->whichZone(hrZoneRange, dp->value(series))] += ride->recIntSecs(); + int offset = lvalue - min; if (offset >= 0 && offset < array.size()) array[offset] += ride->recIntSecs(); } @@ -617,6 +665,7 @@ static void distAggregate(QVector &into, QVector &other) { if (into.size() < other.size()) into.resize(other.size()); for (int i=0; isetCursor(Qt::WaitCursor); + // Iterate over the ride files (not the cpx files since they /might/ not // exist, or /might/ be out of date. foreach (QString rideFileName, RideFileFactory::instance().listRideFiles(main->home)) { @@ -664,8 +721,17 @@ RideFileCache::RideFileCache(MainWindow *main, QDate start, QDate end) distAggregate(kphDistributionDouble, rideCache.kphDistributionDouble); distAggregate(xPowerDistributionDouble, rideCache.xPowerDistributionDouble); distAggregate(npDistributionDouble, rideCache.npDistributionDouble); + + // cumulate timeinzones + for (int i=0; i<10; i++) { + hrTimeInZone[i] += rideCache.hrTimeInZone[i]; + wattsTimeInZone[i] += rideCache.wattsTimeInZone[i]; + } } } + + // set the cursor back to normal + main->setCursor(Qt::ArrowCursor); } // @@ -678,6 +744,9 @@ RideFileCache::serialize(QDataStream *out) // write header head.version = RideFileCacheVersion; + head.CP = CP; + head.LTHR = LTHR; + head.wattsMeanMaxCount = wattsMeanMax.size(); head.hrMeanMaxCount = hrMeanMax.size(); head.cadMeanMaxCount = cadMeanMax.size(); @@ -712,6 +781,10 @@ RideFileCache::serialize(QDataStream *out) 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()); + + // time in zone + out->writeRawData((const char *) wattsTimeInZone.data(), sizeof(unsigned long) * wattsTimeInZone.size()); + out->writeRawData((const char *) hrTimeInZone.data(), sizeof(unsigned long) * hrTimeInZone.size()); } void @@ -759,6 +832,10 @@ RideFileCache::readCache() inFile.readRawData((char *) xPowerDistribution.data(), sizeof(unsigned long) * xPowerDistribution.size()); inFile.readRawData((char *) npDistribution.data(), sizeof(unsigned long) * npDistribution.size()); + // time in zone + inFile.readRawData((char *) wattsTimeInZone.data(), sizeof(unsigned long) * 10); + inFile.readRawData((char *) hrTimeInZone.data(), sizeof(unsigned long) * 10); + // setup the doubles the users use doubleArray(wattsMeanMaxDouble, wattsMeanMax, RideFile::watts); doubleArray(hrMeanMaxDouble, hrMeanMax, RideFile::hr); diff --git a/src/RideFileCache.h b/src/RideFileCache.h index d1b680148..d5242490f 100644 --- a/src/RideFileCache.h +++ b/src/RideFileCache.h @@ -33,13 +33,17 @@ class RideFile; // 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; +static const unsigned int RideFileCacheVersion = 3; +// revision history: +// version date description +// 1 29-Apr-11 Initial - header, mean-max & distribution data blocks +// 2 02-May-11 Added LTHR/CP used to header and Time In Zone block -// The current version of the file has a binary format: +// The cache file (.cpx) has a binary format: // 1 x Header data - describing the version and contents of the cache // n x Blocks - meanmax or distribution arrays +// 1 x Watts TIZ - 10 unsigned longs +// 1 x Heartrate TIZ - 10 unsigned longs // The header is written directly to disk, the only // field which is endian sensitive is the count field @@ -48,6 +52,7 @@ static const unsigned int RideFileCacheVersion = 1; struct RideFileCacheHeader { unsigned int version; + unsigned int wattsMeanMaxCount, hrMeanMaxCount, cadMeanMaxCount, @@ -62,9 +67,13 @@ struct RideFileCacheHeader { kphDistCount, xPowerDistCount, npDistCount; + + int LTHR, // used to calculate Time in Zone (TIZ) + CP; // used to calculate Time in Zone (TIZ) }; + // 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 @@ -106,6 +115,8 @@ class RideFileCache 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 + QVector &wattsZoneArray() { return wattsTimeInZone; } + QVector &hrZoneArray() { return hrTimeInZone; } // explain the array binning / sampling double &distBinSize(RideFile::SeriesType); // return distribution bin size @@ -131,6 +142,10 @@ class RideFileCache QString cacheFileName; // filename of cache file RideFile *ride; + // used for zoning + int CP; + int LTHR; + // 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 @@ -185,6 +200,9 @@ class RideFileCache QVector xPowerDistributionDouble; // RideFile::kph QVector npDistributionDouble; // RideFile::kph + QVector wattsTimeInZone; // time in zone in seconds + QVector hrTimeInZone; // time in zone in seconds + // we need to return doubles not longs, we just use longs // to reduce disk storage void doubleArray(QVector &into, QVector &from, RideFile::SeriesType series);