From c4f82e19b6ca84b6bcfdea1587a2c0e9de0ae3e4 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Mon, 17 May 2010 13:57:04 +0100 Subject: [PATCH] Better Zones Configuration Page The zone ranges configuration page caused a SEGV when deleting the last zone. On inspection the zone configuration needed to be revised since the UI was confusing and didn't allow fine grained user editing (relying upon manual editing of the power.zones file). The UI has been redesigned and fine grained editing of ranges, zones and default zones is now supported. The Zones class has been slightly modified to support the new UI and existing members are better commented. In addition, the read/write functions have been updated to always include the DEFAULTS section and to set defaults according to manual zone setups when it is not present (legacy support). There are now 10 TimeInZone metrics to match the maximum of 10 zones the user can define. Fixes #78. Fixes #34. --- src/ConfigDialog.cpp | 103 +------ src/ConfigDialog.h | 4 - src/DBAccess.cpp | 2 +- src/LTMPlot.cpp | 1 + src/MainWindow.cpp | 13 +- src/Pages.cpp | 696 +++++++++++++++++++++++++++++++++---------- src/Pages.h | 123 +++++--- src/TimeInZone.cpp | 39 +++ src/Zones.cpp | 373 +++++++++++------------ src/Zones.h | 193 ++++++++---- 10 files changed, 991 insertions(+), 556 deletions(-) diff --git a/src/ConfigDialog.cpp b/src/ConfigDialog.cpp index 08ce106ed..2650941fa 100644 --- a/src/ConfigDialog.cpp +++ b/src/ConfigDialog.cpp @@ -30,7 +30,7 @@ ConfigDialog::ConfigDialog(QDir _home, Zones *_zones, MainWindow *mainWindow) : home = _home; - cyclistPage = new CyclistPage(zones); + cyclistPage = new CyclistPage(mainWindow); contentsWidget = new QListWidget; contentsWidget->setViewMode(QListView::IconMode); @@ -43,7 +43,6 @@ ConfigDialog::ConfigDialog(QDir _home, Zones *_zones, MainWindow *mainWindow) : contentsWidget->setUniformItemSizes(true); configPage = new ConfigurationPage(mainWindow); - devicePage = new DevicePage(this); pagesWidget = new QStackedWidget; @@ -60,10 +59,6 @@ ConfigDialog::ConfigDialog(QDir _home, Zones *_zones, MainWindow *mainWindow) : // connect(closeButton, SIGNAL(clicked()), this, SLOT(reject())); // connect(saveButton, SIGNAL(clicked()), this, SLOT(accept())); connect(closeButton, SIGNAL(clicked()), this, SLOT(accept())); - connect(cyclistPage->btnBack, SIGNAL(clicked()), this, SLOT(back_Clicked())); - connect(cyclistPage->btnForward, SIGNAL(clicked()), this, SLOT(forward_Clicked())); - connect(cyclistPage->btnDelete, SIGNAL(clicked()), this, SLOT(delete_Clicked())); - connect(cyclistPage->calendar, SIGNAL(selectionChanged()), this, SLOT(calendarDateChanged())); // connect the pieces... connect(devicePage->typeSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(changedType(int))); @@ -127,10 +122,6 @@ void ConfigDialog::createIcons() } -void ConfigDialog::createNewRange() -{ -} - void ConfigDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous) { if (!current) @@ -177,31 +168,8 @@ void ConfigDialog::save_Clicked() settings->setValue(GC_SB_NAME, settings->value(GC_SB_NAME,tr("Stress Balance"))); settings->setValue(GC_SB_ACRONYM, settings->value(GC_SB_ACRONYM,tr("SB"))); - - // if the CP text entry reads invalid, there's nothing we can do - int cp = cyclistPage->getCP(); - if (cp == 0) { - QMessageBox::warning(this, tr("Invalid CP"), "Please enter valid CP and try again."); - cyclistPage->setCPFocus(); - return; - } - - // if for some reason we have no zones yet, then create them - int range = cyclistPage->getCurrentRange(); - - // if this is new mode, or if no zone ranges are yet defined, set up the new range - if ((range == -1) || (cyclistPage->isNewMode())) - cyclistPage->setCurrentRange(range = zones->insertRangeAtDate(cyclistPage->selectedDate(), cp)); - else - zones->setCP(range, cyclistPage->getText().toInt()); - - zones->setZonesFromCP(range); - - // update the "new zone" checkbox to visible and unchecked - cyclistPage->checkboxNew->setChecked(Qt::Unchecked); - cyclistPage->checkboxNew->setEnabled(true); - - zones->write(home); + // Save Cyclist page stuff + cyclistPage->saveClicked(); // save interval metrics and ride data pages configPage->saveClicked(); @@ -213,70 +181,9 @@ void ConfigDialog::save_Clicked() // Tell MainWindow we changed config, so it can emit the signal // configChanged() to all its children mainWindow->notifyConfigChanged(); -} -void ConfigDialog::moveCalendarToCurrentRange() { - int range = cyclistPage->getCurrentRange(); - - if (range < 0) - return; - - QDate date; - - // put the cursor at the beginning of the selected range if it's not the first - if (range > 0) - date = zones->getStartDate(cyclistPage->getCurrentRange()); - // unless the range is the first range, in which case it goes at the end of that range - // use JulianDay to subtract one day from the end date, which is actually the first - // day of the following range - else - date = QDate::fromJulianDay(zones->getEndDate(cyclistPage->getCurrentRange()).toJulianDay() - 1); - - cyclistPage->setSelectedDate(date); -} - -void ConfigDialog::back_Clicked() -{ - QDate date; - cyclistPage->setCurrentRange(cyclistPage->getCurrentRange() - 1); - moveCalendarToCurrentRange(); -} - -void ConfigDialog::forward_Clicked() -{ - QDate date; - cyclistPage->setCurrentRange(cyclistPage->getCurrentRange() + 1); - moveCalendarToCurrentRange(); -} - -void ConfigDialog::delete_Clicked() { - int range = cyclistPage->getCurrentRange(); - int num_ranges = zones->getRangeSize(); - assert (num_ranges > 1); - QMessageBox msgBox; - msgBox.setText( - tr("Are you sure you want to delete the zone range\n" - "from %1 to %2?\n" - "(%3 range will extend to this date range):") . - arg(zones->getStartDateString(cyclistPage->getCurrentRange())) . - arg(zones->getEndDateString(cyclistPage->getCurrentRange())) . - arg((range > 0) ? "previous" : "next") - ); - QPushButton *deleteButton = msgBox.addButton(tr("Delete"),QMessageBox::YesRole); - msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setDefaultButton(QMessageBox::Cancel); - msgBox.setIcon(QMessageBox::Critical); - msgBox.exec(); - if(msgBox.clickedButton() == deleteButton) - cyclistPage->setCurrentRange(zones->deleteRange(range)); - - zones->write(home); -} - -void ConfigDialog::calendarDateChanged() { - int range = zones->whichRange(cyclistPage->selectedDate()); - assert(range >= 0); - cyclistPage->setCurrentRange(range); + // close + accept(); } // diff --git a/src/ConfigDialog.h b/src/ConfigDialog.h index 1eb3892cd..a6e912ee1 100644 --- a/src/ConfigDialog.h +++ b/src/ConfigDialog.h @@ -21,10 +21,6 @@ class ConfigDialog : public QDialog public slots: void changePage(QListWidgetItem *current, QListWidgetItem *previous); void save_Clicked(); - void back_Clicked(); - void forward_Clicked(); - void delete_Clicked(); - void calendarDateChanged(); // device config slots void changedType(int); diff --git a/src/DBAccess.cpp b/src/DBAccess.cpp index ba6ea64f6..554cd5980 100644 --- a/src/DBAccess.cpp +++ b/src/DBAccess.cpp @@ -40,7 +40,7 @@ // DB Schema Version - YOU MUST UPDATE THIS IF THE SCHEMA VERSION CHANGES!!! // Schema version will change if a) the default metadata.xml is updated // or b) new metrics are added / old changed -static int DBSchemaVersion = 13; +static int DBSchemaVersion = 14; DBAccess::DBAccess(MainWindow* main, QDir home) : main(main), home(home) { diff --git a/src/LTMPlot.cpp b/src/LTMPlot.cpp index d9a8372e4..b5c1175a0 100644 --- a/src/LTMPlot.cpp +++ b/src/LTMPlot.cpp @@ -614,6 +614,7 @@ LTMPlot::pointHover(QwtPlotCurve *curve, int index) if (units == "seconds") { units = "hours"; // we translate from seconds to hours value = ceil(curve->y(index)*10.0)/10.0; + precision = 1; // new more precision since converting to hours } // output the tooltip diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index df62235fc..7a9b56852 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -122,7 +122,6 @@ MainWindow::MainWindow(const QDir &home) : if (!zones_->read(zonesFile)) { QMessageBox::critical(this, tr("Zones File Error"), zones_->errorString()); - zones_->clear(); } else if (! zones_->warningString().isEmpty()) QMessageBox::warning(this, tr("Reading Zones File"), zones_->warningString()); @@ -1335,6 +1334,18 @@ void MainWindow::dateChanged(const QDate &date) void MainWindow::notifyConfigChanged() { + // re-read Zones in case it changed + QFile zonesFile(home.absolutePath() + "/power.zones"); + if (zonesFile.exists()) { + if (!zones_->read(zonesFile)) { + QMessageBox::critical(this, tr("Zones File Error"), + zones_->errorString()); + } + else if (! zones_->warningString().isEmpty()) + QMessageBox::warning(this, tr("Reading Zones File"), zones_->warningString()); + } + + // now tell everyone else configChanged(); } diff --git a/src/Pages.cpp b/src/Pages.cpp index 5f3bf4cc3..424899cc6 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -185,8 +185,8 @@ ConfigurationPage::saveClicked() metadataPage->saveClicked(); } -CyclistPage::CyclistPage(const Zones *_zones): - zones(_zones) +CyclistPage::CyclistPage(MainWindow *main) : + main(main) { boost::shared_ptr settings = GetApplicationSettings(); @@ -198,38 +198,8 @@ CyclistPage::CyclistPage(const Zones *_zones): QVBoxLayout *cpLayout = new QVBoxLayout(cpTab); QVBoxLayout *pmLayout = new QVBoxLayout(pmTab); - lblThreshold = new QLabel(tr("Critical Power:")); - txtThreshold = new QLineEdit(); - - // the validator will prevent numbers above the upper limit - // from being entered, but will not prevent non-negative numbers - // below the lower limit (since it is still plausible a valid - // entry will result) - txtThresholdValidator = new QIntValidator(20,999,this); - txtThreshold->setValidator(txtThresholdValidator); - - btnBack = new QPushButton(this); - btnBack->setText(tr("Back")); - btnForward = new QPushButton(this); - btnForward->setText(tr("Forward")); - btnDelete = new QPushButton(this); - btnDelete->setText(tr("Delete Range")); - checkboxNew = new QCheckBox(this); - checkboxNew->setText(tr("New Range from Date")); - btnForward->setEnabled(false); - txtStartDate = new QLabel("BEGIN"); - txtEndDate = new QLabel("END"); - lblStartDate = new QLabel("Start: "); - lblStartDate->setAlignment(Qt::AlignRight); - lblEndDate = new QLabel("End: "); - lblEndDate->setAlignment(Qt::AlignRight); - - - calendar = new QCalendarWidget(this); - - lblCurRange = new QLabel(this); - lblCurRange->setFrameStyle(QFrame::Panel | QFrame::Sunken); - lblCurRange->setText(QString("Current Zone Range: %1").arg(currentRange + 1)); + zonePage = new ZonePage(main); + cpLayout->addWidget(zonePage); perfManLabel = new QLabel(tr("Performance Manager")); showSBToday = new QCheckBox(tr("Show Stress Balance Today"), this); @@ -256,46 +226,6 @@ CyclistPage::CyclistPage(const Zones *_zones): perfManLTSavg = new QLineEdit(perfManLTSVal.toString(),this); perfManLTSavg->setValidator(perfManLTSavgValidator); - - QDate today = QDate::currentDate(); - calendar->setSelectedDate(today); - - if (zones->getRangeSize() == 0) - setCurrentRange(); - else - { - setCurrentRange(zones->whichRange(today)); - btnDelete->setEnabled(true); - checkboxNew->setCheckState(Qt::Unchecked); - } - - int cp = currentRange != -1 ? zones->getCP(currentRange) : 0; - if (cp > 0) - setCP(cp); - - //Layout - powerLayout = new QHBoxLayout(); - powerLayout->addWidget(lblThreshold); - powerLayout->addWidget(txtThreshold); - - rangeLayout = new QHBoxLayout(); - rangeLayout->addWidget(lblCurRange); - - dateRangeLayout = new QHBoxLayout(); - dateRangeLayout->addWidget(lblStartDate); - dateRangeLayout->addWidget(txtStartDate); - dateRangeLayout->addWidget(lblEndDate); - dateRangeLayout->addWidget(txtEndDate); - - zoneLayout = new QHBoxLayout(); - zoneLayout->addWidget(btnBack); - zoneLayout->addWidget(btnForward); - zoneLayout->addWidget(btnDelete); - zoneLayout->addWidget(checkboxNew); - - calendarLayout = new QHBoxLayout(); - calendarLayout->addWidget(calendar); - // performance manager perfManLayout = new QVBoxLayout(); // outer perfManStartValLayout = new QHBoxLayout(); @@ -313,17 +243,7 @@ CyclistPage::CyclistPage(const Zones *_zones): perfManLayout->addLayout(perfManLTSavgLayout); perfManLayout->addStretch(); - - - cyclistLayout = new QVBoxLayout; - cyclistLayout->addLayout(powerLayout); - cyclistLayout->addLayout(rangeLayout); - cyclistLayout->addLayout(zoneLayout); - cyclistLayout->addLayout(dateRangeLayout); - cyclistLayout->addLayout(calendarLayout); - cyclistLayout->addStretch(); - - cpLayout->addLayout(cyclistLayout); + //cpLayout->addLayout(cyclistLayout); pmLayout->addLayout(perfManLayout); mainLayout = new QVBoxLayout; @@ -331,6 +251,13 @@ CyclistPage::CyclistPage(const Zones *_zones): setLayout(mainLayout); } +void +CyclistPage::saveClicked() +{ + // save zone config (other stuff is saved by configdialog) + zonePage->saveClicked(); +} + void ConfigurationPage::browseWorkoutDir() { @@ -339,84 +266,6 @@ ConfigurationPage::browseWorkoutDir() workoutDirectory->setText(dir); } -QString CyclistPage::getText() -{ - return txtThreshold->text(); -} - -int CyclistPage::getCP() -{ - int cp = txtThreshold->text().toInt(); - return ( - ( - (cp >= txtThresholdValidator->bottom()) && - (cp <= txtThresholdValidator->top()) - ) ? - cp : - 0 - ); -} - -void CyclistPage::setCP(int cp) -{ - txtThreshold->setText(QString("%1").arg(cp)); -} - -void CyclistPage::setSelectedDate(QDate date) -{ - calendar->setSelectedDate(date); -} - -void CyclistPage::setCurrentRange(int range) -{ - int num_ranges = zones->getRangeSize(); - if ((num_ranges == 0) || (range < 0)) { - btnBack->setEnabled(false); - btnDelete->setEnabled(false); - btnForward->setEnabled(false); - calendar->setEnabled(false); - checkboxNew->setCheckState(Qt::Checked); - checkboxNew->setEnabled(false); - currentRange = -1; - lblCurRange->setText("no Current Zone Range"); - txtEndDate->setText("undefined"); - txtStartDate->setText("undefined"); - return; - } - - assert ((range >= 0) && (range < num_ranges)); - currentRange = range; - - // update the labels - lblCurRange->setText(QString("Current Zone Range: %1").arg(currentRange + 1)); - - // update the visibility of the range select buttons - btnForward->setEnabled(currentRange < num_ranges - 1); - btnBack->setEnabled(currentRange > 0); - - // if we have ranges to set to, then the calendar must be on - calendar->setEnabled(true); - - // update the CP display - setCP(zones->getCP(currentRange)); - - // update date limits - txtStartDate->setText(zones->getStartDateString(currentRange)); - txtEndDate->setText(zones->getEndDateString(currentRange)); -} - - -int CyclistPage::getCurrentRange() -{ - return currentRange; -} - - -bool CyclistPage::isNewMode() -{ - return (checkboxNew->checkState() == Qt::Checked); -} - DevicePage::DevicePage(QWidget *parent) : QWidget(parent) { QTabWidget *tabs = new QTabWidget(this); @@ -1369,3 +1218,524 @@ FieldsPage::getDefinitions(QList &fieldList) fieldList.append(add); } } + +// +// Zone Config page +// +ZonePage::ZonePage(MainWindow *main) : main(main) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + + // get current config by reading it in (leave mainwindow zones alone) + QFile zonesFile(main->home.absolutePath() + "/power.zones"); + if (zonesFile.exists()) { + zones.read(zonesFile); + zonesFile.close(); + } + + // setup maintenance pages using current config + schemePage = new SchemePage(this); + cpPage = new CPPage(this); + + tabs = new QTabWidget(this); + tabs->addTab(cpPage, tr("Critical Power History")); + tabs->addTab(schemePage, tr("Default Zones")); + + layout->addWidget(tabs); +} + +void +ZonePage::saveClicked() +{ + zones.setScheme(schemePage->getScheme()); + zones.write(main->home); +} + +SchemePage::SchemePage(ZonePage* zonePage) : zonePage(zonePage) +{ + QGridLayout *mainLayout = new QGridLayout(this); + + addButton = new QPushButton(tr("Add")); + renameButton = new QPushButton(tr("Rename")); + deleteButton = new QPushButton(tr("Delete")); + + QVBoxLayout *actionButtons = new QVBoxLayout; + actionButtons->addWidget(addButton); + actionButtons->addWidget(renameButton); + actionButtons->addWidget(deleteButton); + actionButtons->addStretch(); + + scheme = new QTreeWidget; + scheme->headerItem()->setText(0, tr("Short")); + scheme->headerItem()->setText(1, tr("Long")); + scheme->headerItem()->setText(2, tr("Percent of CP")); + scheme->setColumnCount(3); + scheme->setSelectionMode(QAbstractItemView::SingleSelection); + scheme->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit + scheme->setUniformRowHeights(true); + scheme->setIndentation(0); + scheme->header()->resizeSection(0,90); + scheme->header()->resizeSection(1,200); + scheme->header()->resizeSection(2,80); + + // setup list + for (int i=0; i< zonePage->zones.getScheme().nzones_default; i++) { + + QTreeWidgetItem *add = new QTreeWidgetItem(scheme->invisibleRootItem()); + add->setFlags(add->flags() | Qt::ItemIsEditable); + + // tab name + add->setText(0, zonePage->zones.getScheme().zone_default_name[i]); + // field name + add->setText(1, zonePage->zones.getScheme().zone_default_desc[i]); + + // low + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(zonePage->zones.getScheme().zone_default[i]); + scheme->setItemWidget(add, 2, loedit); + } + + mainLayout->addWidget(scheme, 0,0); + mainLayout->addLayout(actionButtons, 0,1); + + // button connect + connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); + connect(renameButton, SIGNAL(clicked()), this, SLOT(renameClicked())); + connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); +} + +void +SchemePage::addClicked() +{ + // are we at maximum already? + if (scheme->invisibleRootItem()->childCount() == 10) { + QMessageBox err; + err.setText("Maximum of 10 zones reached."); + err.setIcon(QMessageBox::Warning); + err.exec(); + return; + } + + int index = scheme->invisibleRootItem()->childCount(); + + // new item + QTreeWidgetItem *add = new QTreeWidgetItem; + add->setFlags(add->flags() | Qt::ItemIsEditable); + + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(100); + + scheme->invisibleRootItem()->insertChild(index, add); + scheme->setItemWidget(add, 2, loedit); + + // Short + QString text = "New"; + for (int i=0; scheme->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(0, text); + + // long + text = "New"; + for (int i=0; scheme->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(1, text); +} + +void +SchemePage::renameClicked() +{ + // which one is selected? + if (scheme->currentItem()) scheme->editItem(scheme->currentItem(), 0); +} + +void +SchemePage::deleteClicked() +{ + if (scheme->currentItem()) { + int index = scheme->invisibleRootItem()->indexOfChild(scheme->currentItem()); + delete scheme->invisibleRootItem()->takeChild(index); + } +} + +// just for qSorting +struct schemeitem { + QString name, desc; + int lo; + bool operator<(schemeitem right) const { return lo < right.lo; } +}; + +ZoneScheme +SchemePage::getScheme() +{ + // read the scheme widget and return a scheme object + QList table; + ZoneScheme results; + + // read back the details from the table + for (int i=0; iinvisibleRootItem()->childCount(); i++) { + + schemeitem add; + add.name = scheme->invisibleRootItem()->child(i)->text(0); + add.desc = scheme->invisibleRootItem()->child(i)->text(1); + add.lo = ((QDoubleSpinBox *)(scheme->itemWidget(scheme->invisibleRootItem()->child(i), 2)))->value(); + table.append(add); + } + + // sort the list into ascending order + qSort(table); + + // now update the results + results.nzones_default = 0; + foreach(schemeitem zone, table) { + results.nzones_default++; + results.zone_default.append(zone.lo); + results.zone_default_is_pct.append(true); + results.zone_default_name.append(zone.name); + results.zone_default_desc.append(zone.desc); + } + + return results; +} + + +CPPage::CPPage(ZonePage* zonePage) : zonePage(zonePage) +{ + active = false; + + QGridLayout *mainLayout = new QGridLayout(this); + + addButton = new QPushButton(tr("Add CP")); + deleteButton = new QPushButton(tr("Delete CP")); + defaultButton = new QPushButton(tr("Default")); + defaultButton->hide(); + + addZoneButton = new QPushButton(tr("Add Zone")); + deleteZoneButton = new QPushButton(tr("Delete Zone")); + + QVBoxLayout *actionButtons = new QVBoxLayout; + actionButtons->addWidget(addButton); + actionButtons->addWidget(deleteButton); + actionButtons->addWidget(defaultButton); + actionButtons->addStretch(); + + QVBoxLayout *zoneButtons = new QVBoxLayout; + zoneButtons->addWidget(addZoneButton); + zoneButtons->addWidget(deleteZoneButton); + zoneButtons->addStretch(); + + QHBoxLayout *addLayout = new QHBoxLayout; + QLabel *dateLabel = new QLabel(tr("From Date")); + QLabel *cpLabel = new QLabel(tr("Critical Power")); + dateEdit = new QDateEdit; + dateEdit->setDate(QDate::currentDate()); + + cpEdit = new QDoubleSpinBox; + cpEdit->setMinimum(0); + cpEdit->setMaximum(1000); + cpEdit->setSingleStep(1.0); + cpEdit->setDecimals(0); + + addLayout->addWidget(dateLabel); + addLayout->addWidget(dateEdit); + addLayout->addWidget(cpLabel); + addLayout->addWidget(cpEdit); + addLayout->addStretch(); + + ranges = new QTreeWidget; + ranges->headerItem()->setText(0, tr("From Date")); + ranges->headerItem()->setText(1, tr("Critical Power")); + ranges->setColumnCount(2); + ranges->setSelectionMode(QAbstractItemView::SingleSelection); + //ranges->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit + ranges->setUniformRowHeights(true); + ranges->setIndentation(0); + ranges->header()->resizeSection(0,180); + + // setup list of ranges + for (int i=0; i< zonePage->zones.getRangeSize(); i++) { + + QTreeWidgetItem *add = new QTreeWidgetItem(ranges->invisibleRootItem()); + add->setFlags(add->flags() & ~Qt::ItemIsEditable); + + // Embolden ranges with manually configured zones + QFont font; + font.setWeight(zonePage->zones.getZoneRange(i).zonesSetFromCP ? + QFont::Normal : QFont::Black); + + // date + add->setText(0, zonePage->zones.getStartDate(i).toString("MMM d, yyyy")); + add->setFont(0, font); + + // CP + add->setText(1, QString("%1").arg(zonePage->zones.getCP(i))); + add->setFont(1, font); + } + + zones = new QTreeWidget; + zones->headerItem()->setText(0, tr("Short")); + zones->headerItem()->setText(1, tr("Long")); + zones->headerItem()->setText(2, tr("From Watts")); + zones->setColumnCount(3); + zones->setSelectionMode(QAbstractItemView::SingleSelection); + zones->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit + zones->setUniformRowHeights(true); + zones->setIndentation(0); + zones->header()->resizeSection(0,80); + zones->header()->resizeSection(1,150); + + mainLayout->addLayout(addLayout, 0,0); + mainLayout->addWidget(ranges, 2,0); + mainLayout->addWidget(zones, 4,0); + mainLayout->addLayout(actionButtons, 0,1,0,3); + mainLayout->addLayout(zoneButtons, 4,1); + + // button connect + connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); + connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); + connect(defaultButton, SIGNAL(clicked()), this, SLOT(defaultClicked())); + connect(addZoneButton, SIGNAL(clicked()), this, SLOT(addZoneClicked())); + connect(deleteZoneButton, SIGNAL(clicked()), this, SLOT(deleteZoneClicked())); + connect(ranges, SIGNAL(itemSelectionChanged()), this, SLOT(rangeSelectionChanged())); + connect(zones, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(zonesChanged())); +} + +void +CPPage::addClicked() +{ + // get current scheme + zonePage->zones.setScheme(zonePage->schemePage->getScheme()); + + //int index = ranges->invisibleRootItem()->childCount(); + int index = zonePage->zones.addZoneRange(dateEdit->date(), cpEdit->value()); + + // new item + QTreeWidgetItem *add = new QTreeWidgetItem; + add->setFlags(add->flags() & ~Qt::ItemIsEditable); + ranges->invisibleRootItem()->insertChild(index, add); + + // date + add->setText(0, dateEdit->date().toString("MMM d, yyyy")); + + // CP + add->setText(1, QString("%1").arg(cpEdit->value())); +} + +void +CPPage::deleteClicked() +{ + if (ranges->currentItem()) { + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + delete ranges->invisibleRootItem()->takeChild(index); + zonePage->zones.deleteRange(index); + } +} + +void +CPPage::defaultClicked() +{ + if (ranges->currentItem()) { + + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + ZoneRange current = zonePage->zones.getZoneRange(index); + + // unbold + QFont font; + font.setWeight(QFont::Normal); + ranges->currentItem()->setFont(0, font); + ranges->currentItem()->setFont(1, font); + + // set the range to use defaults on the scheme page + zonePage->zones.setScheme(zonePage->schemePage->getScheme()); + zonePage->zones.setZonesFromCP(index); + + // hide the default button since we are now using defaults + defaultButton->hide(); + + // update the zones display + rangeSelectionChanged(); + } +} + +void +CPPage::rangeSelectionChanged() +{ + active = true; + + // wipe away current contents of zones + foreach (QTreeWidgetItem *item, zones->invisibleRootItem()->takeChildren()) { + delete zones->itemWidget(item, 2); + delete item; + } + + // fill with current details + if (ranges->currentItem()) { + + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + ZoneRange current = zonePage->zones.getZoneRange(index); + + if (current.zonesSetFromCP) { + + // reapply the scheme in case it has been changed + zonePage->zones.setScheme(zonePage->schemePage->getScheme()); + zonePage->zones.setZonesFromCP(index); + current = zonePage->zones.getZoneRange(index); + + defaultButton->hide(); + + } else defaultButton->show(); + + for (int i=0; i< current.zones.count(); i++) { + + QTreeWidgetItem *add = new QTreeWidgetItem(zones->invisibleRootItem()); + add->setFlags(add->flags() | Qt::ItemIsEditable); + + // tab name + add->setText(0, current.zones[i].name); + // field name + add->setText(1, current.zones[i].desc); + + // low + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(current.zones[i].lo); + zones->setItemWidget(add, 2, loedit); + connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); + } + } + + active = false; +} + +void +CPPage::addZoneClicked() +{ + // no range selected + if (!ranges->currentItem()) return; + + // are we at maximum already? + if (zones->invisibleRootItem()->childCount() == 10) { + QMessageBox err; + err.setText("Maximum of 10 zones reached."); + err.setIcon(QMessageBox::Warning); + err.exec(); + return; + } + + active = true; + int index = zones->invisibleRootItem()->childCount(); + + // new item + QTreeWidgetItem *add = new QTreeWidgetItem; + add->setFlags(add->flags() | Qt::ItemIsEditable); + + QDoubleSpinBox *loedit = new QDoubleSpinBox(this); + loedit->setMinimum(0); + loedit->setMaximum(1000); + loedit->setSingleStep(1.0); + loedit->setDecimals(0); + loedit->setValue(100); + + zones->invisibleRootItem()->insertChild(index, add); + zones->setItemWidget(add, 2, loedit); + connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); + + // Short + QString text = "New"; + for (int i=0; zones->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(0, text); + + // long + text = "New"; + for (int i=0; zones->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { + text = QString("New (%1)").arg(i+1); + } + add->setText(1, text); + active = false; + + zonesChanged(); +} + +void +CPPage::deleteZoneClicked() +{ + // no range selected + if (ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()) == -1) + return; + + active = true; + if (zones->currentItem()) { + int index = zones->invisibleRootItem()->indexOfChild(zones->currentItem()); + delete zones->invisibleRootItem()->takeChild(index); + } + active = false; + + zonesChanged(); +} + +void +CPPage::zonesChanged() +{ + // only take changes when they are not done programmatically + // the active flag is set when the tree is being modified + // programmatically, but not when users interact with the widgets + if (active == false) { + // get the current zone range + if (ranges->currentItem()) { + + int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + ZoneRange current = zonePage->zones.getZoneRange(index); + + // embolden that range on the list to show it has been edited + QFont font; + font.setWeight(QFont::Black); + ranges->currentItem()->setFont(0, font); + ranges->currentItem()->setFont(1, font); + + // show the default button to undo + defaultButton->show(); + + // we manually edited so save in full + current.zonesSetFromCP = false; + + // create the new zoneinfos for this range + QList zoneinfos; + for (int i=0; i< zones->invisibleRootItem()->childCount(); i++) { + QTreeWidgetItem *item = zones->invisibleRootItem()->child(i); + zoneinfos << ZoneInfo(item->text(0), + item->text(1), + ((QDoubleSpinBox*)zones->itemWidget(item, 2))->value(), + 0); + } + + // now sort the list + qSort(zoneinfos); + + // now fill the highs + for(int i=0; izones.setZoneRange(index, current); + } + } +} diff --git a/src/Pages.h b/src/Pages.h index 1b5e11462..b8e45e0fe 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -31,6 +31,7 @@ class MetadataPage; class KeywordsPage; class FieldsPage; class Colors; +class ZonePage; class ConfigurationPage : public QWidget { @@ -75,23 +76,9 @@ class CyclistPage : public QWidget { Q_OBJECT public: - CyclistPage(const Zones *_zones); - int thresholdPower; - QString getText(); - int getCP(); - void setCP(int cp); - void setSelectedDate(QDate date); - void setCurrentRange(int range = -1); - QPushButton *btnBack; - QPushButton *btnForward; - QPushButton *btnDelete; - QCheckBox *checkboxNew; - QCalendarWidget *calendar; - QLabel *lblCurRange; - QLabel *txtStartDate; - QLabel *txtEndDate; - QLabel *lblStartDate; - QLabel *lblEndDate; + CyclistPage(MainWindow *mainWindow); + void saveClicked(); + QLabel *perfManLabel; QLabel *perfManStartLabel; QLabel *perfManSTSLabel; @@ -101,33 +88,15 @@ class CyclistPage : public QWidget QLineEdit *perfManLTSavg; QCheckBox *showSBToday; - int getCurrentRange(); - bool isNewMode(); - - inline void setCPFocus() { - txtThreshold->setFocus(); - } - - inline QDate selectedDate() { - return calendar->selectedDate(); - } - private: + ZonePage *zonePage; + MainWindow *main; + QGroupBox *cyclistGroup; - const Zones *zones; - int currentRange; - QLabel *lblThreshold; - QLineEdit *txtThreshold; - QIntValidator *txtThresholdValidator; QVBoxLayout *perfManLayout; QHBoxLayout *perfManStartValLayout; QHBoxLayout *perfManSTSavgLayout; QHBoxLayout *perfManLTSavgLayout; - QHBoxLayout *powerLayout; - QHBoxLayout *rangeLayout; - QHBoxLayout *dateRangeLayout; - QHBoxLayout *zoneLayout; - QHBoxLayout *calendarLayout; QVBoxLayout *cyclistLayout; QVBoxLayout *mainLayout; QIntValidator *perfManStartValidator; @@ -322,4 +291,82 @@ class MetadataPage : public QWidget QList fieldDefinitions; }; +class SchemePage : public QWidget +{ + Q_OBJECT + + public: + SchemePage(ZonePage *parent); + ZoneScheme getScheme(); + void saveClicked(); + + public slots: + void addClicked(); + void deleteClicked(); + void renameClicked(); + + private: + ZonePage *zonePage; + QTreeWidget *scheme; + QPushButton *addButton, *renameButton, *deleteButton; +}; + +class CPPage : public QWidget +{ + Q_OBJECT + + public: + CPPage(ZonePage *parent); + + public slots: + void addClicked(); + void deleteClicked(); + void defaultClicked(); + void rangeSelectionChanged(); + void addZoneClicked(); + void deleteZoneClicked(); + void zonesChanged(); + + private: + bool active; + + QDateEdit *dateEdit; + QDoubleSpinBox *cpEdit; + + ZonePage *zonePage; + QTreeWidget *ranges; + QTreeWidget *zones; + QPushButton *addButton, *deleteButton, *defaultButton; + QPushButton *addZoneButton, *deleteZoneButton; +}; + +class ZonePage : public QWidget +{ + Q_OBJECT + + public: + + ZonePage(MainWindow *); + void saveClicked(); + + //ZoneScheme scheme; + Zones zones; + + // Children talk to each other + SchemePage *schemePage; + CPPage *cpPage; + + public slots: + + + protected: + + MainWindow *main; + bool changed; + + QTabWidget *tabs; + + // local versions for modification +}; + #endif diff --git a/src/TimeInZone.cpp b/src/TimeInZone.cpp index 93cb9ea25..dce17578d 100644 --- a/src/TimeInZone.cpp +++ b/src/TimeInZone.cpp @@ -146,6 +146,42 @@ class ZoneTime7 : public ZoneTime { RideMetric *clone() const { return new ZoneTime7(*this); } }; +class ZoneTime8 : public ZoneTime { + + public: + ZoneTime8() + { + setLevel(8); + setSymbol("time_in_zone_L8"); + setName(tr("L8 Time in Zone")); + } + RideMetric *clone() const { return new ZoneTime8(*this); } +}; + +class ZoneTime9 : public ZoneTime { + + public: + ZoneTime9() + { + setLevel(9); + setSymbol("time_in_zone_L9"); + setName(tr("L9 Time in Zone")); + } + RideMetric *clone() const { return new ZoneTime9(*this); } +}; + +class ZoneTime10 : public ZoneTime { + + public: + ZoneTime10() + { + setLevel(10); + setSymbol("time_in_zone_L10"); + setName(tr("L10 Time in Zone")); + } + RideMetric *clone() const { return new ZoneTime10(*this); } +}; + static bool addAllZones() { @@ -156,6 +192,9 @@ static bool addAllZones() { RideMetricFactory::instance().addMetric(ZoneTime5()); RideMetricFactory::instance().addMetric(ZoneTime6()); RideMetricFactory::instance().addMetric(ZoneTime7()); + RideMetricFactory::instance().addMetric(ZoneTime8()); + RideMetricFactory::instance().addMetric(ZoneTime9()); + RideMetricFactory::instance().addMetric(ZoneTime10()); return true; } diff --git a/src/Zones.cpp b/src/Zones.cpp index fa37f9b8e..e98c90611 100644 --- a/src/Zones.cpp +++ b/src/Zones.cpp @@ -27,34 +27,11 @@ #include #include -static QList zone_default; -static QList zone_default_is_pct; -static QList zone_default_name; -static QList zone_default_desc; -static int nzones_default = 0; -// the infinity endpoints are indicated with empty dates -static const QDate date_zero = QDate::QDate(); -static const QDate date_infinity = QDate::QDate(); - -// functions used for sorting zones and ranges -bool Zones::zone_default_index_lessthan(int i1, int i2) { - return (zone_default[i1] * (zone_default_is_pct[i1] ? 250 : 1) < - zone_default[i2] * (zone_default_is_pct[i2] ? 250 : 1)); -} - -bool Zones::zoneptr_lessthan(const ZoneInfo &z1, const ZoneInfo &z2) { - return ((z1.lo < z2.lo) || - ((z1.lo == z2.lo) && (z1.hi < z2.hi))); -} - -bool Zones::rangeptr_lessthan(const ZoneRange &r1, const ZoneRange &r2) { - return (((! r2.begin.isNull()) && - ( r1.begin.isNull() || r1.begin < r2.begin )) || - ((r1.begin == r2.begin) && - (! r1.end.isNull()) && - ( r2.end.isNull() || r1.end < r2.end ))); -} +// the infinity endpoints are indicated with extreme date ranges +// but not zero dates so we can edit and compare them +static const QDate date_zero(1900, 01, 01); +static const QDate date_infinity(9999,12,31); // initialize default static zone parameters void Zones::initializeZoneParameters() @@ -74,19 +51,19 @@ void Zones::initializeZoneParameters() sizeof(initial_zone_default) / sizeof(initial_zone_default[0]); - zone_default.clear(); - zone_default_is_pct.clear(); - zone_default_desc.clear(); - zone_default_name.clear(); - nzones_default = 0; + scheme.zone_default.clear(); + scheme.zone_default_is_pct.clear(); + scheme.zone_default_desc.clear(); + scheme.zone_default_name.clear(); + scheme.nzones_default = 0; - nzones_default = initial_nzones_default; + scheme.nzones_default = initial_nzones_default; - for (int z = 0; z < nzones_default; z ++) { - zone_default.append(initial_zone_default[z]); - zone_default_is_pct.append(true); - zone_default_name.append(QString(initial_zone_default_name[z])); - zone_default_desc.append(QString(initial_zone_default_desc[z])); + for (int z = 0; z < scheme.nzones_default; z ++) { + scheme.zone_default.append(initial_zone_default[z]); + scheme.zone_default_is_pct.append(true); + scheme.zone_default_name.append(QString(initial_zone_default_name[z])); + scheme.zone_default_desc.append(QString(initial_zone_default_desc[z])); } } @@ -94,11 +71,12 @@ void Zones::initializeZoneParameters() bool Zones::read(QFile &file) { defaults_from_user = false; - zone_default.clear(); - zone_default_is_pct.clear(); - zone_default_name.clear(); - zone_default_desc.clear(); - nzones_default = 0; + scheme.zone_default.clear(); + scheme.zone_default_is_pct.clear(); + scheme.zone_default_name.clear(); + scheme.zone_default_desc.clear(); + scheme.nzones_default = 0; + ranges.clear(); // set up possible warning dialog warning = QString(); @@ -147,7 +125,7 @@ bool Zones::read(QFile &file) // the current range in the file // ZoneRange *range = NULL; bool in_range = false; - QDate begin, end; + QDate begin = date_zero, end = date_infinity; int cp; QList zoneInfos; @@ -174,7 +152,7 @@ bool Zones::read(QFile &file) } // only one set of defaults is allowed - if (nzones_default) { + if (scheme.nzones_default) { err = "Only one set of zone defaults may be specified in power.zones file"; return false; } @@ -183,70 +161,65 @@ bool Zones::read(QFile &file) } // check for range specification (may be followed by zone definitions) - for (int r = 0; r < 2; r++) { - if (rangerx[r].indexIn(line, 0) != -1) { + for (int r=0; r<2; r++) { + if (rangerx[r].indexIn(line, 0) != -1) { - if (in_range) { - // if zones are empty, then generate them - ZoneRange range(begin, end, cp); - range.zones = zoneInfos; - if (range.zones.empty()) { - if (range.cp > 0) - setZonesFromCP(range); - else { - err = tr("line %1: read new range without reading " - "any zones for previous one").arg(lineno); - file.close(); - return false; - } - } - // else sort them - else { - qSort(range.zones.begin(), range.zones.end(), zoneptr_lessthan); - } + if (in_range) { - ranges.append(range); - } + // if zones are empty, then generate them + ZoneRange range(begin, end, cp); + range.zones = zoneInfos; - in_range = true; - zones_are_defaults = false; - zoneInfos.clear(); + if (range.zones.empty()) { + if (range.cp > 0) setZonesFromCP(range); + else { + err = tr("line %1: read new range without reading " + "any zones for previous one").arg(lineno); + file.close(); + return false; + } + } else { + qSort(range.zones); + } + ranges.append(range); + } - // QDate begin, end; + in_range = true; + zones_are_defaults = false; + zoneInfos.clear(); - // process the beginning date - if (rangerx[r].cap(1) == "BEGIN") - begin = date_zero; - else { - begin = QDate(rangerx[r].cap(2).toInt(), - rangerx[r].cap(3).toInt(), - rangerx[r].cap(4).toInt() - ); - } + // process the beginning date + if (rangerx[r].cap(1) == "BEGIN") + begin = date_zero; + else { + begin = QDate(rangerx[r].cap(2).toInt(), + rangerx[r].cap(3).toInt(), + rangerx[r].cap(4).toInt()); + } - // process an end date, if any, else it is null - if (rangerx[r].cap(5) == "END") - end = date_infinity; - else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) { - end = QDate(rangerx[r].cap(6).toInt(), - rangerx[r].cap(7).toInt(), - rangerx[r].cap(8).toInt() - ); - } - else { - end = QDate(); - } + // process an end date, if any, else it is null + if (rangerx[r].cap(5) == "END") end = date_infinity; + else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || + rangerx[r].cap(8).toInt()) { - // set up the range, capturing CP if it's specified - // range = new ZoneRange(begin, end); - int nCP = (r ? 11 : 7); - if (rangerx[r].numCaptures() == (nCP)) - cp = rangerx[r].cap(nCP).toInt(); - else - cp = 0; - goto next_line; - } - } + end = QDate(rangerx[r].cap(6).toInt(), + rangerx[r].cap(7).toInt(), + rangerx[r].cap(8).toInt()); + + } else { + end = QDate(); + } + + // set up the range, capturing CP if it's specified + // range = new ZoneRange(begin, end); + int nCP = (r ? 11 : 7); + if (rangerx[r].numCaptures() == (nCP)) cp = rangerx[r].cap(nCP).toInt(); + else cp = 0; + + // bleck + goto next_line; + } + } // check for zone definition if (zonerx.indexIn(line, 0) != -1) { @@ -261,11 +234,9 @@ bool Zones::read(QFile &file) // allow for zone specified as % of CP bool lo_is_pct = false; - if (zonerx.cap(4) == "%") - if (zones_are_defaults) - lo_is_pct = true; - else if (cp > 0) - lo = int(lo * cp / 100); + if (zonerx.cap(4) == "%") { + if (zones_are_defaults) lo_is_pct = true; + else if (cp > 0) lo = int(lo * cp / 100); else { err = tr("attempt to set zone based on % of " "CP without setting CP in line number %1.\n"). @@ -273,6 +244,7 @@ bool Zones::read(QFile &file) file.close(); return false; } + } int hi; // if this is not a zone defaults specification, process possible hi end of zones @@ -284,24 +256,25 @@ bool Zones::read(QFile &file) hi = zonerx.cap(5).toInt(); // allow for zone specified as % of CP - if (zonerx.cap(5) == "%") + if (zonerx.cap(5) == "%") { if (cp > 0) hi = int(hi * cp / 100); else { - err = tr("attempt to set zone based on % of CP " - "without setting CP in line number %1.\n"). - arg(lineno); - file.close(); - return false; + err = tr("attempt to set zone based on % of CP " + "without setting CP in line number %1.\n"). + arg(lineno); + file.close(); + return false; } + } } if (zones_are_defaults) { - nzones_default ++; - zone_default_is_pct.append(lo_is_pct); - zone_default.append(lo); - zone_default_name.append(zonerx.cap(1)); - zone_default_desc.append(zonerx.cap(2)); + scheme.nzones_default ++; + scheme.zone_default_is_pct.append(lo_is_pct); + scheme.zone_default.append(lo); + scheme.zone_default_name.append(zonerx.cap(1)); + scheme.zone_default_desc.append(zonerx.cap(2)); defaults_from_user = true; } else { @@ -325,7 +298,7 @@ bool Zones::read(QFile &file) } } else { - qSort(range.zones.begin(), range.zones.end(), zoneptr_lessthan); + qSort(range.zones); } ranges.append(range); @@ -333,34 +306,27 @@ bool Zones::read(QFile &file) file.close(); // sort the ranges - qSort(ranges.begin(), ranges.end(), rangeptr_lessthan); + qSort(ranges); - // sort the zone defaults, as best we can (may not be right if there's - // a mix of % and absolute ranges) - if (nzones_default) { - QVector zone_default_index(nzones_default); - for (int i = 0; i < nzones_default; i++) - zone_default_index[i] = i; - qSort(zone_default_index.begin(), zone_default_index.end(), zone_default_index_lessthan); + // set the default zones if not in file + if (!scheme.nzones_default) { - QVector zone_default_new(nzones_default); - QVector zone_default_is_pct_new(nzones_default); - QVector zone_default_name_new(nzones_default); - QVector zone_default_desc_new(nzones_default); + // do we have a zone which is explicitly set? + for (int i=0; i 4) + if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) { append_to_warning(tr("Range %1: matching top of zone %2 " "(%3) to bottom of zone %4 (%5).\n"). arg(nr + 1). @@ -419,10 +385,12 @@ bool Zones::read(QFile &file) arg(ranges[nr].zones[nz + 1].name). arg(ranges[nr].zones[nz + 1].lo) ); + } ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo; - } - else if ((nz == ranges[nr].zones.size() - 1) && + + } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) { + append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n"). arg(nr + 1). arg(ranges[nr].zones[nz].name). @@ -500,49 +468,48 @@ void Zones::setCP(int rnum, int cp) // generate a list of zones from CP int Zones::lowsFromCP(QList *lows, int cp) const { - if (nzones_default == 0) - initializeZoneParameters(); lows->clear(); - for (int z = 0; z < nzones_default; z++) - lows->append(zone_default_is_pct[z] ? zone_default[z] * cp / 100 : zone_default[z]); + for (int z = 0; z < scheme.nzones_default; z++) + lows->append(scheme.zone_default_is_pct[z] ? + scheme.zone_default[z] * cp / 100 : scheme.zone_default[z]); - return nzones_default; + return scheme.nzones_default; } // access the zone name QString Zones::getDefaultZoneName(int z) const { - return zone_default_name[z]; + return scheme.zone_default_name[z]; } // access the zone description QString Zones::getDefaultZoneDesc(int z) const { - return zone_default_desc[z]; + return scheme.zone_default_desc[z]; } // set the zones from the CP value (the cp variable) void Zones::setZonesFromCP(ZoneRange &range) { range.zones.clear(); - if (nzones_default == 0) + if (scheme.nzones_default == 0) initializeZoneParameters(); - for (int i = 0; i < nzones_default; i++) { - int lo = zone_default_is_pct[i] ? zone_default[i] * range.cp / 100 : zone_default[i]; + for (int i = 0; i < scheme.nzones_default; i++) { + int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.cp / 100 : scheme.zone_default[i]; int hi = lo; - ZoneInfo zone(zone_default_name[i], zone_default_desc[i], lo, hi); + ZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi); range.zones.append(zone); } // sort the zones (some may be pct, others absolute, so zones need to be sorted, // rather than the defaults - qSort(range.zones.begin(), range.zones.end(), zoneptr_lessthan); + qSort(range.zones); // set zone end dates for (int i = 0; i < range.zones.size(); i ++) range.zones[i].hi = - (i < nzones_default - 1) ? + (i < scheme.nzones_default - 1) ? range.zones[i + 1].lo : INT_MAX; @@ -642,17 +609,16 @@ void Zones::write(QDir home) { QString strzones; - // write the defaults if they were specified by the user - if (defaults_from_user) { - strzones += QString("DEFAULTS:\n"); - for (int z = 0 ; z < nzones_default; z ++) - strzones += QString("%1,%2,%3%4\n"). - arg(zone_default_name[z]). - arg(zone_default_desc[z]). - arg(zone_default[z]). - arg(zone_default_is_pct[z]?"%":""); - strzones += QString("\n"); - } + // always write the defaults (config pane can adjust) + strzones += QString("DEFAULTS:\n"); + for (int z = 0 ; z < scheme.nzones_default; z ++) + strzones += QString("%1,%2,%3%4\n"). + arg(scheme.zone_default_name[z]). + arg(scheme.zone_default_desc[z]). + arg(scheme.zone_default[z]). + arg(scheme.zone_default_is_pct[z]?"%":""); + strzones += QString("\n"); + for (int i = 0; i < ranges.size(); i++) { int cp = getCP(i); @@ -660,10 +626,9 @@ void Zones::write(QDir home) // note this explicitly sets the first and last ranges such that all time is spanned #if USE_SHORT_POWER_ZONES_FORMAT - if (i == 0) - strzones += QString("BEGIN: CP=%1").arg(cp); - else - strzones += QString("%1: CP=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp); + // note: BEGIN is not needed anymore + // since it becomes Jan 01 1900 + strzones += QString("%1: CP=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp); strzones += QString("\n"); // step through and print the zones if they've been explicitly set @@ -709,6 +674,31 @@ void Zones::addZoneRange(QDate _start, QDate _end, int _cp) ranges.append(ZoneRange(_start, _end, _cp)); } +// insert a new zone range using the current scheme +// return the range number +int Zones::addZoneRange(QDate _start, int _cp) +{ + int rnum; + + // where to add this range? + for(rnum=0; rnum < ranges.count(); rnum++) if (ranges[rnum].begin > _start) break; + + // at the end ? + if (rnum == ranges.count()) ranges.append(ZoneRange(_start, date_infinity, _cp)); + else ranges.insert(rnum, ZoneRange(_start, ranges[rnum].begin, _cp)); + + // modify previous end date + if (rnum) ranges[rnum-1].end = _start; + + // set zones from CP + if (_cp > 0) { + setCP(rnum, _cp); + setZonesFromCP(rnum); + } + + return rnum; +} + void Zones::addZoneRange() { ranges.append(ZoneRange(date_zero, date_infinity)); @@ -776,29 +766,20 @@ QColor zoneColor(int z, int) { // delete a range, extend an adjacent (prior if available, otherwise next) // range to cover the same time period, then return the number of the new range -// covering the date range of the deleted range +// covering the date range of the deleted range or -1 if none left int Zones::deleteRange(int rnum) { - assert((ranges.size() > 1) && (rnum >= 0) && (rnum < ranges.size())); - int return_rnum; - // extend an adjacent range - if (rnum == 0) { - return_rnum = 0; - setStartDate(rnum + 1, getStartDate(rnum)); - } - else { - return_rnum = rnum - 1; - setEndDate(return_rnum, getEndDate(rnum)); - } + // check bounds - silently fail, don't assert + assert (rnum < ranges.count() && rnum >= 0); - // drop higher ranges down one slot - for (int r = rnum; r < ranges.size() - 1; r ++) - ranges[r] = ranges[r + 1]; + // extend the previous range to the end of this range + // but only if we have a previous range + if (rnum > 0) setEndDate(rnum-1, getEndDate(rnum)); - // reduce the number of ranges by one - ranges.removeLast(); + // delete this range then + ranges.removeAt(rnum); - return return_rnum; + return rnum-1; } // insert a new range starting at the given date extending to the end of the zone currently @@ -851,6 +832,12 @@ Zones::getFingerprint() const // CP x = ranges[i].cp; CRC.process_bytes(&x, sizeof(int)); + + // each zone definition (manual edit/default changed) + for (int j=0; j +// A zone "scheme" defines how power zones +// are calculated as a percentage of CP +// The default is to use Coggan percentages +// but this can be overriden in power.zones +struct ZoneScheme { + QList zone_default; + QList zone_default_is_pct; + QList zone_default_name; + QList zone_default_desc; + int nzones_default; +}; + +// A zone "info" defines a *single zone* +// in absolute watts terms e.g. +// "L4" "Threshold" is between 270w and 315w +struct ZoneInfo { + QString name, desc; + int lo, hi; + ZoneInfo(const QString &n, const QString &d, int l, int h) : + name(n), desc(d), lo(l), hi(h) {} + + // used by qSort() + bool operator< (ZoneInfo right) const { + return ((lo < right.lo) || ((lo == right.lo) && (hi < right.hi))); + } +}; + +// A zone "range" defines the power zones +// that are active for a *specific date period* +// e.g. between 01/01/2008 and 01/04/2008 +// my CP was 290 and I chose to setup +// 7 zoneinfos from Active Recovery through +// to muscular Endurance +struct ZoneRange { + QDate begin, end; + int cp; + QList zones; + bool zonesSetFromCP; + ZoneRange(const QDate &b, const QDate &e) : + begin(b), end(e), cp(0), zonesSetFromCP(false) {} + ZoneRange(const QDate &b, const QDate &e, int _cp) : + begin(b), end(e), cp(_cp), zonesSetFromCP(false) {} + + // used by qSort() + bool operator< (ZoneRange right) const { + return (((! right.begin.isNull()) && + (begin.isNull() || begin < right.begin )) || + ((begin == right.begin) && (! end.isNull()) && + ( right.end.isNull() || end < right.end ))); + } +}; + + class Zones : public QObject { Q_OBJECT - protected: - struct ZoneInfo { - QString name, desc; - int lo, hi; - ZoneInfo(const QString &n, const QString &d, int l, int h) : - name(n), desc(d), lo(l), hi(h) {} - }; - - struct ZoneRange { - QDate begin, end; - int cp; - QList zones; - bool zonesSetFromCP; - ZoneRange(const QDate &b, const QDate &e) : - begin(b), end(e), cp(0), zonesSetFromCP(false) {} - ZoneRange(const QDate &b, const QDate &e, int _cp) : - begin(b), end(e), cp(_cp), zonesSetFromCP(false) {} - }; + private: + // Scheme + bool defaults_from_user; + ZoneScheme scheme; + // CP History QList ranges; + + // utility QString err, warning; - - void setZonesFromCP(ZoneRange &range); - - static bool zoneptr_lessthan(const ZoneInfo &z1, const ZoneInfo &z2); - static bool rangeptr_lessthan(const ZoneRange &r1, const ZoneRange &r2); - static bool zone_default_index_lessthan(int i1, int i2); - - bool defaults_from_user; + void setZonesFromCP(ZoneRange &range); public: - Zones() : defaults_from_user(false) {} - void clear() { - ranges.clear(); - err = warning = ""; - defaults_from_user = false; + Zones() : defaults_from_user(false) { + initializeZoneParameters(); } + // + // Zone settings - Scheme (& default scheme) + // + ZoneScheme getScheme() const { return scheme; } + void setScheme(ZoneScheme x) { scheme = x; } + + // get defaults from the current scheme + QString getDefaultZoneName(int z) const; + QString getDefaultZoneDesc(int z) const; + + // set zone parameters to either user-specified defaults + // or to defaults using Coggan's coefficients + void initializeZoneParameters(); + + // + // Zone history - Ranges + // + // How many ranges in our history + int getRangeSize() const; + + // Add ranges void addZoneRange(QDate _start, QDate _end, int _cp); + int addZoneRange(QDate _start, int _cp); void addZoneRange(); + // insert a range from the given date to the end date of the range + // presently including the date + int insertRangeAtDate(QDate date, int cp = 0); + + // Get / Set ZoneRange details + ZoneRange getZoneRange(int rnum) { return ranges[rnum]; } + void setZoneRange(int rnum, ZoneRange x) { ranges[rnum] = x; } + + // get and set CP for a given range + int getCP(int rnum) const; + void setCP(int rnum, int cp); + + // calculate and then set zoneinfo for a given range + void setZonesFromCP(int rnum); + + // delete the range rnum, and adjust dates on adjacent zone; return + // the range number of the range extended to cover the deleted zone + int deleteRange(const int rnum); + + // + // read and write power.zones + // bool read(QFile &file); void write(QDir home); const QString &errorString() const { return err; } const QString &warningString() const { return warning; } + + // + // Typical APIs to get details of ranges and zones + // + + // which range is active for a particular date int whichRange(const QDate &date) const; - int numZones(int range) const; + + // which zone is the power value in for a given range int whichZone(int range, double value) const; + + // how many zones are there for a given range + int numZones(int range) const; + + // get zoneinfo for a given range and zone void zoneInfo(int range, int zone, QString &name, QString &description, int &low, int &high) const; + QString summarize(int rnum, QVector &time_in_zone) const; - int getCP(int rnum) const; - void setCP(int rnum, int cp); - QString getDefaultZoneName(int z) const; - QString getDefaultZoneDesc(int z) const; - void setZonesFromCP(int rnum); - int lowsFromCP(QList *lows, int CP) const; - QList getZoneLows(int rnum) const; - QList getZoneHighs(int rnum) const; - QList getZoneNames(int rnum) const; + + // get all highs/lows for zones (plot shading uses these) + int lowsFromCP(QList *lows, int CP) const; + QList getZoneLows(int rnum) const; + QList getZoneHighs(int rnum) const; + QList getZoneNames(int rnum) const; + + // get/set range start and end date QDate getStartDate(int rnum) const; QDate getEndDate(int rnum) const; QString getStartDateString(int rnum) const; QString getEndDateString(int rnum) const; void setEndDate(int rnum, QDate date); void setStartDate(int rnum, QDate date); - int getRangeSize() const; - QDateTime modificationTime; - // set zone parameters to either user-specified defaults - // or to defaults using Coggan's coefficients - static void initializeZoneParameters(); + // When was this last updated? + QDateTime modificationTime; - // delete the range rnum, and adjust dates on adjacent zone; return - // the range number of the range extended to cover the deleted zone - int deleteRange(const int rnum); - - // insert a range from the given date to the end date of the range - // presently including the date - int insertRangeAtDate(QDate date, int cp = 0); - - // calculate a CRC for the zones data - used to see if zones - // data is changed since last referenced in Metric code - // could also be used in Configuration pages (later) - unsigned long getFingerprint() const; + // calculate a CRC for the zones data - used to see if zones + // data is changed since last referenced in Metric code + // could also be used in Configuration pages (later) + unsigned long getFingerprint() const; }; QColor zoneColor(int zone, int num_zones);