From d461551c41fc7626f51bd59a281a0ce4c83de088 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Sat, 28 Dec 2013 21:24:52 +0000 Subject: [PATCH] Interval Compare Pane Now allows you to add intervals and select them and change colors they will use. It will maintain the data as a CompareInterval which also includes a RideFile representation of the interval. This is stored in Context in a QList as compareIntervals and can therefore be re-used across all charts. We now need to think about putting some widgets onto the Compare 'bar' for turning compare mode on and off and deleting / reordering / clearing items. --- src/CompareInterval.cpp | 28 ++++ src/CompareInterval.h | 45 +++++ src/ComparePane.cpp | 347 ++++++++++++++++++++++++++++++++++++++- src/ComparePane.h | 17 +- src/Context.h | 11 ++ src/IntervalTreeView.cpp | 8 +- src/Views.cpp | 4 +- src/src.pro | 2 + 8 files changed, 446 insertions(+), 16 deletions(-) create mode 100644 src/CompareInterval.cpp create mode 100644 src/CompareInterval.h diff --git a/src/CompareInterval.cpp b/src/CompareInterval.cpp new file mode 100644 index 000000000..e9fc25e25 --- /dev/null +++ b/src/CompareInterval.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014 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 "CompareInterval.h" + +#include "Context.h" +#include "RideFile.h" +#include + +CompareInterval::CompareInterval(Context *context, QString name, RideFile *data, QColor color, Context *sourceContext, bool checked) : + context(context), name(name), data(data), color(color), sourceContext(sourceContext), checked(checked) +{ +} diff --git a/src/CompareInterval.h b/src/CompareInterval.h new file mode 100644 index 000000000..24095db06 --- /dev/null +++ b/src/CompareInterval.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2014 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_CompareInterval_h +#define _GC_CompareInterval_h + +class Context; +class RideFile; + +#include +#include + +class CompareInterval +{ + public: + CompareInterval(Context *context, QString name, RideFile *data, QColor color, Context *sourceContext, bool checked); + CompareInterval() : context(NULL), data(NULL), sourceContext(NULL), checked(false) {} + + Context *context; + QString name; + RideFile *data; + QColor color; + Context *sourceContext; + bool checked; + + bool isChecked() const { return checked; } + void setChecked(bool x) { checked=x; } +}; + +#endif diff --git a/src/ComparePane.cpp b/src/ComparePane.cpp index 634b22bf2..039af8b3b 100644 --- a/src/ComparePane.cpp +++ b/src/ComparePane.cpp @@ -17,12 +17,41 @@ */ #include "ComparePane.h" -#include "Context.h" -#include "Athlete.h" +#include "Settings.h" +#include "RideFile.h" +#include "RideMetric.h" +#include "SummaryMetrics.h" +#include "ColorButton.h" +#include "TimeUtils.h" +#include "Units.h" +#include "Zones.h" -ComparePane::ComparePane(QWidget *parent, CompareMode mode) : mode_(mode), QWidget(parent) +// +// A selection of distinct colours, user can adjust also +// +static QList standardColors; +static bool initStandardColors() +{ + standardColors << QColor(Qt::red); + standardColors << QColor(Qt::green); + standardColors << QColor(Qt::blue); + standardColors << QColor(Qt::magenta); + standardColors << QColor(Qt::cyan); + standardColors << QColor(Qt::yellow); + standardColors << QColor(Qt::gray); + standardColors << QColor(Qt::darkCyan); + standardColors << QColor(Qt::darkRed); + standardColors << QColor(Qt::darkGreen); + standardColors << QColor(Qt::darkBlue); + standardColors << QColor(Qt::darkMagenta); +} +static bool init = initStandardColors(); + +ComparePane::ComparePane(Context *context, QWidget *parent, CompareMode mode) : context(context), mode_(mode), QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0,0,0,0); + layout->setSpacing(0); setAcceptDrops(true); setAutoFillBackground(true); @@ -30,8 +59,219 @@ ComparePane::ComparePane(QWidget *parent, CompareMode mode) : mode_(mode), QWidg pal.setBrush(QPalette::Active, QPalette::Window, Qt::white); pal.setBrush(QPalette::Inactive, QPalette::Window, Qt::white); setPalette(pal); + + scrollArea = new QScrollArea(this); + scrollArea->setAutoFillBackground(false); + scrollArea->setWidgetResizable(true); + scrollArea->setFrameStyle(QFrame::NoFrame); + scrollArea->setContentsMargins(0,0,0,0); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + layout->addWidget(scrollArea); + + table = new QTableWidget(this); +#ifdef Q_OS_MAC + table->setAttribute(Qt::WA_MacShowFocusRect, 0); +#endif + table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setWidget(table); + + configChanged(); // set up ready to go... + + connect(context, SIGNAL(configChanged()), this, SLOT(configChanged())); } +void +ComparePane::configChanged() +{ + // refresh table... + refreshTable(); +} + +void +ComparePane::refreshTable() +{ + if (mode_ == interval) { // INTERVALS + + // STEP ONE : SET THE TABLE HEADINGS + + // clear current contents + table->clear(); + table->setRowCount(0); + + // metric summary + QStringList always; + always << "workout_time" << "total_distance"; + QString s = appsettings->value(this, GC_SETTINGS_SUMMARY_METRICS, GC_SETTINGS_SUMMARY_METRICS_DEFAULT).toString(); + if (s == "") s = GC_SETTINGS_SUMMARY_METRICS_DEFAULT; + QStringList metricColumns = always + s.split(","); // always showm metrics plus user defined summary metrics + + // called after config is updated typically + QStringList list; + list << "" // checkbox + << "" // color + << "Athlete" + << "Date" + << "Time"; + + QStringList worklist; // metrics to compute + RideMetricFactory &factory = RideMetricFactory::instance(); + + foreach(QString metric, metricColumns) { + + // get the metric name + const RideMetric *m = factory.rideMetric(metric); + if (m) { + worklist << metric; + QString units; + if (m->units(context->athlete->useMetricUnits) != "seconds") units = m->units(context->athlete->useMetricUnits); + if (units != "") list << QString("%1 (%2)").arg(m->name()).arg(units); + else list << QString("%1").arg(m->name()); + } + } + + list << "Interval"; + + table->setColumnCount(list.count()); + table->setHorizontalHeaderLabels(list); + table->setSortingEnabled(true); + table->verticalHeader()->hide(); + table->setShowGrid(false); + table->setSelectionMode(QAbstractItemView::SingleSelection); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->horizontalHeader()->setStretchLastSection(true); + + // now set what the user can do + table->resizeColumnsToContents(); + +#if QT_VERSION > 0x050000 // fix the first two if we can + for (int i=0; ihorizontalHeader()->setSectionResizeMode(i, QHeaderView::Fixed); + } else { + table->horizontalHeader()->setSectionResizeMode(i, QHeaderView::Interactive); + } + } +#else + table->horizontalHeader()->setResizeMode(QHeaderView::Interactive); +#endif + + // STEP TWO : CLEAR AND RE-ADD TO REFLECT CHANGES + + table->setRowCount(context->compareIntervals.count()); + int counter = 0; + foreach(CompareInterval x, context->compareIntervals) { + + // compute the metrics for this ride + SummaryMetrics metrics; + QHash computed = RideMetric::computeMetrics(context, x.data, context->athlete->zones(), context->athlete->hrZones(), worklist); + for(int i = 0; i < worklist.count(); ++i) { + if (worklist[i] != "") { + RideMetricPtr m = computed.value(worklist[i]); + if (m) metrics.setForSymbol(worklist[i], m->value(true)); + else metrics.setForSymbol(worklist[i], 0.00); + } + } + + // First few cols always the same + // check - color - athlete - date - time + // now create a row on the compare pane + + // Checkbox + QCheckBox *check = new QCheckBox(this); + check->setChecked(x.checked); + table->setCellWidget(counter, 0, check); + connect(check, SIGNAL(stateChanged(int)), this, SLOT(intervalButtonsChanged())); + + // Color Button + ColorButton *colorButton = new ColorButton(this, "Color", x.color); + table->setCellWidget(counter, 1, colorButton); + connect(colorButton, SIGNAL(colorChosen(QColor)), this, SLOT(intervalButtonsChanged())); + + // athlete + QTableWidgetItem *t = new QTableWidgetItem; + t->setText(x.sourceContext->athlete->cyclist); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + table->setItem(counter, 2, t); + + // date + t = new QTableWidgetItem; + t->setText(x.data->startTime().date().toString("dd MMM, yyyy")); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + table->setItem(counter, 3, t); + + // time + t = new QTableWidgetItem; + t->setText(x.data->startTime().time().toString("hh:mm:ss")); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + table->setItem(counter, 4, t); + + // metrics + for(int i = 0; i < worklist.count(); ++i) { + + RideMetricPtr m = computed.value(worklist[i]); + + // get value and convert if needed + double value = metrics.getForSymbol(worklist[i]) + * (context->athlete->useMetricUnits ? 1 : m->conversion()) + + (context->athlete->useMetricUnits ? 0 : m->conversionSum()); + + // use right precision + QString strValue = QString("%1").arg(value, 0, 'f', m->precision()); + + // or maybe its a duration (worry about local lang or translated) + if (m->units(true) == "seconds" || m->units(true) == tr("seconds")) + strValue = time_to_string(value); + + // add to the table + t = new QTableWidgetItem; + t->setText(strValue); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + table->setItem(counter, i + 5, t); + } + + // Interval name + t = new QTableWidgetItem; + t->setText(x.name); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + table->setItem(counter, worklist.count() + 5, t); + + // align center + for (int i=3; i<(worklist.count()+5); i++) + table->item(counter,i)->setTextAlignment(Qt::AlignHCenter); + + table->setRowHeight(counter, 20); + counter++; + } + + + } else { //SEASONS + + //XXX seasons not written yet + } +} + +void +ComparePane::intervalButtonsChanged() +{ + // run through the table and see if anything changed + bool changed = false; + for (int i=0; irowCount(); i++) { + + bool isChecked = static_cast(table->cellWidget(i,0))->isChecked(); + QColor color = static_cast(table->cellWidget(i,1))->getColor(); + + if (context->compareIntervals[i].checked != isChecked || + context->compareIntervals[i].color != color) { + + context->compareIntervals[i].checked = isChecked; + context->compareIntervals[i].color = color; + changed = true; + } + } + if (changed) context->notifyCompareIntervalsChanged(); +} void ComparePane::dragEnterEvent(QDragEnterEvent *event) @@ -51,8 +291,6 @@ ComparePane::dragLeaveEvent(QDragLeaveEvent *event) void ComparePane::dropEvent(QDropEvent *event) { - qDebug()<<"compare pane: dropped:"<mimeData()->formats(); - // set action to copy and accept that so the source data // is left intact and not wiped or removed event->setDropAction(Qt::CopyAction); @@ -72,6 +310,101 @@ ComparePane::dropEvent(QDropEvent *event) stream >> from; // where did this come from? // lets look at the context.. - Context *c = (Context*)(from); - qDebug()<athlete->cyclist<<"compare pane: dropped:"<mimeData()->formats(); + Context *sourceContext = (Context*)(from); + + // NOW LETS UNPACK + if (mode_ == interval) { // INTERVALS + + int count; + + QList newOnes; + + // lets get the basic data + stream >> count; + for (int i=0; i> add.name; // UPDATE COMPARE INTERVAL + + stream >> ridep; + RideFile *ride = (RideFile*)ridep; + + // index into ridefile + stream >> start; + stream >> stop; + stream >> startKM; + stream >> stopKM; + stream >> seq; + + // construct a ridefile for the interval + + //XXX RideFile *data; + add.data = new RideFile(ride->startTime(), ride->recIntSecs()); + add.data->context = context; + + // manage offsets + bool first = true; + double offset = 0.0f, offsetKM = 0.0f; + + foreach(RideFilePoint *p, ride->dataPoints()) { + + if (p->secs > stop) break; + + if (p->secs >= start) { + + // intervals always start from zero when comparing + if (first) { + first = false; + offset = p->secs; + offsetKM = p->km; + } + + add.data->appendPoint(p->secs - offset, p->cad, p->hr, p->km - offsetKM, p->kph, p->nm, + p->watts, p->alt, p->lon, p->lat, p->headwind, + p->slope, p->temp, p->lrbalance, 0); + + // get derived data calculated + RideFilePoint *l = add.data->dataPoints().last(); + l->np = p->np; + l->xp = p->xp; + l->apower = p->apower; + } + } + + // just use standard colors and cycle round + // we will of course repeat, but the user can + // just edit them using the button + add.color = standardColors.at((i + context->compareIntervals.count()) % standardColors.count()); + + // now add but only if not empty + if (!add.data->dataPoints().empty()) newOnes << add; + + } + // how many we get ? + if (newOnes.count()) { + + context->compareIntervals.append(newOnes); + + // refresh the table to reflect the new list + refreshTable(); + + // let all the charts know + context->notifyCompareIntervalsChanged(); + } + + } else { // SEASONS + + //XXX not written yet! + + } } diff --git a/src/ComparePane.h b/src/ComparePane.h index dd5901374..f2b7d1b72 100644 --- a/src/ComparePane.h +++ b/src/ComparePane.h @@ -22,8 +22,13 @@ #include #include #include +#include +#include #include "GcSideBarItem.h" +#include "Context.h" +#include "Athlete.h" +#include "CompareInterval.h" class ComparePane : public QWidget { @@ -33,7 +38,7 @@ class ComparePane : public QWidget enum mode { season, interval }; typedef enum mode CompareMode; - ComparePane(QWidget *parent, CompareMode mode=interval); + ComparePane(Context *context, QWidget *parent, CompareMode mode=interval); protected: void dragEnterEvent(QDragEnterEvent*); @@ -42,12 +47,18 @@ class ComparePane : public QWidget signals: - private slots: + public slots: + void configChanged(); + void intervalButtonsChanged(); protected: + void refreshTable(); private: - CompareMode mode_; // remember the mode we were created as... + Context *context; + CompareMode mode_; // remember the mode we were created as... + QTableWidget *table; + QScrollArea *scrollArea; }; #endif // _GC_ComparePane_h diff --git a/src/Context.h b/src/Context.h index e8eb82b1b..ca6c9ca4e 100644 --- a/src/Context.h +++ b/src/Context.h @@ -22,6 +22,7 @@ #include "TimeUtils.h" // for class DateRange #include "RealtimeData.h" // for class RealtimeData #include "SpecialFields.h" // for class RealtimeData +#include "CompareInterval.h" // what intervals are being compared? class RideFile; class RideItem; @@ -69,6 +70,10 @@ class Context : public QObject QStringList filters; // searchBox filters QStringList homeFilters; // homewindow sidebar filters + // comparing things + bool isCompareIntervals; + QList compareIntervals; + // ********************************************* // APPLICATION EVENTS // ********************************************* @@ -109,6 +114,9 @@ class Context : public QObject void notifyRideClean() { rideClean(ride); } void notifyRideDirty() { rideDirty(ride); } + void notifyCompareIntervals(bool state) { isCompareIntervals = state; emit compareIntervalsStateChanged(state); } + void notifyCompareIntervalsChanged() { emit compareIntervalsChanged(); } + signals: // global filter changed @@ -141,5 +149,8 @@ class Context : public QObject void pause(); void stop(); + // comparing things + void compareIntervalsStateChanged(bool); + void compareIntervalsChanged(); }; #endif // _GC_Context_h diff --git a/src/IntervalTreeView.cpp b/src/IntervalTreeView.cpp index 69e3ec791..b54edb460 100644 --- a/src/IntervalTreeView.cpp +++ b/src/IntervalTreeView.cpp @@ -66,7 +66,7 @@ IntervalTreeView::mimeData (const QList items) const // pack data stream << (quint64)(context); // where did this come from? - stream << items.count(); + stream << (int)items.count(); foreach (QTreeWidgetItem *p, items) { // convert to one of ours @@ -75,9 +75,9 @@ IntervalTreeView::mimeData (const QList items) const // serialize stream << p->text(0); // name stream << (quint64)(i->ride); - stream << i->start << i->stop; // start and stop in secs - stream << i->startKM << i->stopKM; // start and stop km - stream << i->displaySequence; + stream << (quint64)i->start << (quint64)i->stop; // start and stop in secs + stream << (quint64)i->startKM << (quint64)i->stopKM; // start and stop km + stream << (quint64)i->displaySequence; } diff --git a/src/Views.cpp b/src/Views.cpp index 47a75ce9d..71280823a 100644 --- a/src/Views.cpp +++ b/src/Views.cpp @@ -36,7 +36,7 @@ AnalysisView::AnalysisView(Context *context, QStackedWidget *controls) : TabView setSidebar(analSidebar); setPage(a); setBlank(b); - setBottom(new ComparePane(this, ComparePane::interval)); + setBottom(new ComparePane(context, this, ComparePane::interval)); } RideNavigator *AnalysisView::rideNavigator() @@ -124,7 +124,7 @@ HomeView::HomeView(Context *context, QStackedWidget *controls) : TabView(context setSidebar(s); setPage(h); setBlank(b); - setBottom(new ComparePane(this, ComparePane::season)); + setBottom(new ComparePane(context, this, ComparePane::season)); connect(s, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange))); connect(this, SIGNAL(onSelectionChanged()), this, SLOT(justSelected())); diff --git a/src/src.pro b/src/src.pro index bd75807c8..7b33efbd6 100644 --- a/src/src.pro +++ b/src/src.pro @@ -277,6 +277,7 @@ HEADERS += \ Colors.h \ ColorButton.h \ CommPort.h \ + CompareInterval.h \ ComparePane.h \ Computrainer.h \ Computrainer3dpFile.h \ @@ -467,6 +468,7 @@ SOURCES += \ Colors.cpp \ ColorButton.cpp \ CommPort.cpp \ + CompareInterval.cpp \ ComparePane.cpp \ Computrainer.cpp \ Computrainer3dpFile.cpp \