New chart: Plan Adherence (#4836)

* New chart to visualize the adherence to the current plan
* Allowing retrospective analysis of behavior: Shifted, missed,
  completed, unplanned activities
* Grouping is always on a monthly basis
[publish binaries]
This commit is contained in:
Joachim Kohlhammer
2026-03-09 20:34:14 +01:00
committed by GitHub
parent d813585927
commit bbf4722ef2
10 changed files with 2390 additions and 3 deletions

View File

@@ -0,0 +1,474 @@
/*
* Copyright (c) 2026 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* 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 "PlanAdherenceWindow.h"
#include "Athlete.h"
#include "RideMetadata.h"
#include "Seasons.h"
#include "RideItem.h"
#include "Colors.h"
#include "IconManager.h"
#include "PlanAdherence.h"
#define HLO "<h4>"
#define HLC "</h4>"
PlanAdherenceWindow::PlanAdherenceWindow(Context *context)
: GcChartWindow(context), context(context)
{
mkControls();
adherenceView = new PlanAdherenceView();
titleMainCombo->setCurrentText("Route");
titleFallbackCombo->setCurrentText("Workout Code");
setMaxDaysBefore(1);
setMaxDaysAfter(5);
setPreferredStatisticsDisplay(1);
QVBoxLayout *mainLayout = new QVBoxLayout();
setChartLayout(mainLayout);
mainLayout->addWidget(adherenceView);
connect(adherenceView, &PlanAdherenceView::monthChanged, this, &PlanAdherenceWindow::updateActivities);
connect(adherenceView, &PlanAdherenceView::viewActivity, this, [this](QString reference, bool planned) {
for (RideItem *rideItem : this->context->athlete->rideCache->rides()) {
if (rideItem == nullptr || rideItem->fileName.isEmpty()) {
continue;
}
if (rideItem->fileName == reference && rideItem->planned == planned) {
this->context->notifyRideSelected(rideItem);
this->context->mainWindow->selectAnalysis();
break;
}
}
});
connect(context->athlete->rideCache, QOverload<RideItem*>::of(&RideCache::itemChanged), this, &PlanAdherenceWindow::updateActivitiesIfInRange);
connect(context->athlete->seasons, &Seasons::seasonsChanged, this, [this]() {
DateRange dr(this->context->currentSeason()->getStart(), this->context->currentSeason()->getEnd(), this->context->currentSeason()->getName());
adherenceView->setDateRange(dr);
});
connect(context, &Context::seasonSelected, this, [this](Season const *season, bool changed) {
if (changed || first) {
first = false;
DateRange dr(season->getStart(), season->getEnd(), season->getName());
adherenceView->setDateRange(dr);
}
});
connect(context, &Context::seasonSelected, this, &PlanAdherenceWindow::updateActivities);
connect(context, &Context::filterChanged, this, &PlanAdherenceWindow::updateActivities);
connect(context, &Context::homeFilterChanged, this, &PlanAdherenceWindow::updateActivities);
connect(context, &Context::rideAdded, this, &PlanAdherenceWindow::updateActivitiesIfInRange);
connect(context, &Context::rideDeleted, this, &PlanAdherenceWindow::updateActivitiesIfInRange);
connect(context, &Context::rideChanged, this, &PlanAdherenceWindow::updateActivitiesIfInRange);
connect(context, &Context::configChanged, this, &PlanAdherenceWindow::configChanged);
QTimer::singleShot(0, this, [this]() {
configChanged(CONFIG_APPEARANCE);
if (this->context->currentSeason() != nullptr) {
DateRange dr(this->context->currentSeason()->getStart(), this->context->currentSeason()->getEnd(), this->context->currentSeason()->getName());
adherenceView->setDateRange(dr);
}
});
}
QString
PlanAdherenceWindow::getTitleMainField
() const
{
return titleMainCombo->currentText();
}
void
PlanAdherenceWindow::setTitleMainField
(const QString &name)
{
titleMainCombo->setCurrentText(name);
}
QString
PlanAdherenceWindow::getTitleFallbackField
() const
{
return titleFallbackCombo->currentText();
}
void
PlanAdherenceWindow::setTitleFallbackField
(const QString &name)
{
titleFallbackCombo->setCurrentText(name);
}
int
PlanAdherenceWindow::getMaxDaysBefore
() const
{
return maxDaysBeforeSpin->value();
}
void
PlanAdherenceWindow::setMaxDaysBefore
(int days)
{
QSignalBlocker blocker(maxDaysBeforeSpin);
maxDaysBeforeSpin->setValue(days);
if (adherenceView != nullptr) {
adherenceView->setMinAllowedOffset(-maxDaysBeforeSpin->value());
updateActivities();
}
}
int
PlanAdherenceWindow::getMaxDaysAfter
() const
{
return maxDaysAfterSpin->value();
}
void
PlanAdherenceWindow::setMaxDaysAfter
(int days)
{
QSignalBlocker blocker(maxDaysAfterSpin);
maxDaysAfterSpin->setValue(days);
if (adherenceView != nullptr) {
adherenceView->setMaxAllowedOffset(maxDaysAfterSpin->value());
updateActivities();
}
}
int
PlanAdherenceWindow::getPreferredStatisticsDisplay
() const
{
return preferredStatisticsDisplay->currentIndex();
}
void
PlanAdherenceWindow::setPreferredStatisticsDisplay
(int mode)
{
QSignalBlocker blocker(preferredStatisticsDisplay);
preferredStatisticsDisplay->setCurrentIndex(mode);
if (adherenceView != nullptr) {
adherenceView->setPreferredStatisticsDisplay(mode);
}
}
void
PlanAdherenceWindow::configChanged
(qint32 what)
{
bool refreshActivities = false;
if ( (what & CONFIG_FIELDS)
|| (what & CONFIG_USERMETRICS)) {
updateTitleConfigCombos();
}
if (what & CONFIG_APPEARANCE) {
// change colors to reflect preferences
setProperty("color", GColor(CPLOTBACKGROUND));
QColor activeBase = GColor(CPLOTBACKGROUND);
QColor activeWindow = activeBase;
QColor activeText = GCColor::invertColor(activeBase);
QColor activeHl = GColor(CCALCURRENT);
QColor activeHlText = GCColor::invertColor(activeHl);
QColor alternateBg = GCColor::inactiveColor(activeBase, 0.2);
QColor inactiveText = GCColor::inactiveColor(activeText, 1.5);
QColor activeButtonBg = activeBase;
QColor disabledButtonBg = alternateBg;
if (activeBase.lightness() < 20) {
activeWindow = GCColor::inactiveColor(activeWindow, 0.2);
activeButtonBg = alternateBg;
disabledButtonBg = GCColor::inactiveColor(activeButtonBg, 0.3);
inactiveText = GCColor::inactiveColor(activeText, 2.5);
}
palette.setColor(QPalette::Active, QPalette::Window, activeWindow);
palette.setColor(QPalette::Active, QPalette::WindowText, activeText);
palette.setColor(QPalette::Active, QPalette::Base, activeBase);
palette.setColor(QPalette::Active, QPalette::AlternateBase, alternateBg);
palette.setColor(QPalette::Active, QPalette::Text, activeText);
palette.setColor(QPalette::Active, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Active, QPalette::HighlightedText, activeHlText);
palette.setColor(QPalette::Active, QPalette::Button, activeButtonBg);
palette.setColor(QPalette::Active, QPalette::ButtonText, activeText);
palette.setColor(QPalette::Inactive, QPalette::Window, activeWindow);
palette.setColor(QPalette::Inactive, QPalette::WindowText, activeText);
palette.setColor(QPalette::Inactive, QPalette::Base, activeBase);
palette.setColor(QPalette::Inactive, QPalette::AlternateBase, alternateBg);
palette.setColor(QPalette::Inactive, QPalette::Text, activeText);
palette.setColor(QPalette::Inactive, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Inactive, QPalette::HighlightedText, activeHlText);
palette.setColor(QPalette::Inactive, QPalette::Button, activeButtonBg);
palette.setColor(QPalette::Inactive, QPalette::ButtonText, activeText);
palette.setColor(QPalette::Disabled, QPalette::Window, alternateBg);
palette.setColor(QPalette::Disabled, QPalette::WindowText, inactiveText);
palette.setColor(QPalette::Disabled, QPalette::Base, alternateBg);
palette.setColor(QPalette::Disabled, QPalette::AlternateBase, alternateBg);
palette.setColor(QPalette::Disabled, QPalette::Text, inactiveText);
palette.setColor(QPalette::Disabled, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Disabled, QPalette::HighlightedText, activeHlText);
palette.setColor(QPalette::Disabled, QPalette::Button, disabledButtonBg);
palette.setColor(QPalette::Disabled, QPalette::ButtonText, inactiveText);
PaletteApplier::setPaletteRecursively(this, palette, true);
adherenceView->applyNavIcons();
}
if (refreshActivities) {
updateActivities();
}
}
bool
PlanAdherenceWindow::isFiltered
() const
{
return (context->ishomefiltered || context->isfiltered);
}
void
PlanAdherenceWindow::mkControls
()
{
titleMainCombo = new QComboBox();
titleFallbackCombo = new QComboBox();
updateTitleConfigCombos();
maxDaysBeforeSpin = new QSpinBox();
maxDaysBeforeSpin->setMinimum(1);
maxDaysBeforeSpin->setMaximum(14);
maxDaysAfterSpin = new QSpinBox();
maxDaysAfterSpin->setMinimum(1);
maxDaysAfterSpin->setMaximum(14);
preferredStatisticsDisplay = new QComboBox();
preferredStatisticsDisplay->addItem(tr("Absolute"));
preferredStatisticsDisplay->addItem(tr("Percentage"));
QFormLayout *controlsForm = newQFormLayout();
controlsForm->setContentsMargins(0, 10 * dpiYFactor, 0, 10 * dpiYFactor);
controlsForm->addRow(new QLabel(HLO + tr("Activity title") + HLC));
controlsForm->addRow(tr("Field"), titleMainCombo);
controlsForm->addRow(tr("Fallback Field"), titleFallbackCombo);
controlsForm->addItem(new QSpacerItem(0, 20 * dpiYFactor));
controlsForm->addRow(new QLabel(HLO + tr("Adherence window (days)") + HLC));
controlsForm->addRow(tr("Before"), maxDaysBeforeSpin);
controlsForm->addRow(tr("After"), maxDaysAfterSpin);
controlsForm->addItem(new QSpacerItem(0, 20 * dpiYFactor));
controlsForm->addRow(tr("Statistics display"), preferredStatisticsDisplay);
connect(titleMainCombo, &QComboBox::currentIndexChanged, this, &PlanAdherenceWindow::updateActivities);
connect(titleFallbackCombo, &QComboBox::currentIndexChanged, this, &PlanAdherenceWindow::updateActivities);
connect(maxDaysBeforeSpin, &QSpinBox::valueChanged, this, &PlanAdherenceWindow::setMaxDaysBefore);
connect(maxDaysAfterSpin, &QSpinBox::valueChanged, this, &PlanAdherenceWindow::setMaxDaysAfter);
connect(preferredStatisticsDisplay, &QComboBox::currentIndexChanged, this, &PlanAdherenceWindow::setPreferredStatisticsDisplay);
setControls(centerLayoutInWidget(controlsForm));
}
void
PlanAdherenceWindow::updateTitleConfigCombos
()
{
QString mainField = getTitleMainField();
QString fallbackField = getTitleFallbackField();
QSignalBlocker blocker1(titleMainCombo);
QSignalBlocker blocker2(titleFallbackCombo);
titleMainCombo->clear();
titleFallbackCombo->clear();
QList<FieldDefinition> fieldsDefs = GlobalContext::context()->rideMetadata->getFields();
for (const FieldDefinition &fieldDef : fieldsDefs) {
if (fieldDef.isTextField()) {
titleMainCombo->addItem(fieldDef.name);
titleFallbackCombo->addItem(fieldDef.name);
}
}
setTitleMainField(mainField);
setTitleFallbackField(fallbackField);
}
QString
PlanAdherenceWindow::getRideItemTitle
(RideItem const * const rideItem) const
{
QString title = rideItem->getText(getTitleMainField(), rideItem->getText(getTitleFallbackField(), "")).trimmed();
if (title.isEmpty()) {
if (! rideItem->sport.isEmpty()) {
title = tr("Unnamed %1").arg(rideItem->sport);
} else {
title = tr("<unknown>");
}
}
return title;
}
void
PlanAdherenceWindow::updateActivities
()
{
QList<PlanAdherenceEntry> entries;
PlanAdherenceStatistics statistics;
PlanAdherenceOffsetRange offsetRange;
QDate firstVisible = adherenceView->firstVisibleDay();
QDate lastVisible = adherenceView->lastVisibleDay();
QDate today = QDate::currentDate();
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if ( rideItem == nullptr
|| (! rideItem->planned && rideItem->hasLinkedActivity())
|| (context->isfiltered && ! context->filters.contains(rideItem->fileName))
|| (context->ishomefiltered && ! context->homeFilters.contains(rideItem->fileName))) {
continue;
}
QDate rideDate = rideItem->dateTime.date();
QString originalDateString = rideItem->getText("Original Date", "");
QDate originalDate(rideDate);
if (! originalDateString.isEmpty()) {
originalDate = QDate::fromString(originalDateString, "yyyy/MM/dd");
if (! originalDate.isValid()) {
originalDate = rideDate;
}
}
if ( (firstVisible.isValid() && originalDate < firstVisible)
|| (lastVisible.isValid() && originalDate > lastVisible)) {
continue;
}
PlanAdherenceEntry entry;
entry.titlePrimary = getRideItemTitle(rideItem);
entry.iconFile = IconManager::instance().getFilepath(rideItem);
entry.date = originalDate;
entry.isPlanned = rideItem->planned;
if (entry.isPlanned) {
entry.plannedReference = rideItem->fileName;
entry.color = GColor(CCALPLANNED);
} else {
entry.actualReference = rideItem->fileName;
if (rideItem->color.alpha() < 255) {
entry.color = GCColor::invertColor(GColor(CPLOTBACKGROUND));
} else {
entry.color = rideItem->color;
}
}
if (entry.isPlanned && originalDate != rideDate) {
entry.shiftOffset = originalDate.daysTo(rideDate);
offsetRange.min = std::min(offsetRange.min, entry.shiftOffset.value());
offsetRange.max = std::max(offsetRange.max, entry.shiftOffset.value());
} else {
entry.shiftOffset.reset();
}
RideItem *linkedItem = nullptr;
if (! rideItem->getLinkedFileName().isEmpty()) {
linkedItem = context->athlete->rideCache->getRide(rideItem->getLinkedFileName());
}
if (entry.isPlanned && linkedItem != nullptr) {
if (linkedItem->color.alpha() < 255) {
entry.color = GCColor::invertColor(GColor(CPLOTBACKGROUND));
} else {
entry.color = linkedItem->color;
}
entry.titleSecondary = getRideItemTitle(linkedItem);
entry.actualReference = linkedItem->fileName;
entry.actualOffset = originalDate.daysTo(linkedItem->dateTime.date());
offsetRange.min = std::min(offsetRange.min, entry.actualOffset.value());
offsetRange.max = std::max(offsetRange.max, entry.actualOffset.value());
} else {
entry.actualOffset.reset();
}
entries << entry;
++statistics.totalAbs;
if (entry.isPlanned) {
++statistics.plannedAbs;
if (entry.shiftOffset != std::nullopt) {
++statistics.shiftedAbs;
statistics.totalShiftDaysAbs += std::abs(entry.shiftOffset.value());
}
if (entry.actualOffset != std::nullopt) {
if (entry.actualOffset.value() == 0) {
++statistics.onTimeAbs;
}
} else if (entry.date < today) {
++statistics.missedAbs;
}
} else {
++statistics.unplannedAbs;
}
}
std::sort(entries.begin(), entries.end(), [](const PlanAdherenceEntry &a, const PlanAdherenceEntry &b) {
return a.date < b.date;
});
if (statistics.totalAbs > 0) {
statistics.plannedRel = 100.0 * statistics.plannedAbs / statistics.totalAbs;
statistics.unplannedRel = 100.0 * statistics.unplannedAbs / statistics.totalAbs;
}
if (statistics.plannedAbs > 0) {
statistics.onTimeRel = 100.0 * statistics.onTimeAbs / statistics.plannedAbs;
statistics.missedRel = 100.0 * statistics.missedAbs / statistics.plannedAbs;
statistics.shiftedRel = 100.0 * statistics.shiftedAbs / statistics.plannedAbs;
}
if (statistics.shiftedAbs > 0) {
statistics.avgShift = statistics.totalShiftDaysAbs / static_cast<float>(statistics.shiftedAbs);
}
adherenceView->fillEntries(entries, statistics, offsetRange, isFiltered());
}
void
PlanAdherenceWindow::updateActivitiesIfInRange
(RideItem *rideItem)
{
if ( rideItem != nullptr
&& rideItem->dateTime.date() >= adherenceView->firstVisibleDay()
&& rideItem->dateTime.date() <= adherenceView->lastVisibleDay()) {
updateActivities();
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (c) 2026 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* 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_PlanAdherenceWindow_h
#define _GC_PlanAdherenceWindow_h
#include "GoldenCheetah.h"
#include <QtGui>
#include "Context.h"
#include "PlanAdherence.h"
class PlanAdherenceWindow : public GcChartWindow
{
Q_OBJECT
Q_PROPERTY(QString titleMainField READ getTitleMainField WRITE setTitleMainField USER true)
Q_PROPERTY(QString titleFallbackField READ getTitleFallbackField WRITE setTitleFallbackField USER true)
Q_PROPERTY(int maxDaysBefore READ getMaxDaysBefore WRITE setMaxDaysBefore USER true)
Q_PROPERTY(int mayDaysAfter READ getMaxDaysAfter WRITE setMaxDaysAfter USER true)
Q_PROPERTY(int preferredStatisticsDisplay READ getPreferredStatisticsDisplay WRITE setPreferredStatisticsDisplay USER true)
public:
PlanAdherenceWindow(Context *context);
QString getTitleMainField() const;
QString getTitleFallbackField() const;
int getMaxDaysBefore() const;
int getMaxDaysAfter() const;
int getPreferredStatisticsDisplay() const;
public slots:
void setTitleMainField(const QString &name);
void setTitleFallbackField(const QString &name);
void setMaxDaysBefore(int days);
void setMaxDaysAfter(int days);
void setPreferredStatisticsDisplay(int mode);
void configChanged(qint32);
private:
Context *context;
PlanAdherenceView *adherenceView = nullptr;
bool first = true;
QPalette palette;
QComboBox *titleMainCombo;
QComboBox *titleFallbackCombo;
QSpinBox *maxDaysBeforeSpin;
QSpinBox *maxDaysAfterSpin;
QComboBox *preferredStatisticsDisplay;
bool isFiltered() const;
void mkControls();
void updateTitleConfigCombos();
QString getRideItemTitle(RideItem const * const rideItem) const;
private slots:
void updateActivities();
void updateActivitiesIfInRange(RideItem *rideItem);
};
#endif

View File

@@ -26,6 +26,7 @@
#include <QHash>
#include <QMap>
#include <QColor>
#include <utility>
#define ENTRY_TYPE_ACTUAL_ACTIVITY 0

View File

@@ -1085,6 +1085,15 @@ QColor GCColor::getThemeColor(const ColorTheme& theme, int colorIdx)
return color;
}
QColor
GCColor::getSuccessColor(const QPalette &palette)
{
if (isPaletteDark(palette)) {
return QColor("#4CAF50");
}
return QColor("#27ae60");
}
void
GCColor::applyTheme(int index)
{

View File

@@ -148,6 +148,7 @@ class GCColor : public QObject
static Themes &themes();
static void applyTheme(int index);
static QColor getThemeColor(const ColorTheme& theme, int colorIdx);
static QColor getSuccessColor(const QPalette &palette);
// for styling things with current preferences
static QLinearGradient linearGradient(int size, bool active, bool alternate=false);

View File

@@ -45,6 +45,7 @@
#include "LiveMapWebPageWindow.h"
#include "CalendarWindow.h"
#include "AgendaWindow.h"
#include "PlanAdherenceWindow.h"
#ifdef GC_WANT_R
#include "RChart.h"
#endif
@@ -114,6 +115,7 @@ GcWindowRegistry::initialize()
{ GcViewType::VIEW_ANALYSIS|GcViewType::VIEW_TRENDS|GcViewType::VIEW_TRAIN, tr("Web page"),GcWindowTypes::WebPageWindow },
{ GcViewType::VIEW_TRENDS|GcViewType::VIEW_PLAN, tr("Calendar"),GcWindowTypes::Calendar },
{ GcViewType::VIEW_TRENDS|GcViewType::VIEW_PLAN, tr("Agenda"),GcWindowTypes::Agenda },
{ GcViewType::VIEW_PLAN, tr("Plan Adherence"),GcWindowTypes::PlanAdherence },
{ GcViewType::NO_VIEW_SET, "", GcWindowTypes::None }};
// initialize the global registry
GcWindows = GcWindowsInit;
@@ -264,6 +266,7 @@ GcWindowRegistry::newGcWindow(GcWinID id, Context *context)
case GcWindowTypes::Diary:
case GcWindowTypes::Calendar: returning = new CalendarWindow(context); break;
case GcWindowTypes::Agenda: returning = new AgendaWindow(context); break;
case GcWindowTypes::PlanAdherence: returning = new PlanAdherenceWindow(context); break;
default: return NULL; break;
}
if (returning) returning->setProperty("type", QVariant::fromValue<GcWinID>(id));

View File

@@ -82,7 +82,8 @@ enum gcwinid {
Agenda=53,
UserPlan=54,
OverviewPlan=55,
OverviewPlanBlank = 56
OverviewPlanBlank = 56,
PlanAdherence = 57
};
};
typedef enum GcWindowTypes::gcwinid GcWinID;

1496
src/Gui/PlanAdherence.cpp Normal file

File diff suppressed because it is too large Load Diff

319
src/Gui/PlanAdherence.h Normal file
View File

@@ -0,0 +1,319 @@
/*
* Copyright (c) 2026 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* 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 PLANADHERENCE_H
#define PLANADHERENCE_H
#include <QtGui>
#include <QLabel>
#include <QTreeWidget>
#include <QHeaderView>
#include <QStackedWidget>
#include <QToolBar>
#include <QStyledItemDelegate>
#include "TimeUtils.h"
#include "Colors.h"
class StatisticBox : public QFrame {
Q_OBJECT
public:
enum class DisplayState {
Primary,
Secondary
};
explicit StatisticBox(const QString &title, const QString &description, DisplayState startupPreferred = DisplayState::Primary, QWidget* parent = nullptr);
void setValue(const QString &primaryValue, const QString &secondaryValue = QString());
void setPreferredState(const DisplayState &state);
protected:
void changeEvent(QEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private:
QLabel *valueLabel;
QLabel *titleLabel;
QString primaryValue;
QString secondaryValue;
QString description;
DisplayState startupPreferredState;
DisplayState currentPreferredState;
DisplayState currentState;
};
class JointLineTree : public QTreeWidget {
Q_OBJECT
public:
explicit JointLineTree(QWidget *parent = nullptr);
bool isRowHovered(const QModelIndex &index) const;
virtual QColor getHoverBG() const;
virtual QColor getHoverHL() const;
protected:
void mouseMoveEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void leaveEvent(QEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
void changeEvent(QEvent *event) override;
bool viewportEvent(QEvent *event) override;
void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
virtual QMenu *createContextMenu(const QModelIndex &index) = 0;
virtual QString createToolTipText(const QModelIndex &index) const = 0;
void clearHover();
private:
QPersistentModelIndex hoveredIndex;
QPersistentModelIndex contextMenuIndex;
void updateHoveredIndex(const QPoint &pos);
};
struct PlanAdherenceStatistics {
int totalAbs = 0;
int plannedAbs = 0;
float plannedRel = 0;
int onTimeAbs = 0;
float onTimeRel = 0;
int shiftedAbs = 0;
float shiftedRel = 0;
int missedAbs = 0;
float missedRel = 0;
float avgShift = 0;
int unplannedAbs = 0;
float unplannedRel = 0;
int totalShiftDaysAbs = 0;
};
struct PlanAdherenceEntry {
QString titlePrimary;
QString titleSecondary;
QString iconFile;
QColor color;
QDate date;
bool isPlanned;
std::optional<qint64> shiftOffset;
std::optional<qint64> actualOffset;
QString plannedReference;
QString actualReference;
};
Q_DECLARE_METATYPE(PlanAdherenceEntry)
struct PlanAdherenceOffsetRange {
qint64 min = -1;
qint64 max = 1;
};
class PlanAdherenceOffsetHandler {
public:
void setMinAllowedOffset(int minOffset);
void setMaxAllowedOffset(int maxOffset);
void setMinEntryOffset(int minOffset);
void setMaxEntryOffset(int maxOffset);
bool hasMinOverflow() const;
bool hasMaxOverflow() const;
int minVisibleOffset() const;
int maxVisibleOffset() const;
bool isMinOverflow(int offset) const;
bool isMaxOverflow(int offset) const;
int indexFromOffset(int offset) const;
int numCells() const;
protected:
int minAllowedOffset = -1;
int maxAllowedOffset = 1;
int minEntryOffset = -1;
int maxEntryOffset = 1;
};
namespace PlanAdherence {
enum Roles {
DataRole = Qt::UserRole + 1 // [PlanAdherenceEntry] Data to be visualized
};
}
class PlanAdherenceTitleDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit PlanAdherenceTitleDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
const int leftPadding = 10 * dpiXFactor;
const int horMargin = 4 * dpiXFactor;
const int vertMargin = 6 * dpiYFactor;
const int lineSpacing = 2 * dpiYFactor;
const int iconInnerSpacing = 2 * dpiXFactor;
const int iconTextSpacing = 8 * dpiXFactor;
const int maxWidth = 300 * dpiXFactor;
};
class PlanAdherenceOffsetDelegate : public QStyledItemDelegate, public PlanAdherenceOffsetHandler {
Q_OBJECT
public:
explicit PlanAdherenceOffsetDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
const int horMargin = 4 * dpiXFactor;
const int vertMargin = 6 * dpiYFactor;
const int lineSpacing = 2 * dpiYFactor;
};
class PlanAdherenceStatusDelegate : public QStyledItemDelegate {
public:
enum Roles {
Line1Role = Qt::UserRole + 1, // [QString] First line to be shown
Line2Role, // [QString] Second line to be shown (optional, first line will be vertically centered if this is not available)
Line2WeightRole // [QFont::Weight] Font weight to be used for line 2
};
explicit PlanAdherenceStatusDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
const int rightPadding = 10 * dpiXFactor;
const int horMargin = 4 * dpiXFactor;
const int vertMargin = 6 * dpiYFactor;
const int lineSpacing = 2 * dpiYFactor;
};
class PlanAdherenceHeaderView : public QHeaderView, public PlanAdherenceOffsetHandler {
Q_OBJECT
public:
explicit PlanAdherenceHeaderView(Qt::Orientation orientation, QWidget *parent = nullptr);
protected:
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override;
private:
const int horMargin = 4 * dpiXFactor;
};
class PlanAdherenceTree : public JointLineTree {
Q_OBJECT
public:
explicit PlanAdherenceTree(QWidget *parent = nullptr);
void fillEntries(const QList<PlanAdherenceEntry> &entries, const PlanAdherenceOffsetRange &offsets);
QColor getHoverBG() const;
QColor getHoverHL() const;
void setMinAllowedOffset(int offset);
void setMaxAllowedOffset(int offset);
signals:
void viewActivity(QString reference, bool planned);
protected:
QMenu *createContextMenu(const QModelIndex &index) override;
QString createToolTipText(const QModelIndex &index) const override;
private:
PlanAdherenceOffsetDelegate *planAdherenceOffsetDelegate;
PlanAdherenceHeaderView *headerView;
int minAllowedOffset = -1;
int maxAllowedOffset = 5;
};
class PlanAdherenceView : public QWidget {
Q_OBJECT
public:
explicit PlanAdherenceView(QWidget *parent = nullptr);
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
void fillEntries(const QList<PlanAdherenceEntry> &entries, const PlanAdherenceStatistics &statistics, const PlanAdherenceOffsetRange &offsets, bool isFiltered);
public slots:
void setDateRange(const DateRange &dateRange);
void setMinAllowedOffset(int offset);
void setMaxAllowedOffset(int offset);
void setPreferredStatisticsDisplay(int mode);
void applyNavIcons();
signals:
void monthChanged(QDate month);
void viewActivity(QString reference, bool planned);
private:
QToolBar *toolbar;
QAction *prevAction;
QAction *nextAction;
QAction *separator;
QAction *todayAction;
QAction *dateNavigatorAction;
QAction *filterLabelAction;
QToolButton *dateNavigator;
QMenu *dateMenu;
QStackedWidget *viewStack;
PlanAdherenceTree *timeline;
StatisticBox *totalBox;
StatisticBox *plannedBox;
StatisticBox *unplannedBox;
StatisticBox *onTimeBox;
StatisticBox *shiftedBox;
StatisticBox *missedBox;
StatisticBox *avgShiftBox;
StatisticBox *totalShiftDaysBox;
DateRange dateRange;
QDate dateInMonth;
void setNavButtonState();
void updateHeader();
private slots:
void populateDateMenu();
};
#endif

View File

@@ -575,7 +575,7 @@ HEADERS += Charts/Aerolab.h Charts/AerolabWindow.h Charts/AllPlot.h Charts/AllPl
Charts/MetadataWindow.h Charts/MUPlot.h Charts/MUPool.h Charts/MUWidget.h Charts/PfPvPlot.h Charts/PfPvWindow.h \
Charts/PowerHist.h Charts/ReferenceLineDialog.h Charts/RideEditor.h Charts/RideMapWindow.h \
Charts/ScatterPlot.h Charts/ScatterWindow.h Charts/SmallPlot.h Charts/TreeMapPlot.h \
Charts/TreeMapWindow.h Charts/ZoneScaleDraw.h Charts/CalendarWindow.h Charts/AgendaWindow.h
Charts/TreeMapWindow.h Charts/ZoneScaleDraw.h Charts/CalendarWindow.h Charts/AgendaWindow.h Charts/PlanAdherenceWindow.h
# cloud services
HEADERS += Cloud/CalendarDownload.h Cloud/CloudService.h \
@@ -621,6 +621,7 @@ HEADERS += Gui/AboutDialog.h Gui/AddIntervalDialog.h Gui/AnalysisSidebar.h Gui/C
Gui/PerspectiveDialog.h Gui/SplashScreen.h Gui/StyledItemDelegates.h Gui/MetadataDialog.h Gui/ActionButtonBox.h \
Gui/MetricOverrideDialog.h Gui/RepeatScheduleWizard.h \
Gui/Calendar.h Gui/Agenda.h Gui/CalendarData.h Gui/CalendarItemDelegates.h \
Gui/PlanAdherence.h \
Gui/IconManager.h Gui/FilterSimilarDialog.h
# metrics and models
@@ -685,7 +686,7 @@ SOURCES += Charts/Aerolab.cpp Charts/AerolabWindow.cpp Charts/AllPlot.cpp Charts
Charts/MetadataWindow.cpp Charts/MUPlot.cpp Charts/MUWidget.cpp Charts/PfPvPlot.cpp Charts/PfPvWindow.cpp \
Charts/PowerHist.cpp Charts/ReferenceLineDialog.cpp Charts/RideEditor.cpp Charts/RideMapWindow.cpp \
Charts/ScatterPlot.cpp Charts/ScatterWindow.cpp Charts/SmallPlot.cpp Charts/TreeMapPlot.cpp \
Charts/TreeMapWindow.cpp Charts/CalendarWindow.cpp Charts/AgendaWindow.cpp
Charts/TreeMapWindow.cpp Charts/CalendarWindow.cpp Charts/AgendaWindow.cpp Charts/PlanAdherenceWindow.cpp
## Cloud Services / Web resources
SOURCES += Cloud/CalendarDownload.cpp Cloud/CloudService.cpp \
@@ -734,6 +735,7 @@ SOURCES += Gui/AboutDialog.cpp Gui/AddIntervalDialog.cpp Gui/AnalysisSidebar.cpp
Gui/PerspectiveDialog.cpp Gui/SplashScreen.cpp Gui/StyledItemDelegates.cpp Gui/MetadataDialog.cpp Gui/ActionButtonBox.cpp \
Gui/MetricOverrideDialog.cpp Gui/RepeatScheduleWizard.cpp \
Gui/Calendar.cpp Gui/Agenda.cpp Gui/CalendarData.cpp Gui/CalendarItemDelegates.cpp \
Gui/PlanAdherence.cpp \
Gui/IconManager.cpp Gui/FilterSimilarDialog.cpp
## Models and Metrics