From 8a1842a9ba4579a24473fec7cae53f5eb06c8f80 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Sun, 31 May 2020 18:16:13 +0100 Subject: [PATCH] Refactor OverviewWindow for ChartSpace .. the OverviewWindow class has been refactored to extract out the core dashboard UX/UI into a new class ChartSpace. .. additionally, a ChartSpace contains a set of ChartSpaceItem which need to be subclassed by the developer. .. for the Overview a set of OverwiewItem classes have been introduced such as RPEOverviewItem, MetricOverviewItem and so on. These are subclasses of the ChartSpaceItem. .. The overview window implementation is now mostly configuration and assembly of a dashboard using OverviewItems and the ChartSpace. .. This refactor is to enable the ChartSpace to be used as a drop in replacement for the existing TabView. .. There are no functional enhancements in this commit, but the overview chart shouls appear to be unchanged by the user. --- {src/Charts => deprecated}/OverviewWindow.cpp | 6 - {src/Charts => deprecated}/OverviewWindow.h | 0 src/Charts/Overview.cpp | 325 +++ src/Charts/Overview.h | 61 + src/Charts/OverviewItems.cpp | 2054 +++++++++++++++++ src/Charts/OverviewItems.h | 381 +++ src/Gui/ChartSpace.cpp | 946 ++++++++ src/Gui/ChartSpace.h | 237 ++ src/Gui/GcWindowRegistry.cpp | 3 +- src/src.pro | 5 +- 10 files changed, 4009 insertions(+), 9 deletions(-) rename {src/Charts => deprecated}/OverviewWindow.cpp (99%) rename {src/Charts => deprecated}/OverviewWindow.h (100%) create mode 100644 src/Charts/Overview.cpp create mode 100644 src/Charts/Overview.h create mode 100644 src/Charts/OverviewItems.cpp create mode 100644 src/Charts/OverviewItems.h create mode 100644 src/Gui/ChartSpace.cpp create mode 100644 src/Gui/ChartSpace.h diff --git a/src/Charts/OverviewWindow.cpp b/deprecated/OverviewWindow.cpp similarity index 99% rename from src/Charts/OverviewWindow.cpp rename to deprecated/OverviewWindow.cpp index 54c93e01f..122135959 100644 --- a/src/Charts/OverviewWindow.cpp +++ b/deprecated/OverviewWindow.cpp @@ -65,12 +65,6 @@ OverviewWindow::OverviewWindow(Context *context) : view->setRenderHint(QPainter::Antialiasing, true); view->setFrameStyle(QFrame::NoFrame); view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); -#ifdef Q_OS_LINUX - if (QGLFormat::openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_2_0)) { - view->setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers))); - view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); - } -#endif view->setScene(scene); // layout diff --git a/src/Charts/OverviewWindow.h b/deprecated/OverviewWindow.h similarity index 100% rename from src/Charts/OverviewWindow.h rename to deprecated/OverviewWindow.h diff --git a/src/Charts/Overview.cpp b/src/Charts/Overview.cpp new file mode 100644 index 000000000..9d8ef5cf8 --- /dev/null +++ b/src/Charts/Overview.cpp @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2017 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 "Overview.h" +#include "ChartSpace.h" +#include "OverviewItems.h" + +static QIcon grayConfig, whiteConfig, accentConfig; + +OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), context(context), configured(false) +{ + setContentsMargins(0,0,0,0); + setProperty("color", GColor(COVERVIEWBACKGROUND)); + //setProperty("nomenu", true); + setShowTitle(false); + setControls(NULL); + + QHBoxLayout *main = new QHBoxLayout; + + space = new ChartSpace(context); + main->addWidget(space); + + // all the widgets + setChartLayout(main); + + // tell space when a ride is selected + connect(this, SIGNAL(rideItemChanged(RideItem*)), space, SLOT(rideSelected(RideItem*))); +} + +QString +OverviewWindow::getConfiguration() const +{ + // return a JSON snippet to represent the entire config + QString config; + + // setup + config = "{\n \"version\":\"2.0\",\n"; + config += " \"CHARTS\":[\n"; + + // do cards + foreach(ChartSpaceItem *item, space->allItems()) { + + // basic stuff first - name, type etc + config += " { "; + config += "\"type\":" + QString("%1").arg(static_cast(item->type)) + ","; + config += "\"deep\":" + QString("%1").arg(item->deep) + ","; + config += "\"column\":" + QString("%1").arg(item->column) + ","; + config += "\"order\":" + QString("%1").arg(item->order) + ","; + + // now the actual card settings + switch(item->type) { + case OverviewItemType::RPE: + { + RPEOverviewItem *rpe = reinterpret_cast(item); + } + break; + case OverviewItemType::METRIC: + { + MetricOverviewItem *metric = reinterpret_cast(item); + config += "\"symbol\":\"" + QString("%1").arg(metric->symbol) + "\","; + } + break; + case OverviewItemType::META: + { + MetaOverviewItem *meta = reinterpret_cast(item); + config += "\"symbol\":\"" + QString("%1").arg(meta->symbol) + "\","; + } + break; + case OverviewItemType::PMC: + { + PMCOverviewItem *pmc = reinterpret_cast(item); + config += "\"symbol\":\"" + QString("%1").arg(pmc->symbol) + "\","; + } + break; + case OverviewItemType::ROUTE: + { + RouteOverviewItem *route = reinterpret_cast(item); + } + break; + case OverviewItemType::INTERVAL: + { + IntervalOverviewItem *interval = reinterpret_cast(item); + config += "\"xsymbol\":\"" + QString("%1").arg(interval->xsymbol) + "\","; + config += "\"ysymbol\":\"" + QString("%1").arg(interval->ysymbol) + "\","; + config += "\"zsymbol\":\"" + QString("%1").arg(interval->zsymbol) + "\"" + ","; + } + break; + case OverviewItemType::ZONE: + { + ZoneOverviewItem *zone = reinterpret_cast(item); + config += "\"series\":" + QString("%1").arg(static_cast(zone->series)) + ","; + } + break; + } + + config += "\"name\":\"" + item->name + "\""; + + config += " }"; + + if (space->allItems().last() != item) config += ","; + + config += "\n"; + } + + config += " ]\n}\n"; + + return config; +} + +void +OverviewWindow::setConfiguration(QString config) +{ + // XXX hack because we're not in the default layout and don't want to + // XXX this is just to handle setup for the very first time its run ! + if (configured == true) return; + configured = true; + + // DEFAULT CONFIG (FOR NOW WHEN NOT IN THE DEFAULT LAYOUT) + // + // default column widths - max 10 columns; + // note the sizing is such that each card is the equivalent of a full screen + // so we can embed charts etc without compromising what they can display + + defaultsetup: // I know, but its easier than lots of nested if clauses above + + if (config == "") { + + // column 0 + ChartSpaceItem *add; + add = new PMCOverviewItem(space, "coggan_tss"); + space->addItem(1,0,9, add); + + add = new MetaOverviewItem(space, "Sport", "Sport"); + space->addItem(2,0,5, add); + + add = new MetaOverviewItem(space, "Workout Code", "Workout Code"); + space->addItem(3,0,5, add); + + add = new MetricOverviewItem(space, "Duration", "workout_time"); + space->addItem(4,0,9, add); + + add = new MetaOverviewItem(space, "Notes", "Notes"); + space->addItem(5,0,13, add); + + // column 1 + add = new MetricOverviewItem(space, "HRV rMSSD", "rMSSD"); + space->addItem(1,1,9, add); + + add = new MetricOverviewItem(space, "Heartrate", "average_hr"); + space->addItem(2,1,5, add); + + add = new ZoneOverviewItem(space, "Heartrate Zones", RideFile::hr); + space->addItem(3,1,11, add); + + add = new MetricOverviewItem(space, "Climbing", "elevation_gain"); + space->addItem(4,1,5, add); + + add = new MetricOverviewItem(space, "Cadence", "average_cad"); + space->addItem(5,1,5, add); + + add = new MetricOverviewItem(space, "Work", "total_work"); + space->addItem(6,1,5, add); + + // column 2 + add = new RPEOverviewItem(space, "RPE"); + space->addItem(1,2,9, add); + + add = new MetricOverviewItem(space, "Stress", "coggan_tss"); + space->addItem(2,2,5, add); + + add = new ZoneOverviewItem(space, "Fatigue Zones", RideFile::wbal); + space->addItem(3,2,11, add); + + add = new IntervalOverviewItem(space, "Intervals", "elapsed_time", "average_power", "workout_time"); + space->addItem(4,2,17, add); + + // column 3 + add = new MetricOverviewItem(space, "Power", "average_power"); + space->addItem(1,3,9, add); + + add = new MetricOverviewItem(space, "IsoPower", "coggan_np"); + space->addItem(2,3,5, add); + + add = new ZoneOverviewItem(space, "Power Zones", RideFile::watts); + space->addItem(3,3,11, add); + + add = new MetricOverviewItem(space, "Peak Power Index", "peak_power_index"); + space->addItem(4,3,8, add); + + add = new MetricOverviewItem(space, "Variability", "coggam_variability_index"); + space->addItem(5,3,8, add); + + // column 4 + add = new MetricOverviewItem(space, "Distance", "total_distance"); + space->addItem(1,4,9, add); + + add = new MetricOverviewItem(space, "Speed", "average_speed"); + space->addItem(2,4,5, 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); + + } else { + + // + // But by default we parse and apply (dropping back to default setup on error) + // + // parse + QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8()); + if (doc.isEmpty() || doc.isNull()) { + config=""; + goto defaultsetup; + } + + // parsed so lets work through it and setup the overview + QJsonObject root = doc.object(); + + // check version + QString version = root["version"].toString(); + if (version != "2.0") { + config=""; + goto defaultsetup; + } + + // cards + QJsonArray CHARTS = root["CHARTS"].toArray(); + foreach(const QJsonValue val, CHARTS) { + + // convert so we can inspect + QJsonObject obj = val.toObject(); + + // get the basics + QString name = obj["name"].toString(); + int column = obj["column"].toInt(); + int order = obj["order"].toInt(); + int deep = obj["deep"].toInt(); + int type = obj["type"].toInt(); + + + // lets create the cards + ChartSpaceItem *add=NULL; + switch(type) { + + case OverviewItemType::RPE : + { + add = new RPEOverviewItem(space, name); + space->addItem(order,column,deep, add); + } + break; + + case OverviewItemType::METRIC : + { + QString symbol=obj["symbol"].toString(); + add = new MetricOverviewItem(space, name,symbol); + space->addItem(order,column,deep, add); + } + break; + + case OverviewItemType::META : + { + QString symbol=obj["symbol"].toString(); + add = new MetaOverviewItem(space, name,symbol); + space->addItem(order,column,deep, add); + } + break; + + case OverviewItemType::PMC : + { + QString symbol=obj["symbol"].toString(); + add = new PMCOverviewItem(space, symbol); // doesn't have a title + space->addItem(order,column,deep, add); + } + break; + + case OverviewItemType::ZONE : + { + RideFile::SeriesType series = static_cast(obj["series"].toInt()); + add = new ZoneOverviewItem(space, name, series); + space->addItem(order,column,deep, add); + + } + break; + + case OverviewItemType::ROUTE : + { + add = new RouteOverviewItem(space, name); // doesn't have a title + space->addItem(order,column,deep, add); + } + break; + + case OverviewItemType::INTERVAL : + { + QString xsymbol=obj["xsymbol"].toString(); + QString ysymbol=obj["ysymbol"].toString(); + QString zsymbol=obj["zsymbol"].toString(); + + add = new IntervalOverviewItem(space, name, xsymbol, ysymbol, zsymbol); // doesn't have a title + space->addItem(order,column,deep, add); + } + break; + } + } + } + + // put in place + space->updateGeometry(); +} diff --git a/src/Charts/Overview.h b/src/Charts/Overview.h new file mode 100644 index 000000000..fd220a1ff --- /dev/null +++ b/src/Charts/Overview.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 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 + */ + +#ifndef _GC_OverviewWindow_h +#define _GC_OverviewWindow_h 1 + +// basics +#include "GoldenCheetah.h" +#include "Settings.h" +#include "Units.h" +#include "Colors.h" +#include "Context.h" +#include "Athlete.h" +#include "RideItem.h" +#include "RideMetric.h" +#include "HrZones.h" + +#include "ChartSpace.h" + +class OverviewWindow : public GcChartWindow +{ + Q_OBJECT + + Q_PROPERTY(QString config READ getConfiguration WRITE setConfiguration USER true) + + public: + + OverviewWindow(Context *context); + + // used by children + Context *context; + + public slots: + + // get/set config + QString getConfiguration() const; + void setConfiguration(QString x); + + private: + + // gui setup + ChartSpace *space; + bool configured; +}; + +#endif // _GC_OverviewWindow_h diff --git a/src/Charts/OverviewItems.cpp b/src/Charts/OverviewItems.cpp new file mode 100644 index 000000000..2242b4187 --- /dev/null +++ b/src/Charts/OverviewItems.cpp @@ -0,0 +1,2054 @@ +/* + * Copyright (c) 2020 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 "OverviewItems.h" + +#include "TabView.h" +#include "Athlete.h" +#include "RideCache.h" +#include "IntervalItem.h" + +#include "Zones.h" +#include "HrZones.h" +#include "PaceZones.h" + +#include "PMCData.h" +#include "RideMetadata.h" + +#include +#include +#include + +#include +#include +#include +#include + +RPEOverviewItem::RPEOverviewItem(ChartSpace *parent, QString name) : ChartSpaceItem(parent, name) +{ + + // a META widget, "RPE" using the FOSTER modified 0-10 scale + this->type = OverviewItemType::RPE; + + sparkline = new Sparkline(this, SPARKDAYS+1, name); + rperating = new RPErating(this, name); +} + +RouteOverviewItem::RouteOverviewItem(ChartSpace *parent, QString name) : ChartSpaceItem(parent, name) +{ + this->type = OverviewItemType::ROUTE; + routeline = new Routeline(this, name); +} + +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"; + +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"; + +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"; + +static const QStringList timeInZonesWBAL = QStringList() + << "wtime_in_zone_L1" + << "wtime_in_zone_L2" + << "wtime_in_zone_L3" + << "wtime_in_zone_L4"; + +ZoneOverviewItem::ZoneOverviewItem(ChartSpace *parent, QString name, RideFile::seriestype series) : ChartSpaceItem(parent, name) +{ + + this->type = OverviewItemType::ZONE; + this->series = series; + + // basic chart setup + chart = new QChart(this); + chart->setBackgroundVisible(false); // draw on canvas + chart->legend()->setVisible(false); // no legends + chart->setTitle(""); // none wanted + chart->setAnimationOptions(QChart::NoAnimation); + + // we have a mid sized font for chart labels etc + chart->setFont(parent->midfont); + + // needs a set of bars + barset = new QBarSet(tr("Time In Zone"), this); + barset->setLabelFont(parent->midfont); + + if (series == RideFile::hr) { + barset->setLabelColor(GColor(CHEARTRATE)); + barset->setBorderColor(GColor(CHEARTRATE)); + barset->setBrush(GColor(CHEARTRATE)); + } else if (series == RideFile::watts) { + barset->setLabelColor(GColor(CPOWER)); + barset->setBorderColor(GColor(CPOWER)); + barset->setBrush(GColor(CPOWER)); + } else if (series == RideFile::wbal) { + barset->setLabelColor(GColor(CWBAL)); + barset->setBorderColor(GColor(CWBAL)); + barset->setBrush(GColor(CWBAL)); + } else if (series == RideFile::kph) { + barset->setLabelColor(GColor(CSPEED)); + barset->setBorderColor(GColor(CSPEED)); + barset->setBrush(GColor(CSPEED)); + } + + + // + // HEARTRATE + // + if (series == RideFile::hr && parent->context->athlete->hrZones(false)) { + // set the zero values + for(int i=0; icontext->athlete->hrZones(false)->getScheme().nzones_default; i++) { + *barset << 0; + categories << parent->context->athlete->hrZones(false)->getScheme().zone_default_name[i]; + } + } + + // + // POWER + // + if (series == RideFile::watts && parent->context->athlete->zones(false)) { + // set the zero values + for(int i=0; icontext->athlete->zones(false)->getScheme().nzones_default; i++) { + *barset << 0; + categories << parent->context->athlete->zones(false)->getScheme().zone_default_name[i]; + } + } + + // + // PACE + // + if (series == RideFile::kph && parent->context->athlete->paceZones(false)) { + // set the zero values + for(int i=0; icontext->athlete->paceZones(false)->getScheme().nzones_default; i++) { + *barset << 0; + categories << parent->context->athlete->paceZones(false)->getScheme().zone_default_name[i]; + } + } + + // + // W'BAL + // + if (series == RideFile::wbal) { + categories << "Low" << "Med" << "High" << "Ext"; + *barset << 0 << 0 << 0 << 0; + } + + // bar series and categories setup, same for all + barseries = new QBarSeries(this); + barseries->setLabelsPosition(QAbstractBarSeries::LabelsOutsideEnd); + barseries->setLabelsVisible(true); + barseries->setLabelsFormat("@value %"); + barseries->append(barset); + chart->addSeries(barseries); + + + // x-axis labels etc + barcategoryaxis = new QBarCategoryAxis(this); + barcategoryaxis->setLabelsFont(parent->midfont); + barcategoryaxis->setLabelsColor(QColor(100,100,100)); + barcategoryaxis->setGridLineVisible(false); + barcategoryaxis->setCategories(categories); + + // config axes + QPen axisPen(GColor(CCARDBACKGROUND)); + axisPen.setWidth(1); // almost invisible + chart->createDefaultAxes(); + chart->setAxisX(barcategoryaxis, barseries); + barcategoryaxis->setLinePen(axisPen); + barcategoryaxis->setLineVisible(false); + chart->axisY(barseries)->setLinePen(axisPen); + chart->axisY(barseries)->setLineVisible(false); + chart->axisY(barseries)->setLabelsVisible(false); + chart->axisY(barseries)->setRange(0,100); + chart->axisY(barseries)->setGridLineVisible(false); +} + +MetricOverviewItem::MetricOverviewItem(ChartSpace *parent, QString name, QString symbol) : ChartSpaceItem(parent, name) +{ + // metric + this->type = OverviewItemType::METRIC; + this->symbol = symbol; + + RideMetricFactory &factory = RideMetricFactory::instance(); + this->metric = const_cast(factory.rideMetric(symbol)); + 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); + +} + +PMCOverviewItem::PMCOverviewItem(ChartSpace *parent, QString symbol) : ChartSpaceItem(parent, "") +{ + // PMC doesn't have a title as we show multiple things + // metric + this->type = OverviewItemType::PMC; + this->symbol = symbol; + +} + +MetaOverviewItem::MetaOverviewItem(ChartSpace *parent, QString name, QString symbol) : ChartSpaceItem(parent, name) +{ + + // metric or meta or pmc + this->type = OverviewItemType::META; + this->symbol = symbol; + + // Get the field type + fieldtype = -1; + foreach(FieldDefinition p, parent->context->athlete->rideMetadata()->getFields()) { + if (p.name == symbol) { + fieldtype = p.type; + break; + } + } + + // sparkline if are we numeric? + if (fieldtype == FIELD_INTEGER || fieldtype == FIELD_DOUBLE) { + sparkline = new Sparkline(this, SPARKDAYS+1, name); + } else { + sparkline = NULL; + } +} + +IntervalOverviewItem::IntervalOverviewItem(ChartSpace *parent, QString name, QString xsymbol, QString ysymbol, QString zsymbol) : ChartSpaceItem(parent, name) +{ + this->type = OverviewItemType::INTERVAL; + this->xsymbol = xsymbol; + this->ysymbol = ysymbol; + this->zsymbol = zsymbol; + + // we may plot the metric sparkline if the tile is big enough + bubble = new BubbleViz(this, "intervals"); + + RideMetricFactory &factory = RideMetricFactory::instance(); + const RideMetric *xm = factory.rideMetric(xsymbol); + const RideMetric *ym = factory.rideMetric(ysymbol); + bubble->setAxisNames(xm ? xm->name() : "NA", ym ? ym->name() : "NA"); +} + + +void +RPEOverviewItem::setData(RideItem *item) +{ + if (item == NULL || item->ride() == NULL) return; + + // get last 30 days, if they exist + QList points; + + // get the metadata value + value = item->getText("RPE", "0"); + rperating->setValue(value); + double v = value.toDouble(); + if (std::isinf(v) || std::isnan(v)) v=0; + points << QPointF(SPARKDAYS, value.toDouble()); + + // set the chart values with the last 10 rides + int index = parent->context->athlete->rideCache->rides().indexOf(item); + + int offset = 1; + double min = v; + double max = v; + double sum=0, count=0, avg = 0; + while(index-offset >=0) { // ultimately go no further back than first ever ride + + // get value from items before me + RideItem *prior = parent->context->athlete->rideCache->rides().at(index-offset); + + // are we still in range ? + const qint64 old = prior->dateTime.daysTo(item->dateTime); + if (old > SPARKDAYS) break; + + // only activities with matching sport flags + if (prior->isRun == item->isRun && prior->isSwim == item->isSwim) { + + v = prior->getText("RPE", "0").toDouble(); + if (std::isinf(v) || std::isnan(v)) v=0; + + // new no zero value + if (v) { + sum += v; + count++; + + points< max) max = v; + } + } + + offset++; + } + + if (count) avg = sum / count; + else avg = 0; + + // which way up should the arrow be? + up = v > avg ? true : false; + + // add some space, if only one value +/- 10% + double diff = (max-min)/10.0f; + showrange=true; + if (diff==0) showrange=false; + + // update the sparkline + sparkline->setPoints(points); + + // set range + sparkline->setRange(min-diff,max+diff); // add 10% to each direction + + // set the values for upper lower + upper = QString("%1").arg(max); + lower = QString("%1").arg(min); + mean = QString("%1").arg(avg, 0, 'f', 0); +} + +void +MetricOverviewItem::setData(RideItem *item) +{ + if (item == NULL || item->ride() == NULL) return; + + // get last 30 days, if they exist + QList points; + + // get the metric value + value = item->getStringForSymbol(symbol, parent->context->athlete->useMetricUnits); + if (value == "nan") value =""; + double v = (units == tr("seconds")) ? item->getForSymbol(symbol, parent->context->athlete->useMetricUnits) : value.toDouble(); + if (std::isinf(v) || std::isnan(v)) v=0; + + points << QPointF(SPARKDAYS, v); + + // set the chart values with the last 10 rides + int index = parent->context->athlete->rideCache->rides().indexOf(item); + + int offset = 1; + double min = v; + double max = v; + double sum=0, count=0, avg = 0; + while(index-offset >=0) { // ultimately go no further back than first ever ride + + // get value from items before me + RideItem *prior = parent->context->athlete->rideCache->rides().at(index-offset); + + // are we still in range ? + const qint64 old = prior->dateTime.daysTo(item->dateTime); + if (old > SPARKDAYS) break; + + // only activities with matching sport flags + if (prior->isRun == item->isRun && prior->isSwim == item->isSwim) { + + if (units == tr("seconds")) v = prior->getForSymbol(symbol, parent->context->athlete->useMetricUnits); + else { + QString vs = prior->getStringForSymbol(symbol, parent->context->athlete->useMetricUnits); + if (vs == "nan") vs="0"; + v = vs.toDouble(); + } + + if (std::isinf(v) || std::isnan(v)) v=0; + + // new no zero value + if (v) { + sum += v; + count++; + + points< max) max = v; + } + } + + offset++; + } + + if (count) avg = sum / count; + else avg = 0; + + // which way up should the arrow be? + up = v > avg ? true : false; + + // add some space, if only one value +/- 10% + double diff = (max-min)/10.0f; + showrange=true; + if (diff==0) { + showrange=false; + diff = value.toDouble()/10.0f; + } + + // update the sparkline + sparkline->setPoints(points); + + // set range + sparkline->setRange(min-diff,max+diff); // add 10% to each direction + + // set the values for upper lower + if (units == tr("seconds")) { + upper = time_to_string(max, true); + lower = time_to_string(min, true); + mean = time_to_string(avg, true); + } else { + upper = QString("%1").arg(max); + lower = QString("%1").arg(min); + + // we need the same precision + const RideMetricFactory &factory = RideMetricFactory::instance(); + const RideMetric *m = factory.rideMetric(symbol); + + if (m) mean = m->toString(parent->context->athlete->useMetricUnits, avg); + else mean = QString("%1").arg(avg, 0, 'f', 0); + } +} + +void +MetaOverviewItem::setData(RideItem *item) +{ + if (item == NULL || item->ride() == NULL) return; + + // non-numeric META + if (!sparkline) value = item->getText(symbol, ""); + + // set the sparkline for numeric meta fields + if (sparkline) { + + // get last 30 days, if they exist + QList points; + + // include current activity value + double v; + + // get the metadata value + value = item->getText(symbol, "0"); + if (fieldtype == FIELD_DOUBLE) v = value.toDouble(); + else v = value.toInt(); + if (std::isinf(v) || std::isnan(v)) v=0; + points << QPointF(SPARKDAYS, v); + + // set the chart values with the last 10 rides + int index = parent->context->athlete->rideCache->rides().indexOf(item); + + int offset = 1; + double min = v; + double max = v; + double sum=0, count=0, avg = 0; + while(index-offset >=0) { // ultimately go no further back than first ever ride + + // get value from items before me + RideItem *prior = parent->context->athlete->rideCache->rides().at(index-offset); + + // are we still in range ? + const qint64 old = prior->dateTime.daysTo(item->dateTime); + if (old > SPARKDAYS) break; + + // only activities with matching sport flags + if (prior->isRun == item->isRun && prior->isSwim == item->isSwim) { + + double v; + + if (fieldtype == FIELD_DOUBLE) v = prior->getText(symbol, "").toDouble(); + else v = prior->getText(symbol, "").toInt(); + + if (std::isinf(v) || std::isnan(v)) v=0; + + // new no zero value + if (v) { + sum += v; + count++; + + points< max) max = v; + } + } + offset++; + } + + if (count) avg = sum / count; + else avg = 0; + + // which way up should the arrow be? + up = v > avg ? true : false; + + // add some space, if only one value +/- 10% + double diff = (max-min)/10.0f; + showrange=true; + if (diff==0) { + showrange=false; + diff = value.toDouble()/10.0f; + } + + // update the sparkline + sparkline->setPoints(points); + + // set range + sparkline->setRange(min-diff,max+diff); // add 10% to each direction + + // set the values for upper lower + upper = QString("%1").arg(max); + lower = QString("%1").arg(min); + mean = QString("%1").arg(avg, 0, 'f', 0); + } +} + +void +PMCOverviewItem::setData(RideItem *item) +{ + if (item == NULL || item->ride() == NULL) return; + + // get lts, sts, sb, rr for the input metric + PMCData *pmc = parent->context->athlete->getPMCFor(symbol); + + QDate date = item ? item->dateTime.date() : QDate(); + lts = pmc->lts(date); + sts = pmc->sts(date); + stress = pmc->stress(date); + sb = pmc->sb(date); + rr = pmc->rr(date); + +} + +void +ZoneOverviewItem::setData(RideItem *item) +{ + if (item == NULL || item->ride() == NULL) return; + + // 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); + + 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; ireplace(i, round(item->getForSymbol(timeInZonesHR[i]))); + } + + } else { + + for(int i=0; ireplace(i, 0); + } + + } else { + + for(int i=0; ireplace(i, 0); + } + } + 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; ireplace(i, round(item->getForSymbol(timeInZones[i]))); + } + + } else { + + for(int i=0; ireplace(i, 0); + } + + } else { + + for(int i=0; ireplace(i, 0); + } + } + 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; ireplace(i, round(item->getForSymbol(paceTimeInZones[i]))); + } + + } else { + + for(int i=0; ireplace(i, 0); + } + + } else { + + for(int i=0; ireplace(i, 0); + } + } + break; + + case RideFile::wbal: + { + // get total time in zones + double sum=0; + for(int i=0; i<4; i++) sum += round(item->getForSymbol(timeInZonesWBAL[i])); + + // update as percent of total + for(int i=0; i<4; i++) { + double time =round(item->getForSymbol(timeInZonesWBAL[i])); + if (time > 0 && sum > 0) barset->replace(i, round((time/sum) * 100)); + else barset->replace(i, 0); + } + } + break; + + } // switch +} + +void +RouteOverviewItem::setData(RideItem *item) +{ + if (item == NULL || item->ride() == NULL) return; + + if (scene() != NULL) { + + // only if we're place on the scene + if (item->ride() && item->ride()->areDataPresent()->lat) { + if (!routeline->isVisible()) routeline->show(); + routeline->setData(item); + } else routeline->hide(); + } +} + +void +IntervalOverviewItem::setData(RideItem *item) +{ + if (item == NULL || item->ride() == NULL) return; + + double minx = 999999999; + double maxx =-999999999; + double miny = 999999999; + double maxy =-999999999; + + //set the x, y series + QList points; + foreach(IntervalItem *interval, item->intervals()) { + // get the x and y VALUE + double x = interval->getForSymbol(xsymbol, parent->context->athlete->useMetricUnits); + double y = interval->getForSymbol(ysymbol, parent->context->athlete->useMetricUnits); + double z = interval->getForSymbol(zsymbol, parent->context->athlete->useMetricUnits); + + BPointF add; + add.x = x; + add.y = y; + add.z = z; + add.fill = interval->color; + add.label = interval->name; + points << add; + + if (xmaxx) maxx=x; + if (y>maxy) maxy=y; + } + + + // set scale + double ydiff = (maxy-miny) / 10.0f; + if (miny >= 0 && ydiff > miny) miny = ydiff; + double xdiff = (maxx-minx) / 10.0f; + if (minx >= 0 && xdiff > minx) minx = xdiff; + maxx=round(maxx); minx=round(minx); + maxy=round(maxy); miny=round(miny); + + // set range before points to filter + bubble->setRange(minx,maxx,miny,maxy); + bubble->setPoints(points); +} + + +void +RPEOverviewItem::itemGeometryChanged() { + + QRectF geom = geometry(); + + if (sparkline) { + + // make space for the rpe rating widget if needed + int minh=6; + if (!drag && geom.height() > (ROWHEIGHT*5)+20) { + rperating->show(); + rperating->setGeometry(20+(ROWHEIGHT*2), ROWHEIGHT*3, geom.width()-40-(ROWHEIGHT*4), ROWHEIGHT*2); + } else { // not set for meta or metric + rperating->hide(); + } + minh=7; + + // space enough? + if (!drag && geom.height() > (ROWHEIGHT*minh)) { + sparkline->show(); + sparkline->setGeometry(20, ROWHEIGHT*(minh-2), geom.width()-40, geom.height()-20-(ROWHEIGHT*(minh-2))); + } else { + sparkline->hide(); + } + } +} + +void +MetricOverviewItem::itemGeometryChanged() { + + QRectF geom = geometry(); + + if (sparkline) { + + // make space for the rpe rating widget if needed + int minh=6; + + // space enough? + if (!drag && geom.height() > (ROWHEIGHT*minh)) { + sparkline->show(); + sparkline->setGeometry(20, ROWHEIGHT*(minh-2), geom.width()-40, geom.height()-20-(ROWHEIGHT*(minh-2))); + } else { + sparkline->hide(); + } + } +} + +void +MetaOverviewItem::itemGeometryChanged() { + + QRectF geom = geometry(); + + if (sparkline) { + + // make space for the rpe rating widget if needed + int minh=6; + + // space enough? + if (!drag && geom.height() > (ROWHEIGHT*minh)) { + sparkline->show(); + sparkline->setGeometry(20, ROWHEIGHT*(minh-2), geom.width()-40, geom.height()-20-(ROWHEIGHT*(minh-2))); + } else { + sparkline->hide(); + } + } +} + +void +PMCOverviewItem::itemGeometryChanged() { } + +void +RouteOverviewItem::itemGeometryChanged() { + + QRectF geom = geometry(); + + // route map needs adding to scene etc + if (!drag) { + if (myRideItem && myRideItem->ride() && myRideItem->ride()->areDataPresent()->lat) routeline->show(); + routeline->setGeometry(20,ROWHEIGHT+40, geom.width()-40, geom.height()-(60+ROWHEIGHT)); + + } else routeline->hide(); +} + +void +IntervalOverviewItem::itemGeometryChanged() { + + QRectF geom = geometry(); + + if (!drag) bubble->show(); + + // disable animation when changing geometry + bubble->setGeometry(20,20+(ROWHEIGHT*2), geom.width()-40, geom.height()-(40+(ROWHEIGHT*2))); +} + + +void +ZoneOverviewItem::dragChanged(bool drag) +{ + if (chart) { + if (drag) chart->hide(); + else chart->show(); + } +} +void +ZoneOverviewItem::itemGeometryChanged() { + + QRectF geom = geometry(); + + // if we contain charts etc lets update their geom + if (!drag) chart->show(); + + // disable animation when changing geometry + chart->setAnimationOptions(QChart::NoAnimation); + chart->setGeometry(20,20+(ROWHEIGHT*2), geom.width()-40, geom.height()-(40+(ROWHEIGHT*2))); +} + +void +RPEOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + + // we need the metric units + double addy = 0; + if (rperating->isVisible()) addy=ROWHEIGHT*2; // shift up for rperating + + // mid is slightly higher to account for space around title, move mid up + double mid = (ROWHEIGHT*1.5f) + ((geometry().height() - (ROWHEIGHT*2)) / 2.0f) - (addy/2); + + // if we're deep enough to show the sparkline then stop + if (geometry().height() > (ROWHEIGHT*6)) mid=((ROWHEIGHT*1.5f) + (ROWHEIGHT*3) / 2.0f) - (addy/2); + + // we align centre and mid + QFontMetrics fm(parent->bigfont); + QRectF rect = QFontMetrics(parent->bigfont, parent->device()).boundingRect(value); + + painter->setPen(GColor(CPLOTMARKER)); + painter->setFont(parent->bigfont); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + + // paint the range and mean if the chart is shown + if (showrange && sparkline && sparkline->isVisible()) { + + // in small font max min at top bottom right of chart + double top = sparkline->geometry().top(); + double bottom = sparkline->geometry().bottom(); + double right = sparkline->geometry().right(); + + painter->setPen(QColor(100,100,100)); + painter->setFont(parent->smallfont); + + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(upper) - 80, + top - 40 + (fm.ascent() / 2.0f)), upper); + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(lower) - 80, + bottom -40), lower); + + painter->setPen(QColor(50,50,50)); + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(mean) - 80, + ((top+bottom)/2) + (fm.tightBoundingRect(mean).height()/2) - 60), mean); + } + + // regardless we always show up/down/same + QPointF bl = QPointF((geometry().width() - rect.width()) / 2.0f, mid + (fm.ascent() / 3.0f)); + QRectF trect = fm.tightBoundingRect(value); + 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); + + 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 +MetricOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + + double addy = 0; + if (units != "" && units != tr("seconds")) addy = QFontMetrics(parent->smallfont).height(); + + // mid is slightly higher to account for space around title, move mid up + double mid = (ROWHEIGHT*1.5f) + ((geometry().height() - (ROWHEIGHT*2)) / 2.0f) - (addy/2); + + // if we're deep enough to show the sparkline then stop + if (geometry().height() > (ROWHEIGHT*6)) mid=((ROWHEIGHT*1.5f) + (ROWHEIGHT*3) / 2.0f) - (addy/2); + + // we align centre and mid + QFontMetrics fm(parent->bigfont); + QRectF rect = QFontMetrics(parent->bigfont, parent->device()).boundingRect(value); + + painter->setPen(GColor(CPLOTMARKER)); + painter->setFont(parent->bigfont); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + + // now units + if (units != "" && addy > 0) { + painter->setPen(QColor(100,100,100)); + painter->setFont(parent->smallfont); + + painter->drawText(QPointF((geometry().width() - QFontMetrics(parent->smallfont).width(units)) / 2.0f, + mid + (fm.ascent() / 3.0f) + addy), units); // divided by 3 to account for "gap" at top of font + } + + // paint the range and mean if the chart is shown + if (showrange && sparkline && sparkline->isVisible()) { + //sparkline->paint(painter, option, widget); + + // in small font max min at top bottom right of chart + double top = sparkline->geometry().top(); + double bottom = sparkline->geometry().bottom(); + double right = sparkline->geometry().right(); + + painter->setPen(QColor(100,100,100)); + painter->setFont(parent->smallfont); + + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(upper) - 80, + top - 40 + (fm.ascent() / 2.0f)), upper); + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(lower) - 80, + bottom -40), lower); + + painter->setPen(QColor(50,50,50)); + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(mean) - 80, + ((top+bottom)/2) + (fm.tightBoundingRect(mean).height()/2) - 60), mean); + } + + // regardless we always show up/down/same + QPointF bl = QPointF((geometry().width() - rect.width()) / 2.0f, mid + (fm.ascent() / 3.0f)); + QRectF trect = fm.tightBoundingRect(value); + 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); + + 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 +MetaOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + + if (!sparkline && fieldtype >= 0) { // textual metadata field + + // mid is slightly higher to account for space around title, move mid up + double mid = (ROWHEIGHT*1.5f) + ((geometry().height() - (ROWHEIGHT*2)) / 2.0f); + + // we align centre and mid + QFontMetrics fm(parent->bigfont); + QRectF rect = QFontMetrics(parent->bigfont, parent->device()).boundingRect(value); + + if (fieldtype == FIELD_TEXTBOX) { + // long texts need to be formatted into a smaller font an word wrapped + painter->setPen(QColor(150,150,150)); + painter->setFont(parent->smallfont); + + // draw text and wrap / truncate to bounding rectangle + painter->drawText(QRectF(ROWHEIGHT, ROWHEIGHT*2.5, geometry().width()-(ROWHEIGHT*2), + geometry().height()-(ROWHEIGHT*4)), value); + } else { + + // any other kind of metadata just paint it + painter->setPen(GColor(CPLOTMARKER)); + painter->setFont(parent->bigfont); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + } + + + } + + if (sparkline) { // if its a numeric metadata field + + double addy = 0; + + // mid is slightly higher to account for space around title, move mid up + double mid = (ROWHEIGHT*1.5f) + ((geometry().height() - (ROWHEIGHT*2)) / 2.0f) - (addy/2); + + // if we're deep enough to show the sparkline then stop + if (geometry().height() > (ROWHEIGHT*6)) mid=((ROWHEIGHT*1.5f) + (ROWHEIGHT*3) / 2.0f) - (addy/2); + + // we align centre and mid + QFontMetrics fm(parent->bigfont); + QRectF rect = QFontMetrics(parent->bigfont, parent->device()).boundingRect(value); + + painter->setPen(GColor(CPLOTMARKER)); + painter->setFont(parent->bigfont); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + + // paint the range and mean if the chart is shown + if (showrange && sparkline) { + + if (sparkline->isVisible()) { + //sparkline->paint(painter, option, widget); + + // in small font max min at top bottom right of chart + double top = sparkline->geometry().top(); + double bottom = sparkline->geometry().bottom(); + double right = sparkline->geometry().right(); + + painter->setPen(QColor(100,100,100)); + painter->setFont(parent->smallfont); + + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(upper) - 80, + top - 40 + (fm.ascent() / 2.0f)), upper); + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(lower) - 80, + bottom -40), lower); + + + painter->setPen(QColor(50,50,50)); + painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(mean) - 80, + ((top+bottom)/2) + (fm.tightBoundingRect(mean).height()/2) - 60), mean); + } + + // regardless we always show up/down/same + QPointF bl = QPointF((geometry().width() - rect.width()) / 2.0f, mid + (fm.ascent() / 3.0f)); + QRectF trect = fm.tightBoundingRect(value); + 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); + + 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 +PMCOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + + // Lets use friendly names for TSB et al, as described on the TrainingPeaks website + // here: http://home.trainingpeaks.com/blog/article/4-new-mobile-features-you-should-know-about + // as written by Ben Pryhoda their Senior Director of Product, Device and API Integrations + // we will make this configurable later anyway, as calling SB 'Form' is rather dodgy. + QFontMetrics tfm(parent->titlefont, parent->device()); + QFontMetrics bfm(parent->bigfont, parent->device()); + + // 4 measures to show, depending upon how much space + // so prioritise - SB then LTS, STS, RR + + double nexty = ROWHEIGHT; + // + // Stress Balance + // + painter->setPen(QColor(200,200,200)); + painter->setFont(parent->titlefont); + QString string = QString(tr("Form")); + QRectF rect = tfm.boundingRect(string); + painter->drawText(QPointF(ROWHEIGHT / 2.0f, + nexty + (tfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += rect.height() + 30; + + painter->setPen(PMCData::sbColor(sb, GColor(CPLOTMARKER))); + painter->setFont(parent->bigfont); + string = QString("%1").arg(round(sb)); + rect = bfm.boundingRect(string); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + nexty + (bfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += ROWHEIGHT*2; + + // + // Long term Stress + // + if (deep > 7) { + + painter->setPen(QColor(200,200,200)); + painter->setFont(parent->titlefont); + QString string = QString(tr("Fitness")); + QRectF rect = tfm.boundingRect(string); + painter->drawText(QPointF(ROWHEIGHT / 2.0f, + nexty + (tfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += rect.height() + 30; + + painter->setPen(PMCData::ltsColor(lts, GColor(CPLOTMARKER))); + painter->setFont(parent->bigfont); + string = QString("%1").arg(round(lts)); + rect = bfm.boundingRect(string); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + nexty + (bfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += ROWHEIGHT*2; + + } + + // + // Short term Stress + // + if (deep > 11) { + + painter->setPen(QColor(200,200,200)); + painter->setFont(parent->titlefont); + QString string = QString(tr("Fatigue")); + QRectF rect = tfm.boundingRect(string); + painter->drawText(QPointF(ROWHEIGHT / 2.0f, + nexty + (tfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += rect.height() + 30; + + painter->setPen(PMCData::stsColor(sts, GColor(CPLOTMARKER))); + painter->setFont(parent->bigfont); + string = QString("%1").arg(round(sts)); + rect = bfm.boundingRect(string); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + nexty + (bfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += ROWHEIGHT*2; + + } + + // + // Ramp Rate + // + if (deep > 14) { + + painter->setPen(QColor(200,200,200)); + painter->setFont(parent->titlefont); + QString string = QString(tr("Risk")); + QRectF rect = tfm.boundingRect(string); + painter->drawText(QPointF(ROWHEIGHT / 2.0f, + nexty + (tfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += rect.height() + 30; + + painter->setPen(PMCData::rrColor(rr, GColor(CPLOTMARKER))); + painter->setFont(parent->bigfont); + string = QString("%1").arg(round(rr)); + rect = bfm.boundingRect(string); + painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, + nexty + (bfm.ascent() / 3.0f)), string); // divided by 3 to account for "gap" at top of font + nexty += ROWHEIGHT*2; + + } +} + +// no custom painting for these guys, they contain widgets only +void RouteOverviewItem::itemPaint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) { } +void IntervalOverviewItem::itemPaint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) { } +void ZoneOverviewItem::itemPaint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) { } + +// +// Below here are all the overviewitem viz +// +RPErating::RPErating(RPEOverviewItem *parent, QString name) : QGraphicsItem(NULL), parent(parent), name(name), hover(false) +{ + setGeometry(20,20,100,100); + setZValue(11); + setAcceptHoverEvents(true); +} + +static QString FosterDesc[11]={ + QObject::tr("Rest"), // 0 + QObject::tr("Very, very easy"), // 1 + QObject::tr("Easy"), // 2 + QObject::tr("Moderate"), // 3 + QObject::tr("Somewhat hard"), // 4 + QObject::tr("Hard"), // 5 + QObject::tr("Hard+"), // 6 + QObject::tr("Very hard"), // 7 + QObject::tr("Very hard+"), // 8 + QObject::tr("Very hard++"), // 9 + QObject::tr("Maximum")// 10 +}; + +static QColor FosterColors[11]={ + QColor(Qt::lightGray),// 0 + QColor(Qt::lightGray),// 1 + QColor(Qt::darkGreen),// 2 + QColor(Qt::darkGreen),// 3 + QColor(Qt::darkGreen),// 4 + QColor(Qt::darkYellow),// 5 + QColor(Qt::darkYellow),// 6 + QColor(Qt::darkYellow),// 7 + QColor(Qt::darkRed),// 8 + QColor(Qt::darkRed),// 9 + QColor(Qt::red),// 10 +}; + +void +RPErating::setValue(QString value) +{ + // RPE values from other sources (e.g. TodaysPlan) are "double" + this->value = value; + int v = qRound(value.toDouble()); + if (v <0 || v>10) { + color = GColor(CPLOTMARKER); + description = QObject::tr("Invalid"); + } else { + description = FosterDesc[v]; + color = FosterColors[v]; + } +} + +QVariant RPErating::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionChange && parent->scene()) prepareGeometryChange(); + return QGraphicsItem::itemChange(change, value); +} + +void +RPErating::setGeometry(double x, double y, double width, double height) +{ + geom = QRectF(x,y,width,height); + + // we need to go onto the scene ! + if (scene() == NULL && parent->scene()) parent->scene()->addItem(this); + + // set our geom + prepareGeometryChange(); +} + +bool +RPErating::sceneEvent(QEvent *event) +{ + // skip whilst dragging and resizing + if (parent->parent->state != ChartSpace::NONE) return false; + + if (event->type() == QEvent::GraphicsSceneHoverMove) { + + if (hover) { + + // set value based upon the location of the mouse + QPoint vpos = parent->parent->view->mapFromGlobal(QCursor::pos()); + QPointF pos = parent->parent->view->mapToScene(vpos); + QPointF cpos = pos - parent->geometry().topLeft() - geom.topLeft(); + + // new value should + double width = geom.width() / 13; // always a block each side for a margin + + double x = round((cpos.x() - width)/width); + if (x >=0 && x<=10) { + + // set to the new value + setValue(QString("%1").arg(x)); + parent->value = value; + parent->update(); + + } + } + + // mouse moved so hover paint anyway + update(); + + } else if (hover && event->type() == QEvent::GraphicsSceneMousePress) { + + applyEdit(); + update(); + + } else if (event->type() == QEvent::GraphicsSceneHoverLeave) { + + cancelEdit(); + update(); + + } else if (event->type() == QEvent::GraphicsSceneHoverEnter) { + + // remember what it was + oldvalue = value; + hover = true; + update(); + } + return false; +} + +void +RPErating::cancelEdit() +{ + if (value != oldvalue || parent->value != oldvalue) { + parent->value=oldvalue; + value=oldvalue; + parent->update(); + setValue(oldvalue); + } + hover = false; + update(); + +} + +void +RPErating::applyEdit() +{ + // update the item - if we have one + RideItem *item = parent->parent->property("ride").value(); + + // did it change? + if (item && item->ride() && item->getText("RPE","") != value) { + + // change it -- this smells, since it should be abstracted in RideItem XXX + item->ride()->setTag("RPE", value); + item->notifyRideMetadataChanged(); + item->setDirty(true); + + // now oldvalue is value! + oldvalue = value; + } + + hover = false; + update(); +} + +void +RPErating::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) +{ + painter->setPen(Qt::NoPen); + + // hover? + if (hover) { + QColor darkgray(120,120,120,120); + painter->fillRect(QRectF(parent->x()+geom.x(), parent->y()+geom.y(), geom.width(),geom.height()), QBrush(darkgray)); + } + + painter->setPen(QPen(color)); + QFontMetrics tfm(parent->parent->titlefont); + QRectF rect = tfm.boundingRect(description); + painter->setFont(parent->parent->titlefont); + painter->drawText(QPointF(parent->x()+geom.x()+((geometry().width() - rect.width()) / 2.0f), + parent->y()+geom.y()+geom.height()-ROWHEIGHT), description); // divided by 3 to account for "gap" at top of font + + + // paint the blocks + double width = geom.width() / 13; // always a block each side for a margin + int i=0; + for(; i<= qRound(value.toDouble()); i++) { + + // draw a rectangle with a 5px gap + painter->setPen(Qt::NoPen); + painter->fillRect(geom.x()+parent->x()+(width *(i+1)), parent->y()+geom.y()+ROWHEIGHT*1.5f, width-5, ROWHEIGHT*0.25f, QBrush(FosterColors[i])); + } + + for(; i<= 10; i++) { + + // draw a rectangle with a 5px gap + painter->setPen(Qt::NoPen); + painter->fillRect(geom.x()+parent->x()+(width *(i+1)), parent->y()+geom.y()+ROWHEIGHT*1.5f, width-5, ROWHEIGHT*0.25f, QBrush(GColor(CCARDBACKGROUND).darker(200))); + } +} + +double BPointF::score(BPointF &other) +{ + // match score + // 100 * n characters that match in label + // +1000 if color same (class) + // -(10 * sizediff) size important + double score = 0; + + // must be the same class + if (fill != other.fill) return 0; + else score += 1000; + + // oh, this is a peach + if (label == other.label) return 10000; + + for(int i=0; iscene()) prepareGeometryChange(); + return QGraphicsItem::itemChange(change, value); +} + +void +BubbleViz::setGeometry(double x, double y, double width, double height) +{ + geom = QRectF(x,y,width,height); + + // we need to go onto the scene ! + if (scene() == NULL && parent->scene()) parent->scene()->addItem(this); + + // set our geom + prepareGeometryChange(); +} + +bool +BubbleViz::sceneEvent(QEvent *event) +{ + // skip whilst dragging and resizing + if (parent->parent->state != ChartSpace::NONE) return false; + + if (event->type() == QEvent::GraphicsSceneHoverMove) { + + // set value based upon the location of the mouse + QPoint vpos = parent->parent->view->mapFromGlobal(QCursor::pos()); + QPointF pos = parent->parent->view->mapToScene(vpos); + + QRectF canvas= QRectF(parent->x()+geom.x(), parent->y()+geom.y(), geom.width(),geom.height()); + QRectF plotarea = QRectF(canvas.x() + ROWHEIGHT * 2 + 20, canvas.y()+ROWHEIGHT, + canvas.width() - ROWHEIGHT * 2 - 20 - ROWHEIGHT, + canvas.height() - ROWHEIGHT * 2 - 20 - ROWHEIGHT); + if (plotarea.contains(pos)) { + plotpos = QPointF(pos.x()-plotarea.x(), pos.y()-plotarea.y()); + hover=true; + update(); + } else if (hover == true) { + hover=false; + update(); + } + } + return false; +} + +class BubbleVizTuple { +public: + double score; + int newindex, oldindex; + +}; +bool scoresBiggerThan(const BubbleVizTuple i1, const BubbleVizTuple i2) +{ + return i1.score > i2.score; +} +void +BubbleViz::setPoints(QList p) +{ + oldpoints = this->points; + oldmean = this->mean; + + double sum=0, count=0; + this->points.clear(); + foreach(BPointF point, p) { + + if (point.x < minx || point.x > maxx || !std::isfinite(point.x) || std::isnan(point.x) || point.x == 0 || + point.y < miny || point.y > maxy || !std::isfinite(point.y) || std::isnan(point.y) || point.y == 0 || + point.z == 0 || !std::isfinite(point.z) || std::isnan(point.z)) continue; + + this->points << point; + sum += point.z; + count++; + } + mean = sum/count; + + // so now we need to setup a transition + // we match the oldpoints to the new points by scoring + QList matches; + for(int i=0; i scores; + QVector available(oldpoints.count()); + available.fill(true); + + // get all the scores + for(int newindex =0; newindex < points.count(); newindex++) { + for (int oldindex =0; oldindex < oldpoints.count(); oldindex++) { + BubbleVizTuple add; + add.newindex = newindex; + add.oldindex = oldindex; + add.score = points[newindex].score(oldpoints[oldindex]); + if (add.score > 0) scores << add; + } + } + + // sort scores high to low + qSort(scores.begin(), scores.end(), scoresBiggerThan); + + // now assign - from best match to worst + foreach(BubbleVizTuple score, scores){ + if (available[score.oldindex]) { + available[score.oldindex]=false; // its now taken + matches[score.newindex]=oldpoints[score.oldindex]; + } + } + + // add non-matches to the end + for(int i=0; istop(); + animator->setStartValue(0); + animator->setEndValue(256); + animator->setEasingCurve(QEasingCurve::OutQuad); + animator->setDuration(1000); + animator->start(); +} + +static double pointDistance(QPointF a, QPointF b) +{ + double distance = sqrt(pow(b.x()-a.x(),2) + pow(b.y()-a.y(),2)); + return distance; +} + +// just draw a rect for now +void +BubbleViz::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) +{ + // blank when no points + if (points.count() == 0 || miny==maxy || minx==maxx) return; + + painter->setPen(GColor(CPLOTMARKER)); + + // chart canvas + QRectF canvas= QRectF(parent->x()+geom.x(), parent->y()+geom.y(), geom.width(),geom.height()); + //DIAG painter->drawRect(canvas); + + // plotting space + QRectF plotarea = QRectF(canvas.x() + ROWHEIGHT * 2 + 20, canvas.y()+ROWHEIGHT, + canvas.width() - ROWHEIGHT * 2 - 20 - ROWHEIGHT, + canvas.height() - ROWHEIGHT * 2 - 20 - ROWHEIGHT); + //DIAG painter->drawRect(plotarea); + + // clip to canvas -- draw points first so all axis etc are overlayed + painter->save(); + painter->setClipRect(plotarea); + + // scale values to plot area + double xratio = plotarea.width() / (maxx*1.03); + double yratio = plotarea.height() / (maxy*1.03); // boundary space + + // old values when transitioning + double oxratio = plotarea.width() / (oldmaxx*1.03); + double oyratio = plotarea.height() / (oldmaxy*1.03); // boundary space + + // run through each point + double area = 10000; // max size + + // remember the one we are nearest + BPointF nearest; + double nearvalue = -1; + + int index=0; + foreach(BPointF point, points) { + + if (point.x < minx || point.x > maxx || + point.y < miny || point.y > maxy || + !std::isfinite(point.z) || std::isnan(point.z)) { + index++; + continue; + } + + // resize if transitioning + QPointF center(plotarea.left() + (xratio * point.x), plotarea.bottom() - (yratio * point.y)); + int alpha = 200; + + double size = (point.z / mean) * area; + if (size > area * 6) size=area*6; + if (size < 600) size=600; + + if (transition < 256 && oldpoints.count()) { + if (oldpoints[index].x != 0 || oldpoints[index].y != 0) { + // where it was + QPointF oldcenter = QPointF(plotarea.left() + (oxratio * oldpoints[index].x), + plotarea.bottom() - (oyratio * oldpoints[index].y)); + + // transition to new point + center.setX(center.x() - (double(255-transition) * ((center.x()-oldcenter.x())/255.0f))); + center.setY(center.y() - (double(255-transition) * ((center.y()-oldcenter.y())/255.0f))); + + // transition bubble size + double oldsize = (oldpoints[index].z / oldmean) * area; + if (oldsize > area * 6) oldsize=area*6; + if (oldsize < 600) oldsize=600; + size = size - (double(255-transition) * ((size-oldsize)/255.0f)); + + } else { + // just make it appear + alpha = (200.0f/255.0f) * transition; + } + } + + // once transitioned clear them away + if (transition == 256 && oldpoints.count()) oldpoints.clear(); + + QColor color = point.fill; + color.setAlpha(alpha); + painter->setBrush(color); + painter->setPen(QColor(150,150,150)); + + double radius = sqrt(size/3.1415927f); + painter->drawEllipse(center, radius, radius); + + // is the cursor hovering over me? + double distance; + if (transition == 256 && hover && (distance=pointDistance(center, plotarea.topLeft()+plotpos)) <= radius) { + + // is this the nearest ? + if (nearvalue == -1 || distance < nearvalue) { + nearest = point; + nearvalue = distance; + } + + } + index++; + } + + // if we're transitioning + while (transition < 256 && index < oldpoints.count()) { + QPointF oldcenter = QPointF(plotarea.left() + (oxratio * oldpoints[index].x), + plotarea.bottom() - (oyratio * oldpoints[index].y)); + + // fade out + QColor color = oldpoints[index].fill; + color.setAlpha(200 - (200.0f/255.0f) * double(transition)); + painter->setBrush(color); + painter->setPen(Qt::NoPen); + + double size = (oldpoints[index].z/oldmean) * area; + if (size > area * 6) size=area*6; + if (size < 600) size=600; + double radius = sqrt(size/3.1415927f); + painter->drawEllipse(oldcenter, radius, radius); + + // hide the old ones + index++; + } + + painter->setBrush(Qt::NoBrush); + + // clip to canvas + painter->setClipRect(canvas); + + // x-axis labels + QRectF xlabelspace = QRectF(plotarea.x(), plotarea.bottom() + 20, plotarea.width(), ROWHEIGHT); + painter->setPen(Qt::red); + //DIAG painter->drawRect(xlabelspace); + + // x-axis title + QRectF xtitlespace = QRectF(plotarea.x(), xlabelspace.bottom(), plotarea.width(), ROWHEIGHT); + painter->setPen(Qt::yellow); + //DIAG painter->drawRect(xtitlespace); + + QTextOption midcenter; + midcenter.setAlignment(Qt::AlignVCenter|Qt::AlignCenter); + + painter->setPen(QColor(150,150,150)); + painter->setFont(parent->parent->smallfont); + painter->drawText(xtitlespace, xlabel, midcenter); + + + // y-axis labels + QRectF ylabelspace = QRectF(plotarea.x()-20-ROWHEIGHT, plotarea.y(), ROWHEIGHT, plotarea.height()); + painter->setPen(Qt::red); + //DIAG painter->drawRect(ylabelspace); + + // y-axis title + //DIAGQRectF ytitlespace = QRectF(plotarea.x()-20-(ROWHEIGHT*2), plotarea.y(), ROWHEIGHT, plotarea.height()); + //DIAGpainter->setPen(Qt::yellow); + //DIAGpainter->drawRect(ytitlespace); + + painter->setPen(QColor(150,150,150)); + painter->setFont(parent->parent->smallfont); + //XXX FIXME XXX painter->drawText(xtitlespace, xlabel, midcenter); + + // draw axis, from minx, to maxx (see tufte for 'range' axis on scatter plots + QPen axisPen(QColor(150,150,150)); + axisPen.setWidth(5); + painter->setPen(axisPen); + + // x-axis + painter->drawLine(QPointF(plotarea.left() + (minx * xratio), plotarea.bottom()), + QPointF(plotarea.left() + (maxx * xratio), plotarea.bottom())); + + // x-axis range + QFontMetrics sfm(parent->parent->smallfont); + QRectF bminx = sfm.tightBoundingRect(QString("%1").arg(minx)); + QRectF bmaxx = sfm.tightBoundingRect(QString("%1").arg(maxx)); + painter->drawText(xlabelspace.left() + (minx*xratio) - (bminx.width()/2), xlabelspace.bottom(), QString("%1").arg(minx)); + painter->drawText(xlabelspace.left() + (maxx*xratio) - (bmaxx.width()/2), xlabelspace.bottom(), QString("%1").arg(maxx)); + + // draw minimum value + painter->drawLine(QPointF(plotarea.left(), plotarea.bottom() - (miny*yratio)), + QPointF(plotarea.left(), plotarea.bottom() - (maxy*yratio))); + // y-axis range + QRectF bminy = sfm.tightBoundingRect(QString("%1").arg(miny)); + QRectF bmaxy = sfm.tightBoundingRect(QString("%1").arg(maxy)); + painter->drawText(ylabelspace.right() - bmaxy.width(), ylabelspace.bottom()-(maxy*yratio) + (bmaxy.height()/2), QString("%1").arg(maxy)); + painter->drawText(ylabelspace.right() - bminy.width(), ylabelspace.bottom()-(miny*yratio) + (bminy.height()/2), QString("%1").arg(miny)); + + // hover point? + painter->setPen(GColor(CPLOTMARKER)); + + if (hover && nearvalue >= 0) { + + painter->setFont(parent->parent->titlefont); + QFontMetrics tfm(parent->parent->titlefont); + + // where is it? + QPointF center(plotarea.left() + (xratio * nearest.x), plotarea.bottom() - (yratio * nearest.y)); + + // xlabel + QString xlab = QString("%1").arg(nearest.x, 0, 'f', 0); + bminx = tfm.tightBoundingRect(QString("%1").arg(xlab)); + bminx.moveTo(center.x() - (bminx.width()/2), xlabelspace.bottom()-bminx.height()); + painter->fillRect(bminx, QBrush(GColor(CCARDBACKGROUND))); // overwrite range labels + painter->drawText(center.x() - (bminx.width()/2), xlabelspace.bottom(), xlab); + + // ylabel + QString ylab = QString("%1").arg(nearest.y, 0, 'f', 0); + bminy = tfm.tightBoundingRect(QString("%1").arg(ylab)); + bminy.moveTo(ylabelspace.right() - bminy.width(), center.y() - (bminy.height()/2)); + painter->fillRect(bminy, QBrush(GColor(CCARDBACKGROUND))); // overwrite range labels + painter->drawText(ylabelspace.right() - bminy.width(), center.y() + (bminy.height()/2), ylab); + + // plot marker + QPen pen(Qt::NoPen); + painter->setPen(pen); + painter->setBrush(GColor(CPLOTMARKER)); + + // draw the one we are near with no alpha + double size = (nearest.z/mean) * area; + if (size > area * 6) size=area*6; + if (size < 600) size=600; + double radius = sqrt(size/3.1415927f) + 20; + painter->drawEllipse(center, radius, radius); + + // clip to card, but happily write all over the title! + painter->setClipping(false); + + // now put the label at the top of the canvas + painter->setPen(QPen(GColor(CPLOTMARKER))); + bminx = tfm.tightBoundingRect(nearest.label); + painter->drawText(canvas.center().x()-(bminx.width()/2.0f), + canvas.top()+bminx.height()-10, nearest.label); + } + + painter->restore(); +} + +Sparkline::Sparkline(QGraphicsWidget *parent, int count, QString name) + : QGraphicsItem(NULL), parent(parent), name(name) +{ + Q_UNUSED(count) + + min = max = 0.0f; + setGeometry(20,20,100,100); + setZValue(11); +} + +void +Sparkline::setRange(double min, double max) +{ + this->min = min; + this->max = max; +} + +void +Sparkline::setPoints(QListx) +{ + points = x; +} + +QVariant Sparkline::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionChange && parent->scene()) prepareGeometryChange(); + return QGraphicsItem::itemChange(change, value); +} + +void +Sparkline::setGeometry(double x, double y, double width, double height) +{ + geom = QRectF(x,y,width,height); + + // we need to go onto the scene ! + if (scene() == NULL && parent->scene()) parent->scene()->addItem(this); + + // set our geom + prepareGeometryChange(); +} + +void +Sparkline::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) +{ + // if no points just leave blank + if (points.isEmpty() || (max-min)==0) return; + + // so draw a line connecting the points + 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; + + // draw a sparkline -- need more than 1 point ! + if (points.count() > 1) { + + QPainterPath path; + path.moveTo((points[0].x()*xfactor)+xoffset, bottom-((points[0].y()-min)*yfactor)); + for(int i=1; isetPen(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)); + } + } +} + +Routeline::Routeline(QGraphicsWidget *parent, QString name) : QGraphicsItem(NULL), parent(parent), name(name) +{ + setGeometry(20,20,100,100); + setZValue(11); + animator=new QPropertyAnimation(this, "transition"); +} + +void +Routeline::setData(RideItem *item) +{ + // no data, no plot + if (item == NULL || !item->ride() || item->ride()->areDataPresent()->lat == false) { + path = QPainterPath(); + return; + } + oldpath = path; + owidth = width; + oheight = height; + + // step 1 normalise the points + + // set points as ratio from topleft corner + // and also calculate aspect ratio - to ensure + // values are mapped to maintain the ratio (!) + + // + // Find the top left and bottom right extents + // of the trace and calculate offset, factor + // and ratios to apply to each data point + // + double minlat=999, minlon=999; + double maxlat=-999, maxlon=-999; + + foreach(RideFilePoint *p, item->ride()->dataPoints()) { + + // ignore zero values and out of bounds + if (p->lat == 0 || p->lon == 0 || + p->lon < -180 || p->lon > 180 || + p->lat < -90 || p->lat > 90) continue; + + if (p->lat > maxlat) maxlat=p->lat; + if (p->lat < minlat) minlat=p->lat; + if (p->lon < minlon) minlon=p->lon; + if (p->lon > maxlon) maxlon=p->lon; + } + + // calculate aspect ratio + path = QPainterPath(); + double xdiff = (maxlon - minlon); + double ydiff = (maxlat - minlat); + double aspectratio = ydiff/xdiff; + width = geom.width(); + + // create a painterpath that uses a 1x1 aspect ratio + // based upon the GPS co-ords + int div = item->ride()->dataPoints().count() / ROUTEPOINTS; + int count=0; + height = geom.width() * aspectratio; + int lines=0; + foreach(RideFilePoint *p, item->ride()->dataPoints()){ + + // ignore zero values and out of bounds + if (p->lat == 0 || p->lon == 0 || + p->lon < -180 || p->lon > 180 || + p->lat < -90 || p->lat > 90) continue; + + // filter out most of the points so we end up with ROUTEPOINTS points + if (--count < 0) { // first + + //path.moveTo(xoff+(geom.width() / (xdiff / (p->lon - minlon))), + // yoff+(geom.height()-(geom.height() / (ydiff / (p->lat - minlat))))); + + path.moveTo((geom.width() / (xdiff / (p->lon - minlon))), + (height-(height / (ydiff / (p->lat - minlat))))); + count=div; + + } else if (count == 0) { + + //path.lineTo(xoff+(geom.width() / (xdiff / (p->lon - minlon))), + // yoff+(geom.height()-(geom.height() / (ydiff / (p->lat - minlat))))); + path.lineTo((geom.width() / (xdiff / (p->lon - minlon))), + (height-(height / (ydiff / (p->lat - minlat))))); + count=div; + lines++; + + } + } + + // if we have a transition + animator->stop(); + if (oldpath.elementCount()) { + animator->setStartValue(0); + animator->setEndValue(256); + animator->setEasingCurve(QEasingCurve::OutQuad); + animator->setDuration(1000); + animator->start(); + } else { + transition = 256; + } +} + +QVariant Routeline::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionChange && parent->scene()) prepareGeometryChange(); + return QGraphicsItem::itemChange(change, value); +} + +void +Routeline::setGeometry(double x, double y, double width, double height) +{ + geom = QRectF(x,y,width,height); + + // we need to go onto the scene ! + if (scene() == NULL && parent->scene()) parent->scene()->addItem(this); + + // set our geom + prepareGeometryChange(); +} + +void +Routeline::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) +{ + painter->save(); + + QPen pen(QColor(150,150,150)); + painter->setPen(pen); + + // draw the route, but scale it to fit what we have + double scale = geom.width() / width; + double oscale = geom.width() / width; + if (height * scale > geom.height()) scale = geom.height() / height; + if (oheight * oscale > geom.height()) oscale = geom.height() / oheight; + + // set clipping before we translate! + painter->setClipRect(parent->x()+geom.x(), parent->y()+geom.y(), geom.width(), geom.height()); + + // and center it too + double midx=scale*width/2; + double midy=scale*height/2; + double omidx=oscale*owidth/2; + double omidy=oscale*oheight/2; + QPointF translate(boundingRect().x() + ((geom.width()/2)-midx), + boundingRect().y()+((geom.height()/2)-midy)); + + QPointF otranslate(boundingRect().x() + ((geom.width()/2)-omidx), + boundingRect().y()+((geom.height()/2)-omidy)); + painter->translate(translate); + + // set painter scale - and keep original aspect ratio + painter->scale(scale,scale); + pen.setWidth(20.0f); + painter->setPen(pen); + + + // silly little animated morph from old to new + if(transition < 256) { + // transition! + QPainterPath tpath; + for(int i=0; idrawPath(tpath); + } else { + painter->drawPath(path); + } + painter->restore(); + return; +} diff --git a/src/Charts/OverviewItems.h b/src/Charts/OverviewItems.h new file mode 100644 index 000000000..9a88a5f0f --- /dev/null +++ b/src/Charts/OverviewItems.h @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2020 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 + */ + +#ifndef _GC_OverviewItem_h +#define _GC_OverviewItem_h 1 + +// basics +#include "ChartSpace.h" +#include + +// qt charts for zone chart +#include +#include +#include +#include + +// subwidgets for viz inside each overview item +class RPErating; +class BPointF; +class Sparkline; +class BubbleViz; +class Routeline; + +// sparklines number of points - look back 6 weeks +#define SPARKDAYS 42 + +// number of points in a route viz - trial and error gets 250 being reasonable +#define ROUTEPOINTS 250 + +// types we use +enum OverviewItemType { RPE, METRIC, META, ZONE, INTERVAL, PMC, ROUTE }; + +class RPEOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + public: + + RPEOverviewItem(ChartSpace *parent, QString name); + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *item); + + // RPE meta field + Sparkline *sparkline; + RPErating *rperating; + + // for setting sparkline & painting + bool up, showrange; + QString value, upper, lower, mean; +}; + +class MetricOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + public: + + MetricOverviewItem(ChartSpace *parent, QString name, QString symbol); + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *item); + + QString symbol; + RideMetric *metric; + QString units; + + bool up, showrange; + QString value, upper, lower, mean; + + Sparkline *sparkline; +}; + +class MetaOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + public: + + MetaOverviewItem(ChartSpace *parent, QString name, QString symbol); + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *item); + + QString symbol; + int fieldtype; + + // for numeric metadata items + bool up, showrange; + QString value, upper, lower, mean; + + Sparkline *sparkline; +}; + +class PMCOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + public: + + PMCOverviewItem(ChartSpace *parent, QString symbol); + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *item); + + QString symbol; + + double sts, lts, sb, rr, stress; +}; + +class ZoneOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + public: + + ZoneOverviewItem(ChartSpace *parent, QString name, RideFile::seriestype); + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *item); + void dragChanged(bool x); + + RideFile::seriestype series; + + QChart *chart; + QBarSet *barset; + QBarSeries *barseries; + QStringList categories; + QBarCategoryAxis *barcategoryaxis; +}; + +class RouteOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + public: + + RouteOverviewItem(ChartSpace *parent, QString name); + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *item); + + Routeline *routeline; +}; + +class IntervalOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + public: + + IntervalOverviewItem(ChartSpace *parent, QString name, QString xs, QString ys, QString zs); + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *item); + + QString xsymbol, ysymbol, zsymbol; + BubbleViz *bubble; +}; + +// +// below are theviz widgets used by the overview items +// + +// for now the basics are x and y and a radius z, color fill +class BPointF { +public: + + BPointF() : x(0), y(0), z(0), fill(GColor(Qt::gray)) {} + + double score(BPointF &other); + + double x,y,z; + QColor fill; + QString label; +}; + + +// bubble chart, very very basic just a visualisation +class BubbleViz : public QObject, public QGraphicsItem +{ + // need to be a qobject for metaproperties + Q_OBJECT + Q_INTERFACES(QGraphicsItem) + + // want a meta property for property animation + Q_PROPERTY(int transition READ getTransition WRITE setTransition) + + public: + BubbleViz(IntervalOverviewItem *parent, QString name=""); // 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; } + + // transition animation 0-255 + int getTransition() const {return transition;} + void setTransition(int x) { if (transition !=x) {transition=x; update();}} + + // null members for now just get hooked up + void setPoints(QListpoints); + + void setRange(double minx, double maxx, double miny, double maxy) { + oldminx = this->minx; + oldminy = this->miny; + oldmaxx = this->maxx; + oldmaxy = this->maxy; + this->minx=minx; + this->maxx=maxx; + this->miny=miny; + this->maxy=maxy; + } + void setAxisNames(QString xlabel, QString ylabel) { this->xlabel=xlabel; this->ylabel=ylabel; update(); } + + // needed as pure virtual in QGraphicsItem + QVariant itemChange(GraphicsItemChange change, const QVariant &value); + void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*); + QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(), + parent->geometry().y() + geom.y(), + geom.width(), geom.height()); + } + + // watch hovering + bool sceneEvent(QEvent *event); + + private: + IntervalOverviewItem *parent; + QRectF geom; + QString name; + + // where is the cursor? + bool hover; + QPointF plotpos; + + // for animated transition + QList oldpoints; // for animation + int transition; + double oldmean; + double oldminx,oldmaxx,oldminy,oldmaxy; + QPropertyAnimation *animator; + + // chart settings + QList points; + double minx,maxx,miny,maxy; + QString xlabel, ylabel; + double mean, max; +}; + +// RPE rating viz and widget to set value +class RPErating : public QGraphicsItem +{ + + public: + RPErating(RPEOverviewItem *parent, QString name=""); // 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 setValue(QString); + + // needed as pure virtual in QGraphicsItem + QVariant itemChange(GraphicsItemChange change, const QVariant &value); + void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*); + QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(), + parent->geometry().y() + geom.y(), + geom.width(), geom.height()); + } + + // for interaction + bool sceneEvent(QEvent *event); + void cancelEdit(); + void applyEdit(); + + private: + RPEOverviewItem *parent; + QString name; + QString description; + QRectF geom; + QString value, oldvalue; + QColor color; + + // interaction + bool hover; + +}; + +// tufte style sparkline to plot metric history +class Sparkline : public QGraphicsItem +{ + public: + Sparkline(QGraphicsWidget *parent, int count,QString name=""); // 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 setPoints(QList); + void setRange(double min, double max); // upper lower + + // needed as pure virtual in QGraphicsItem + QVariant itemChange(GraphicsItemChange change, const QVariant &value); + void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*); + QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(), + parent->geometry().y() + geom.y(), + geom.width(), geom.height()); + } + + private: + QGraphicsWidget *parent; + QRectF geom; + QString name; + double min, max; + QList points; +}; + +// visualisation of a GPS route as a shape +class Routeline : public QObject, public QGraphicsItem +{ + Q_OBJECT + Q_INTERFACES(QGraphicsItem) + + Q_PROPERTY(int transition READ getTransition WRITE setTransition) + + public: + Routeline(QGraphicsWidget *parent, QString name=""); // create and say how many days + + // transition animation 0-255 + int getTransition() const {return transition;} + void setTransition(int x) { if (transition !=x) {transition=x; update();}} + + // we monkey around with this *A LOT* + void setGeometry(double x, double y, double width, double height); + QRectF geometry() { return geom; } + + void setData(RideItem *item); + + // needed as pure virtual in QGraphicsItem + QVariant itemChange(GraphicsItemChange change, const QVariant &value); + void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*); + QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(), + parent->geometry().y() + geom.y(), + geom.width(), geom.height()); + } + + private: + QGraphicsWidget *parent; + QRectF geom; + QString name; + QPainterPath path, oldpath; + double width, height; // size of painterpath, so we scale to fit on paint + + // animating + int transition; + QPropertyAnimation *animator; + double owidth, oheight; // size of painterpath, so we scale to fit on paint +}; + +#endif // _GC_OverviewItem_h diff --git a/src/Gui/ChartSpace.cpp b/src/Gui/ChartSpace.cpp new file mode 100644 index 000000000..5dd9f0fc5 --- /dev/null +++ b/src/Gui/ChartSpace.cpp @@ -0,0 +1,946 @@ +/* + * Copyright (c) 2020 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 "ChartSpace.h" + +#include "TabView.h" +#include "Athlete.h" +#include "RideCache.h" + +#include +#include +#include + +#include +#include +#include +#include + +static QIcon grayConfig, whiteConfig, accentConfig; + +ChartSpace::ChartSpace(Context *context) : + state(NONE), context(context), group(NULL), _viewY(0), + yresizecursor(false), xresizecursor(false), block(false), scrolling(false), + setscrollbar(false), lasty(-1) +{ + setContentsMargins(0,0,0,0); + + // no longer a gc chart ... + //setProperty("color", GColor(COVERVIEWBACKGROUND)); XXX?? + //setProperty("nomenu", true); + //setShowTitle(false); + //setControls(NULL); + + QHBoxLayout *main = new QHBoxLayout; + + // add a view and scene and centre + scene = new QGraphicsScene(this); + view = new QGraphicsView(this); + view->viewport()->setAttribute(Qt::WA_AcceptTouchEvents, false); // stops it stealing focus on mouseover + scrollbar = new QScrollBar(Qt::Vertical, this); + + // how to move etc + //view->setDragMode(QGraphicsView::ScrollHandDrag); + view->setRenderHint(QPainter::Antialiasing, true); + view->setFrameStyle(QFrame::NoFrame); + view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + view->setScene(scene); + + // layout + main->addWidget(view); + main->addWidget(scrollbar); + + // all the widgets + setLayout(main); + + // by default these are the column sizes (user can adjust) + columns << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200; + + // for changing the view + group = new QParallelAnimationGroup(this); + viewchange = new QPropertyAnimation(this, "viewRect"); + viewchange->setEasingCurve(QEasingCurve(QEasingCurve::OutQuint)); + + // for scrolling the view + scroller = new QPropertyAnimation(this, "viewY"); + scroller->setEasingCurve(QEasingCurve(QEasingCurve::Linear)); + + // watch the view for mouse events + view->setMouseTracking(true); + scene->installEventFilter(this); + + // once all widgets created we can connect the signals + connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32))); + connect(scroller, SIGNAL(finished()), this, SLOT(scrollFinished())); + connect(scrollbar, SIGNAL(valueChanged(int)), this, SLOT(scrollbarMoved(int))); + + // set the widgets etc + configChanged(CONFIG_APPEARANCE); + + // we're ready to plot, but not configured + configured=false; + stale=true; + current=NULL; +} + +// add the item +void +ChartSpace::addItem(int order, int column, int deep, ChartSpaceItem *item) +{ + item->order= order; + item->column = column; + item->deep = deep; + items.append(item); +} + +// when a ride is selected we need to notify all the ChartSpaceItems +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) { + stale=true; + return; + } + + // don't replot .. we already did this one + if (current == 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:"<state != ChartSpace::NONE) return false; + + // repaint when mouse enters and leaves + if (event->type() == QEvent::GraphicsSceneHoverLeave || + event->type() == QEvent::GraphicsSceneHoverEnter) { + + // force repaint + update(); + scene()->update(); + + // repaint when in the corner + } else if (event->type() == QEvent::GraphicsSceneHoverMove && inCorner() != incorner) { + + incorner = inCorner(); + update(); + scene()->update(); + } + return false; +} + +bool +ChartSpaceItem::inCorner() +{ + QPoint vpos = parent->view->mapFromGlobal(QCursor::pos()); + QPointF spos = parent->view->mapToScene(vpos); + + if (geometry().contains(spos.x(), spos.y())) { + if (spos.y() - geometry().top() < (ROWHEIGHT+40) && + geometry().width() - (spos.x() - geometry().x()) < (ROWHEIGHT+40)) + return true; + } + return false; + +} + +bool +ChartSpaceItem::underMouse() +{ + QPoint vpos = parent->view->mapFromGlobal(QCursor::pos()); + QPointF spos = parent->view->mapToScene(vpos); + + if (geometry().contains(spos.x(), spos.y())) return true; + return false; +} + +// ChartSpaceItems need to show they are in config mode +void +ChartSpaceItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt, QWidget *widget) { + + if (drag) painter->setBrush(QBrush(GColor(CPLOTMARKER))); + else painter->setBrush(GColor(CCARDBACKGROUND)); + + QPainterPath path; + path.addRoundedRect(QRectF(0,0,geometry().width(),geometry().height()), ROWHEIGHT/5, ROWHEIGHT/5); + painter->setPen(Qt::NoPen); + //painter->fillPath(path, brush.color()); + painter->drawPath(path); + painter->setPen(GColor(CPLOTGRID)); + //XXXpainter->drawLine(QLineF(0,ROWHEIGHT*2,geometry().width(),ROWHEIGHT*2)); + //painter->fillRect(QRectF(0,0,geometry().width()+1,geometry().height()+1), brush); + //titlefont.setWeight(QFont::Bold); + if (GCColor::luminance(GColor(CCARDBACKGROUND)) < 127) painter->setPen(QColor(200,200,200)); + else painter->setPen(QColor(70,70,70)); + + painter->setFont(parent->titlefont); + painter->drawText(QPointF(ROWHEIGHT /2.0f, QFontMetrics(parent->titlefont, parent->device()).height()), name); + + // only paint contents if not dragging + if (drag) return; + + // not dragging so we can get to work painting the rest + if (parent->state != ChartSpace::DRAG && underMouse()) { + + if (inCorner()) { + + // if hovering over the button show a background to indicate + // that pressing a button is good + QPainterPath path; + path.addRoundedRect(QRectF(geometry().width()-40-ROWHEIGHT,0, + ROWHEIGHT+40, ROWHEIGHT+40), ROWHEIGHT/5, ROWHEIGHT/5); + painter->setPen(Qt::NoPen); + QColor darkgray(GColor(CCARDBACKGROUND).lighter(200)); + painter->setBrush(darkgray); + painter->drawPath(path); + painter->fillRect(QRectF(geometry().width()-40-ROWHEIGHT, 0, ROWHEIGHT+40-(ROWHEIGHT/5), ROWHEIGHT+40), QBrush(darkgray)); + painter->fillRect(QRectF(geometry().width()-40-ROWHEIGHT, ROWHEIGHT/5, ROWHEIGHT+40, ROWHEIGHT+40-(ROWHEIGHT/5)), QBrush(darkgray)); + + // draw the config button and make it more obvious + // when hovering over the card + painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, accentConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1))); + + } else { + + // hover on card - make it more obvious there is a config button + painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, whiteConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1))); + } + + } else painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, grayConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1))); + + itemPaint(painter, opt, widget); +} + +static bool ChartSpaceItemSort(const ChartSpaceItem* left, const ChartSpaceItem* right) +{ + return (left->column < right->column ? true : (left->column == right->column && left->order < right->order ? true : false)); +} + +void +ChartSpace::updateGeometry() +{ + + bool animated=false; + + // prevent a memory leak + group->stop(); + delete group; + group = new QParallelAnimationGroup(this); + + // order the items to their positions + qSort(items.begin(), items.end(), ChartSpaceItemSort); + + int y=SPACING; + int maxy = y; + int column=-1; + + int x=SPACING; + + // just set their geometry for now, no interaction + for(int i=0; i