diff --git a/src/Charts/HomeWindow.cpp b/src/Charts/HomeWindow.cpp index f63aad670..184c8f920 100644 --- a/src/Charts/HomeWindow.cpp +++ b/src/Charts/HomeWindow.cpp @@ -1206,7 +1206,7 @@ GcWindowDialog::GcWindowDialog(GcWinID type, Context *context, GcChartWindow **h } // special case - if (type == GcWindowTypes::Overview) { + if (type == GcWindowTypes::Overview || type == GcWindowTypes::OverviewTrends) { static_cast(win)->setConfiguration(""); } diff --git a/src/Charts/Overview.cpp b/src/Charts/Overview.cpp index fae0a4541..29f4cbce6 100644 --- a/src/Charts/Overview.cpp +++ b/src/Charts/Overview.cpp @@ -24,7 +24,7 @@ static QIcon grayConfig, whiteConfig, accentConfig; -OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), context(context), configured(false) +OverviewWindow::OverviewWindow(Context *context, int scope) : GcChartWindow(context), context(context), configured(false), scope(scope) { setContentsMargins(0,0,0,0); setProperty("color", GColor(COVERVIEWBACKGROUND)); @@ -39,14 +39,15 @@ OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), conte main->setSpacing(0); main->setContentsMargins(0,0,0,0); - space = new ChartSpace(context); + space = new ChartSpace(context, scope); main->addWidget(space); // all the widgets setChartLayout(main); // tell space when a ride is selected - connect(this, SIGNAL(rideItemChanged(RideItem*)), space, SLOT(rideSelected(RideItem*))); + if (scope & ANALYSIS) connect(this, SIGNAL(rideItemChanged(RideItem*)), space, SLOT(rideSelected(RideItem*))); + if (scope & TRENDS) connect(this, SIGNAL(dateRangeChanged(DateRange)), space, SLOT(dateRangeChanged(DateRange))); connect(addTile, SIGNAL(triggered(bool)), this, SLOT(addTile())); connect(space, SIGNAL(itemConfigRequested(ChartSpaceItem*)), this, SLOT(configItem(ChartSpaceItem*))); } @@ -54,7 +55,7 @@ OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), conte void OverviewWindow::addTile() { - AddChartWizard *p = new AddChartWizard(context, space); + AddChartWizard *p = new AddChartWizard(context, space, scope); p->exec(); // no mem leak delete on close dialog } @@ -92,6 +93,8 @@ OverviewWindow::getConfiguration() const //UNUSED RPEOverviewItem *rpe = reinterpret_cast(item); } break; + + case OverviewItemType::TOPN: case OverviewItemType::METRIC: { MetricOverviewItem *metric = reinterpret_cast(item); @@ -172,83 +175,156 @@ OverviewWindow::setConfiguration(QString config) if (config == "") { - // column 0 - ChartSpaceItem *add; - add = new PMCOverviewItem(space, "coggan_tss"); - space->addItem(1,0,9, add); + if (scope == ANALYSIS) { - add = new MetaOverviewItem(space, "Sport", "Sport"); - space->addItem(2,0,5, add); + // column 0 + ChartSpaceItem *add; + add = new PMCOverviewItem(space, "coggan_tss"); + space->addItem(1,0,9, add); - add = new MetaOverviewItem(space, "Workout Code", "Workout Code"); - space->addItem(3,0,5, add); + add = new MetaOverviewItem(space, "Sport", "Sport"); + space->addItem(2,0,5, add); - add = new MetricOverviewItem(space, "Duration", "workout_time"); - space->addItem(4,0,9, add); + add = new MetaOverviewItem(space, "Workout Code", "Workout Code"); + space->addItem(3,0,5, add); - add = new MetaOverviewItem(space, "Notes", "Notes"); - space->addItem(5,0,13, add); + add = new MetricOverviewItem(space, "Duration", "workout_time"); + space->addItem(4,0,9, add); - // column 1 - add = new MetricOverviewItem(space, "HRV rMSSD", "rMSSD"); - space->addItem(1,1,9, add); + add = new MetaOverviewItem(space, "Notes", "Notes"); + space->addItem(5,0,13, add); - add = new MetricOverviewItem(space, "Heartrate", "average_hr"); - space->addItem(2,1,5, add); + // column 1 + add = new MetricOverviewItem(space, "HRV rMSSD", "rMSSD"); + space->addItem(1,1,9, add); - add = new ZoneOverviewItem(space, "Heartrate Zones", RideFile::hr); - space->addItem(3,1,11, add); + add = new MetricOverviewItem(space, "Heartrate", "average_hr"); + space->addItem(2,1,5, add); - add = new MetricOverviewItem(space, "Climbing", "elevation_gain"); - space->addItem(4,1,5, add); + add = new ZoneOverviewItem(space, "Heartrate Zones", RideFile::hr); + space->addItem(3,1,11, add); - add = new MetricOverviewItem(space, "Cadence", "average_cad"); - space->addItem(5,1,5, add); + add = new MetricOverviewItem(space, "Climbing", "elevation_gain"); + space->addItem(4,1,5, add); - add = new MetricOverviewItem(space, "Work", "total_work"); - space->addItem(6,1,5, add); + add = new MetricOverviewItem(space, "Cadence", "average_cad"); + space->addItem(5,1,5, add); - // column 2 - add = new RPEOverviewItem(space, "RPE"); - space->addItem(1,2,9, add); + add = new MetricOverviewItem(space, "Work", "total_work"); + space->addItem(6,1,5, add); - add = new MetricOverviewItem(space, "Stress", "coggan_tss"); - space->addItem(2,2,5, add); + // column 2 + add = new RPEOverviewItem(space, "RPE"); + space->addItem(1,2,9, add); - add = new ZoneOverviewItem(space, "Fatigue Zones", RideFile::wbal); - space->addItem(3,2,11, add); + add = new MetricOverviewItem(space, "Stress", "coggan_tss"); + space->addItem(2,2,5, add); - add = new IntervalOverviewItem(space, "Intervals", "elapsed_time", "average_power", "workout_time"); - space->addItem(4,2,17, add); + add = new ZoneOverviewItem(space, "Fatigue Zones", RideFile::wbal); + space->addItem(3,2,11, add); - // column 3 - add = new MetricOverviewItem(space, "Power", "average_power"); - space->addItem(1,3,9, add); + add = new IntervalOverviewItem(space, "Intervals", "elapsed_time", "average_power", "workout_time"); + space->addItem(4,2,17, add); - add = new MetricOverviewItem(space, "IsoPower", "coggan_np"); - space->addItem(2,3,5, add); + // column 3 + add = new MetricOverviewItem(space, "Power", "average_power"); + space->addItem(1,3,9, add); - add = new ZoneOverviewItem(space, "Power Zones", RideFile::watts); - space->addItem(3,3,11, add); + add = new MetricOverviewItem(space, "IsoPower", "coggan_np"); + space->addItem(2,3,5, add); - add = new MetricOverviewItem(space, "Peak Power Index", "peak_power_index"); - space->addItem(4,3,8, add); + add = new ZoneOverviewItem(space, "Power Zones", RideFile::watts); + space->addItem(3,3,11, add); - add = new MetricOverviewItem(space, "Variability", "coggam_variability_index"); - space->addItem(5,3,8, add); + add = new MetricOverviewItem(space, "Peak Power Index", "peak_power_index"); + space->addItem(4,3,8, add); - // column 4 - add = new MetricOverviewItem(space, "Distance", "total_distance"); - space->addItem(1,4,9, add); + add = new MetricOverviewItem(space, "Variability", "coggam_variability_index"); + space->addItem(5,3,8, add); - add = new MetricOverviewItem(space, "Speed", "average_speed"); - space->addItem(2,4,5, add); + // column 4 + add = new MetricOverviewItem(space, "Distance", "total_distance"); + space->addItem(1,4,9, add); - add = new ZoneOverviewItem(space, "Pace Zones", RideFile::kph); - space->addItem(3,4,11, add); + add = new MetricOverviewItem(space, "Speed", "average_speed"); + space->addItem(2,4,5, add); - add = new RouteOverviewItem(space, "Route"); - space->addItem(4,4,17, add); + add = new ZoneOverviewItem(space, "Pace Zones", RideFile::kph); + space->addItem(3,4,11, add); + + add = new RouteOverviewItem(space, "Route"); + space->addItem(4,4,17, add); + + } + + if (scope == TRENDS) { + + ChartSpaceItem *add; + + // column 0 + add = new KPIOverviewItem(space, tr("Distance"), 0, 10000, "{ round(sum(metrics(Distance))); }", "km"); + space->addItem(0,0,8, add); + + add = new TopNOverviewItem(space, tr("Going Long"), "total_distance"); + space->addItem(1,0,25, add); + + add = new KPIOverviewItem(space, tr("Weekly Hours"), 0, 15, "{ weeks <- (daterange(stop)-daterange(start))/7; round(10*sum(metrics(Duration)/3600)/weeks)/10; }", tr("hours")); + space->addItem(2,0,7, add); + + // column 1 + add = new KPIOverviewItem(space, tr("Peak Power Index"), 0, 150, "{ round(sort(descend, metrics(Power_Index))[0]); }", "%"); + space->addItem(0,1,8, add); + + add = new MetricOverviewItem(space, tr("Max Power"), "max_power"); + space->addItem(1,1,7, add); + + add = new MetricOverviewItem(space, tr("Average Power"), "average_power"); + space->addItem(2,1,7, add); + + add = new ZoneOverviewItem(space, tr("Power Zones"), RideFile::hr); + space->addItem(3,1,9, add); + + add = new MetricOverviewItem(space, tr("Total TSS"), "coggan_tss"); + space->addItem(4,1,7, add); + + // column 2 + add = new KPIOverviewItem(space, tr("Total Hours"), 0, 0, "{ round(sum(metrics(Duration))/3600); }", "hours"); + space->addItem(0,2,8, add); + + add = new TopNOverviewItem(space, tr("Going Hard"), "skiba_wprime_exp"); + space->addItem(1,2,25, add); + + add = new MetricOverviewItem(space, tr("Total W' Work"), "skiba_wprime_exp"); + space->addItem(2,2,7, add); + + // column 3 + add = new KPIOverviewItem(space, tr("W' Ratio"), 0, 100, "{ round((sum(metrics(W'_Work)) / sum(metrics(Work))) * 100); }", "%"); + space->addItem(0,3,8, add); + + add = new KPIOverviewItem(space, tr("Peak CP Estimate "), 0, 360, "{ round(max(estimates(cp3,cp))); }", "watts"); + space->addItem(1,3,7, add); + + add = new KPIOverviewItem(space, tr("Peak W' Estimate "), 0, 25, "{ round(max(estimates(cp3,w')/1000)*10)/10; }", "kJ"); + space->addItem(2,3,7, add); + + + add = new ZoneOverviewItem(space, tr("Fatigue Zones"), RideFile::wbal); + space->addItem(3,3,9, add); + + add = new MetricOverviewItem(space, tr("Total Work"), "total_work"); + space->addItem(4,3,7, add); + + // column 4 + add = new MetricOverviewItem(space, tr("Intensity Factor"), "coggan_if"); + space->addItem(0,4,8, add); + + add = new TopNOverviewItem(space, tr("Going Deep"), "skiba_wprime_low"); + space->addItem(1,4,25, add); + + add = new KPIOverviewItem(space, tr("IF > 0.85"), 0, 0, "{ count(metrics(IF)[x>0.85]); }", "activities"); + space->addItem(2,4,7, add); + + } } else { @@ -298,6 +374,14 @@ OverviewWindow::setConfiguration(QString config) } break; + case OverviewItemType::TOPN : + { + QString symbol=obj["symbol"].toString(); + add = new TopNOverviewItem(space, name,symbol); + space->addItem(order,column,deep, add); + } + break; + case OverviewItemType::METRIC : { QString symbol=obj["symbol"].toString(); @@ -406,8 +490,8 @@ OverviewConfigDialog::close() main->removeWidget(item->config()); // doesn't work xxx todo ! // update after config changed - if (item->parent->context->currentRideItem()) - item->setData(const_cast(item->parent->context->currentRideItem())); + if (item->parent->scope & ANALYSIS && item->parent->currentRideItem) item->setData(item->parent->currentRideItem); + if (item->parent->scope & TRENDS ) item->setDateRange(item->parent->currentDateRange); } accept(); diff --git a/src/Charts/Overview.h b/src/Charts/Overview.h index 991051fee..62e3217cd 100644 --- a/src/Charts/Overview.h +++ b/src/Charts/Overview.h @@ -31,6 +31,7 @@ #include "HrZones.h" #include "ChartSpace.h" +#include "OverviewItems.h" class OverviewWindow : public GcChartWindow { @@ -40,7 +41,7 @@ class OverviewWindow : public GcChartWindow public: - OverviewWindow(Context *context); + OverviewWindow(Context *context, int scope=ANALYSIS); // used by children Context *context; @@ -62,6 +63,7 @@ class OverviewWindow : public GcChartWindow // gui setup ChartSpace *space; bool configured; + int scope; }; class OverviewConfigDialog : public QDialog diff --git a/src/Charts/OverviewItems.cpp b/src/Charts/OverviewItems.cpp index 8d9f853eb..fa3044713 100644 --- a/src/Charts/OverviewItems.cpp +++ b/src/Charts/OverviewItems.cpp @@ -50,15 +50,16 @@ static bool _registerItems() // get the factory ChartSpaceItemRegistry ®istry = ChartSpaceItemRegistry::instance(); - // Register TYPE SHORT DESCRIPTION SCOPE CREATOR - registry.addItem(OverviewItemType::METRIC, QObject::tr("Metric"), QObject::tr("Metric and Sparkline"), 0, MetricOverviewItem::create); - registry.addItem(OverviewItemType::KPI, QObject::tr("KPI"), QObject::tr("KPI calculation and progress bar"), 0, KPIOverviewItem::create); - registry.addItem(OverviewItemType::META, QObject::tr("Metadata"), QObject::tr("Metadata and Sparkline"), 0, MetaOverviewItem::create); - registry.addItem(OverviewItemType::ZONE, QObject::tr("Zones"), QObject::tr("Zone Histogram"), 0, ZoneOverviewItem::create); - registry.addItem(OverviewItemType::RPE, QObject::tr("RPE"), QObject::tr("RPE Widget"), 0, RPEOverviewItem::create); - registry.addItem(OverviewItemType::INTERVAL, QObject::tr("Intervals"), QObject::tr("Interval Bubble Chart"), 0, IntervalOverviewItem::create); - registry.addItem(OverviewItemType::PMC, QObject::tr("PMC"), QObject::tr("PMC Status Summary"), 0, PMCOverviewItem::create); - registry.addItem(OverviewItemType::ROUTE, QObject::tr("Route"), QObject::tr("Route Summary"), 0, RouteOverviewItem::create); + // Register TYPE SHORT DESCRIPTION SCOPE CREATOR + registry.addItem(OverviewItemType::METRIC, QObject::tr("Metric"), QObject::tr("Metric and Sparkline"), ANALYSIS|TRENDS, MetricOverviewItem::create); + registry.addItem(OverviewItemType::KPI, QObject::tr("KPI"), QObject::tr("KPI calculation and progress bar"), ANALYSIS|TRENDS, KPIOverviewItem::create); + registry.addItem(OverviewItemType::TOPN, QObject::tr("Bests"), QObject::tr("Ranked list of bests"), TRENDS, TopNOverviewItem::create); + registry.addItem(OverviewItemType::META, QObject::tr("Metadata"), QObject::tr("Metadata and Sparkline"), ANALYSIS, MetaOverviewItem::create); + registry.addItem(OverviewItemType::ZONE, QObject::tr("Zones"), QObject::tr("Zone Histogram"), ANALYSIS|TRENDS, ZoneOverviewItem::create); + registry.addItem(OverviewItemType::RPE, QObject::tr("RPE"), QObject::tr("RPE Widget"), ANALYSIS, RPEOverviewItem::create); + registry.addItem(OverviewItemType::INTERVAL, QObject::tr("Intervals"), QObject::tr("Interval Bubble Chart"), ANALYSIS, IntervalOverviewItem::create); + registry.addItem(OverviewItemType::PMC, QObject::tr("PMC"), QObject::tr("PMC Status Summary"), ANALYSIS, PMCOverviewItem::create); + registry.addItem(OverviewItemType::ROUTE, QObject::tr("Route"), QObject::tr("Route Summary"), ANALYSIS, RouteOverviewItem::create); return true; } @@ -71,7 +72,7 @@ RPEOverviewItem::RPEOverviewItem(ChartSpace *parent, QString name) : ChartSpaceI // a META widget, "RPE" using the FOSTER modified 0-10 scale this->type = OverviewItemType::RPE; - sparkline = new Sparkline(this, SPARKDAYS+1, name); + sparkline = new Sparkline(this, name); rperating = new RPErating(this, name); } @@ -112,40 +113,40 @@ RouteOverviewItem::~RouteOverviewItem() } static const QStringList timeInZones = QStringList() - << "percent_in_zone_L1" - << "percent_in_zone_L2" - << "percent_in_zone_L3" - << "percent_in_zone_L4" - << "percent_in_zone_L5" - << "percent_in_zone_L6" - << "percent_in_zone_L7" - << "percent_in_zone_L8" - << "percent_in_zone_L9" - << "percent_in_zone_L10"; + << "time_in_zone_L1" + << "time_in_zone_L2" + << "time_in_zone_L3" + << "time_in_zone_L4" + << "time_in_zone_L5" + << "time_in_zone_L6" + << "time_in_zone_L7" + << "time_in_zone_L8" + << "time_in_zone_L9" + << "time_in_zone_L10"; static const QStringList paceTimeInZones = QStringList() - << "percent_in_zone_P1" - << "percent_in_zone_P2" - << "percent_in_zone_P3" - << "percent_in_zone_P4" - << "percent_in_zone_P5" - << "percent_in_zone_P6" - << "percent_in_zone_P7" - << "percent_in_zone_P8" - << "percent_in_zone_P9" - << "percent_in_zone_P10"; + << "time_in_zone_P1" + << "time_in_zone_P2" + << "time_in_zone_P3" + << "time_in_zone_P4" + << "time_in_zone_P5" + << "time_in_zone_P6" + << "time_in_zone_P7" + << "time_in_zone_P8" + << "time_in_zone_P9" + << "time_in_zone_P10"; static const QStringList timeInZonesHR = QStringList() - << "percent_in_zone_H1" - << "percent_in_zone_H2" - << "percent_in_zone_H3" - << "percent_in_zone_H4" - << "percent_in_zone_H5" - << "percent_in_zone_H6" - << "percent_in_zone_H7" - << "percent_in_zone_H8" - << "percent_in_zone_H9" - << "percent_in_zone_H10"; + << "time_in_zone_H1" + << "time_in_zone_H2" + << "time_in_zone_H3" + << "time_in_zone_H4" + << "time_in_zone_H5" + << "time_in_zone_H6" + << "time_in_zone_H7" + << "time_in_zone_H8" + << "time_in_zone_H9" + << "time_in_zone_H10"; static const QStringList timeInZonesWBAL = QStringList() << "wtime_in_zone_L1" @@ -279,7 +280,8 @@ MetricOverviewItem::MetricOverviewItem(ChartSpace *parent, QString name, QString if (metric) units = metric->units(parent->context->athlete->useMetricUnits); // we may plot the metric sparkline if the tile is big enough - sparkline = new Sparkline(this, SPARKDAYS+1, name); + bool bigdot = parent->scope == ANALYSIS ? true : false; + sparkline = new Sparkline(this, name, bigdot); } @@ -288,6 +290,25 @@ MetricOverviewItem::~MetricOverviewItem() delete sparkline; } +TopNOverviewItem::TopNOverviewItem(ChartSpace *parent, QString name, QString symbol) : ChartSpaceItem(parent, name) +{ + // metric + this->type = OverviewItemType::TOPN; + this->symbol = symbol; + + RideMetricFactory &factory = RideMetricFactory::instance(); + this->metric = const_cast(factory.rideMetric(symbol)); + if (metric) units = metric->units(parent->context->athlete->useMetricUnits); + + animator=new QPropertyAnimation(this, "transition"); +} + +TopNOverviewItem::~TopNOverviewItem() +{ + animator->stop(); + delete animator; +} + PMCOverviewItem::PMCOverviewItem(ChartSpace *parent, QString symbol) : ChartSpaceItem(parent, "") { // PMC doesn't have a title as we show multiple things @@ -319,7 +340,7 @@ MetaOverviewItem::MetaOverviewItem(ChartSpace *parent, QString name, QString sym // sparkline if are we numeric? if (fieldtype == FIELD_INTEGER || fieldtype == FIELD_DOUBLE) { - sparkline = new Sparkline(this, SPARKDAYS+1, name); + sparkline = new Sparkline(this, name); } else { sparkline = NULL; } @@ -372,6 +393,25 @@ KPIOverviewItem::setData(RideItem *item) itemGeometryChanged(); } +void +KPIOverviewItem::setDateRange(DateRange dr) +{ + // calculate the value... + DataFilter parser(this, parent->context, program); + Result res = parser.evaluate(dr); + + // set to zero for daft values + value = QString("%1").arg(res.number); + if (value == "nan") value =""; + value=Utils::removeDP(value); + + // now set the progressbar + progressbar->setValue(start, stop, value.toDouble()); + + // show/hide widgets on the basis of geometry + itemGeometryChanged(); +} + void RPEOverviewItem::setData(RideItem *item) { @@ -542,6 +582,169 @@ MetricOverviewItem::setData(RideItem *item) } } +void +MetricOverviewItem::setDateRange(DateRange dr) +{ + Specification spec; + spec.setDateRange(dr); + + // aggregate sum and count etc + double v=0; // value + double c=0; // count + bool first=true; + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + // get value and count + double value = item->getForSymbol(symbol, parent->context->athlete->useMetricUnits); + double count = item->getCountForSymbol(symbol); + if (count <= 0) count = 1; + + // ignore zeroes when aggregating? + if (metric->aggregateZero() == false && value == 0) continue; + + // what we gonna do with this? + switch(metric->type()) { + case RideMetric::StdDev: + case RideMetric::MeanSquareRoot: + case RideMetric::Average: + v += value*count; + c += count; + break; + case RideMetric::Total: + case RideMetric::RunningTotal: + v += value; + break; + case RideMetric::Peak: + if (first || value > v) v = value; + break; + case RideMetric::Low: + if (first || value < v) v = value; + break; + break; + } + first = false; + } + + // now apply averaging etc + switch(metric->type()) { + case RideMetric::StdDev: + case RideMetric::MeanSquareRoot: + case RideMetric::Average: + if (c) v = v / c; + else v = 0; + break; + default: break; + } + + // get the metric value + value = Utils::removeDP(QString("%1").arg(v,0,'f',metric->precision())); + if (value == "nan") value =""; + if (std::isinf(v) || std::isnan(v)) v=0; + + + // metric history + QList points; + + // how many days + QDate earliest(1900,01,01); + sparkline->setDays(earliest.daysTo(dr.to) - earliest.daysTo(dr.from)); + + double min=0, max=0; + double sum=0; + first=true; + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + double value = item->getForSymbol(symbol, parent->context->athlete->useMetricUnits); + + // no zero values + if (value == 0) continue; + + // cum sum for Total and RunningTotals + if (metric->type() == RideMetric::Total || metric->type() == RideMetric::RunningTotal) { + sum += value; + value = sum; + } + + points << QPointF(earliest.daysTo(item->dateTime.date()) - earliest.daysTo(dr.from), value); + + if (value < min) min=value; + if (first || value > max) max=value; + first = false; + } + + // update the sparkline + sparkline->setPoints(points); + + // set range + sparkline->setRange(min*1.1,max*1.1); // add 10% to each direction + +} + +static bool entrylessthan(struct topnentry &a, const topnentry &b) +{ + return a.v < b.v; +} + +void +TopNOverviewItem::setDateRange(DateRange dr) +{ + // clear out the old values + ranked.clear(); + + // filtering + Specification spec; + spec.setDateRange(dr); + + // pmc data + PMCData stressdata(parent->context, spec, "coggan_tss"); + maxvalue=""; + maxv=0; // must never have -ve max + minv=0; // always zero minimum + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + // get value and count + double v = item->getForSymbol(symbol, parent->context->athlete->useMetricUnits); + QString value = item->getStringForSymbol(symbol, parent->context->athlete->useMetricUnits); + int index = stressdata.indexOf(item->dateTime.date()); + double tsb = 0; + if (index >= 0 && index < stressdata.sb().count()) tsb = stressdata.sb()[index]; + + // add to the list + QColor color = (item->color.red() == 1 && item->color.green() == 1 && item->color.blue() == 1) ? GColor(CPLOTMARKER) : item->color; + ranked << topnentry(item->dateTime.date(), v, value, color, tsb); + + // biggest value? + if (v > maxv) { + maxvalue=value; + maxv = v; + } + + // minv should be 0 unless it goes negative + if (v < minv) minv=v; + } + + // sort the list + if (metric->type() == RideMetric::Low) qSort(ranked.begin(), ranked.end(), entrylessthan); + else qSort(ranked); + + // change painting details + itemGeometryChanged(); + + // animate the transition + animator->stop(); + animator->setStartValue(0); + animator->setEndValue(100); + animator->setEasingCurve(QEasingCurve::OutQuad); + animator->setDuration(400); + animator->start(); +} + void MetaOverviewItem::setData(RideItem *item) { @@ -649,6 +852,112 @@ PMCOverviewItem::setData(RideItem *item) } +void +ZoneOverviewItem::setDateRange(DateRange dr) +{ + QVector vals(10); // max 10 seems ok + vals.fill(0); + + // stop any animation before starting, just in case- stops a crash + // when we update a chart in the middle of its animation + if (chart) chart->setAnimationOptions(QChart::NoAnimation);; + + // enable animation when setting values (disabled at all other times) + if (chart) chart->setAnimationOptions(QChart::SeriesAnimations); + + Specification spec; + spec.setDateRange(dr); + + // aggregate sum and count etc + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + switch(series) { + + // + // HEARTRATE + // + case RideFile::hr: + { + if (parent->context->athlete->hrZones(item->isRun)) { + + int numhrzones; + int hrrange = parent->context->athlete->hrZones(item->isRun)->whichRange(item->dateTime.date()); + + if (hrrange > -1) { + + numhrzones = parent->context->athlete->hrZones(item->isRun)->numZones(hrrange); + for(int i=0; igetForSymbol(timeInZonesHR[i]); + } + } + } + } + break; + + // + // POWER + // + default: + case RideFile::watts: + { + if (parent->context->athlete->zones(item->isRun)) { + + int numzones; + int range = parent->context->athlete->zones(item->isRun)->whichRange(item->dateTime.date()); + + if (range > -1) { + + numzones = parent->context->athlete->zones(item->isRun)->numZones(range); + for(int i=0; igetForSymbol(timeInZones[i]); + } + } + } + } + break; + + // + // PACE + // + case RideFile::kph: + { + if ((item->isRun || item->isSwim) && parent->context->athlete->paceZones(item->isSwim)) { + + int numzones; + int range = parent->context->athlete->paceZones(item->isSwim)->whichRange(item->dateTime.date()); + + if (range > -1) { + + numzones = parent->context->athlete->paceZones(item->isSwim)->numZones(range); + for(int i=0; igetForSymbol(paceTimeInZones[i]); + } + } + } + } + break; + + case RideFile::wbal: + { + for(int i=0; i<4; i++) { + vals[i] += item->getForSymbol(timeInZonesWBAL[i]); + } + } + break; + } + } + + // now update the barset converting to percentages + double sum=0; + for(int i=0; ireplace(i, round(vals[i]/sum * 100)); + else barset->replace(i, round(vals[i]/sum * 100)); + } +} + void ZoneOverviewItem::setData(RideItem *item) { @@ -676,9 +985,17 @@ ZoneOverviewItem::setData(RideItem *item) if (hrrange > -1) { + double sum=0; numhrzones = parent->context->athlete->hrZones(item->isRun)->numZones(hrrange); for(int i=0; ireplace(i, round(item->getForSymbol(timeInZonesHR[i]))); + sum += item->getForSymbol(timeInZonesHR[i]); + } + + // update as percent of total + for(int i=0; i<4; i++) { + double time =round(item->getForSymbol(timeInZonesHR[i])); + if (time > 0 && sum > 0) barset->replace(i, round((time/sum) * 100)); + else barset->replace(i, 0); } } else { @@ -706,9 +1023,17 @@ ZoneOverviewItem::setData(RideItem *item) if (range > -1) { + double sum=0; numzones = parent->context->athlete->zones(item->isRun)->numZones(range); for(int i=0; ireplace(i, round(item->getForSymbol(timeInZones[i]))); + sum += item->getForSymbol(timeInZones[i]); + } + + // update as percent of total + for(int i=0; i<4; i++) { + double time =round(item->getForSymbol(timeInZones[i])); + if (time > 0 && sum > 0) barset->replace(i, round((time/sum) * 100)); + else barset->replace(i, 0); } } else { @@ -735,9 +1060,17 @@ ZoneOverviewItem::setData(RideItem *item) if (range > -1) { + double sum=0; numzones = parent->context->athlete->paceZones(item->isSwim)->numZones(range); for(int i=0; ireplace(i, round(item->getForSymbol(paceTimeInZones[i]))); + sum += item->getForSymbol(paceTimeInZones[i]); + } + + // update as percent of total + for(int i=0; i<4; i++) { + double time =round(item->getForSymbol(paceTimeInZones[i])); + if (time > 0 && sum > 0) barset->replace(i, round((time/sum) * 100)); + else barset->replace(i, 0); } } else { @@ -899,6 +1232,9 @@ MetricOverviewItem::itemGeometryChanged() { } } +// painter truncates the list depending upon the size of the widget +void TopNOverviewItem::itemGeometryChanged() { } + void MetaOverviewItem::itemGeometryChanged() { @@ -1160,18 +1496,105 @@ MetricOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem QRectF trirect(bl.x() + trect.width() + ROWHEIGHT, bl.y() - trect.height(), trect.height()*0.66f, trect.height()); - // trend triangle - QPainterPath triangle; - painter->setBrush(QBrush(QColor(up ? Qt::darkGreen : Qt::darkRed))); - painter->setPen(Qt::NoPen); + // activity show if current one is up or down on trend for last 30 days.. + if (parent->scope == ANALYSIS) { - triangle.moveTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); - triangle.lineTo((trirect.left() + trirect.right()) / 2.0f, up ? trirect.top() : trirect.bottom()); - triangle.lineTo(trirect.right(), (trirect.top()+trirect.bottom())/2.0f); - triangle.lineTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); + // trend triangle + QPainterPath triangle; + painter->setBrush(QBrush(QColor(up ? Qt::darkGreen : Qt::darkRed))); + painter->setPen(Qt::NoPen); - painter->drawPath(triangle); + triangle.moveTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); + triangle.lineTo((trirect.left() + trirect.right()) / 2.0f, up ? trirect.top() : trirect.bottom()); + triangle.lineTo(trirect.right(), (trirect.top()+trirect.bottom())/2.0f); + triangle.lineTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); + painter->drawPath(triangle); + } + +} + +void +TopNOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + + // CALC GEOM / OFFSETS + QFontMetrics fm(parent->smallfont); + painter->setFont(parent->smallfont); + + // we paint the table, so lets work out the geometry and count etc + QRectF paintarea = QRectF(20,ROWHEIGHT*2, geometry().width()-40, geometry().height()-20-(ROWHEIGHT*2)); + + // max rows + double margins = 30; + double rowheight = fm.ascent() + margins; + int maxrows = (paintarea.height()-margins) / rowheight; + + // min and max values for what is being painted + minv=0; + maxv=0; + for (int i=0; i maxv) maxv = v; + + // strings for rect sizing (remember neg values are longer strings) + if (ranked[i].value.length() > maxvalue.length()) maxvalue = ranked[i].value; + + // minv should be 0 unless it goes negative + if (v < minv) minv=v; + } + + // number rect + QRectF numrect = QRectF(0,0, fm.boundingRect(QString(" %1.").arg(maxrows)).width(), fm.boundingRect(QString(" %1.").arg(maxrows)).height()); + + // date rect + QRectF daterect = QRectF(0,0, fm.boundingRect("31 May yy").width(), fm.boundingRect("31 May yy").height()); + + // value rect + QRectF valuerect = QRectF(0,0, fm.boundingRect(maxvalue).width(), fm.boundingRect(maxvalue).height()); + + // bar rect + int width = paintarea.width() - (numrect.width() + daterect.width() + valuerect.width() + (margins * 6)); + QRectF barrect = QRectF(0,10, width, 30); + + + // PAINT + for (int i=0; isetPen(QColor(100,100,100)); + painter->drawText(paintarea.topLeft()+QPointF(margins, margins+(i*rowheight)+fm.ascent()), QString("%1.").arg(i+1)); + + // date + QString datestring = ranked[i].date.toString("d MMM yy"); + painter->drawText(paintarea.topLeft()+numrect.topRight()+QPointF(margins*2, margins+(i*rowheight)+fm.ascent()), datestring); + + // bar width bearing in mind range might start at -ve value + double width = (ranked[i].v / (maxv-minv)) * barrect.width() * (double(transition)/100.00); + + // 0 width is invisible, always at least 5 + if (width == 0) width = 5; + + // push rect across to account for 0 being in middle of bar when -ve values + // NOTE: minv must NEV£R be > 0 ! + double offset = (barrect.width() / (maxv-minv)) * fabs(minv); + + // rectangles for full and this value + QRectF fullbar(paintarea.left()+numrect.width()+daterect.width()+(margins*3), paintarea.top()+margins+(i*rowheight)+fm.ascent()-35, barrect.width(), barrect.height()); + QRectF bar(offset+paintarea.left()+numrect.width()+daterect.width()+(margins*3), paintarea.top()+margins+(i*rowheight)+fm.ascent()-35, width, barrect.height()); + + // draw rects + QBrush brush(QColor(100,100,100,100)); + painter->fillRect(fullbar, brush); + QBrush markerbrush(ranked[i].color); + painter->fillRect(bar, markerbrush); + + // value + painter->setPen(QColor(100,100,100)); + painter->drawText(paintarea.topLeft()+QPointF(numrect.width()+daterect.width()+fullbar.width()+(margins*4),0)+QPointF(margins, margins+(i*rowheight)+fm.ascent()), ranked[i].value); + } } void @@ -1402,7 +1825,7 @@ OverviewItemConfig::OverviewItemConfig(ChartSpaceItem *item) : QWidget(item->par } // single metric names - if (item->type == OverviewItemType::METRIC || item->type == OverviewItemType::PMC) { + if (item->type == OverviewItemType::TOPN || item->type == OverviewItemType::METRIC || item->type == OverviewItemType::PMC) { metric1 = new MetricSelect(this, item->parent->context, MetricSelect::Metric); layout->addRow(tr("Metric"), metric1); connect(metric1, SIGNAL(textChanged(QString)), this, SLOT(dataChanged())); @@ -1577,6 +2000,14 @@ OverviewItemConfig::setWidgets() } break; + case OverviewItemType::TOPN: + { + TopNOverviewItem *mi = reinterpret_cast(item); + name->setText(mi->name); + metric1->setSymbol(mi->symbol); + } + break; + case OverviewItemType::META: { MetaOverviewItem *mi = reinterpret_cast(item); @@ -1659,6 +2090,17 @@ OverviewItemConfig::dataChanged() } break; + case OverviewItemType::TOPN: + { + TopNOverviewItem *mi = reinterpret_cast(item); + mi->name = name->text(); + if (metric1->isValid()) { + mi->symbol = metric1->rideMetric()->symbol(); + mi->units = metric1->rideMetric()->units(mi->parent->context->athlete->useMetricUnits); + } + } + break; + case OverviewItemType::META: { MetaOverviewItem *mi = reinterpret_cast(item); @@ -1852,7 +2294,7 @@ void RPErating::applyEdit() { // update the item - if we have one - RideItem *item = parent->parent->current; + RideItem *item = parent->parent->currentRideItem; // did it change? if (item && item->ride() && item->getText("RPE","") != value) { @@ -2316,11 +2758,9 @@ BubbleViz::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) painter->restore(); } -Sparkline::Sparkline(QGraphicsWidget *parent, int count, QString name) - : QGraphicsItem(NULL), parent(parent), name(name) +Sparkline::Sparkline(QGraphicsWidget *parent, QString name, bool bigdot) + : QGraphicsItem(NULL), parent(parent), name(name), sparkdays(SPARKDAYS), bigdot(bigdot) { - Q_UNUSED(count) - min = max = 0.0f; setGeometry(20,20,100,100); setZValue(11); @@ -2364,7 +2804,7 @@ Sparkline::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) if (points.isEmpty() || (max-min)==0) return; // so draw a line connecting the points - double xfactor = (geom.width() - (ROWHEIGHT*6)) / SPARKDAYS; + double xfactor = (geom.width() - (ROWHEIGHT*6)) / sparkdays; double xoffset = boundingRect().left()+(ROWHEIGHT*2); double yfactor = (geom.height()-(ROWHEIGHT)) / (max-min); double bottom = boundingRect().bottom()-ROWHEIGHT/2; @@ -2385,13 +2825,15 @@ Sparkline::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) painter->setPen(pen); painter->drawPath(path); - // and the last one is a dot for this value - double x = (points.first().x()*xfactor)+xoffset-25; - double y = bottom-((points.first().y()-min)*yfactor)-25; - if (std::isfinite(x) && std::isfinite(y)) { - painter->setBrush(QBrush(GColor(CPLOTMARKER).darker(150))); - painter->setPen(Qt::NoPen); - painter->drawEllipse(QRectF(x, y, 50, 50)); + if (bigdot) { + // and the last one is a dot for this value + double x = (points.first().x()*xfactor)+xoffset-25; + double y = bottom-((points.first().y()-min)*yfactor)-25; + if (std::isfinite(x) && std::isfinite(y)) { + painter->setBrush(QBrush(GColor(CPLOTMARKER).darker(150))); + painter->setPen(Qt::NoPen); + painter->drawEllipse(QRectF(x, y, 50, 50)); + } } } } diff --git a/src/Charts/OverviewItems.h b/src/Charts/OverviewItems.h index 88974b7fd..cd29e5f1d 100644 --- a/src/Charts/OverviewItems.h +++ b/src/Charts/OverviewItems.h @@ -46,7 +46,7 @@ class ProgressBar; #define ROUTEPOINTS 250 // types we use start from 100 to avoid clashing with main chart types -enum OverviewItemType { RPE=100, METRIC, META, ZONE, INTERVAL, PMC, ROUTE, KPI }; +enum OverviewItemType { RPE=100, METRIC, META, ZONE, INTERVAL, PMC, ROUTE, KPI, TOPN }; // // Configuration widget for ALL Overview Items @@ -103,6 +103,7 @@ class KPIOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange); QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -131,6 +132,7 @@ class RPEOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -157,6 +159,7 @@ class MetricOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange); QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -172,6 +175,61 @@ class MetricOverviewItem : public ChartSpaceItem Sparkline *sparkline; }; +// top N uses this to hold details for date range +struct topnentry { + +public: + + topnentry(QDate date, double v, QString value, QColor color, int tsb) : date(date), v(v), value(value), color(color), tsb(tsb) {} + inline bool operator<(const topnentry &other) const { return (v > other.v); } + inline bool operator>(const topnentry &other) const { return (v < other.v); } + QDate date; + double v; // for sorting + QString value; // as should be shown + QColor color; // ride color + int tsb; // on the day +}; + +class TopNOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + // want a meta property for property animation + Q_PROPERTY(int transition READ getTransition WRITE setTransition) + + public: + + TopNOverviewItem(ChartSpace *parent, QString name, QString symbol); + ~TopNOverviewItem(); + + // transition animation 0-100 + int getTransition() const {return transition;} + void setTransition(int x) { if (transition !=x) {transition=x; update();}} + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *) {} // doesn't support analysis view + void setDateRange(DateRange); + QWidget *config() { return new OverviewItemConfig(this); } + + // create and config + static ChartSpaceItem *create(ChartSpace *parent) { return new TopNOverviewItem(parent, "PowerIndex", "power_index"); } + + QString symbol; + RideMetric *metric; + QString units; + + QList ranked; + + // maximums to index from + QString maxvalue; + double maxv,minv; + + // animation + int transition; + QPropertyAnimation *animator; +}; + class MetaOverviewItem : public ChartSpaceItem { Q_OBJECT @@ -184,6 +242,7 @@ class MetaOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -211,6 +270,7 @@ class PMCOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -233,6 +293,7 @@ class ZoneOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange); void dragChanged(bool x); QWidget *config() { return new OverviewItemConfig(this); } @@ -260,6 +321,7 @@ class RouteOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -280,6 +342,7 @@ class IntervalOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -422,12 +485,13 @@ class RPErating : public QGraphicsItem class Sparkline : public QGraphicsItem { public: - Sparkline(QGraphicsWidget *parent, int count,QString name=""); // create and say how many days + Sparkline(QGraphicsWidget *parent, QString name="", bool bigdot=true); // create and say how many days // we monkey around with this *A LOT* void setGeometry(double x, double y, double width, double height); QRectF geometry() { return geom; } + void setDays(int n) { sparkdays=n; } // defaults to SPARKDAYS void setPoints(QList); void setRange(double min, double max); // upper lower @@ -444,6 +508,8 @@ class Sparkline : public QGraphicsItem QRectF geom; QString name; double min, max; + int sparkdays; + bool bigdot; QList points; }; diff --git a/src/Core/DataFilter.cpp b/src/Core/DataFilter.cpp index 87ef32d59..872bbc9bb 100644 --- a/src/Core/DataFilter.cpp +++ b/src/Core/DataFilter.cpp @@ -173,7 +173,7 @@ static struct { { "argsort", 2 }, // argsort(ascend|descend, list) - return a sorting index (ala numpy.argsort). - { "sort", 0 }, // sort(ascend|descend, list1 [, list2, listn]) - sorts each list together, based upon list1, no limit to the + { "multisort", 0 }, // multisort(ascend|descend, list1 [, list2, listn]) - sorts each list together, based upon list1, no limit to the // number of lists but they must have the same length. the first list contains the values that define // the sort order. since the sort is 'in-situ' the lists must all be user symbols. returns number of items // sorted. this is impure from a functional programming perspective, but allows us to avoid using dataframes @@ -297,6 +297,8 @@ static struct { { "rank", 2 }, // rank(ascend|descend, list) - returns ranks for the list + { "sort", 2 }, // sort(ascend|descend, list) - returns sorted list + // add new ones above this line { "", -1 } }; @@ -401,8 +403,8 @@ DataFilter::builtins(Context *context) } else if (i == 55) { - // argsort - returning << "sort(ascend|descend, list [, list2 .. ,listn])"; + // multisort + returning << "multisort(ascend|descend, list [, list2 .. ,listn])"; } else if (i == 56) { @@ -521,6 +523,11 @@ DataFilter::builtins(Context *context) // rank returning << "rank(ascend|descend, list)"; + } else if (i == 97) { + + // sort + returning << "sort(ascend|descend, list)"; + } else { QString function; @@ -1928,36 +1935,51 @@ void Leaf::validateFilter(Context *context, DataFilterRuntime *df, Leaf *leaf) validateFilter(context, df, leaf->fparms[1]); } - } else if (leaf->function == "sort") { + } else if (leaf->function == "multisort") { if (leaf->fparms.count() < 2) { leaf->inerror = true; - DataFiltererrors << QString(tr("sort(ascend|descend, list [, .. list n])")); + DataFiltererrors << QString(tr("multisort(ascend|descend, list [, .. list n])")); } // need ascend|descend then a list if (leaf->fparms.count() > 0 && leaf->fparms[0]->type != Leaf::Symbol) { leaf->inerror = true; - DataFiltererrors << QString(tr("sort(ascend|descend, list [, .. list n]), need to specify ascend or descend")); + DataFiltererrors << QString(tr("multisort(ascend|descend, list [, .. list n]), need to specify ascend or descend")); } // need all remaining parameters to be symbols - for(int i=1; ifparms.count(); i++) { + + // make sure the parameter makes sense + validateFilter(context, df, leaf->fparms[i]); // check parameter is actually a symbol if (leaf->fparms[i]->type != Leaf::Symbol) { leaf->inerror = true; - DataFiltererrors << QString(tr("sort: list arguments must be a symbol")); + DataFiltererrors << QString(tr("multisort: list arguments must be a symbol")); } else { QString symbol = *(leaf->fparms[i]->lvalue.n); if (!df->symbols.contains(symbol)) { DataFiltererrors << QString(tr("'%1' is not a user symbol").arg(symbol)); leaf->inerror = true; } - } } + } else if (leaf->function == "sort") { + + // need ascend|descend then a list + if (leaf->fparms.count() != 2 || leaf->fparms[0]->type != Leaf::Symbol) { + + leaf->inerror = true; + DataFiltererrors << QString(tr("sort(ascend|descend, list), need to specify ascend or descend")); + + } else { + + validateFilter(context, df, leaf->fparms[1]); + } + } else if (leaf->function == "rank") { // need ascend|descend then a list @@ -2710,6 +2732,34 @@ Result DataFilter::evaluate(RideItem *item, RideFilePoint *p) return res; } +Result DataFilter::evaluate(DateRange dr) +{ + // if there is no current ride item then there is no data + // so it really is ok to baulk at no current ride item here + // we must always have a ride since context is used + if (context->currentRideItem() == NULL || !treeRoot || DataFiltererrors.count()) return Result(0); + + // reset stack + rt.stack = 0; + + Result res(0); + + // if we are a set of functions.. + if (rt.functions.count()) { + + // ... start at main + if (rt.functions.contains("main")) + res = treeRoot->eval(&rt, rt.functions.value("main"), 0, 0, const_cast(context->currentRideItem()), NULL, NULL, Specification(), dr); + + } else { + + // otherwise just evaluate the entire tree + res = treeRoot->eval(&rt, treeRoot, 0, 0, const_cast(context->currentRideItem()), NULL, NULL, Specification(), dr); + } + + return res; +} + QStringList DataFilter::check(QString query) { // since we may use it afterwards @@ -2902,6 +2952,9 @@ Result::vectorize(int count) // used by lowerbound struct comparedouble { bool operator()(const double p1, const double p2) { return p1 < p2; } }; +// qsort descend +static bool doubledescend(const double &s1, const double &s2) { return s1 > s2; } + // date arithmetic, a bit of a brute force, but need to rely upon // QDate arithmetic for handling months (so we don't have to) static int monthsTo(QDate from, QDate to) @@ -4138,6 +4191,29 @@ Result Leaf::eval(DataFilterRuntime *df, Leaf *leaf, float x, long it, RideItem return returning; } + // sort + if (leaf->function == "sort") { + Result returning(0); + + // ascending or descending? + QString symbol = *(leaf->fparms[0]->lvalue.n); + bool ascending= (symbol=="ascend") ? true : false; + + // use the utils function to actually do it + Result v = eval(df, leaf->fparms[1],x, it, m, p, c, s, d); + + if (ascending) qSort(v.vector); + else qSort(v.vector.begin(), v.vector.end(), doubledescend); + + // put the index into the result we are returning. + foreach(double x, v.vector) { + returning.number += x; + returning.vector << x; + } + + return returning; + } + // arguniq if (leaf->function == "arguniq") { Result returning(0); @@ -4282,7 +4358,7 @@ Result Leaf::eval(DataFilterRuntime *df, Leaf *leaf, float x, long it, RideItem } // sort - if (leaf->function == "sort") { + if (leaf->function == "multisort") { // ascend/descend? QString symbol = *(leaf->fparms[0]->lvalue.n); @@ -4306,7 +4382,7 @@ Result Leaf::eval(DataFilterRuntime *df, Leaf *leaf, float x, long it, RideItem // diff length? if (current.vector.count() != len) { - fprintf(stderr, "sort list '%s': not the same length, ignored\n", symbol.toStdString().c_str()); fflush(stderr); + fprintf(stderr, "multisort list '%s': not the same length, ignored\n", symbol.toStdString().c_str()); fflush(stderr); continue; } diff --git a/src/Core/DataFilter.h b/src/Core/DataFilter.h index 243aadc6a..d99b6b97b 100644 --- a/src/Core/DataFilter.h +++ b/src/Core/DataFilter.h @@ -187,6 +187,7 @@ class DataFilter : public QObject // RideItem always available and supplies th context Result evaluate(RideItem *rideItem, RideFilePoint *p); + Result evaluate(DateRange dr); QStringList getErrors() { return errors; }; void colorSyntax(QTextDocument *content, int pos); diff --git a/src/Gui/AddChartWizard.cpp b/src/Gui/AddChartWizard.cpp index e7326b948..e614b0ebb 100644 --- a/src/Gui/AddChartWizard.cpp +++ b/src/Gui/AddChartWizard.cpp @@ -33,7 +33,7 @@ // // Main wizard - if passed a service name we are in edit mode, not add mode. -AddChartWizard::AddChartWizard(Context *context, ChartSpace *space) : QWizard(context->mainWindow), context(context), space(space) +AddChartWizard::AddChartWizard(Context *context, ChartSpace *space, int scope) : QWizard(context->mainWindow), context(context), scope(scope), space(space) { #ifdef Q_OS_MAC setWizardStyle(QWizard::ModernStyle); @@ -100,6 +100,9 @@ AddChartType::initializePage() // iterate over names, as they are sorted alphabetically foreach(ChartSpaceItemDetail detail, registry.items()) { + // limit the type of chart to add + if ((detail.scope&wizard->scope) == 0) continue; + // get the service QCommandLinkButton *p = new QCommandLinkButton(detail.quick, detail.description, this); p->setStyleSheet(QString("font-size: %1px;").arg(font.pointSizeF() * dpiXFactor)); diff --git a/src/Gui/AddChartWizard.h b/src/Gui/AddChartWizard.h index b489eac25..f4b58919e 100644 --- a/src/Gui/AddChartWizard.h +++ b/src/Gui/AddChartWizard.h @@ -31,7 +31,7 @@ class AddChartWizard : public QWizard public: - AddChartWizard(Context *context, ChartSpace *space); + AddChartWizard(Context *context, ChartSpace *space, int scope); QSize sizeHint() const { return QSize(600,650); } Context *context; @@ -39,6 +39,7 @@ public: // what type of chart int type; + int scope; // add here ChartSpace *space; diff --git a/src/Gui/ChartSpace.cpp b/src/Gui/ChartSpace.cpp index 6ac74fe76..16e18ef79 100644 --- a/src/Gui/ChartSpace.cpp +++ b/src/Gui/ChartSpace.cpp @@ -34,8 +34,8 @@ static QIcon grayConfig, whiteConfig, accentConfig; ChartSpaceItemRegistry *ChartSpaceItemRegistry::_instance; -ChartSpace::ChartSpace(Context *context) : - state(NONE), context(context), group(NULL), _viewY(0), +ChartSpace::ChartSpace(Context *context, int scope) : + state(NONE), context(context), scope(scope), group(NULL), _viewY(0), yresizecursor(false), xresizecursor(false), block(false), scrolling(false), setscrollbar(false), lasty(-1) { @@ -92,7 +92,7 @@ ChartSpace::ChartSpace(Context *context) : // we're ready to plot, but not configured configured=false; stale=true; - current=NULL; + currentRideItem=NULL; } // add the item @@ -103,7 +103,8 @@ ChartSpace::addItem(int order, int column, int deep, ChartSpaceItem *item) item->column = column; item->deep = deep; items.append(item); - if (current) item->setData(current); + if (scope&ANALYSIS && currentRideItem) item->setData(currentRideItem); + if (scope&TRENDS) item->setDateRange(currentDateRange); } void @@ -125,31 +126,46 @@ void ChartSpace::rideSelected(RideItem *item) { // don't plot when we're not visible, unless we have nothing plotted yet - if (!isVisible() && current != NULL && item != NULL) { + if (!isVisible() && currentRideItem != NULL && item != NULL) { stale=true; return; } // don't replot .. we already did this one - if (current == item && stale == false) { + if (currentRideItem == item && stale == false) { return; } -// profiling the code -//QTime timer; -//timer.start(); - // ride item changed foreach(ChartSpaceItem *ChartSpaceItem, items) ChartSpaceItem->setData(item); -// profiling the code -//qDebug()<<"took:"<setDateRange(dr); + + // update + updateView(); + + // ok, remember we did this one stale=false; } diff --git a/src/Gui/ChartSpace.h b/src/Gui/ChartSpace.h index 701211e50..7d0512855 100644 --- a/src/Gui/ChartSpace.h +++ b/src/Gui/ChartSpace.h @@ -47,6 +47,9 @@ class ChartSpace; class ChartSpaceItemFactory; +// we need a scope for a chart space, one or more of +enum OverviewScope { ANALYSIS=0x01, TRENDS=0x02 }; + // must be subclassed to add items to a ChartSpace class ChartSpaceItem : public QGraphicsWidget { @@ -58,6 +61,7 @@ class ChartSpaceItem : public QGraphicsWidget virtual void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) =0; virtual void itemGeometryChanged() =0; virtual void setData(RideItem *item)=0; + virtual void setDateRange(DateRange )=0; virtual QWidget *config()=0; // must supply a widget to configure // what type am I- managed by user @@ -127,18 +131,20 @@ class ChartSpace : public QWidget public: - ChartSpace(Context *context); + ChartSpace(Context *context, int scope); // current state for event processing enum { NONE, DRAG, XRESIZE, YRESIZE } state; // used by children Context *context; + int scope; QGraphicsView *view; QFont titlefont, bigfont, midfont, smallfont; // the item we are currently showing - RideItem *current; + RideItem *currentRideItem; + DateRange currentDateRange; // to get paint device QGraphicsView *device() { return view; } @@ -150,8 +156,9 @@ class ChartSpace : public QWidget public slots: - // ride item changed + // user selection void rideSelected(RideItem *item); + void dateRangeChanged(DateRange); // for smooth scrolling void setViewY(int x) { if (_viewY != x) {_viewY =x; updateView();} } diff --git a/src/Gui/GcWindowRegistry.cpp b/src/Gui/GcWindowRegistry.cpp index aeff4ace5..37784c101 100644 --- a/src/Gui/GcWindowRegistry.cpp +++ b/src/Gui/GcWindowRegistry.cpp @@ -76,8 +76,9 @@ GcWindowRegistry* GcWindows; void GcWindowRegistry::initialize() { - static GcWindowRegistry GcWindowsInit[35] = { + static GcWindowRegistry GcWindowsInit[36] = { // name GcWinID + { VIEW_HOME|VIEW_DIARY, tr("Overview "),GcWindowTypes::OverviewTrends }, { VIEW_HOME|VIEW_DIARY, tr("User Chart"),GcWindowTypes::UserTrends }, { VIEW_HOME|VIEW_DIARY, tr("Trends"),GcWindowTypes::LTM }, { VIEW_HOME|VIEW_DIARY, tr("TreeMap"),GcWindowTypes::TreeMap }, @@ -243,8 +244,10 @@ GcWindowRegistry::newGcWindow(GcWinID id, Context *context) case GcWindowTypes::RouteSegment: returning = new GcChartWindow(context); break; #endif #if GC_HAVE_OVERVIEW - case GcWindowTypes::Overview: returning = new OverviewWindow(context); break; + case GcWindowTypes::Overview: returning = new OverviewWindow(context, ANALYSIS); break; + case GcWindowTypes::OverviewTrends: returning = new OverviewWindow(context, TRENDS); break; #else + case GcWindowTypes::OverviewTrends: case GcWindowTypes::Overview: returning = new GcChartWindow(context); break; #endif case GcWindowTypes::SeasonPlan: returning = new PlanningWindow(context); break; diff --git a/src/Gui/GcWindowRegistry.h b/src/Gui/GcWindowRegistry.h index 56f8a384d..a54cb618e 100644 --- a/src/Gui/GcWindowRegistry.h +++ b/src/Gui/GcWindowRegistry.h @@ -72,8 +72,8 @@ enum gcwinid { Python = 43, PythonSeason = 44, UserTrends=45, - UserAnalysis=46 - + UserAnalysis=46, + OverviewTrends=47 }; }; typedef enum GcWindowTypes::gcwinid GcWinID; diff --git a/src/Metrics/BasicRideMetrics.cpp b/src/Metrics/BasicRideMetrics.cpp index 8a36e2082..36450d32d 100644 --- a/src/Metrics/BasicRideMetrics.cpp +++ b/src/Metrics/BasicRideMetrics.cpp @@ -929,6 +929,7 @@ class TotalWork : public RideMetric { void initialize() { setName(tr("Work")); + setType(RideMetric::Total); setMetricUnits(tr("kJ")); setImperialUnits(tr("kJ")); setDescription(tr("Total Work in kJ computed from power data")); diff --git a/src/Resources/application.qrc b/src/Resources/application.qrc index 79a7b47d9..468f6a68d 100644 --- a/src/Resources/application.qrc +++ b/src/Resources/application.qrc @@ -75,6 +75,7 @@ images/twitter.png images/cyclist.png images/imetrics.png + images/medal.png images/power.png images/arduino.png images/query.png diff --git a/src/Resources/images/medal.png b/src/Resources/images/medal.png new file mode 100644 index 000000000..220d49df7 Binary files /dev/null and b/src/Resources/images/medal.png differ