From be1d3d878b42cafc85dbf0bc654d7149c02402a2 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Thu, 4 Mar 2021 17:34:05 -0500 Subject: [PATCH 01/28] Add 1130X200 to the list of tested machines. Also update warnings based on new test data and correctly label the rise time lock setting on some 1030X sessions. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 7c9e6f95..758ff8e0 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -314,6 +314,7 @@ static const PRS1TestedModel s_PRS1TestedModels[] = { { "1030X150", 3, 6, "DreamStation BiPAP S/T 30 with AAM" }, { "1130X110", 3, 6, "DreamStation BiPAP AVAPS 30" }, { "1131X150", 3, 6, "DreamStation BiPAP AVAPS 30 AE" }, + { "1130X200", 3, 6, "DreamStation BiPAP AVAPS 30" }, { "", 0, 0, "" }, }; @@ -6213,7 +6214,7 @@ bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size) case 2: // Breath Rate (fixed BPM) breath_rate = data[pos+1]; timed_inspiration = data[pos+2]; - if (breath_rate < 9 || breath_rate > 13) UNEXPECTED_VALUE(breath_rate, "9-13"); + if (breath_rate < 9 || breath_rate > 15) UNEXPECTED_VALUE(breath_rate, "9-15"); if (timed_inspiration < 8 || timed_inspiration > 20) UNEXPECTED_VALUE(timed_inspiration, "8-20"); this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Fixed)); this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_RATE, breath_rate)); @@ -6248,17 +6249,22 @@ bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size) // Rise time if (data[pos] < 1 || data[pos] > 6) UNEXPECTED_VALUE(data[pos], "1-6"); // 1-6 have been seen this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, data[pos])); + } else { + UNEXPECTED_VALUE(flexmode, "BiFlex or RiseTime"); } // Timed inspiration specified in the backup breath rate. break; - case 0x2f: // Rise Time lock? (was flex lock on F0V6, 0x80 for locked) + case 0x2f: // Flex / Rise Time lock CHECK_VALUE(len, 1); - if (cpapmode == PRS1_MODE_S) { + if (flexmode == FLEX_BiFlex) { + CHECK_VALUE(cpapmode, PRS1_MODE_S); CHECK_VALUES(data[pos], 0, 0x80); // Bi-Flex Lock this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[pos] != 0)); + } else if (flexmode == FLEX_RiseTime) { + CHECK_VALUES(data[pos], 0, 0x80); // Rise Time Lock + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, data[pos] != 0)); } else { - CHECK_VALUE(data[pos], 0); // Rise Time Lock? not yet observed on F3V6 - //this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, data[pos] != 0)); + UNEXPECTED_VALUE(flexmode, "BiFlex or RiseTime"); } break; case 0x35: // Humidifier setting @@ -7147,7 +7153,7 @@ void PRS1DataChunk::ParseHumidifierSettingV3(unsigned char byte1, unsigned char } else if (humidadaptive) { // All humidity levels seen. } else if (humidfixed) { - if (humidlevel == 0) UNEXPECTED_VALUE(humidlevel, "1-5"); + // All humidity levels seen. } } } From 562cd9cc37af2e997533f00a4b095b75c98ac2fe Mon Sep 17 00:00:00 2001 From: LoudSnorer Date: Sun, 21 Mar 2021 14:15:47 -0400 Subject: [PATCH 02/28] fix display issues for short span events --- oscar/Graphs/gFlagsLine.cpp | 16 +++++++++------- oscar/Graphs/gLineOverlay.cpp | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/oscar/Graphs/gFlagsLine.cpp b/oscar/Graphs/gFlagsLine.cpp index 50f39e37..f4738c1f 100644 --- a/oscar/Graphs/gFlagsLine.cpp +++ b/oscar/Graphs/gFlagsLine.cpp @@ -366,23 +366,25 @@ void gFlagsLine::paint(QPainter &painter, gGraph &w, const QRegion ®ion) x1 = double(X - minx) * xmult + left; x2 = double(X2 - minx) * xmult + left; + int width = x1-x2; + width = qMax(2,width); // Insure Rectangle will be visable. Flag events are 2 pixels wide. brush = QBrush(color); - painter.fillRect(x2, bartop, x1-x2, bottom-bartop, brush); - if (!w.selectingArea() && !hover && QRect(x2, bartop, x1-x2, bottom-bartop).contains(w.graphView()->currentMousePos())) { + painter.fillRect(x2, bartop, width, bottom-bartop, brush); + if (!w.selectingArea() && !hover && QRect(x2, bartop, width , bottom-bartop).contains(w.graphView()->currentMousePos())) { hover = true; painter.setPen(QPen(Qt::red,1)); - painter.drawRect(x2, bartop, x1-x2, bottom-bartop); + painter.drawRect(x2, bartop, width, bottom-bartop); int x,y; - int s = *dptr; - int m = s / 60; - s %= 60; + double s = *dptr; + double m; + s=60*modf(s/60,&m); QString lab = QString("%1").arg(schema::channel[m_code].fullname()); if (m>0) { lab += QObject::tr(" (%2 min, %3 sec)").arg(m).arg(s); } else { - lab += QObject::tr(" (%3 sec)").arg(m).arg(s); + lab += QObject::tr(" (%3 sec)").arg(s); } GetTextExtent(lab, x, y); w.ToolTip(lab, x2 - 10, bartop + (3 * w.printScaleY()), TT_AlignRight, tooltipTimeout); diff --git a/oscar/Graphs/gLineOverlay.cpp b/oscar/Graphs/gLineOverlay.cpp index b6e1484f..ce42c37c 100644 --- a/oscar/Graphs/gLineOverlay.cpp +++ b/oscar/Graphs/gLineOverlay.cpp @@ -28,7 +28,6 @@ void gLineOverlayBar::paint(QPainter &painter, gGraph &w, const QRegion ®ion) if (!schema::channel[m_code].enabled()) return; - int left = region.boundingRect().left(); int topp = region.boundingRect().top(); // FIXME: Misspelling intentional. double width = region.boundingRect().width(); @@ -42,10 +41,12 @@ void gLineOverlayBar::paint(QPainter &painter, gGraph &w, const QRegion ®ion) double xx = w.max_x - w.min_x; //double yy = w.max_y - w.min_y; + + if (xx <= 0) { return; } + double jj = width / xx; - if (xx <= 0) { return; } double x1, x2; @@ -138,12 +139,20 @@ void gLineOverlayBar::paint(QPainter &painter, gGraph &w, const QRegion ®ion) x1 = jj * double(X - w.min_x); x2 = jj * double(Y - w.min_x); - x2 += (int(x1)==int(x2)) ? 1 : 0; - x2 = qMax(0.0, x2)+left; x1 = qMin(width, x1)+left; - painter.fillRect(QRect(x2, start_py, x1-x2, height), brush); + // x2 represents the begining of a span in pixels + // x1 represent the end of the span in pixels + // BUG HERE + //x2 += (int(x1)==int(x2)) ? 1 : 0; + // Fixed BY + int duration = x1-x2; + if (duration<2) duration=2; // display minial span with 2 pixels. + x2 =x1-duration; + + painter.fillRect(QRect(x2, start_py, duration, height), brush); + } }/* else if (m_flt == FT_Dot) { //////////////////////////////////////////////////////////////////////////// From 9777ecd2ab15d5de1f6aeb439ba2eba2c33e52e9 Mon Sep 17 00:00:00 2001 From: LoudSnorer Date: Sun, 21 Mar 2021 16:21:48 -0400 Subject: [PATCH 03/28] This allows the Overview Custon range to be saved and resurvive a reloaded of OSCAR. a New feature has been been added but can not be activated until a UI file is changed - another submission. This new feature will utilize the current displayed range as the custom range. Other wise the Overview custom range uses the values from the calendar. This changes does not require and data or translations. --- oscar/SleepLib/profiles.h | 6 ++++ oscar/overview.cpp | 72 +++++++++++++++++++++++++-------------- oscar/overview.h | 5 +-- 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/oscar/SleepLib/profiles.h b/oscar/SleepLib/profiles.h index 4c218082..6acf9b6a 100644 --- a/oscar/SleepLib/profiles.h +++ b/oscar/SleepLib/profiles.h @@ -369,6 +369,8 @@ const QString STR_US_PrefCalcMax = "PrefCalcMax"; const QString STR_US_ShowUnknownFlags = "ShowUnknownFlags"; const QString STR_US_StatReportMode = "StatReportMode"; const QString STR_US_LastOverviewRange = "LastOverviewRange"; +const QString STR_US_CustomOverviewRangeStart = "CustomOverviewRangeStart"; +const QString STR_US_CustomOverviewRangeEnd = "CustomOverviewRangeEnd"; // Values for StatReportMode const int STAT_MODE_STANDARD = 0; @@ -742,6 +744,8 @@ class UserSettings : public PrefSettings int statReportMode() const { return getPref(STR_US_StatReportMode).toInt(); } inline bool showUnknownFlags() const { return m_showUnownFlags; } int lastOverviewRange() const { return getPref(STR_US_LastOverviewRange).toInt(); } + QDate customOverviewRangeStart () const { return getPref(STR_US_CustomOverviewRangeStart).toDate(); } + QDate customOverviewRangeEnd () const { return getPref(STR_US_CustomOverviewRangeEnd).toDate(); } void setUnitSystem(UnitSystem us) { setPref(STR_US_UnitSystem, (int)us); } void setEventWindowSize(double size) { setPref(STR_US_EventWindowSize, size); } @@ -754,6 +758,8 @@ class UserSettings : public PrefSettings void setStatReportMode(int i) { setPref(STR_US_StatReportMode, i); } void setShowUnknownFlags(bool b) { setPref(STR_US_ShowUnknownFlags, m_showUnownFlags=b); } void setLastOverviewRange(int i) { setPref(STR_US_LastOverviewRange, i); } + void setCustomOverviewRangeStart(QDate i) { setPref(STR_US_CustomOverviewRangeStart, i); } + void setCustomOverviewRangeEnd(QDate i) { setPref(STR_US_CustomOverviewRangeEnd, i); } bool m_calculateRDI, m_showUnownFlags, m_skipEmptyDays; int m_prefCalcMiddle, m_prefCalcMax; diff --git a/oscar/overview.cpp b/oscar/overview.cpp index de2b930b..755f4b05 100644 --- a/oscar/overview.cpp +++ b/oscar/overview.cpp @@ -151,11 +151,6 @@ Overview::~Overview() disconnect(ui->dateEnd->calendarWidget(), SIGNAL(currentPageChanged(int, int)), this, SLOT(dateEnd_currentPageChanged(int, int))); disconnect(ui->dateStart->calendarWidget(), SIGNAL(currentPageChanged(int, int)), this, SLOT(dateStart_currentPageChanged(int, int))); - // Don't save custom date range. Default to last 3 months - if (p_profile->general->lastOverviewRange() == 8) { - p_profile->general->setLastOverviewRange(4); - } - // Save graph orders and pin status, etc... GraphView->SaveSettings("Overview");//no trans @@ -325,9 +320,6 @@ void Overview::updateGraphCombo() { ui->graphCombo->clear(); gGraph *g; - // ui->graphCombo->addItem("Show All Graphs"); - // ui->graphCombo->addItem("Hide All Graphs"); - // ui->graphCombo->addItem("---------------"); for (int i = 0; i < GraphView->size(); i++) { g = (*GraphView)[i]; @@ -345,6 +337,7 @@ void Overview::updateGraphCombo() updateCube(); } +#if 0 void Overview::ResetGraphs() { QDate start = ui->dateStart->date(); @@ -366,6 +359,7 @@ void Overview::ResetGraph(QString name) g->setDay(nullptr); GraphView->redraw(); } +#endif void Overview::RedrawGraphs() { @@ -430,6 +424,9 @@ void Overview::on_dateEnd_dateChanged(const QDate &date) qint64 d2 = qint64(QDateTime(date, QTime(23, 0, 0)/*, Qt::UTC*/).toTime_t()) * 1000L; GraphView->SetXBounds(d1, d2); ui->dateStart->setMaximumDate(date); + if (customMode) { + p_profile->general->setCustomOverviewRangeEnd(date); + } } void Overview::on_dateStart_dateChanged(const QDate &date) @@ -438,6 +435,10 @@ void Overview::on_dateStart_dateChanged(const QDate &date) qint64 d2 = qint64(QDateTime(ui->dateEnd->date(), QTime(23, 0, 0)/*, Qt::UTC*/).toTime_t()) * 1000L; GraphView->SetXBounds(d1, d2); ui->dateEnd->setMinimumDate(date); + if (customMode) { + p_profile->general->setCustomOverviewRangeStart(date); + } + } // Zoom to 100% button clicked or called back from 100% zoom in popup menu @@ -463,7 +464,6 @@ void Overview::ResetGraphOrder(int type) // Process new range selection from combo button void Overview::on_rangeCombo_activated(int index) { - p_profile->general->setLastOverviewRange(index); // type of range in last use ui->dateStart->setMinimumDate(p_profile->FirstDay()); // first and last dates for ANY machine type ui->dateEnd->setMaximumDate(p_profile->LastDay()); @@ -474,22 +474,6 @@ void Overview::on_rangeCombo_activated(int index) end = max(end, p_profile->LastDay(MT_SLEEPSTAGE)); QDate start; - if (index == 8) { // Custom - ui->dateStartLabel->setEnabled(true); - ui->dateEndLabel->setEnabled(true); - ui->dateEnd->setEnabled(true); - ui->dateStart->setEnabled(true); - - ui->dateStart->setMaximumDate(ui->dateEnd->date()); - ui->dateEnd->setMinimumDate(ui->dateStart->date()); - p_profile->general->setLastOverviewRange(8); - return; - } - - ui->dateEnd->setEnabled(false); - ui->dateStart->setEnabled(false); - ui->dateStartLabel->setEnabled(false); - ui->dateEndLabel->setEnabled(false); if (index == 0) { start = end.addDays(-6); @@ -507,10 +491,48 @@ void Overview::on_rangeCombo_activated(int index) start = end.addYears(-1).addDays(1); } else if (index == 7) { // Everything start = p_profile->FirstDay(); + } else if (index == 8 || index == 9) { // Custom + // Validate save Overview Custom Range for first access. + if (!p_profile->general->customOverviewRangeStart().isValid() + || (!p_profile->general->customOverviewRangeEnd().isValid() ) + || (index==9 /* New Coustom mode - to reset custom range to displayed date range*/) + ) { + // Reset Custom Range to current range displayed + // on first initialization of this version of OSCAR + // or on new custom Mode to reset range. + qint64 istart,iend; + GraphView->GetXBounds(istart , iend); + start = QDateTime::fromMSecsSinceEpoch( istart ).date(); + end = QDateTime::fromMSecsSinceEpoch( iend ).date(); + p_profile->general->setCustomOverviewRangeStart(start); + p_profile->general->setCustomOverviewRangeEnd(end); + index=8; + ui->rangeCombo->setCurrentIndex(index); + } else if (customMode) { // last mode was custom. + // Reset Custom Range to current range in calendar widget + // Custom mode MUST be initialized to false when the Custom Instance is created. + start = ui->dateStart->date(); + end = ui->dateEnd->date(); + p_profile->general->setCustomOverviewRangeStart(start); + p_profile->general->setCustomOverviewRangeEnd(end); + } else { + // have a change in RangeCombo selection. Use last saved values. + start = p_profile->general->customOverviewRangeStart() ; + end = p_profile->general->customOverviewRangeEnd() ; + } } if (start < p_profile->FirstDay()) { start = p_profile->FirstDay(); } + customMode = (index == 8) ; + ui->dateStartLabel->setEnabled(customMode); + ui->dateEndLabel->setEnabled(customMode); + ui->dateEnd->setEnabled(customMode); + ui->dateStart->setEnabled(customMode); + + + p_profile->general->setLastOverviewRange(index); // type of range in last use + // Ensure that all summary files are available and update version numbers if required int size = start.daysTo(end); qDebug() << "Overview range combo from" << start << "to" << end << "with" << size << "days"; diff --git a/oscar/overview.h b/oscar/overview.h index 5a434b18..5513c329 100644 --- a/oscar/overview.h +++ b/oscar/overview.h @@ -50,7 +50,7 @@ class Overview : public QWidget void ResetFont(); //! \brief Recalculates Overview chart info, but keeps the date set - void ResetGraphs(); + //void ResetGraphs(); //! \brief Reset graphs to uniform heights void ResetGraphLayout(); @@ -80,7 +80,7 @@ class Overview : public QWidget //! \brief List of SummaryCharts shown on the overview page QVector OverviewCharts; - void ResetGraph(QString name); + //void ResetGraph(QString name); void RebuildGraphs(bool reset = true); @@ -128,6 +128,7 @@ class Overview : public QWidget QIcon *icon_on; QIcon *icon_off; MyLabel *dateLabel; + bool customMode=false; //! \brief Updates the calendar highlighting for the calendar object for this date. void UpdateCalendarDay(QDateEdit *calendar, QDate date); From 2d502f6a6804c764b90a6a8197e35d7f08bc7130 Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Tue, 23 Mar 2021 09:42:47 +1100 Subject: [PATCH 04/28] Revert viatom data version to avoid purge --- oscar/SleepLib/loader_plugins/viatom_loader.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oscar/SleepLib/loader_plugins/viatom_loader.h b/oscar/SleepLib/loader_plugins/viatom_loader.h index 97697daf..7b326ac4 100644 --- a/oscar/SleepLib/loader_plugins/viatom_loader.h +++ b/oscar/SleepLib/loader_plugins/viatom_loader.h @@ -14,7 +14,7 @@ #include "SleepLib/machine_loader.h" const QString viatom_class_name = "Viatom"; -const int viatom_data_version = 3; //CN increased from 2 +const int viatom_data_version = 2; /*! \class ViatomLoader From 00ad97ff70184d56af7f6fde360943b8957f5c49 Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Tue, 23 Mar 2021 09:43:14 +1100 Subject: [PATCH 05/28] Update loader version change comment --- oscar/SleepLib/loader_plugins/cms50_loader.cpp | 9 +++++---- oscar/SleepLib/loader_plugins/cms50f37_loader.cpp | 9 +++++---- oscar/SleepLib/loader_plugins/dreem_loader.cpp | 9 +++++---- oscar/SleepLib/loader_plugins/somnopose_loader.cpp | 9 +++++---- oscar/SleepLib/loader_plugins/viatom_loader.cpp | 9 +++++---- oscar/SleepLib/loader_plugins/zeo_loader.cpp | 9 +++++---- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/cms50_loader.cpp b/oscar/SleepLib/loader_plugins/cms50_loader.cpp index 2d79cfb8..b7b89888 100644 --- a/oscar/SleepLib/loader_plugins/cms50_loader.cpp +++ b/oscar/SleepLib/loader_plugins/cms50_loader.cpp @@ -7,10 +7,11 @@ * for more details. */ //******************************************************************************************** -/// IMPORTANT!!! -//******************************************************************************************** -// Please INCREMENT the cms50_data_version in cms50_loader.h when making changes to this loader -// that change loader behaviour or modify channels. +// Please only INCREMENT the cms50_data_version in cms50_loader.h when making changes +// that change loader behaviour or modify channels in a manner that fixes old data imports. +// Note that changing the data version will require a reimport of existing data for which OSCAR +// does not keep a backup - so it should be avoided if possible. +// i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** #include diff --git a/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp b/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp index 59b02928..fa88bc38 100644 --- a/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp +++ b/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp @@ -7,10 +7,11 @@ * for more details. */ //******************************************************************************************** -/// IMPORTANT!!! -//******************************************************************************************** -// Please INCREMENT the cms50_data_version in cms50_loader.h when making changes to this loader -// that change loader behaviour or modify channels. +// Please only INCREMENT the cms50f37_data_version in cms50f37_loader.h when making changes +// that change loader behaviour or modify channels in a manner that fixes old data imports. +// Note that changing the data version will require a reimport of existing data for which OSCAR +// does not keep a backup - so it should be avoided if possible. +// i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** // #include diff --git a/oscar/SleepLib/loader_plugins/dreem_loader.cpp b/oscar/SleepLib/loader_plugins/dreem_loader.cpp index 872dacca..a6c032d6 100644 --- a/oscar/SleepLib/loader_plugins/dreem_loader.cpp +++ b/oscar/SleepLib/loader_plugins/dreem_loader.cpp @@ -7,10 +7,11 @@ * for more details. */ //******************************************************************************************** -// IMPORTANT!!! -//******************************************************************************************** -// Please INCREMENT the dreem_data_version in dreem_loader.h when making changes to this loader -// that change loader behaviour or modify channels. +// Please only INCREMENT the dreem_data_version in dreem_loader.h when making changes +// that change loader behaviour or modify channels in a manner that fixes old data imports. +// Note that changing the data version will require a reimport of existing data for which OSCAR +// does not keep a backup - so it should be avoided if possible. +// i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** #include diff --git a/oscar/SleepLib/loader_plugins/somnopose_loader.cpp b/oscar/SleepLib/loader_plugins/somnopose_loader.cpp index 605a3a89..40f45de0 100644 --- a/oscar/SleepLib/loader_plugins/somnopose_loader.cpp +++ b/oscar/SleepLib/loader_plugins/somnopose_loader.cpp @@ -7,10 +7,11 @@ * for more details. */ //******************************************************************************************** -// IMPORTANT!!! -//******************************************************************************************** -// Please INCREMENT the somnopose_data_version in somnopose_loader.h when making changes to this loader -// that change loader behaviour or modify channels. +// Please only INCREMENT the somnopose_data_version in somnopose_loader.h when making changes +// that change loader behaviour or modify channels in a manner that fixes old data imports. +// Note that changing the data version will require a reimport of existing data for which OSCAR +// does not keep a backup - so it should be avoided if possible. +// i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** #include diff --git a/oscar/SleepLib/loader_plugins/viatom_loader.cpp b/oscar/SleepLib/loader_plugins/viatom_loader.cpp index 7c2f3e44..19147c4e 100644 --- a/oscar/SleepLib/loader_plugins/viatom_loader.cpp +++ b/oscar/SleepLib/loader_plugins/viatom_loader.cpp @@ -9,10 +9,11 @@ * for more details. */ //******************************************************************************************** -// IMPORTANT!!! -//******************************************************************************************** -// Please INCREMENT the viatom_data_version in viatom_loader.h when making changes to this loader -// that change loader behaviour or modify channels. +// Please only INCREMENT the viatom_data_version in viatom_loader.h when making changes +// that change loader behaviour or modify channels in a manner that fixes old data imports. +// Note that changing the data version will require a reimport of existing data for which OSCAR +// does not keep a backup - so it should be avoided if possible. +// i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** #include diff --git a/oscar/SleepLib/loader_plugins/zeo_loader.cpp b/oscar/SleepLib/loader_plugins/zeo_loader.cpp index e282d075..bdaf4c4d 100644 --- a/oscar/SleepLib/loader_plugins/zeo_loader.cpp +++ b/oscar/SleepLib/loader_plugins/zeo_loader.cpp @@ -8,10 +8,11 @@ * for more details. */ //******************************************************************************************** -// IMPORTANT!!! -//******************************************************************************************** -// Please INCREMENT the zeo_data_version in zel_loader.h when making changes to this loader -// that change loader behaviour or modify channels. +// Please only INCREMENT the zeo_data_version in zeo_loader.h when making changes +// that change loader behaviour or modify channels in a manner that fixes old data imports. +// Note that changing the data version will require a reimport of existing data for which OSCAR +// does not keep a backup - so it should be avoided if possible. +// i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** #include From 37483de62a432d2183de85d4c3822c9cf20ca3cc Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Sun, 28 Mar 2021 14:15:10 +1100 Subject: [PATCH 06/28] Display Weight/BMI/Zombie graphs --- oscar/overview.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/oscar/overview.cpp b/oscar/overview.cpp index de2b930b..4260b8c3 100644 --- a/oscar/overview.cpp +++ b/oscar/overview.cpp @@ -236,8 +236,20 @@ void Overview::CreateAllGraphs() { } // for chit WEIGHT = createGraph(STR_GRAPH_Weight, STR_TR_Weight, STR_TR_Weight, YT_Weight); + weight = new SummaryChart("Weight", GT_LINE); + weight->setMachineType(MT_JOURNAL); + weight->addSlice(Journal_Weight, QColor("black"), ST_SETAVG); + WEIGHT->AddLayer(weight); BMI = createGraph(STR_GRAPH_BMI, STR_TR_BMI, tr("Body\nMass\nIndex")); + bmi = new SummaryChart("BMI", GT_LINE); + bmi->setMachineType(MT_JOURNAL); + bmi->addSlice(Journal_BMI, QColor("black"), ST_SETAVG); + BMI->AddLayer(bmi); ZOMBIE = createGraph(STR_GRAPH_Zombie, STR_TR_Zombie, tr("How you felt\n(0-10)")); + zombie = new SummaryChart("Zombie", GT_LINE); + zombie->setMachineType(MT_JOURNAL); + zombie->addSlice(Journal_ZombieMeter, QColor("black"), ST_SETAVG); + ZOMBIE->AddLayer(zombie); } // Recalculates Overview chart info From 2331bbba267cc795f4d08af042456f77ed905edb Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Sun, 28 Mar 2021 14:20:19 +1100 Subject: [PATCH 07/28] Don't ResetBounds when setDay is called to avoid changing overview graphs to full date range on BMI/Weight change --- oscar/Graphs/gGraph.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oscar/Graphs/gGraph.cpp b/oscar/Graphs/gGraph.cpp index 5244d6be..9c82f9b8 100644 --- a/oscar/Graphs/gGraph.cpp +++ b/oscar/Graphs/gGraph.cpp @@ -275,7 +275,9 @@ void gGraph::setDay(Day *day) } rmin_y = rmax_y = 0; - ResetBounds(); + // This resets weight and bmi overview graphs to full date range when they are changed. + // is it required ever? + // ResetBounds(); } void gGraph::setZoomY(short zoom) From a0b9488aa96d5e63bc611afe6332eb293f597d76 Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Sun, 28 Mar 2021 14:22:31 +1100 Subject: [PATCH 08/28] Save notes immediately when focus leaves notes widget. Fix saving of weight changes when using up/down arrows --- oscar/daily.cpp | 130 ++++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/oscar/daily.cpp b/oscar/daily.cpp index 84b8a86f..98e2e8d4 100644 --- a/oscar/daily.cpp +++ b/oscar/daily.cpp @@ -509,6 +509,9 @@ Daily::Daily(QWidget *parent,gGraphView * shared) connect(GraphView, SIGNAL(updateCurrentTime(double)), this, SLOT(on_LineCursorUpdate(double))); connect(GraphView, SIGNAL(updateRange(double,double)), this, SLOT(on_RangeUpdate(double,double))); connect(GraphView, SIGNAL(GraphsChanged()), this, SLOT(updateGraphCombo())); + + // Watch for focusOut events on the JournalNotes widget + ui->JournalNotes->installEventFilter(this); // qDebug() << "Finished making new Daily object"; // sleep(3); } @@ -521,9 +524,11 @@ Daily::~Daily() disconnect(sessionbar, SIGNAL(sessionClicked(Session*)), this, SLOT(doToggleSession(Session*))); disconnect(webView,SIGNAL(anchorClicked(QUrl)),this,SLOT(Link_clicked(QUrl))); + ui->JournalNotes->removeEventFilter(this); - if (previous_date.isValid()) + if (previous_date.isValid()) { Unload(previous_date); + } // Save graph orders and pin status, etc... GraphView->SaveSettings("Daily"); @@ -2206,6 +2211,9 @@ void Daily::on_JournalNotesUnderline_clicked() void Daily::on_prevDayButton_clicked() { + if (previous_date.isValid()) { + Unload(previous_date); + } if (!p_profile->ExistsAndTrue("SkipEmptyDays")) { LoadDate(previous_date.addDays(-1)); } else { @@ -2220,8 +2228,23 @@ void Daily::on_prevDayButton_clicked() } } +bool Daily::eventFilter(QObject *object, QEvent *event) +{ + if (object == ui->JournalNotes && event->type() == QEvent::FocusOut) { + // Trigger immediate save of journal when we focus out from it so we never + // lose any journal entry text... + if (previous_date.isValid()) { + Unload(previous_date); + } + } + return false; +} + void Daily::on_nextDayButton_clicked() { + if (previous_date.isValid()) { + Unload(previous_date); + } if (!p_profile->ExistsAndTrue("SkipEmptyDays")) { LoadDate(previous_date.addDays(1)); } else { @@ -2252,6 +2275,9 @@ void Daily::on_calButton_toggled(bool checked) void Daily::on_todayButton_clicked() { + if (previous_date.isValid()) { + Unload(previous_date); + } // QDate d=QDate::currentDate(); // if (d > p_profile->LastDay()) { QDate lastcpap = p_profile->LastDay(MT_CPAP); @@ -2424,21 +2450,10 @@ void Daily::on_bookmarkTable_itemChanged(QTableWidgetItem *item) void Daily::on_weightSpinBox_valueChanged(double arg1) { - // Update the BMI display - double kg; - if (p_profile->general->unitSystem()==US_English) { - kg=((arg1*pound_convert) + (ui->ouncesSpinBox->value()*ounce_convert)) / 1000.0; - } else kg=arg1; - double height=p_profile->user->height()/100.0; - if ((height>0) && (kg>0)) { - double bmi=kg/(height * height); - ui->BMI->display(bmi); - ui->BMI->setVisible(true); - ui->BMIlabel->setVisible(true); - } else { - ui->BMI->setVisible(false); - ui->BMIlabel->setVisible(false); - } + // This is called if up/down arrows are used, in which case editingFinished is + // never called. So always call editingFinished instead + Q_UNUSED(arg1); + this->on_weightSpinBox_editingFinished(); } void Daily::on_weightSpinBox_editingFinished() @@ -2457,7 +2472,25 @@ void Daily::on_weightSpinBox_editingFinished() } else { kg=arg1; } - journal->settings[Journal_Weight]=kg; + if (journal->settings.contains(Journal_Weight)) { + QVariant old = journal->settings[Journal_Weight]; + if (old == kg && kg > 0) { + // No change to weight - skip + return; + } + } else if (kg == 0) { + // Still zero - skip + return; + } + if (kg > 0) { + journal->settings[Journal_Weight]=kg; + } else { + // Weight now zero - remove from journal + auto jit = journal->settings.find(Journal_Weight); + if (jit != journal->settings.end()) { + journal->settings.erase(jit); + } + } gGraphView *gv=mainwin->getOverview()->graphView(); gGraph *g; if (gv) { @@ -2470,66 +2503,35 @@ void Daily::on_weightSpinBox_editingFinished() ui->BMI->setVisible(true); ui->BMIlabel->setVisible(true); journal->settings[Journal_BMI]=bmi; - if (gv) { - g=gv->findGraph(STR_GRAPH_BMI); - if (g) g->setDay(nullptr); - } } else { + // BMI now zero - remove it + auto jit = journal->settings.find(Journal_BMI); + if (jit != journal->settings.end()) { + journal->settings.erase(jit); + } + // And make it invisible ui->BMI->setVisible(false); ui->BMIlabel->setVisible(false); } + if (gv) { + g=gv->findGraph(STR_GRAPH_BMI); + if (g) g->setDay(nullptr); + } journal->SetChanged(true); } void Daily::on_ouncesSpinBox_valueChanged(int arg1) { - // just update for BMI display - double height=p_profile->user->height()/100.0; - double kg=((ui->weightSpinBox->value()*pound_convert) + (arg1*ounce_convert)) / 1000.0; - if ((height>0) && (kg>0)) { - double bmi=kg/(height * height); - ui->BMI->display(bmi); - ui->BMI->setVisible(true); - ui->BMIlabel->setVisible(true); - } else { - ui->BMI->setVisible(false); - ui->BMIlabel->setVisible(false); - } + // This is called if up/down arrows are used, in which case editingFinished is + // never called. So always call editingFinished instead + Q_UNUSED(arg1); + this->on_weightSpinBox_editingFinished(); } void Daily::on_ouncesSpinBox_editingFinished() { - double arg1=ui->ouncesSpinBox->value(); - Session *journal=GetJournalSession(previous_date); - if (!journal) { - journal=CreateJournalSession(previous_date); - } - double height=p_profile->user->height()/100.0; - double kg=((ui->weightSpinBox->value()*pound_convert) + (arg1*ounce_convert)) / 1000.0; - journal->settings[Journal_Weight]=kg; - - gGraph *g; - if (mainwin->getOverview()) { - g=mainwin->getOverview()->graphView()->findGraph(STR_GRAPH_Weight); - if (g) g->setDay(nullptr); - } - - if ((height>0) && (kg>0)) { - double bmi=kg/(height * height); - ui->BMI->display(bmi); - ui->BMI->setVisible(true); - ui->BMIlabel->setVisible(true); - - journal->settings[Journal_BMI]=bmi; - if (mainwin->getOverview()) { - g=mainwin->getOverview()->graphView()->findGraph(STR_GRAPH_BMI); - if (g) g->setDay(nullptr); - } - } else { - ui->BMI->setVisible(false); - ui->BMIlabel->setVisible(false); - } - journal->SetChanged(true); + // This is functionally identical to the weightSpinBox_editingFinished, so just call that + this->on_weightSpinBox_editingFinished(); } QString Daily::GetDetailsText() From 00225103fd4313096b7eeb2653a21752d62c0c85 Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Sun, 28 Mar 2021 14:22:57 +1100 Subject: [PATCH 09/28] Save notes immediately when focus leaves notes widget. --- oscar/daily.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oscar/daily.h b/oscar/daily.h index 35e9d4cb..94aad87a 100644 --- a/oscar/daily.h +++ b/oscar/daily.h @@ -304,6 +304,8 @@ private: */ void UpdateEventsTree(QTreeWidget * tree,Day *day); + virtual bool eventFilter(QObject *object, QEvent *event) override; + void updateCube(); From 3362fa4a4d1cf858c9dab91d04ad1560b9900391 Mon Sep 17 00:00:00 2001 From: Arie Klerk Date: Mon, 29 Mar 2021 20:54:36 +0200 Subject: [PATCH 10/28] We have a new translator for BULGARIAN! Here's his first update. --- Translations/Bulgarian.bg.ts | 43 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Translations/Bulgarian.bg.ts b/Translations/Bulgarian.bg.ts index 2e3b91be..d777a896 100644 --- a/Translations/Bulgarian.bg.ts +++ b/Translations/Bulgarian.bg.ts @@ -11,7 +11,7 @@ &About - &Относно + За &приложение @@ -22,12 +22,13 @@ Credits - + Заслуги GPL License - + As a whole this actually should read "Общ публичен лиценз на ГНУ", but that is a bit long on a tab. I think it would be acceptable to just say "license GPL" and then the explanation is written in the tab. The Bulgarian wikipedia page for the GPL gives a couple of translation options. + лиценз GPL @@ -37,27 +38,27 @@ Show data folder - + Покажи папката на данните About OSCAR %1 - + За предложението OSCAR %1 Sorry, could not locate About file. - + За съжаление, файлът За приложение не се намери. Sorry, could not locate Credits file. - + За съжаление, файлът Заслуги не се намери. Sorry, could not locate Release Notes. - + За съжаление, Бележки по изданието не се намери. @@ -72,12 +73,12 @@ As this is a pre-release version, it is recommended that you <b>back up your data folder manually</b> before proceeding, because attempting to roll back later may break things. - + Тъй като това е предварително издание, препоръчано е <b>да направите ръчно архивиране на своята папка с данни</b> преди да продължите, защото е възможно при по-късен опит за връщане назад нещата да се повредят. To see if the license text is available in your language, see %1. - + За да проверите дали съществува превода на лиценз на Вашия език, вижте %1. @@ -85,12 +86,12 @@ Could not find the oximeter file: - + Файлът на оксиметър не се намери: Could not open the oximeter file: - + Не може да се отвори файлът на оксиметър: @@ -108,12 +109,12 @@ Could not find the oximeter file: - + Файлът на оксиметър не се намери: Could not open the oximeter file: - + Не може да се отвори файлът на оксиметър: @@ -121,7 +122,7 @@ Checking for newer OSCAR versions - + Проверяваме за за нова версия на OSCAR @@ -191,12 +192,12 @@ I'm feeling ... - + Чувствам се ... If height is greater than zero in Preferences Dialog, setting weight here will show Body Mass Index (BMI) value - + Ако в настройки ръст е над нула, задаване на тегло тук ще се покажи стойността на индекса на телесната маса (ИТМ) @@ -211,7 +212,7 @@ Show/hide available graphs. - + Покажи или скрий достъпни графики. @@ -361,7 +362,7 @@ Unable to display Pie Chart on this system - + @@ -391,7 +392,7 @@ Sorry, this machine only provides compliance data. - + За съжаление, тази машина предоставя само данни за съответствие. @@ -441,7 +442,7 @@ <b>Please Note:</b> All settings shown below are based on assumptions that nothing has changed since previous days. - + <b>Моля, Забележете:</b>Всичките настройки, които са показани надолу, се основават на предположения, че нищо не се е променило от предишните дни. From 85b4013b2d681fcccf1ae4aec0761e39d57918f4 Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Thu, 1 Apr 2021 11:32:58 +1100 Subject: [PATCH 11/28] Fix stage & position session toggle --- oscar/daily.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/oscar/daily.cpp b/oscar/daily.cpp index 98e2e8d4..8511a362 100644 --- a/oscar/daily.cpp +++ b/oscar/daily.cpp @@ -576,6 +576,7 @@ void Daily::Link_clicked(const QUrl &url) // webView->page()->mainFrame()->setScrollBarValue(Qt::Vertical, webView->page()->mainFrame()->scrollBarMaximum(Qt::Vertical)-i); } else if (code=="toggleoxisession") { // Enable/Disable Oximetry session day=p_profile->GetDay(previous_date,MT_OXIMETER); + if (!day) return; Session *sess=day->find(sid, MT_OXIMETER); if (!sess) return; @@ -585,6 +586,20 @@ void Daily::Link_clicked(const QUrl &url) // Reload day LoadDate(previous_date); // webView->page()->mainFrame()->setScrollBarValue(Qt::Vertical, webView->page()->mainFrame()->scrollBarMaximum(Qt::Vertical)-i); + } else if (code=="togglestagesession") { // Enable/Disable Sleep Stage session + day=p_profile->GetDay(previous_date,MT_SLEEPSTAGE); + if (!day) return; + Session *sess=day->find(sid, MT_SLEEPSTAGE); + if (!sess) return; + sess->setEnabled(!sess->enabled()); + LoadDate(previous_date); + } else if (code=="togglepositionsession") { // Enable/Disable Position session + day=p_profile->GetDay(previous_date,MT_POSITION); + if (!day) return; + Session *sess=day->find(sid, MT_POSITION); + if (!sess) return; + sess->setEnabled(!sess->enabled()); + LoadDate(previous_date); } else if (code=="cpap") { day=p_profile->GetDay(previous_date,MT_CPAP); if (day) { @@ -1010,7 +1025,7 @@ QString Daily::getSessionInformation(Day * day) case MT_SLEEPSTAGE: type="stage"; html+=tr("Sleep Stage Sessions"); break; - case MT_POSITION: type="stage"; + case MT_POSITION: type="position"; html+=tr("Position Sensor Sessions"); break; @@ -1535,10 +1550,10 @@ QVariant MyTextBrowser::loadResource(int type, const QUrl &url) void Daily::Load(QDate date) { - qDebug() << "Daily::Load called for" << date.toString() << "using" << QApplication::font().toString(); + qDebug() << "Daily::Load called for" << date.toString() << "using" << QApplication::font().toString(); - qDebug() << "Setting App font in Daily::Load"; - setApplicationFont(); + qDebug() << "Setting App font in Daily::Load"; + setApplicationFont(); dateDisplay->setText(""+date.toString(Qt::SystemLocaleLongDate)+""); previous_date=date; From e358d31f269dd01bf63a420267e9d9a23f387ac1 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 1 Apr 2021 21:39:06 -0700 Subject: [PATCH 12/28] Add logic to read rolling files created by DeVilbiss BLUE CPAPs --- .../loader_plugins/intellipap_loader.cpp | 469 +++++++++++++----- 1 file changed, 332 insertions(+), 137 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index 7c6cedb8..e4a98e87 100644 --- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp +++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp @@ -607,38 +607,6 @@ int IntellipapLoader::OpenDV5(const QString & path) // May be same as what we call large leak time for other machines? //////////////////////////////////////////////////////////////////////////// -class RollingFile -{ -public: - RollingFile () { } - - ~RollingFile () { - if (data) - delete [] data; - data = nullptr; - } - - bool open (QString fn); // Open the file - bool close(); // close the file - unsigned char * get(); // read the next record in the file - - int numread () {return number_read;}; // Return number of records read - int recnum () {return record_number;}; // Return last-read record number - -private: - QString filename; - QFile file; - int record_length; - int wrap_record; - bool wrapping = false; - - int number_read = 0; // Number of records read - - int record_number = 0; // Number of record. First record in the file is #1. First record read is wrap_record; - - unsigned char * data = nullptr; -}; - struct DV6TestedModel { QString model; @@ -658,9 +626,9 @@ struct DV6_S_Data // Daily summary Session * sess; unsigned char u1; //00 (position) ***/ - unsigned int start_time; //01 - unsigned int stop_time; //05 - unsigned int atpressure_time;//09 + unsigned int start_time; //01 Start time for date + unsigned int stop_time; //05 End time + unsigned int written; //09 timestamp when this record was written EventDataType hours; //13 // EventDataType unknown14; //14 EventDataType pressureAvg; //15 @@ -796,13 +764,14 @@ PACK (struct SET_BIN_REC { // Unless explicitly noted, all other DV6_x_REC are definitions for the repeating data structure that follows the header PACK (struct DV6_HEADER { unsigned char unknown; // 0 always zero - unsigned char filetype; // 1 always "R" + unsigned char filetype; // 1 e.g. "R" for a R.BIN file unsigned char serial[11]; // 2 serial number - unsigned char numRecords[4]; // 13 Number of records in file (always 180,000) + unsigned char numRecords[4]; // 13 Number of records in file (always fixed, 180,000 for R.BIN) unsigned char recordLength; // 17 Length of data record (always 117) unsigned char recordStart[4]; // 18 First record in wrap-around buffer unsigned char unknown_22[21]; // 22 Unknown values - unsigned char unknown_43[12]; // 43 Seems always to be zero + unsigned char unknown_43[8]; // 43 Seems always to be zero + unsigned char lasttime[4]; // 51 OSCAR only: Last timestamp, in history files only unsigned char checksum; // 55 Checksum }); @@ -902,6 +871,20 @@ struct DV6_SessionInfo { CPAPMode mode = MODE_UNKNOWN; }; +QString card_path; +QString backup_path; +QString history_path; + +MachineInfo info; +Machine * mach = nullptr; + +bool rebuild_from_backups = false; +bool create_backups = false; + +QMap DailySummaries; +QMap SessionData; +SET_BIN_REC * settings; + unsigned int ep = 0; // Convert a 4-character number in DV6 data file to a standard int @@ -918,41 +901,199 @@ unsigned int convertTime (unsigned char time[]) { return ((time[3] << 24) + (time[2] << 16) + (time[1] << 8) + time[0]) + ep; // Time as Unix epoch time } -bool RollingFile::open(QString fn) { +class RollingBackup +{ +public: + RollingBackup () {} + ~RollingBackup () { + } - filename = fn; - file.setFileName(filename); + bool open (const QString filetype, DV6_HEADER * newhdr); // Open the file + bool close(); // close the file + bool save(QByteArray dataBA); // save the next record in the file + +private: + DV6_HEADER hdr; // file header + QString filetype; + QFile hFile; + + int record_length; // Length of record block in incoming file + const int maxHistFileSize = 20*10e6; // Maximum size of file before we create a new file + + int numWritten; // Number of records written + quint32 lastTimestamp; +}; + +bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) { + if (!create_backups) + return true; + + this->filetype = filetype; + + QDir hpath(history_path); + QStringList filters; + + numWritten = 0; + + filters.append(filetype); + filters[0].insert(1, "_*"); + hpath.setNameFilters(filters); + hpath.setFilter(QDir::Files); + hpath.setSorting(QDir::Name | QDir::Reversed); + + QStringList fileNames = hpath.entryList(); // Get list of files + QFile histfile(fileNames.first()); + +// bool needNewFile = false; + + // Handle first time a history file is being created + if (fileNames.isEmpty()) { + memcpy (&hdr, newhdr, sizeof(DV6_HEADER)); + for (int i = 0; i < 4; i++) { + hdr.recordStart[i] = 0; + hdr.lasttime[i] = 0; + } + record_length = hdr.recordLength; + } + + // We have an existing history record + if (! fileNames.isEmpty()) { + // See if this file is large enough that we want to create a new file + if (histfile.size() > maxHistFileSize) { + memcpy (&hdr, newhdr, sizeof(DV6_HEADER)); + for (int i = 0; i < 4; i++) + hdr.recordStart[i] = 0; + + if (!histfile.open(QIODevice::ReadOnly)) { + qWarning() << "DV6 RollingBackup could not open" << fileNames.first() << "for reading, error code" << histfile.error() << histfile.errorString(); + return false; + } + record_length = hdr.recordLength; + +#ifdef ROLLBACKUP + wrap_record = convertNum(hdr.recordStart); + if (!histfile.seek(sizeof(DV6_HEADER) + (wrap_record-1) * record_length)) { + qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString(); + file.close(); + return false; + } +#endif + + } + } + + return true; +} + +bool RollingBackup::close() { + if (!create_backups) + return true; + return true; +} + +bool RollingBackup::save(QByteArray dataBA) { + Q_UNUSED(dataBA) + if (!create_backups) + return true; + return true; +} + +class RollingFile +{ +public: + RollingFile () { } + + ~RollingFile () { + if (data) + delete [] data; + data = nullptr; + if (hdr) + delete hdr; + hdr = nullptr; + } + + bool open (QString fn); // Open the file + bool close(); // close the file + unsigned char * get(); // read the next record in the file + + int numread () {return number_read;}; // Return number of records read + int recnum () {return record_number;}; // Return last-read record number + + RollingBackup rb; + +private: + QString filename; + QFile file; + int record_length; + int wrap_record; + bool wrapping = false; + + int number_read = 0; // Number of records read + + int record_number = 0; // Number of record. First record in the file is #1. First record read is wrap_record; + + DV6_HEADER * hdr; // file header + + unsigned char * data = nullptr; // record pointer +}; + +bool RollingFile::open(QString filetype) { + + filename = filetype; + file.setFileName(card_path + "/" +filetype); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "DV6 RollingFile could not open" << filename << "for reading, error code" << file.error() << file.errorString(); return false; } + // Save header for use in making backups of data + hdr = new DV6_HEADER; QByteArray dataBA = file.read(sizeof(DV6_HEADER)); - DV6_HEADER * hdr = (DV6_HEADER *) dataBA.data(); + memcpy (hdr, dataBA.data(), sizeof(DV6_HEADER)); + + // Extract control information from header record_length = hdr->recordLength; wrap_record = convertNum(hdr->recordStart); record_number = wrap_record; number_read = 0; wrapping = false; + // Create buffer to hold each record as it is read data = new unsigned char[record_length]; + // Seek to first data record in file if (!file.seek(sizeof(DV6_HEADER) + wrap_record * record_length)) { qWarning() << "DV6 RollingFile unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString(); file.close(); return false; } +#ifdef ROLLBACKUP + if (!rb.open(filetype, hdr)) { + qWarning() << "DV6 RollingBackup failed"; + file.close(); + return false; + } +#endif - qDebug() << "RollingFile opening" << filename << "at wrap record" << wrap_record; + qDebug() << "DV6 RollingFile opening" << filename << "at wrap record" << wrap_record; return true; } bool RollingFile::close() { file.close(); - if (data != nullptr) + +#ifdef ROLLBACKUP + rb.close(); +#endif + + if (data) delete [] data; data = nullptr; + if (hdr) + delete hdr; + hdr = nullptr; + return true; } @@ -987,6 +1128,11 @@ unsigned char * RollingFile::get() { file.close(); return nullptr; } +#ifdef ROLLBACKUP + if (!rb.save(dataBA)) { + qWarning() << "DV6 RollingBackup failed"; + } +#endif number_read++; @@ -995,21 +1141,51 @@ unsigned char * RollingFile::get() { return data; } -MachineInfo info; -Machine * mach = nullptr; +// Returns empty QByteArray() on failure. +QByteArray fileChecksum(const QString &fileName, + QCryptographicHash::Algorithm hashAlgorithm) +{ + QFile f(fileName); + if (f.open(QFile::ReadOnly)) { + QCryptographicHash hash(hashAlgorithm); + bool res = hash.addData(&f); + f.close(); + if (res) { + return hash.result(); + } + } + return QByteArray(); +} -bool rebuild_from_backups = false; +/*** +// Return the OSCAR date that the last data was written. +// This will be considered to be the last day for which we have any data. +// Adjust to get the correct date for sessions starting after midnight. +QDate getLastDate () { + return QDate(); +} +***/ -QMap DailySummaries; -QMap SessionData; -SET_BIN_REC * settings; +// Return date used within OSCAR, assuming day ends at split time in preferences (usually noon) +QDate getNominalDate (QDateTime dt) { + QDate d = dt.date(); + QTime tm = dt.time(); + QTime daySplitTime = p_profile->session->getPref(STR_IS_DaySplitTime).toTime(); + if (tm < daySplitTime) + d = d.addDays(-1); + return d; +} +QDate getNominalDate (unsigned int dt) { + QDateTime xdt = QDateTime::fromSecsSinceEpoch(dt); + return getNominalDate(xdt); +} /////////////////////////////////////////////// // U.BIN - Open and parse session list and create session data structures // with session start and stop times. /////////////////////////////////////////////// -bool load6Sessions (const QString & path) { +bool load6Sessions () { RollingFile rf; unsigned int ts1,ts2; @@ -1018,7 +1194,7 @@ bool load6Sessions (const QString & path) { qDebug() << "Parsing U.BIN"; - if (!rf.open(path+"/U.BIN")) { + if (!rf.open("U.BIN")) { qWarning() << "Unable to open U.BIN"; return false; } @@ -1079,12 +1255,12 @@ bool load6Settings (const QString & path) { // S.BIN - Open and load day summary list //////////////////////////////////////////////////////////////////////////////////////// -bool load6DailySummaries (const QString & path) { +bool load6DailySummaries () { RollingFile rf; DailySummaries.clear(); - if (!rf.open(path+"/S.BIN")) { + if (!rf.open("S.BIN")) { qWarning() << "Unable to open S.BIN"; return false; } @@ -1100,7 +1276,13 @@ bool load6DailySummaries (const QString & path) { dailyData.start_time = convertTime(rec->begin); dailyData.stop_time = convertTime(rec->end); - dailyData.atpressure_time = convertTime(rec->written); + dailyData.written = convertTime(rec->written); + +#ifdef DEBUG6 + qDebug() << "DV6 S.BIN start" << dailyData.start_time + << "stop" << dailyData.stop_time + << "at pressure?" << dailyData.atpressure_time; +#endif dailyData.hours = float(rec->hours) / 10.0F; dailyData.pressureSetMin = float(rec->pressureSetMin) / 10.0F; @@ -1135,6 +1317,26 @@ bool load6DailySummaries (const QString & path) { DailySummaries[dailyData.start_time] = dailyData; +/**** Previous loader did this: + if (!mach->sessionlist.contains(ts1)) { // Check if already imported + qDebug() << "Detected new Session" << ts1; + R.sess = new Session(mach, ts1); + R.sess->SetChanged(true); + + R.sess->really_set_first(qint64(ts1) * 1000L); + R.sess->really_set_last(qint64(ts2) * 1000L); + + if (data[49] != data[50]) { + R.sess->settings[CPAP_PressureMin] = R.pressureSetMin; + R.sess->settings[CPAP_PressureMax] = R.pressureSetMax; + R.sess->settings[CPAP_Mode] = MODE_APAP; + } else { + R.sess->settings[CPAP_Mode] = MODE_CPAP; + R.sess->settings[CPAP_Pressure] = R.pressureSetMin; + } + R.hasMaskPressure = false; +***/ + } while (true); rf.close(); @@ -1293,14 +1495,14 @@ int create6Sessions() { // Parse R.BIN for high resolution flow data //////////////////////////////////////////////////////////////////////////////////////// -bool load6HighResData (const QString & path) { +bool load6HighResData () { RollingFile rf; Session *sess = nullptr; unsigned int rec_ts1, previousRecBegin = 0; bool inSession = false; // true if we are adding data to this session - if (!rf.open(path+"/R.BIN")) { + if (!rf.open("R.BIN")) { qWarning() << "DV6 Unable to open R.BIN"; return false; } @@ -1806,14 +2008,14 @@ bool load6HighResData (const QString & path) { // Parse L.BIN for per minute data //////////////////////////////////////////////////////////////////////////////////////// -bool load6PerMinute (const QString & path) { +bool load6PerMinute () { RollingFile rf; Session *sess = nullptr; unsigned int rec_ts1, previousRecBegin = 0; bool inSession = false; // true if we are adding data to this session - if (!rf.open(path+"/L.BIN")) { + if (!rf.open("L.BIN")) { qWarning() << "DV6 Unable to open L.BIN"; return false; } @@ -1960,7 +2162,7 @@ bool load6PerMinute (const QString & path) { // Parse E.BIN for event data //////////////////////////////////////////////////////////////////////////////////////// -bool load6EventData (const QString & path) { +bool load6EventData () { RollingFile rf; Session *sess = nullptr; @@ -1977,7 +2179,7 @@ bool load6EventData (const QString & path) { EventList * SN = nullptr; EventList * FL = nullptr; - if (!rf.open(path+"/E.BIN")) { + if (!rf.open("E.BIN")) { qWarning() << "DV6 Unable to open E.BIN"; return false; } @@ -2170,63 +2372,20 @@ int addSessions() { } -// Returns empty QByteArray() on failure. -QByteArray fileChecksum(const QString &fileName, - QCryptographicHash::Algorithm hashAlgorithm) -{ - QFile f(fileName); - if (f.open(QFile::ReadOnly)) { - QCryptographicHash hash(hashAlgorithm); - if (hash.addData(&f)) { - return hash.result(); - } - } - return QByteArray(); -} - -/**** -// Return the OSCAR date that the last data was written. -// This will be considered to be the last day for which we have any data. -// Adjust to get the correct date for sessions starting after midnight. -QDate getLastDate () { - return QDate(); -} - -// Return date used within OSCAR, assuming day ends at noon -QDate getOscarDate (QDateTime dt) { - QDate d = dt.date(); - QTime tm = dt.time(); - if (tm.hour() < 11) - d = d.addDays(-1); - return d; -} -***/ - //////////////////////////////////////////////////////////////////////////////////////// // Create backup of input files -// Create dated backup files when necesaary +// Create dated backup of settings file if changed //////////////////////////////////////////////////////////////////////////////////////// bool backup6 (const QString & path) { - // Are backups enabled? - if (!p_profile->session->backupCardData()) + if (rebuild_from_backups || !create_backups) return true; - QString backup_path = mach->getBackupPath(); - QString history_path = backup_path + "/DV6/HISTORY"; - - // Compare QDirs rather than QStrings because separators may be different, especially on Windows. - // We want to check whether import and backup paths are the same, regardless of variations in the string representations. QDir ipath(path); + QDir cpath(card_path); QDir bpath(backup_path); - if (ipath == bpath) { - // Don't create backups if importing from backup folder - rebuild_from_backups = true; - return true; - } - if ( ! bpath.exists()) { if ( ! bpath.mkpath(backup_path) ) { qWarning() << "Could not create DV6 backup directory" << backup_path; @@ -2249,67 +2408,103 @@ bool backup6 (const QString & path) { bool backup_settings = true; QStringList filters; - filters << "set_*.bin"; + + QFile settingsFile; + QString inputFile = cpath.absolutePath() + "/SET.BIN"; + settingsFile.setFileName(inputFile); + + filters << "SET_*.BIN"; hpath.setNameFilters(filters); hpath.setFilter(QDir::Files); - QDir::Name | QDir::Reversed; + hpath.setSorting(QDir::Name | QDir::Reversed); QStringList fileNames = hpath.entryList(); // Get list of files if (! fileNames.isEmpty()) { QString lastFile = fileNames.first(); - QString newFile = ipath.absolutePath() + "/set.bin"; - qDebug() << "last settings file is" << lastFile << "new file is" << newFile; - QByteArray newMD5 = fileChecksum(newFile, QCryptographicHash::Md5); - QByteArray oldMD5 = fileChecksum(lastFile, QCryptographicHash::Md5); + qDebug() << "last settings file is" << lastFile << "new file is" << settingsFile; + QByteArray newMD5 = fileChecksum(settingsFile.fileName(), QCryptographicHash::Md5); + QByteArray oldMD5 = fileChecksum(hpath.absolutePath()+"/"+lastFile, QCryptographicHash::Md5); if (newMD5 == oldMD5) backup_settings = false; } - if (backup_settings) { - QString newFile = hpath.absolutePath() + "/set-" + "1234" + ".bin"; - qDebug() << "history filename is" << newFile; + if (backup_settings && !DailySummaries.isEmpty()) { + DV6_S_Data ds = DailySummaries.last(); + QString newFile = hpath.absolutePath() + "/SET_" + getNominalDate(ds.start_time).toString("yyyyMMdd") + ".BIN"; + if (!settingsFile.copy(inputFile, newFile)) { + qWarning() << "DV6 backup could not copy" << inputFile << "to" << newFile << ", error code" << settingsFile.error() << settingsFile.errorString(); + } } // We're done! return true; } +//////////////////////////////////////////////////////////////////////////////////////// +// Initialize DV6 environment +//////////////////////////////////////////////////////////////////////////////////////// + +bool init6Environment (const QString & path) { + + // Create Machine database record if it doesn't exist already + mach = p_profile->CreateMachine(info); + if (mach == nullptr) { + qWarning() << "Could not create DV6 Machine data structure"; + return false; + } + + backup_path = mach->getBackupPath(); + history_path = backup_path + "/HISTORY"; + + // Compare QDirs rather than QStrings because separators may be different, especially on Windows. + QDir ipath(path); + QDir bpath(backup_path); + + if (ipath == bpath) { + // Don't create backups if importing from backup folder + rebuild_from_backups = true; + create_backups = false; + } else { + rebuild_from_backups = false; + create_backups = p_profile->session->backupCardData(); + } + + return true; +} + //////////////////////////////////////////////////////////////////////////////////////// // Open a DV6 SD card, parse everything, add to OSCAR database //////////////////////////////////////////////////////////////////////////////////////// int IntellipapLoader::OpenDV6(const QString & path) { - QString newpath = path + DV6_DIR; + card_path = path + DV6_DIR; - // Prime the machine database's info field with stuff relevant to this machine + // 1. Prime the machine database's info field with this machine info = newInfo(); - // VER.BIN - Parse model number, serial, etc. - if (!load6VersionInfo(newpath)) + // 2. VER.BIN - Parse model number, serial, etc. into info structure + if (!load6VersionInfo(card_path)) return -1; - // Now, create Machine database record if it doesn't exist already - mach = p_profile->CreateMachine(info); - if (mach == nullptr) { - qWarning() << "Could not create Machine data structure"; - return -1; - } - - // SET.BIN - Parse settings file (which is only the latest settings) - if (!load6Settings(newpath)) + // 3. Initialize rest of the DV6 loader environment + if (!init6Environment (path)) return -1; - // S.BIN - Open and parse day summary list and create a list of days - if (!load6DailySummaries(newpath)) + // 4. SET.BIN - Parse settings file (which is only the latest settings) + if (!load6Settings(card_path)) return -1; - // Back up data files (must do after parsing VER.BIN, S.BIN, and creating Machine) + // 5. S.BIN - Open and parse day summary list and create a list of days + if (!load6DailySummaries()) + return -1; + + // 6. Back up data files (must do after parsing VER.BIN, S.BIN, and creating Machine) if (!backup6(path)) return -1; - // U.BIN - Open and parse session list and create a list of session times + // 7. U.BIN - Open and parse session list and create a list of session times // (S.BIN must already be loaded) - if (!load6Sessions(newpath)) + if (!load6Sessions()) return -1; // Create OSCAR session list from session times and summary data @@ -2317,15 +2512,15 @@ int IntellipapLoader::OpenDV6(const QString & path) return -1; // R.BIN - Open and parse flow data - if (!load6HighResData(newpath)) + if (!load6HighResData()) return -1; // L.BIN - Open and parse per minute data - if (!load6PerMinute(newpath)) + if (!load6PerMinute()) return -1; // E.BIN - Open and parse event data - if (!load6EventData(newpath)) + if (!load6EventData()) return -1; // Finalize input From e699ea182d435794ca1633102b3c7f71c2b1ef50 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Fri, 2 Apr 2021 17:17:50 -0700 Subject: [PATCH 13/28] Update version checking to allow QT 6 --- OSCAR_QT.pro | 14 ++++++++++---- oscar/oscar.pro | 16 +++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/OSCAR_QT.pro b/OSCAR_QT.pro index df4e72bb..a9aa515c 100644 --- a/OSCAR_QT.pro +++ b/OSCAR_QT.pro @@ -1,7 +1,13 @@ -lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,9) { - message("You need to Qt 5.9 or newer to build OSCAR with Help Pages") - lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,7) { - error("You need Qt 5.7 or newer to build OSCAR") +lessThan(QT_MAJOR_VERSION,5) { + error("You need Qt 5.7 or newer to build OSCAR"); +} + +if (equals(QT_MAJOR_VERSION,5)) { + lessThan(QT_MINOR_VERSION,9) { + message("You need Qt 5.9 to build OSCAR with Help Pages") + } + lessThan(QT_MINOR_VERSION,7) { + error("You need Qt 5.7 or newer to build OSCAR"); } } diff --git a/oscar/oscar.pro b/oscar/oscar.pro index eb704b56..b2f8e977 100644 --- a/oscar/oscar.pro +++ b/oscar/oscar.pro @@ -6,14 +6,20 @@ message(Platform is $$QMAKESPEC ) -lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,9) { - message("You need Qt 5.9 to build OSCAR with Help Pages") - DEFINES += helpless -} -lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,7) { +lessThan(QT_MAJOR_VERSION,5) { error("You need Qt 5.7 or newer to build OSCAR"); } +if (equals(QT_MAJOR_VERSION,5)) { + lessThan(QT_MINOR_VERSION,9) { + message("You need Qt 5.9 to build OSCAR with Help Pages") + DEFINES += helpless + } + lessThan(QT_MINOR_VERSION,7) { + error("You need Qt 5.7 or newer to build OSCAR"); + } +} + # get rid of the help browser, at least for now DEFINES += helpless From e3a87a31481d36e3a7365ddbb6bc9c2fe9c46d6c Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Sun, 4 Apr 2021 04:56:06 -0700 Subject: [PATCH 14/28] Update release notes with recent changes. --- Htmldocs/release_notes.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index e2dd1c9b..304a8ca6 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -12,16 +12,20 @@
http://www.apneaboard.com/wiki/index.php/OSCAR_Release_Notes

Changes and fixes in OSCAR v1.X.Y -
Portions of OSCAR are © 2019-2020 by +
Portions of OSCAR are © 2019-2021 by The OSCAR Team

    +
  • [new] Add support for DeVilbiss BLUE (DV6x) CPAP machines [experimental].
  • [new] Additional Philips Respironics devices tested and fully supported:
    • DreamStation Go Auto (500G120)
    • DreamStation Auto CPAP with A-Flex (500X140)
    • +
    • DreamStation BiPAP AVAPS 30 (1130X200)
  • [new] Add support for DreamStation Go humidifier Target Time setting.
  • +
  • [new] Add Bulgarian translation; update other languages.
  • +
  • [new] Improve Somnopose import options.
  • [fix] Correct calculation of average leak rate on Welcome page.
  • [fix] Correct installation of non-English Release Notes on Windows.
  • [fix] About/Credits page now offers Google translations to other languages.
  • @@ -33,6 +37,10 @@
  • [fix] Purge currently selected day no longer deletes bookmarks for that day.
  • [fix] Remove warning from Chromebook when importing from previously used local folder.
  • [fix] Update link to Contec drivers.
  • +
  • [fix] Fix display problems for short duration events.
  • +
  • [fix] Statistics headings will now be 99.5% or Max, depending on machine type and preference settings.
  • +
  • [fix] Mark exported Journal backup file as UTF-8.
  • +
  • [fix] Improve error message when unable to access OSCAR database.

Changes and fixes in OSCAR v1.2.0 From 3114cf517d00b59b0664d510e73193ea220d681e Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Sun, 4 Apr 2021 04:56:39 -0700 Subject: [PATCH 15/28] Improve file identification in Icon loader Will no longer attempt to process SleepStyle data with Icon loader A new loader for SleepStyle machines is forthcoming. --- oscar/SleepLib/loader_plugins/icon_loader.cpp | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/oscar/SleepLib/loader_plugins/icon_loader.cpp b/oscar/SleepLib/loader_plugins/icon_loader.cpp index f2ad2963..097d1e85 100644 --- a/oscar/SleepLib/loader_plugins/icon_loader.cpp +++ b/oscar/SleepLib/loader_plugins/icon_loader.cpp @@ -66,6 +66,26 @@ bool FPIconLoader::Detect(const QString & givenpath) return false; } + // ICON serial numbers (directory names) are all digits (SleepStyle are mixed alpha and numeric) + QString serialDir(dir.path() + "/FPHCARE/ICON"); + QDir iconDir(serialDir); + + iconDir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); + iconDir.setSorting(QDir::Name); + QFileInfoList flist = iconDir.entryInfoList(); + + bool ok; + + for (int i = 0; i < flist.size(); i++) { + QFileInfo fi = flist.at(i); + QString filename = fi.fileName(); + + filename.toInt(&ok); + + if (!ok) { + return false; + } + } return true; } From 94faff5297d9671def8762790813a59129ef135b Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Mon, 5 Apr 2021 12:05:05 +1000 Subject: [PATCH 16/28] Allow purge current day for all machine types --- oscar/mainwindow.cpp | 100 ++++++++++++++++++++++++++++++++----------- oscar/mainwindow.h | 6 +++ oscar/mainwindow.ui | 54 ++++++++++++++++++++--- 3 files changed, 129 insertions(+), 31 deletions(-) diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index 390fed99..f49bc6ef 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -1805,51 +1805,101 @@ void MainWindow::RestartApplication(bool force_login, QString cmdline) } void MainWindow::on_actionPurge_Current_Day_triggered() +{ + this->purgeDay(MT_CPAP); +} + +void MainWindow::on_actionPurgeCurrentDayOximetry_triggered() +{ + this->purgeDay(MT_OXIMETER); +} + +void MainWindow::on_actionPurgeCurrentDaySleepStage_triggered() +{ + this->purgeDay(MT_SLEEPSTAGE); +} + +void MainWindow::on_actionPurgeCurrentDayPosition_triggered() +{ + this->purgeDay(MT_POSITION); +} + +void MainWindow::on_actionPurgeCurrentDayAllExceptNotes_triggered() +{ + this->purgeDay(MT_UNKNOWN); +} + +void MainWindow::on_actionPurgeCurrentDayAll_triggered() +{ + this->purgeDay(MT_JOURNAL); +} + +// Purge data for a given machine type. +// Special handling: MT_JOURNAL == All data. MT_UNKNOWN == All except journal +void MainWindow::purgeDay(MachineType type) { if (!daily) return; QDate date = daily->getDate(); - qDebug() << "Purging CPAP data from" << date; + qDebug() << "Purging data from" << date; daily->Unload(date); - Day *day = p_profile->GetDay(date, MT_CPAP); + Day *day = p_profile->GetDay(date, MT_UNKNOWN); Machine *cpap = nullptr; - if (day) - cpap = day->machine(MT_CPAP); + if (!day) + return; - if (cpap) { - QList::iterator s; + QList::iterator s; - QList list; - for (s = day->begin(); s != day->end(); ++s) { - Session *sess = *s; + QList list; + for (s = day->begin(); s != day->end(); ++s) { + Session *sess = *s; + if (type == MT_JOURNAL || (type == MT_UNKNOWN && sess->type() != MT_JOURNAL) || + sess->type() == type) { + list.append(*s); + qDebug() << "Purging session from " << (*s)->machine()->loaderName() << " ID:" << (*s)->session() << "["+QDateTime::fromTime_t((*s)->session()).toString()+"]"; + qDebug() << "First Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realFirst()).toString(); + qDebug() << "Last Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realLast()).toString(); if (sess->type() == MT_CPAP) { - list.append(*s); - qDebug() << "Purging session ID:" << (*s)->session() << "["+QDateTime::fromTime_t((*s)->session()).toString()+"]"; - qDebug() << "First Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realFirst()).toString(); - qDebug() << "Last Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realLast()).toString(); + cpap = day->machine(MT_CPAP); } + } else { + qDebug() << "Skipping session from " << (*s)->machine()->loaderName() << " ID:" << (*s)->session() << "["+QDateTime::fromTime_t((*s)->session()).toString()+"]"; + } + } + + if (list.size() > 0) { + if (cpap) { + QFile rxcache(p_profile->Get("{" + STR_GEN_DataFolder + "}/RXChanges.cache" )); + rxcache.remove(); + + QFile sumfile(cpap->getDataPath()+"Summaries.xml.gz"); + sumfile.remove(); } - QFile rxcache(p_profile->Get("{" + STR_GEN_DataFolder + "}/RXChanges.cache" )); - rxcache.remove(); - - QFile sumfile(cpap->getDataPath()+"Summaries.xml.gz"); - sumfile.remove(); - // m->day.erase(m->day.find(date)); - + QSet machines; for (int i = 0; i < list.size(); i++) { Session *sess = list.at(i); + machines += sess->machine(); sess->Destroy(); // remove the summary and event files delete sess; } - // save purge date where later import should start - QDate pd = cpap->purgeDate(); - if (pd.isNull() || day->date() < pd) - cpap->setPurgeDate(day->date()); + for (auto & mach : machines) { + mach->SaveSummaryCache(); + } + + if (cpap) { + // save purge date where later import should start + QDate pd = cpap->purgeDate(); + if (pd.isNull() || day->date() < pd) + cpap->setPurgeDate(day->date()); + } + } else { + // No data purged... could notify user? + return; } - day = p_profile->GetDay(date, MT_CPAP); + day = p_profile->GetDay(date, MT_UNKNOWN); Q_UNUSED(day); daily->clearLastDay(); diff --git a/oscar/mainwindow.h b/oscar/mainwindow.h index 93834d6b..d2352277 100644 --- a/oscar/mainwindow.h +++ b/oscar/mainwindow.h @@ -268,6 +268,11 @@ class MainWindow : public QMainWindow //! \brief Destroy the CPAP data for the currently selected day, so it can be freshly imported again void on_actionPurge_Current_Day_triggered(); + void on_actionPurgeCurrentDayOximetry_triggered(); + void on_actionPurgeCurrentDaySleepStage_triggered(); + void on_actionPurgeCurrentDayPosition_triggered(); + void on_actionPurgeCurrentDayAllExceptNotes_triggered(); + void on_actionPurgeCurrentDayAll_triggered(); void on_action_Sidebar_Toggle_toggled(bool arg1); @@ -372,6 +377,7 @@ private: QList selectCPAPDataCards(const QString & prompt); void importCPAPDataCards(const QList & datacards); void addMachineToMenu(Machine* mach, QMenu* menu); + void purgeDay(MachineType type); // QString getWelcomeHTML(); void FreeSessions(); diff --git a/oscar/mainwindow.ui b/oscar/mainwindow.ui index 1b68d8c9..10d6b4ea 100644 --- a/oscar/mainwindow.ui +++ b/oscar/mainwindow.ui @@ -2903,7 +2903,19 @@ p, li { white-space: pre-wrap; } Purge ALL Machine Data - + + + Purge &Current Selected Day + + + + + + + + + + @@ -3084,11 +3096,6 @@ p, li { white-space: pre-wrap; } Change &User - - - Purge &Current Selected Day - - true @@ -3318,6 +3325,41 @@ p, li { white-space: pre-wrap; } true + + + Purge Current Selected Day + + + + + &CPAP + + + + + &Oximetry + + + + + &Sleep Stage + + + + + &Position + + + + + &All except Notes + + + + + All including &Notes + + From 113d592af399b284888a4f073c69f8548fdaf36c Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Mon, 5 Apr 2021 12:12:16 +1000 Subject: [PATCH 17/28] Allow purge current day for all machine types --- Htmldocs/release_notes.html | 1 + 1 file changed, 1 insertion(+) diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index e2dd1c9b..c5b72957 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -33,6 +33,7 @@

  • [fix] Purge currently selected day no longer deletes bookmarks for that day.
  • [fix] Remove warning from Chromebook when importing from previously used local folder.
  • [fix] Update link to Contec drivers.
  • +
  • [new] Purge Current Selected Day allows purge of each machine type separately
  • Changes and fixes in OSCAR v1.2.0 From 889bc8842c8c6c11e7625d54f804ff79056c616c Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Fri, 9 Apr 2021 14:03:37 +1000 Subject: [PATCH 18/28] Logger.cpp: Add #define to allow all debug messages to be seen I have DEFINES+=HARDLOG set up in my QT Debug configurations so I can always see messages in the event of a crash. --- oscar/logger.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oscar/logger.cpp b/oscar/logger.cpp index 6594f3e6..e3f4d108 100644 --- a/oscar/logger.cpp +++ b/oscar/logger.cpp @@ -70,7 +70,9 @@ void initializeLogger() s_LoggerRunning.lock(); // wait until the thread begins running s_LoggerRunning.unlock(); // we no longer need the lock } +#ifndef HARDLOG qInstallMessageHandler(MyOutputHandler); // NOTE: comment this line out when debugging a crash, otherwise the deferred output will mislead you. +#endif if (b) { qDebug() << "Started logging thread"; } else { From 4234cb34a46f29d3119ac87ae0d8f20873f181de Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Fri, 9 Apr 2021 14:05:54 +1000 Subject: [PATCH 19/28] Fix problem with truncated per-minute data in graphs. --- oscar/SleepLib/loader_plugins/intellipap_loader.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index e4a98e87..c40357ff 100644 --- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp +++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp @@ -89,6 +89,7 @@ int IntellipapLoader::OpenDV5(const QString & path) QString newpath = path + SL_DIR; QString filename; + qDebug() << "DV5 Loader started"; ////////////////////////// // Parse the Settings File @@ -1281,7 +1282,7 @@ bool load6DailySummaries () { #ifdef DEBUG6 qDebug() << "DV6 S.BIN start" << dailyData.start_time << "stop" << dailyData.stop_time - << "at pressure?" << dailyData.atpressure_time; + << "written" << dailyData.written; #endif dailyData.hours = float(rec->hours) / 10.0F; @@ -2050,18 +2051,18 @@ bool load6PerMinute () { << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1; continue; } - +/**** // Look for a gap in DV6_L records. They should be at one minute intervals. // If there is a gap, we are probably in a new session if (inSession && ((rec_ts1 - previousRecBegin) > 60)) { -// qDebug() << "L.BIN record gap, current" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") -// << "previous" << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss"); + qDebug() << "L.BIN record gap, current" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") + << "previous" << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss"); sess->set_last(maxleak->last()); sess = nullptr; leak = maxleak = MV = TV = RR = Pressure = nullptr; inSession = false; } - +****/ // Skip over sessions until we find one that this record is in while (rec_ts1 > sinfo->end) { #ifdef DEBUG6 @@ -2477,6 +2478,7 @@ bool init6Environment (const QString & path) { int IntellipapLoader::OpenDV6(const QString & path) { + qDebug() << "DV6 loader started"; card_path = path + DV6_DIR; // 1. Prime the machine database's info field with this machine From bbfa4aed6bd11c8b357315b0513ed0ef7dec95e6 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Fri, 9 Apr 2021 14:08:30 +1000 Subject: [PATCH 20/28] Update release notes for latest changes (nothing important here) --- Htmldocs/release_notes.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index b20a2275..24370a09 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -15,7 +15,7 @@
    Portions of OSCAR are © 2019-2021 by The OSCAR Team

      -
    • [new] Add support for DeVilbiss BLUE (DV6x) CPAP machines [experimental].
    • +
    • [new] Add support for DeVilbiss BLUE (DV6x) CPAP machines.
    • [new] Additional Philips Respironics devices tested and fully supported:
      • DreamStation Go Auto (500G120)
      • @@ -26,6 +26,7 @@
      • [new] Add support for DreamStation Go humidifier Target Time setting.
      • [new] Add Bulgarian translation; update other languages.
      • [new] Improve Somnopose import options.
      • +
      • [new] Purge Current Selected Day allows purge of each machine type separately
      • [fix] Correct calculation of average leak rate on Welcome page.
      • [fix] Correct installation of non-English Release Notes on Windows.
      • [fix] About/Credits page now offers Google translations to other languages.
      • @@ -41,7 +42,6 @@
      • [fix] Statistics headings will now be 99.5% or Max, depending on machine type and preference settings.
      • [fix] Mark exported Journal backup file as UTF-8.
      • [fix] Improve error message when unable to access OSCAR database.
      • -
      • [new] Purge Current Selected Day allows purge of each machine type separately

      Changes and fixes in OSCAR v1.2.0 From 037b13c73c8f138cd4f612321fb884ae6126408a Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Tue, 13 Apr 2021 17:47:15 -0600 Subject: [PATCH 21/28] Adjust labels on Daily and Overview pages to show 99.5% instead of Max when max is not used Note that "Max" on the Statistics page really is the "max" and not 99.5% --- oscar/SleepLib/day.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oscar/SleepLib/day.cpp b/oscar/SleepLib/day.cpp index a752b6cf..a18f657a 100644 --- a/oscar/SleepLib/day.cpp +++ b/oscar/SleepLib/day.cpp @@ -182,7 +182,7 @@ QString Day::calcMiddleLabel(ChannelID code) } QString Day::calcMaxLabel(ChannelID code) { - return QObject::tr("%1 %2").arg(p_profile->general->prefCalcMax() ? QObject::tr("Peak") : STR_TR_Max).arg(schema::channel[code].label()); + return QObject::tr("%1 %2").arg(p_profile->general->prefCalcMax() ? QObject::tr("99.5%") : STR_TR_Max).arg(schema::channel[code].label()); } QString Day::calcPercentileLabel(ChannelID code) { From 5a64262ff6d4fcef583717798f760a919a33f7e1 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Tue, 13 Apr 2021 17:48:39 -0600 Subject: [PATCH 22/28] Minor fix to #ifdefs in DV6 loader --- oscar/SleepLib/loader_plugins/intellipap_loader.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index c40357ff..4a5a4717 100644 --- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp +++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp @@ -923,12 +923,14 @@ private: int numWritten; // Number of records written quint32 lastTimestamp; + unsigned int wrap_record; }; bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) { if (!create_backups) return true; +#ifdef ROLLBACKUP this->filetype = filetype; QDir hpath(history_path); @@ -971,17 +973,20 @@ bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) { } record_length = hdr.recordLength; -#ifdef ROLLBACKUP wrap_record = convertNum(hdr.recordStart); if (!histfile.seek(sizeof(DV6_HEADER) + (wrap_record-1) * record_length)) { - qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString(); - file.close(); + qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record + << "in" + histfile.fileName() << histfile.error() << histfile.errorString(); + histfile.close(); return false; } -#endif } } +#else + Q_UNUSED(filetype) + Q_UNUSED(newhdr) +#endif return true; } From 940b247cb981cfa0bdeb6047a52ff9ad525eb3a3 Mon Sep 17 00:00:00 2001 From: LoudSnorer Date: Sat, 17 Apr 2021 14:09:40 -0400 Subject: [PATCH 23/28] Adding "snapshot" menu item to Overview Range widget after "custom". This item copies the current displayed Overview Range to the Custom Range and its calendar (start and end). the then selects the "Custom" range. This is a shortcut mechanism to modifing the start and end calendar widgets. Note: The label "snapshot" is my best quess on the name of this item. note: The label will need to be translated, --- oscar/overview.ui | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/oscar/overview.ui b/oscar/overview.ui index d3f00533..ef1d1713 100644 --- a/oscar/overview.ui +++ b/oscar/overview.ui @@ -122,6 +122,11 @@ Custom + + + Snapshot + + From 3591f112a9708e18adc90958eacfcfaf0ae7cf45 Mon Sep 17 00:00:00 2001 From: LoudSnorer Date: Sat, 17 Apr 2021 21:54:56 -0400 Subject: [PATCH 24/28] MinutesAtPressure (timeAtPressure) has been refactored. TimeAtPressure Graph now has the same look and feel as the Pressure Graph. Configuration chamges to the Pressure Graph now also impact the TimeAtPressureGraph. Changes are identified in MinauteatPressure.cpp (first 100 lines) --- oscar/Graphs/MinutesAtPressure.cpp | 2757 ++++++++++++++++------------ oscar/Graphs/MinutesAtPressure.h | 240 ++- 2 files changed, 1751 insertions(+), 1246 deletions(-) diff --git a/oscar/Graphs/MinutesAtPressure.cpp b/oscar/Graphs/MinutesAtPressure.cpp index 2bc6dc50..728ef328 100644 --- a/oscar/Graphs/MinutesAtPressure.cpp +++ b/oscar/Graphs/MinutesAtPressure.cpp @@ -6,6 +6,79 @@ * License. See the file COPYING in the main directory of the source code * for more details. */ + /* + +MinutesAtPressure Graph + +The MinutesAtPressure (TimeAtPressure) Graph is the Pressure Graph transposed - with similar look and feel as other graphs. + The Y-Axis (pressure) becomes the X-Axis (MinutesAtPressure). + The X-Axis (Time) becomes the Y-Axis (duration in minutes). + +The MinutesAtPressure Graph uses the configurable Plot and Overlay Settings from the Pressure Graph, +EPAP and IPAP(CPAP_Pressure) will both be conditionally displayed +Events (H, CA, OA, UA) are displayed as tick marks similar to the Pressure Graph. +Events (LL, CSR) are also displayed like the pressure Graph with lightgrap or lightgreen background coloring. +This gives the MinutesAtPressure a similar look and feel as other graphs. +The MetaData (top label Bar) now contains the total duration for each pressure pressure bucket (range of pressures) as well as the events that occured. + +The MinutesAtPressure tool-tips now contains just the names of the Event Ticks (similar to the pressure Graph) +The tooltip information is minimal and only contains the name of the event and the number of occurrences for that pressure range. + +On Mouse Over + +1. Each data point will be highlight with a small dot. +2. Tooltips will be displayed for the appropriate Pressure Range and the respective data point will be displayed with a square box - similar to the current current implementation. + +When there is no data available (the time selected is between sessions in a day) then "No Data" will be displayed. +When no graphs are selected then "Plots Disabled" will be displayed - just like the Pressure Graph. + +The X-Axis start and end pressure now use the Machine limits. If plot data has a data outside the range is appopaitely updated. + +Refactoring was done to + +1. Reduce duplicate/similar instances of code +2. Shorten long methods +3. Enhance readability, +5. Add Dynamic Meta data for Pressure times and Events. +6. insure data accuracy. +7. Conditional compilation for features + +Total Duration displayed by Minutes AT Pressure graphs is based on the actual waveform form files. +In some cases this duration is different that the session time indicated in the daily session information - due to reasons below. + +Issues found while testing based on 1.2.0 base. +* Event Flags do not display short SPANS. +* Pressure Graph does not display short SPANS. +* sessions times (as displayed in daily session information) are not always the same as the first and last time in the waveforms. + for example for resmed the the session start time is about 40 seconds before the first sample in the waveform (eventlist). + also the session end time is not always the same as the last sample in the waveform. typically 1 second different (either before or after waveform). +* multiple eventlists in a session. time betwwen eventlists is small - under 1-2 minutes. + This is not the same as multiple sessions per day. + +Naming convention +pixel == at point on the display area. +pixels == distance between to pixels +Pressure == a value in cmH20 +Bucket == A range of pressures to collect the ammount of time in that pressure range + + +some messages from Apnea Board. + +0000042: Graphs Daily "Time at Pressure" graph x-axis goes to -1 and plot trace showing behind other graphs + Graphs Daily "Time at Pressure" graph x-axis goes to -1 and plot trace showing behind other graphs On the "Daily" tab, + the Time at Pressure plot's x-axis minimum is a value of -1. i + Also, the plot traces on this graph jump to unreasonably high values (e.g., 508) + which are shown behind the other graphs above it. + Picture available on GitLab All 2-high All Issue 0000049 from SH 1.1.0 GitLab Issue List + +0000038: Graphs any "Time at Pressure graph does not show the right pressure + Graphs any "Time at Pressure graph does not show the right pressure, i + e.g. CPAP with constant pressure at 7.5 cmH2O , but the Time at Pressure graph only has a peak around 6.2 cmH2O + " This is especially true when looking at data at constant pressure. + It was found with DV64 data, but is presumably true for other machine types as well as the graphs do not differentiate on machine types - to be confirmed All 2-high + All Issue 0000054 from SH 1.1.0 GitLab Issue List + */ + #include #include #include @@ -18,20 +91,287 @@ #include "Graphs/gXAxis.h" #include "Graphs/gYAxis.h" +#include "common_gui.h" +#include "Graphs/gLineChart.h" + +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// Compile time Features +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +#define ENABLE_DISPLAY_SPAN_EVENTS_AS_BACKGROUND // Enables SPAN event to be displayed as a background (like pressure graph) +#define ENABLE_DISPLAY_FLAG_EVENTS_AS_TICKS // Enables FLAG events to be displayed as tick (like pressure graph) + +#define ENABLE_BUCKET_PRESSURE_AVERAGE // Average method + // Bucket pressure is in the middle of the pressure range. + // New definition of bucket Pressure-0.1 - Pressure0.1 with INTERVALS_PER_CCMH2O=5 + // Original bucket definition. Pressure - Pressure+0.2 wiyh INTERVALS_PER_CCMH2O=5 +#define EXTRA_SPACE_ON_X_AXIS // adds a small space (Pressure/(INTERVALS_PER_CCMH2O*2) to each end. +#define ENABLE_SMOOTH_CURVES // decreases performance. + +//#define ALIGN_X_AXIS_ON_INTEGER_BOUNDS +//#define ENABLE_DISPLAY_FLAG_EVENTS_AS_GRAPH // enable graphing of flag events. Overlays plots on top on pressure plots. + // ENABLE_DISPLAY_FLAG_EVENTS_AS_TICKS is enabled instead. + // ENABLE_DISPLAY_FLAG_EVENTS_AS_TICKS and ENABLE_DISPLAY_FLAG_EVENTS_AS_GRAPH can both be enabled +// Compile time Constants +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + +#define HIGHEST_POSSIBLE_PRESSURE 60 // should be the lowest possible pressure that a CPAP machine accepts - Plus spare. +#define INTERVALS_PER_CCMH2O 5 // must be a positive integer > 0. Five (5) produces good graphs. Other values will work. + // 10 also loogs good. larger number have smaller intervals and the starting pressure interval will be huge + // relation to the rest of the pressure intervals - making the graph unusable. +#define NUMBER_OF_CATMULLROMSPLINE_POINTS 5 // Higher the number decreases performance. 5 produces smooth curves. must be >= 1. 1 connects points with straight line. + // ENABLE_SMOOTH_CURVES must also be enabled. + +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +//#define ENABLE_MAP_DRAWING_DEBUG // ENABLE DEBUG / TESTING definitions -MinutesAtPressure::MinutesAtPressure() :Layer(NoChannel) -{ - m_remap = nullptr; - m_minpressure = 3; - m_maxpressure = 30; - m_minimum_height = 0; +#ifdef ENABLE_MAP_DRAWING_DEBUG +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// Define Enable common debug / test features + +//#define ENABLE_HOURS_TIME_DISPLAY +//#define ENABLE_MOUSE_DEBUG_INFO +//#define ENABLE_MAP_DRAWING_RECT_DEBUG +//#define TEST_DURATION +//#define MAP_LOG_EVENTS +//#define ENABLE_UNEVEN_MACHINE_MIN_MAX_TEST + +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// Define Display macros to enhance displays + + +#define DEBUGQ qDebug() +#define DEBUGT qDebug()< +#define DEBUG qDebug()<5) return 1; + if (value<0.5) return 3; + return 2; } -MinutesAtPressure::~MinutesAtPressure() -{ - while (recalculating()) {}; + + +EventDataType pressureToBucket(EventDataType pressure, int bucketsPerPressure) { + return pressure * bucketsPerPressure; } +EventDataType convertBucketToPressure(EventDataType bucket, int bucketsPerPressure) { + return bucket / bucketsPerPressure; +} + +EventDataType pressureToXaxis( EventDataType pressure, EventDataType pixelsPerPressure , EventDataType minpressure , QRectF& drawingRect ) { + return ((pressure-minpressure) * pixelsPerPressure) + drawingRect.left() ; +} + +EventDataType convertXaxisToPressure( EventDataType mouseXaxis, EventDataType pixelsPerPressure , EventDataType minpressure , QRectF& drawingRect ) { + return minpressure + ( EventDataType( mouseXaxis - drawingRect.left()) / pixelsPerPressure ); +} + +EventDataType msecToMinutes(EventDataType value) { + return value / 60000.0; // convert milli-seconds to minutes. +} + +EventDataType getSetting(Session * sess,ChannelID code) { + auto setting=sess->settings[code]; + enum schema::DataType datatype = schema::channel[code].datatype(); + if (!( datatype == schema::DEFAULT || datatype == schema::DOUBLE )) return -1; + return setting.toDouble(); +} + +QString timeString(EventDataType milliSeconds) { +#if 1 + EventDataType h,m,s = milliSeconds; + if (s<0) return QString(); + s = 60*modf(s/60000,&m); + m = 60*modf(m/60,&h ); + + // These string are existing translations + static const char* TR_TIME_FMT_S =" (%3 sec)" ; + static const char* TR_TIME_FMT_MS =" (%2 min, %3 sec)" ; + static const char* TR_TIME_FMT_HMS ="%1 hours, %2 minutes and %3 seconds" ; + + if (m>0) { + if (h<=0) { + return QObject::tr(TR_TIME_FMT_MS).arg(m,0,'f',0).arg(s,0,'f',0); + } else { + return QString(" (%1)").arg(QObject::tr(TR_TIME_FMT_HMS).arg(h,0,'f',0).arg(m,0,'f',0).arg(s,2,'f',0)); + } + } else if (s<3 && s>0.01) { + return QObject::tr(TR_TIME_FMT_S).arg(s,1,'f',1); + } + return QObject::tr(TR_TIME_FMT_S).arg(s,0,'f',0); +#else + EventDataType time= milliSeconds; + QString unit; // these are already translated. + if (time>60*1000) { + if (time<=60*60*1000) { + time /=(60*1000); + unit=STR_UNIT_Minutes; + } else { + time /=(60*60*1000); + unit=STR_UNIT_Hours; + } + } else { + // have seconds. + unit=STR_UNIT_Seconds; + time /= (1000); + } + //DEBUG <(tableSize,-1); +} + +void PressureInfo::finishCalcs() +{ + peaktime = 0; + peakevents = 0; + firstPlotBucket = 0; + lastPlotBucket = 0; + + int val; + + for (int i=0, end=times.size(); i0) numEvents[cod]+=val; + peakevents = qMax(val, peakevents); + } + } +} + +void PressureInfo::init() { + chan = schema::channel[code]; + peaktime = peakevents = 0; + firstPlotBucket = 0; + lastPlotBucket = 0; +}; + +void PressureInfo::AddChannel(ChannelID c) +{ + chans.append(c); + events[c].resize(tableSize); +} + +void PressureInfo::AddChannels(QList & chans) +{ + for (int i=0; imachine()->loaderName() == "PRS1") ? 2 : INTERVALS_PER_CCMH2O; + numberXaxisDivisions=qMin(2*bucketsPerPressure,10); +} + +EventDataType PressureInfo:: rawToPressure ( EventStoreType raw,EventDataType gain) { + return EventDataType(raw)*gain; +} + +EventStoreType PressureInfo:: rawToBucketId ( EventStoreType raw,EventDataType gain) { + EventDataType pressure = rawToPressure(raw,gain)+sampleIntervalStart; + EventStoreType ret = floor(pressure * bucketsPerPressure ); + //DEBUG <mutex.unlock(); } +// find pressure given the time of the event. +void RecalcMAP::updateFlagData(int ¤tLoc, int & currentEL,int& currentData,qint64 eventTime, QVector &dataArray, PressureInfo & info ) +{ + for (; currentELcount()); + for (; currentLoc<(int)EL->count() ; currentLoc++) { + if (m_quit) return ; + qint64 sampleTime = EL->time(currentLoc); + int raw = EL->raw(currentLoc); + EventDataType gain= EL->gain(); + EventStoreType data = info.rawToBucketId(raw,gain); + if (data>=tableSize) { + data=tableSize-1; + } + if (sampleTime<=eventTime) { + currentData=data; + if (sampleTime=0) { + #if defined(MAP_LOG_EVENTS) + DEBUG << NAME(chanId) + < &dataArray, PressureInfo & info ) { + EventStoreType useddata = ~0; + for (; currentELcount() ); + for (; currentLoc<(int)EL->count() ; currentLoc++) { + if (m_quit) return ; + qint64 sampleTime = EL->time(currentLoc); + int raw = EL->raw(currentLoc); + EventDataType gain= EL->gain(); + EventStoreType data = info.rawToBucketId(raw,gain); + if (data>=tableSize) { + data=tableSize-1; + } + if (sampleTime0) { + dataArray[currentData]++; + #if defined(MAP_LOG_EVENTS) + DEBUG + << NAME(chanId) + <=eventTime) return; + currentData=data; + } + currentLoc=0; + } + return ; +} + +void RecalcMAP::updateEventsChannel(Session*sess,ChannelID chanId, QVector &dataArray, PressureInfo & info ) +{ + this->chanId=chanId; + int qtyEvents=0; + EventDataType duration = 0, gain; + + qint64 t , start; + EventStoreType *dptr; + EventStoreType *eptr; + quint32 *tptr; + int cnt= 0; + + schema::ChanType chanType = schema::channel[ chanId ].type(); + + auto channelEvents = sess->eventlist.value(chanId); + // Loop through event lists + for (int index =0; indexfirst(); + tptr = EL->rawTime(); + dptr = EL->rawData(); + cnt = EL->count(); + eptr = dptr + cnt; + gain = EL->gain(); + + int currentLoc =0; + int currentEL =0; + int currentData =-1; + + for (; dptr < eptr; dptr++) { + if (m_quit) return ; + t = start + *tptr++; + if (tmaxx) continue; + if (tsmaxx) t=maxx; + qtyEvents++; + updateSpanData(currentLoc , currentEL , currentData , ts , t , dataArray , info ) ; + } else { + if (t>maxx) continue; + if (schema::channel[ chanId ].type() == schema::FLAG) { + updateFlagData(currentLoc , currentEL , currentData , t , dataArray , info ) ; + } + } + } + } + return ; +} + +void RecalcMAP::updateEvents(Session*sess,PressureInfo & info) { + QHash >::iterator ei = sess->eventlist.find(info.code); + if (ei == sess->eventlist.end()) return ; + for (const auto & cod : info.chans) { + updateEventsChannel(sess,cod, info.events[cod],info); + if (m_quit) return ; + } +} + +void RecalcMAP::updateTimesValues(qint64 d1,qint64 d2, int key,PressureInfo & info) { + qint64 duration = (d2 - d1); + info.times[key] += duration; + info.totalDuration+=duration; +} + +//! \brief Updates Time At Pressure from session *sess +void RecalcMAP::updateTimes(PressureInfo & info) { + //DEBUGF <count(); + if (ELsize < 1) continue; + gain = EL->gain(); + #if 1 + // Workaround for the popout function. when the MAP popout graph is created the time selction mixx and miny are both zero. + // this indicates that there is no data to be displayed. WHY ?? + // This workaround uses the session min/max times when the selection min/max times are zero. + if (map->numCloned>0) { + bool cloneWorkAround =false; + if (info.minTime==0) { + cloneWorkAround = true; + info.minTime = EL->first(); + } + if (cloneWorkAround) { + if (info.maxTimelast()) info.maxTime=EL->last(); + } + } + #endif + + // Skip if outside of range + if ((EL->first() > info.maxTime) || (EL->last() < info.minTime)) { + continue; + } + + // adjust for multiple sessions. + // EL->first and last are for the current session while minTime and MaxTime are for a set of seesion for the day. + minx = qMax(info.minTime , EL->first()); + maxx = qMin(info.maxTime , EL->last()); + + lasttime = 0; + lastdata = 0; + data = 0; + first = true; + + // Scan through pressure samples + for (int e = 0; e < ELsize; ++e) { + if (m_quit) return ; + + time = EL->time(e); + EventStoreType raw = EL->raw(e); + test_data(e,ELsize,raw, time ,info.minTime ,info.maxTime,gain,EL); + data = ipap_info->rawToBucketId(raw,gain); + //DEBUG << OO(e=,e) << TIME(time) <first()) <last()) <=tableSize) { + data=tableSize-1; + } + + if ((time < minx) || first) { + lasttime = time; + lastdata = data; + first = false; + continue; + } + + if (lastdata != data) { + d1 = qMax(minx, lasttime); + d2 = qMin(maxx, time); + updateTimesValues(d1,d2,lastdata,info) ; + lasttime = time; + lastdata = data; + } + if (time > maxx) { + break; + } + + } + if ((lasttime>0) &&((lasttime <= maxx) || (lastdata == data))) { + d1 = qMax(minx, lasttime); + d2 = qMin(maxx, EL->last()); + updateTimesValues(d1,d2, lastdata,info) ; + } + } +} + +void RecalcMAP:: setSelectionRange(gGraph* graph) { + graph->graphView()->GetXBounds(minTime, maxTime); +} + +void RecalcMAP::run() +{ + QMutexLocker locker(&map->mutex); + map->m_recalculating = true; + Day * day = map->m_day; + if (!day) return; + + + + // Get the channels for specified Channel types + QList chans; + #if defined(ENABLE_DISPLAY_FLAG_EVENTS_AS_GRAPH) || defined(ENABLE_DISPLAY_FLAG_EVENTS_AS_TICKS) + chans = day->getSortedMachineChannels(schema::FLAG); + chans.removeAll(CPAP_VSnore); + chans.removeAll(CPAP_VSnore2); + chans.removeAll(CPAP_FlowLimit); + chans.removeAll(CPAP_RERA); + #endif + + // Get the channels for specified Channel types + QList chansSpan ; + #ifdef ENABLE_DISPLAY_SPAN_EVENTS_AS_BACKGROUND + chansSpan = day->getSortedMachineChannels(schema::SPAN); + chansSpan.removeAll(CPAP_Ramp); + #endif + + ChannelID ipapcode = CPAP_Pressure; // default + for (auto & ch : { CPAP_IPAPSet, CPAP_IPAP, CPAP_PressureSet } ) { + if (day->channelExists(ch)) { + ipapcode = ch; + break; + } + } + + ChannelID epapcode = NoChannel; // default + for (auto & ch : { CPAP_EPAPSet, CPAP_EPAP } ) { + if (day->channelExists(ch)) { + epapcode = ch; + break; + } + } + + PressureInfo IPAP(ipapcode, minTime, maxTime), EPAP(epapcode, minTime, maxTime); + ipap_info=&IPAP; + + chans+=chansSpan; + IPAP.AddChannels(chans); + + EventDataType minP = HIGHEST_POSSIBLE_PRESSURE; + EventDataType maxP = 0; + auto & sessions = day->sessions; + + #if defined(TEST_DURATION) + if (sessions.size()==1) + { + auto & eventLists = sess->eventlist.value(ipapcode); + if (eventLists.size()==1) { + if (sess->first()!=minTime ) { + DEBUG << "Session" << DATETIME(sess->first()) << "sessFirst" << TIME(sess->first()) << "minTime.." << TIME(minTime) << OO(diffMs,sess->first()-minTime) << NAME(info.code) ; + } + if (sess->last() !=maxTime) { + DEBUG << "Session" << DATETIME(sess->first()) << "SessEnd.." << TIME(sess->last()) << "MaxTime,," << TIME(maxTime) << OO(diffMs,sess->last()-maxTime) << NAME(info.code) ; + } + } + } + #endif + + for ( int idx=0; idxeventlist.value(ipapcode); + EPAP.eventLists = sess->eventlist.value(epapcode); + + updateTimes(IPAP); + updateTimes(EPAP); + + EventDataType value = getSetting(sess, CPAP_PressureMin); + if (value >=0.1 && minP >value) minP=value; + value = getSetting(sess, CPAP_PressureMax); + if (value >=0.1 && maxP maxP) minP=maxP; + IPAP.setMachineTimes(minP,maxP); + + #ifdef ENABLE_UNEVEN_MACHINE_MIN_MAX_TEST + int dayInMonth= day->date().day(); + if ((dayInMonth&1)!=0) + { + machinePressureMin -= 0.05; + machinePressureMax += 0.05; + } + #endif + + if (m_quit) { + m_done = true; + return; + } + + IPAP.finishCalcs(); + EPAP.finishCalcs(); + + map->timelock.lock(); + map->epap = EPAP; + map->ipap = IPAP; + map->timelock.unlock(); + map->recalcFinished(); + m_done = true; +} + + +//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< MinutesAtPressure class <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + +MinutesAtPressure::MinutesAtPressure() :Layer(NoChannel) +{ + m_remap = nullptr; + m_minimum_height = 0; +} +MinutesAtPressure::~MinutesAtPressure() +{ + while (recalculating()) {}; +} void MinutesAtPressure::SetDay(Day *day) { Layer::SetDay(day); - - // look at session summaryValues. - Machine * cpap = nullptr; - if (day) cpap = day->machine(MT_CPAP); - if (cpap) { - EventDataType minpressure = 30; - EventDataType maxpressure = 0; - - // look at overall pressure ranges across all days for this machine and find the min and max - QList channels = { CPAP_Pressure, CPAP_EPAP, CPAP_IPAP, CPAP_PressureSet, CPAP_EPAPSet, CPAP_IPAPSet }; - for (const auto d : cpap->day) { - for (const auto sess : d->sessions) { - //qDebug() << sess->session(); - for (auto ch : channels) { - if (sess->channelExists(ch)) { - // Filter out 0 pressures. - if (sess->Min(ch) > 0) { - minpressure = qMin(sess->Min(ch), minpressure); - } - maxpressure = qMax(sess->Max(ch), maxpressure); - //qDebug() << ch << sess->Min(ch) << sess->Max(ch); - } - } - } - } - - m_minpressure = floor(minpressure)-1; - m_maxpressure = ceil(maxpressure); - - /* const int minimum_cells = 12; - int c = m_maxpressure - m_minpressure; - - - if (c < minimum_cells) { - int v = minimum_cells - c; - m_minpressure -= v/2.0; - m_minpressure = qMax((EventStoreType)4, m_minpressure); - - m_maxpressure = m_minpressure + minimum_cells; - } */ - // QFontMetrics FM(*defaultfont); - // quint32 chantype = schema::SPAN | schema::FLAG | schema::MINOR_FLAG; - // QList chans = day->getSortedMachineChannels(chantype); - // m_minimum_height = (chans.size()+3) * FM.height() - 5; - } - + //if (m_day) DEBUGTF << day->date().toString("dd MMM yyyy hh:mm:ss.zzz"); m_empty = false; m_recalculating = false; @@ -99,44 +784,23 @@ void MinutesAtPressure::SetDay(Day *day) m_empty = !m_day || !(m_day->channelExists(CPAP_Pressure) || m_day->channelExists(CPAP_EPAP) || m_day->channelExists(CPAP_PressureSet) || m_day->channelExists(CPAP_EPAPSet)); } - int MinutesAtPressure::minimumHeight() + +int MinutesAtPressure::minimumHeight() { return m_minimum_height; } - bool MinutesAtPressure::isEmpty() { return m_empty; } -float pressureMult = 5; - void MinutesAtPressure::paint(QPainter &painter, gGraph &graph, const QRegion ®ion) { - QRectF rect = region.boundingRect(); - rect.translate(0.0f, 0.001f); - - //int cells = m_maxpressure-m_minpressure+1; - - - int top = rect.top()-10; - float width = rect.width(); - float height = rect.height(); - float left = rect.left(); - //float pix = width / float(cells); - float bottom = rect.bottom(); - - - //int numchans = chans.size(); - - //int cells_high = numchans + 2; - - //height += 10; - //float hix = height / cells_high; + QRectF boundingRect = region.boundingRect(); m_minx = graph.min_x; m_maxx = graph.max_x; @@ -146,8 +810,36 @@ void MinutesAtPressure::paint(QPainter &painter, gGraph &graph, const QRegion &r // it's a pretty useless graph to popout, probably should just block it's popout instead. recalculate(&graph); } + if (!initialized) return; + // conditional display of TimeAtPressure Plots based on Plots displayed by the Pressure Graph. + // if no Plots are displayed on the Pressure Graph then Displays "Plots Disabled" + // if Pressure Graph is not displayed then "time at Pressure" displays both Plots + // the max Y Axis value is updated for the displayed plot. + setEnabled(graph); + bool display_pressure = isEnabled(ipap.code); + bool display_epap = isEnabled(epap.code); + if (!( display_epap || display_pressure )) { + // No Data + QString msg = QObject::tr("Plots Disabled"); + int x, y; + GetTextExtent(msg, x, y, bigfont); + graph.renderText(msg, boundingRect, Qt::AlignCenter, 0, Qt::gray, bigfont); + return; + } + // check for empty data. + if ( ipap.lastPlotBucket ==0 ) display_pressure = false; + if ( epap.lastPlotBucket ==0 ) display_epap = false; + if (!( display_epap || display_pressure )) { + QString msg = QObject::tr("No Data"); + int x, y; + GetTextExtent(msg, x, y, bigfont); + graph.renderText(msg, boundingRect, Qt::AlignCenter, 0, Qt::gray, bigfont); + return; + } + + // need to Check if windows has changed. m_lastminx = m_minx; m_lastmaxx = m_maxx; @@ -157,1124 +849,155 @@ void MinutesAtPressure::paint(QPainter &painter, gGraph &graph, const QRegion &r // do nothing between, it should hang until complete. mutex.unlock(); //while (recalculating()) { QThread::yieldCurrentThread(); } // yield or whatever - } if (!painter.isActive()) return; + // Recalculating in the background... So we just draw an old copy until then the new data is ready // (it will refresh itself when complete) // The only time we can't draw when at the end of the recalc when the map variables are being updated // So use a mutex to lock QMutexLocker TimeLock(&timelock); + //////////////////////////////////////////////////////////////////// + // calculate pressure Ranges + //////////////////////////////////////////////////////////////////// + int peaktime=0; + + EventDataType minpressure = ipap.machinePressureMin; + EventDataType maxpressure = ipap.machinePressureMax; + //DEBUG < maxpressure) { + minpressure = HIGHEST_POSSIBLE_PRESSURE; + maxpressure=0; + } + //DEBUG <lineThickness(); - - int mouseOverKey = 0; - if (ipap.min_pressure > 0) { - double xp,yp; - - //////////////////////////////////////////////////////////////////// - // Draw X Axis labels - //////////////////////////////////////////////////////////////////// - double pstep = xstep * pressureMult; - - xp = left; - for (int i=0, end=max-min; i<=end; ++i) { - yp = bottom+1; - painter.drawLine(xp, yp, xp, yp+6); - if (i>0) { // skip the first mid tick - painter.drawLine(xp-pstep/2, yp, xp-pstep/2, yp+4); - } - - label = QString("%1").arg(i+minpressure); - GetTextExtent(label, w, h); - graph.renderText(label, xp-w/2, yp+h+4); - xp+= pstep; - } - - schema::Channel & ichan = schema::channel[ipap.code]; - schema::Channel & echan = schema::channel[epap.code]; - - QPoint mouse=graph.graphView()->currentMousePos(); - if (region.contains(mouse)) { - float p = minpressure + (mouse.x() - left) / pstep; - mouseOverKey = floor(p*pressureMult); - - float ipap_minutes = ipap.times[mouseOverKey] / 60.0; - float epap_minutes = epap.times[mouseOverKey] / 60.0; - QString str = QString("%1 %2").arg(mouseOverKey / pressureMult,3,'f',1).arg(STR_UNIT_CMH2O)+"\n"; - bool good = false; - - if (ipap_minutes > 0) { - good = true; - str += ichan.label()+": "+QString("%1 %2").arg(ipap_minutes,3,'f',1).arg(STR_UNIT_Minutes)+"\n"; - } - if (epap_minutes > 0) { - good = true; - str += echan.label()+": "+QString("%1 %2").arg(epap_minutes,3,'f',1).arg(STR_UNIT_Minutes)+"\n"; - } - if (good) { - str+="\n"; - int nc = ipap.chans.size(); - for (int i=0;iqMax(0, min-1)) { - s2 = double(ipap.times[qMax(0, min-1)]/60.0); - - if (s2 < 0) s2=0; - } else s2 =0; - - double lastyp = bottom - (s2 * ystep); - int tmax = qMin(ipap.times.size(), max); - const auto & ipaptimes = ipap.times; - for (int i=qMax(min,1); i0) { - double evpeak = ipap.peakevents; - double bot = bottom+1; - double g = 1.0; - double r = double(height+3) / (evpeak); - if (r < h+4) { - g = 2.0; - r = double(height+3) / (evpeak/g); - if (r < h+4) { - g = 5.0; - r = double(height+3) / (evpeak/g); - if (r < h+4) { - g = 20.0; - //r = double(height+3) / (evpeak/g); - } - } - } - evpeak = ceil(evpeak/g)*g; - r = double(height+3) / (evpeak / g); - - yp = bot; - widest_YAxis+=2; - for (double f=0.0; f<=evpeak; f+=g) { - painter.setPen(Qt::black); - - painter.drawLine(left-widest_YAxis, bot, left-widest_YAxis-4, bot); - painter.setPen(QColor(128,128,128,64)); - // painter.drawLine(left, bot, left+width, bot); - - - label = QString("%1").arg(f); - GetTextExtent(label, w, h); - graph.renderText(label, left-widest_YAxis-w-8, bot+h/2-2 ); - bot -= r; - } - - estep = double(height) / ipap.peakevents; - for (const auto ch : ipap.chans) { - //(ch != CPAP_AHI) && - //if ((ch != CPAP_Hypopnea) && (ch != CPAP_Obstructive) && (ch != CPAP_ClearAirway) && (ch != CPAP_Apnea)) continue; - schema::Channel & chan = schema::channel[ch]; - QColor col = chan.defaultColor(); - col.setAlpha(40); - painter.setPen(col); - painter.setPen(QPen(col, lineThickness)); - - xp = left; - if (ipap.events.size()>qMax(min-1,0)) { - s2 = ipap.events[ch][qMax(min-1,0)]; - } else s2 = 0; - lastyp = bottom - (s2 * estep); - int tmax = qMin(ipap.events.size(), max); - - const auto & ipapev = ipap.events[ch]; - for (int i=qMax(min,1); i0) { - estep = double(height) / epap.peakevents; - for (int k=0; k qMax(min,0)) { - s2 = double(epaptimes[qMax(min,0)]/60.0); - } else { - s2 = 0; - } - xp=left, lastyp = bottom - (s2 * ystep); - int tmax = qMin(epaptimes.size(), max); - for (int i=qMax(min,1); icurrentMousePos(); - - float ypos = top; - - int titleWidth = graph.graphView()->titleWidth; - int marginWidth = gYAxis::Margin; - - QString text = schema::channel[m_presChannel].label(); - QRectF rec(titleWidth-4, ypos, marginWidth, hix); - rec.moveRight(left - 4); -// graph.renderText(text, rec, Qt::AlignRight | Qt::AlignVCenter); - - if (rec.contains(mouse)) { - QString text = schema::channel[m_presChannel].description(); - graph.ToolTip(text, mouse.x() + 10, mouse.y(), TT_AlignLeft); - } - int w,h; - GetTextExtent(text, w,h); - graph.renderText(text, (left-4) - w, ypos + hix/2.0 + float(h)/2.0); - - text = STR_UNIT_Minutes; - rec = QRectF(titleWidth-4, ypos+hix, marginWidth, hix); - rec.moveRight(left - 4); - - GetTextExtent(text, w,h); - graph.renderText(text, (left-4) - w, ypos + hix + hix/2.0 + float(h)/2.0); -// graph.renderText(text, rec, Qt::AlignRight | Qt::AlignVCenter); - - float xpos = left; - for (it = times.begin(); it != times_end; ++it) { - QString text = QString::number(it.key()); - QString value = QString("%1").arg(float(it.value()) / 60.0, 5, 'f', 1); - QRectF rec(xpos, top, pix-1, hix); - - GetTextExtent(text, w,h); - - painter.fillRect(rec, QColor("orange")); - graph.renderText(text, xpos + pix/2 - w/2, top + hix /2 + h/2); - - GetTextExtent(value, w,h); - -// rec.moveTop(top + hix); - graph.renderText(value, xpos + pix/2 - w/2, top + hix+ hix /2+ h/2); - - xpos += pix; - } - - ypos += hix * 2; - // left = rect.left(); - - auto eit; - //auto ev_end = events.end(); - auto vit; - - - int row = 0; - for (int i=0; i< numchans; ++i) { - ChannelID code = chans.at(i); - - schema::Channel & chan = schema::channel[code]; - if (!chan.enabled()) - continue; - schema::ChanType type = chan.type(); - eit = events.find(code); - - xpos = left; - - auto eit_end = eit.value().end(); - - QString text = chan.label(); - rec = QRectF(titleWidth, ypos, marginWidth, hix); - rec.moveRight(xpos - 4); - - if (rec.contains(mouse)) { - QString text = chan.fullname(); - if (type == schema::SPAN) { - text += "\n"+QObject::tr("(% of time)"); - } - graph.ToolTip(text, mouse.x() + 10, mouse.y(), TT_AlignLeft); - } - - GetTextExtent(text, w,h); - - graph.renderText(text, (left-4) - w, ypos + hix/2.0 + float(h)/2.0); - - for (it = times.begin(), vit = eit.value().begin(); vit != eit_end; ++vit, ++it) { - float minutes = float(it.value()) / 60.0; - float value = vit.value(); - - QString fmt = "%1"; - if (type != schema::SPAN) { - //fmt = "%1"; - value = (minutes > 0.000001) ? (value * 60.0) / minutes : 0; - } else { - //fmt = "%1%"; - value = (minutes > 0.000001) ? (100/minutes) * (value / 60.0) : 0; - } - - QRectF rec(xpos, ypos, pix-1, hix); - if ((row & 1) == 0) { - painter.fillRect(rec, QColor(245,245,255,240)); - } - - text = QString(fmt).arg(value,5,'f',2); - - GetTextExtent(text, w,h); - - graph.renderText(text, xpos + pix/2 - w/2, ypos + hix /2 + h/2); - // painter.drawText(rec, Qt::AlignCenter, QString(fmt).arg(value,5,'f',2)); - xpos += pix; - - } - ypos += hix; - row++; - } - - float maxmins = float(maxtime) / 60.0; - float ymult = height / maxmins; - - - row = 0; - - xpos = left ;//+ pix / 2; - - float y1, y2; - it = times.begin(); - float bottom = top+height; - - if (it != times_end) { - QVector P; - QVector tap; - P.resize(26); - tap.reserve(260); - - for (; it != times_end; ++it) { - int p = it.key(); - // No ASSERTS!!! (p < 255); - float v = float(it.value()) / 60.0; - P[p] = v; - } - for (int i=0;i<10;++i) { - tap.append(0); - } - for (int i=1; i<24; ++i) { - - float p0 = P[i-1]; - float p1 = P[i]; - float p2 = P[i+1]; - float p3 = P[i+2]; - - // Calculate Catmull-Rom Splines in between samples - tap.append(P[i]); - - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.1))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.2))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.3))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.4))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.5))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.6))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.7))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.8))); - tap.append(qMax(0.0f,CatmullRomSpline(p0,p1,p2,p3,0.9))); - } - tap.append(P[24]); - tap.append(P[25]); - - painter.setPen(QPen(QColor(Qt::gray), 2)); - - float minutes = tap[35]; - y1 = minutes * ymult; - - int tapsize = tap.size(); - //xpos += pix / 10; - for (int i=36; ieventlist.find(code); - - // Done already if no channel - if (ei == sess->eventlist.end()) - return; - - pressureMult = (sess->machine()->loaderName() == "PRS1") ? 2 : 5; - // Loop through event lists - - for (const auto & EL : ei.value()) { - gain = EL->gain(); - - // Don't bother with short sessions - ELsize = EL->count(); - if (ELsize < 1) continue; - - lasttime = 0; - lastdata = 0; - - first = true; - - // Skip if outside of range - if ((EL->first() > maxx) || (EL->last() < minx)) { - continue; - } - - // Scan through pressure samples - for (int e = 0; e < ELsize; ++e) { - if (m_quit) { - m_done = true; - return; - } - - time = EL->time(e); - data = floor(float(EL->raw(e)) * gain * pressureMult); // pressure times ten, so can look at .1 intervals in an integer - - if (data>=300) { - qWarning() << "data >= 300 in RecalcMAP::updateTimes!"; - return; - } - - if ((time < minx) || first) { - lasttime = time; - lastdata = data; - - first = false; - continue; - } - - if (lastdata != data) { - d1 = qMax(minx, lasttime); - d2 = qMin(maxx, time); - - duration = (d2 - d1) / 1000L; - key = lastdata; - info.times[key] += duration; - - - for (const auto & cod : info.chans) { - schema::Channel & chan = schema::channel[cod]; - if (chan.type() == schema::SPAN) { - info.events[cod][key] += val = sess->rangeSum(cod, d1, d2); - } else { - info.events[cod][key] += val = sess->rangeCount(cod, d1, d2); - } - } - - lasttime = time; - lastdata = data; - } - if (time > maxx) { - break; - } - - } - if ((lasttime < maxx) || (lastdata == data)) { - d1 = qMax(lasttime, minx); - d2 = qMin(maxx, EL->last()); - - duration = (d2 - d1) / 1000L; - key = lastdata; - info.times[key] += duration; - - for (const auto & cod : info.chans) { - schema::Channel & chan = schema::channel[cod]; - if (chan.type() == schema::SPAN) { - info.events[cod][key] += sess->rangeSum(cod, d1, d2); - } else { - info.events[cod][key] += sess->rangeCount(cod, d1, d2); - } - } - } - } -} - - -void PressureInfo::finishCalcs() -{ - peaktime = peakevents = 0; - min_pressure = max_pressure = 0; - - int val; - - for (int i=0, end=times.size(); i 0) { - if (min_pressure == 0) { - min_pressure = i; - } - max_pressure = i; - } - } - - //chans.push_front(CPAP_AHI); - - int size = events[CPAP_Obstructive].size(); - -/* events[CPAP_AHI].resize(size); - - - auto OB = events.find(CPAP_Obstructive); - auto HY = events.find(CPAP_Hypopnea); - auto A = events.find(CPAP_Apnea); - auto CA = events.find(CPAP_ClearAirway); - - for (int i = 0; i < size; i++) { - - val = 0; - - if (OB != events.end()) - val += OB.value()[i]; - if (HY != events.end()) - val += HY.value()[i]; - if (A != events.end()) - val += A.value()[i]; - if (CA != events.end()) - val += CA.value()[i]; - - events[CPAP_AHI][i] = val; - } */ - - for (int i = 0; i < size; i++) { - for (const auto & cod : chans) { - if ((cod == CPAP_AHI) || (schema::channel[cod].type() == schema::SPAN)) continue; - val = events[cod][i]; - peakevents = qMax(val, peakevents); - } - } -} - - -void RecalcMAP::run() -{ - QMutexLocker locker(&map->mutex); - map->m_recalculating = true; - Day * day = map->m_day; - if (!day) return; - - // Get the channels for specified Channel types - QList chans = day->getSortedMachineChannels(schema::FLAG); - - chans.removeAll(CPAP_VSnore); - chans.removeAll(CPAP_VSnore2); - chans.removeAll(CPAP_FlowLimit); - chans.removeAll(CPAP_RERA); -// for (int i=0;i ipapChannels = { CPAP_IPAPSet, CPAP_IPAP, CPAP_PressureSet }; // preferred, if present - ChannelID ipapcode = CPAP_Pressure; // default - for (auto & ch : ipapChannels) { - if (day->channelExists(ch)) { - ipapcode = ch; - break; - } - } - QList epapChannels = { CPAP_EPAPSet, CPAP_EPAP }; // preferred, if present - ChannelID epapcode = NoChannel; // default - for (auto & ch : epapChannels) { - if (day->channelExists(ch)) { - epapcode = ch; - break; - } - } - - qint64 minx, maxx; - map->m_graph->graphView()->GetXBounds(minx, maxx); - PressureInfo IPAP(ipapcode, minx, maxx), EPAP(epapcode, minx, maxx); - - IPAP.AddChannels(chans); - EPAP.AddChannels(chans); - - //ChannelID code; - -// QList badchans; -// for (int i=0 ; i < chans.size(); ++i) { -// code = chans.at(i); -// // if (!day->channelExists(code)) badchans.push_back(code); -// } - -// for (int i=0; i < badchans.size(); ++i) { -// code = badchans.at(i); -// chans.removeAll(code); -// } - - -// int numchans = chans.size(); -// // Zero the pressure counts -// for (int i=map->m_minpressure; i <= map->m_maxpressure; i++) { -// times[i] = 0; - -// for (int c = 0; c < numchans; ++c) { -// code = chans.at(c); -// events[code].insert(i, 0); -// } -// } - - - - for (const auto & sess : day->sessions) { - - updateTimes(EPAP, sess); - updateTimes(IPAP, sess); - - if (m_quit) { - m_done = true; - return; - } - - -/* auto ei = sess->eventlist.find(ipapcode); - if (ei == sess->eventlist.end()) - continue; - - const auto & evec = ei.value(); - int esize = evec.size(); - for (int ei = 0; ei < esize; ++ei) { - const EventList *EL = evec.at(ei); - EventDataType gain = EL->gain(); - quint32 ELsize = EL->count(); - if (ELsize < 1) return; - qint64 lasttime = 0; //EL->time(0); - EventStoreType lastdata = 0; // EL->raw(0); - - bool first = true; - if ((EL->first() > maxx) || (EL->last() < minx)) { - continue; - } - - for (quint32 e = 0; e < ELsize; ++e) { - qint64 time = EL->time(e); - EventStoreType data = EL->raw(e); - - if ((time < minx)) { - lasttime = time; - lastdata = data; - first = false; - goto skip; - } - - if (first) { - lasttime = time; - lastdata = data; - first = false; - } - - if (lastdata != data) { - qint64 d1 = qMax(minx, lasttime); - qint64 d2 = qMin(maxx, time); - - - int duration = (d2 - d1) / 1000L; - EventStoreType key = floor(lastdata * gain); - if (key <= 30) { - times[key] += duration; - for (int c = 0; c < chans.size(); ++c) { - ChannelID code = chans.at(c); - schema::Channel & chan = schema::channel[code]; - if (chan.type() == schema::SPAN) { - events[code][key] += sess->rangeSum(code, d1, d2); - } else { - events[code][key] += sess->rangeCount(code, d1, d2); - } - } - } - lasttime = time; - lastdata = data; - } - if (time > maxx) { - - break; - } -skip: - if (m_quit) { - m_done = true; - return; - } - } - if (lasttime < maxx) { - qint64 d1 = qMax(lasttime, minx); - qint64 d2 = qMin(maxx, EL->last()); - - int duration = (d2 - d1) / 1000L; - EventStoreType key = floor(lastdata * gain); - if (key <= 30) { - times[key] += duration; - for (int c = 0; c < chans.size(); ++c) { - ChannelID code = chans.at(c); - schema::Channel & chan = schema::channel[code]; - if (chan.type() == schema::SPAN) { - events[code][key] += sess->rangeSum(code, d1, d2); - } else { - events[code][key] += sess->rangeCount(code, d1, d2); - } - } - } - - } - - - } */ - } - - - EPAP.finishCalcs(); - IPAP.finishCalcs(); - -/* - int maxtime = 0; - - QList trash; - for (auto it=times.begin(), end=times.end(); it != end; ++it) { - //EventStoreType key = it.key(); - int value = it.value(); -// if (value == 0) { -// trash.append(key); -// } else { - maxtime = qMax(value, maxtime); -// } - } - chans.push_front(CPAP_AHI); - - int maxevents = 0, val; - - for (int i = map->m_minpressure; i <= map->m_maxpressure; i++) { - val = events[CPAP_Obstructive][i] + - events[CPAP_Hypopnea][i] + - events[CPAP_Apnea][i] + - events[CPAP_ClearAirway][i]; - - events[CPAP_AHI].insert(i, val); - // maxevents = qMax(val, maxevents); - } - - for (int i = map->m_minpressure; i <= map->m_maxpressure; i++) { - for (int j=0 ; j < chans.size(); ++j) { - code = chans.at(j); - if ((code == CPAP_AHI) || (schema::channel[code].type() == schema::SPAN)) continue; - val = events[code][i]; - maxevents = qMax(val, maxevents); - } - } - -// for (int i=0; i< trash.size(); ++i) { -// EventStoreType key = trash.at(i); - -// times.remove(key); -// for (auto eit=events.begin(), end=events.end(); eit != end; ++eit) { -// eit.value().remove(key); -// } -// } -*/ - - map->timelock.lock(); - -// map->times = times; -// map->events = events; - map->epap = EPAP; - map->ipap = IPAP; -// map->chans = chans; - // map->m_presChannel = ipapcode; - map->timelock.unlock(); - - map->recalcFinished(); - m_done = true; } void MinutesAtPressure::recalculate(gGraph * graph) { - while (recalculating()) m_remap->quit(); m_remap = new RecalcMAP(this); m_remap->setAutoDelete(true); - m_graph = graph; + m_remap->setSelectionRange(graph); + m_graph=graph; QThreadPool * tp = QThreadPool::globalInstance(); -// tp->reserveThread(); if (graph->printing()) { m_remap->run(); @@ -1286,11 +1009,6 @@ void MinutesAtPressure::recalculate(gGraph * graph) } - - // Start recalculating in another thread, organize a callback to redraw when done.. - - - } void MinutesAtPressure::recalcFinished() @@ -1298,34 +1016,16 @@ void MinutesAtPressure::recalcFinished() if (m_graph && !m_graph->printing()) { // Can't call this using standard timedRedraw function, we are in another thread, so have to use a throwaway timer QTimer::singleShot(0, m_graph->graphView(), SLOT(refreshTimeout())); + // this causes MinutesAtPressure:: paint to be called. } m_remap = nullptr; m_recalculating = false; -// QThreadPool * tp = QThreadPool::globalInstance(); -// tp->releaseThread(); - + initialized=true; } bool MinutesAtPressure::mouseMoveEvent(QMouseEvent *, gGraph *graph) { -// int y = event->y() - m_rect.top(); -// int x = event->x() - graph->graphView()->titleWidth; - -// double w = m_rect.width() - gYAxis::Margin; - -// double xmult = (graph->blockZoom() ? double(graph->rmax_x - graph->rmin_x) : - //double(graph->max_x - graph->min_x)) / w; - -// double a = x - gYAxis::Margin; -// if (a < 0) a = 0; -// if (a > w) a = w; - -// double b = a * xmult; -// double c= b + (graph->blockZoom() ? graph->rmin_x : graph->min_x); - -// graph->graphView()->setCurrentTime(c); - if (graph) graph->timedRedraw(0); return false; } @@ -1343,3 +1043,684 @@ bool MinutesAtPressure::mouseReleaseEvent(QMouseEvent *event, gGraph *graph) Q_UNUSED(graph); return false; } + + +bool MinutesAtPressure::isEnabled(ChannelID id) { + return m_enabled[id]; +} ; + +void MinutesAtPressure::setEnabled(gGraph &graph) { + QList channels; + channels+=ipap.code; + channels+=epap.code; + channels+=ipap.chans; + + gGraphView *graphView = graph.graphView(); + gGraph* pressureGraph = graphView->findGraph(STR_GRAPH_Pressure); + gLineChart * pressureGraphLC = NULL; + if (!pressureGraph ) return; + pressureGraphLC = dynamic_cast(pressureGraph->getLineChart()); + if (!pressureGraph->visible()) return; + if (!pressureGraphLC) return; + m_enabled.clear(); + for (QList::iterator it = channels.begin(); it != channels.end(); ++it) { + ChannelID ch=*it; + bool value; + schema::Channel & chan =schema::channel[ch] ; + value = chan.enabled(); + if (chan.type() == schema::WAVEFORM) { + value=pressureGraphLC->plotEnabled(ch); + } else { + value &= pressureGraphLC->m_flags_enabled[ch]; + } + //DEBUGF << FULLNAME(ch) << O(value); + m_enabled[ch]=value; + } +}; + +EventDataType getStep(int &stepi, EventDataType& stepmult ) { + static const QList stepArray {1.0, 2.0,5.0}; + return stepmult * stepArray[stepi]; +} + +void decStep(int &stepi, EventDataType& stepmult ) { + stepmult = stepmult / 10; + Q_UNUSED(stepi); +} + +void MapPainter::calculatePeakY(int peaktime ){ + GetTextExtent("W", singleCharWidth, textHeight); + + peakMinutes = msecToMinutes(peaktime+1); // peakMinutes must not be zero. + + static const QList stepArray {1.0, 2.0,5.0}; + //static const QList stepArray {1.0, 2.5,5.0, 7.5}; + int stepArraySize=stepArray.size(); + int height = drawingRect.height(); + + height -= qMin ( int(drawingRect.height()/10), qMax(textHeight, drawTickLength)); + + int maxsteps=ceil(height / textHeight); + #define MINSTEPS 1 + #define MAXSTEPS 15 + maxsteps=qMax (MINSTEPS, qMin(maxsteps,MAXSTEPS)); + EventDataType minStep = peakMinutes / maxsteps; + + int stepi=0; // o - ArraySize-1 + EventDataType stepmult=1; //10**n + int numberSteps=1; + EventDataType step = 1; + + yPixelsPerStep = 0; + EventDataType totalMinutes = 0; + EventDataType pixelsPerMinute = 0; + bool up=false; // find smallest step + + // find smallest step that where step label do not overlap + for (;;) { + step = stepmult * stepArray[stepi]; + if (step>minStep) { + if (!up) { + // very low levels. + stepmult = stepmult / 10; + continue; + } + numberSteps = ceil(peakMinutes/step); + if (numberSteps==0) numberSteps=1; + totalMinutes = step*numberSteps; + if (totalMinutes>=peakMinutes) { + // this works. + break; + } + } + up=true; + stepi=(stepi+1)%stepArraySize; // next step module array size. + if (stepi==0) stepmult*=10; + } + + // determine Y-axis scale + pixelsPerMinute = height / totalMinutes; + totalMinutes = step*numberSteps; + pixelsPerMinute = height / totalMinutes; + yPixelsPerStep = height / numberSteps; + + // update parameters required for the Y-axis + yPixelsPerMsec = pixelsPerMinute/60000; + yMinutesPerStep=step; + peakMinutes = totalMinutes; + //DEBUG << O(drawingRect.height() ) << O(textHeight) << O(peaktime) << O(peakmult) << O(yPixelsPerMsec) << O(yMinutesPerStep) << O(peakMinutes) ; + +} + +int MapPainter::drawYaxis(int peaktime) { + MapPainter::calculatePeakY(peaktime ); + //////////////////////////////////////////////////////////////////// + // Draw Y Axis labels + //////////////////////////////////////////////////////////////////// + QString label; + int labelWidth,labelHeight; + EventDataType bot = drawingRect.bottom(); + int left= boundingRect.left(); + int width= boundingRect.width(); + int widest_YAxis = 2; + EventDataType limit =peakMinutes +(yMinutesPerStep/2) ; + for (EventDataType f=0.0; f& enabled , EventDataType minpressure , EventDataType maxpressure) + { + int top=boundingRect.top(); + int bottom=boundingRect.bottom(); + //////////////////////////////////////////////////////////////////// + // Draw mouse over events + //////////////////////////////////////////////////////////////////// + mouseOverKey = -1; + QPoint mouse=graph.graphView()->currentMousePos(); + + bool toolTipOff=false; + if (mouse.x()==0 && mouse.y() ==0) { + toolTipOff=true; + mouse=last_mouse; + } else { + last_mouse=mouse; + } + + graphSelected= (mouse.y()<=boundingRect.bottom() && mouse.y()>=boundingRect.top() ); + bool eventOccured = false; + if ((mouse.x()boundingRect.right() )) { + graphSelected= false; + // note until Session start times are synced with waveforms start time. there will be a difference in the total time displayed. + // so don't display the total waveform time, because the user can see the difference between sessions times and the total duration + // calculated. both the first and last times can be different for resmed machines. This can be confusing so don't display questionable data. + + topBarLabel = displayMetaData(ipap.chan.label(),minpressure, minpressure, maxpressure, timeString(ipap.totalDuration),"",""); + //topBarLabel = displayMetaData(ipap.chan.label(),minpressure, minpressure, maxpressure, "" ,"",""); + //So just display original Label instead of total Duration. + // topBarLabel = QObject::tr("Peak %1").arg(msecToMinutes(qMax(ipap.peaktime, epap.peaktime)),1,'f',1); + Q_UNUSED(maxpressure); + } else { + // Mouse is in the horizantile ploting area of all graphs. + EventDataType pMousePressure = minpressure + ( (mouse.x() - drawingRect.left()) / pixelsPerPressure); + mouseOverKey = floor((pMousePressure+sampleIntervalStart)*bucketsPerPressure); + EventDataType mouseOverPressure = (EventDataType)mouseOverKey/bucketsPerPressure; + + int bucketX = ((mouseOverPressure-minpressure)*pixelsPerPressure) +drawingRect.left() ; + + // Draw veritical line for mouse cursor. jump to closest pressure bucket. + painter.setPen(QPen(QColor(128,128,128,30), 1.5*AppSetting->lineThickness())); + painter.drawLine(bucketX, top, bucketX, bottom); + + bool epapEnabled = enabled[epap.code] ; + topBarLabel = displayMetaData( + ipap.chan.label(), + mouseOverPressure, + mouseOverPressure-sampleIntervalStart, + mouseOverPressure+sampleIntervalEnd, + timeString(ipap.times[mouseOverKey]) , + epapEnabled?epap.chan.label():"", + epapEnabled?timeString(epap.times[mouseOverKey]):"" + ); + + + QString toolTipLabel = QString(); + int nc = ipap.chans.size(); + for (int i=0;i0 && opacity<=255) { + color.setAlpha(opacity); + //DEBUG << FULLNAME(channelId) << O(color.name()) << O(opacity); + } + linePen=QPen(color, AppSetting->lineThickness()); + pointEnhancePen =QPen(QColor(Qt::black), 2.5*AppSetting->lineThickness()); + pointSelectionPen=QPen(color, 1.5*AppSetting->lineThickness()); +} + +void MapPainter::setChannelInfo(ChannelID id, QVector dataArray, EventDataType yPixelsPerUnit ,int startBucket ,int endBucket) +{ + channel = &schema::channel[id]; + chanType = channel->type(); //schema::channel[id].type() ; + this->dataArray = dataArray; + this->yPixelsPerUnit = yPixelsPerUnit; + this->startBucket = startBucket; + this->endBucket = endBucket; + setPenColorAlpha(id , chanType!=schema::WAVEFORM ? 50 : 255); +} + +// converts a y value to a graph point +// Adjusting values from the min and max graph ranges. +EventDataType MapPainter::verifyYaxis(EventDataType value) { + EventDataType top=drawingRect.top(); + EventDataType bottom=drawingRect.bottom(); + if (value<=top) { + return top+2; + } else if (value>=bottom) { + return bottom; + } + return value; +} + +EventDataType MapPainter::dataToYaxis(int pp) { + EventDataType val=verifyYaxis (drawingRect.bottom() - ((pp*yPixelsPerUnit))); + return val; +} + + +// Initializes values used based +void MapPainter::initCatmullRomSpline(EventDataType pixelsPerBucket,int numberOfPoints) +{ + catmullRomSplineNumberOfPoints = numberOfPoints; + catmullRomSplineIncrement = 1.0 / catmullRomSplineNumberOfPoints ; + catmullRomSplineInterval = 0.0f; + catmullRomSplineXstep = pixelsPerBucket / catmullRomSplineNumberOfPoints; +} + +// Draws a line between two points +// The line will be stright if anti-aliasing is turned off. +// otherwise the line will be be curved to fit the data. +EventDataType MapPainter::drawSegment( int bucket ,EventDataType lastxp,EventDataType lastyp) +{ + + #if defined(ENABLE_SMOOTH_CURVES) + EventDataType xp=lastxp; + EventDataType dM1 = dataToYaxis(dataArray[bucket-1]); + EventDataType yp = dataToYaxis(dataArray[bucket +0]); + EventDataType d1 = dataToYaxis(dataArray[bucket +1]); + EventDataType d2 = dataToYaxis(dataArray[bucket +2]); + + catmullRomSplineInterval=catmullRomSplineIncrement; + + for (int loop=0;loop1) { + yp= CatmullRomSpline( dM1, yp , d1, d2 , catmullRomSplineInterval) ; + } + yp=verifyYaxis(yp); + xp+=catmullRomSplineXstep; + + painter.drawLine(lastxp, lastyp, xp, yp); + lastxp = xp; + lastyp = yp; + } + #else + EventDataType yp = dataToYaxis(dataArray[bucket +1]); + yp=verifyYaxis(yp); + EventDataType xp= lastxp+pixelsPerBucket; + painter.drawLine(lastxp, lastyp, xp, yp); + #endif + return yp; +} + +void MapPainter::drawPoint(bool accent,int xp, int yp) { + if (!graphSelected) return; + if (yp>=drawingRect.bottom() && chanType!=schema::WAVEFORM) return; + //DEBUG << FULLNAME(info.code) << OO(id,channel.id()); + if (accent) { + painter.setPen(pointEnhancePen); + } else { + painter.setPen(pointSelectionPen); + } + int radius=1; + painter.drawEllipse(QPoint(xp,yp),radius,radius); + painter.setPen(linePen); +} + +// Draw a plot of points on Graphs +// CPAP_Pressure or CPAP_EPAP or Events +void MapPainter::drawPlot() { + tickPen = QPen(Qt::black, 1); + bool started=false; + EventDataType yp=drawingRect.bottom(); + EventDataType xp=drawingRect.left(); + + painter.setPen(linePen); + for (int i=startGraphBucket; i<=endBucket; ++i,xp+=pixelsPerBucket) { + //DEBUG << O(i) << OO(data,dataArray[i]); + bool accent = (i==mouseOverKey); + if (!started ) { + if (dataArray[i]<=0) continue; + if (i>=startBucket) { + //draw vertical line to first point. + started=true; + // following used to test Y axis labels position. + //int tmp=dataArray[i]; + //if (tmp>61000 && tmp<63000) tmp=60000; + //yp = dataToYaxis(tmp); + yp = dataToYaxis(dataArray[i]); + //DEBUG << OO(bucket,i) <=endBucket) { + // draw vertical line to last point. + EventDataType lastxp=xp; + if (yp >=drawingRect.bottom() ) { + //last point was at bottom. + lastxp-=pixelsPerBucket; + yp = dataToYaxis(dataArray[i]); + } + drawPoint( accent ,xp, yp); + painter.drawLine(lastxp ,drawingRect.bottom(), xp, yp); + return; + } + drawPoint( accent ,xp, yp); + yp=drawSegment (i,xp,yp) ; + } +} + +// Draw a an Event tick at the top of ther graph +#ifdef ENABLE_DISPLAY_SPAN_EVENTS_AS_BACKGROUND +void MapPainter::drawSpanEvents() { + if (dataArray.isEmpty()) return; + EventDataType xp = drawingRect.left(); + EventDataType pixelsPerBucket = this->pixelsPerBucket; + #ifdef ENABLE_BUCKET_PRESSURE_AVERAGE + EventDataType pixelsPerBucket2 = pixelsPerBucket/2; + pixelsPerBucket = pixelsPerBucket2; + #endif + QRectF box= QRectF(xp,boundingRect.top(),pixelsPerBucket,boundingRect.height()); + int tickTop = boundingRect.top(); + int tickHeight =boundingRect.height(); + QColor color=schema::channel[channel->id()].defaultColor(); + color.setAlpha(128); + + for (int i=startGraphBucket; i=endGraphBucket) { + #ifdef ENABLE_BUCKET_PRESSURE_AVERAGE + if (i==endGraphBucket) { + pixelsPerBucket = pixelsPerBucket2; + } else { + return; + } + #else + return; + #endif + } + int data=dataArray[i]; + if (data>0) { + box.setRect(xp,tickTop,pixelsPerBucket,tickHeight); + painter.fillRect(box,color); + } + xp+=pixelsPerBucket; + #ifdef ENABLE_BUCKET_PRESSURE_AVERAGE + pixelsPerBucket=this->pixelsPerBucket; + #endif + } +} +#endif + +#ifdef ENABLE_DISPLAY_FLAG_EVENTS_AS_TICKS +void MapPainter::drawEventTick() { + EventDataType xp=drawingRect.left(); + int tickLength=drawTickLength; + + int top = boundingRect.top(); + int bottom = boundingRect.bottom(); + + painter.setPen(tickPen); + for (int i=startGraphBucket; iid(),70); + setPenColorAlpha(ChannelID(NoChannel),70); + drawPlot(); + #endif + #ifdef ENABLE_DISPLAY_FLAG_EVENTS_AS_TICKS + tickPen = QPen(Qt::black, 1); + tickEnhancePen = QPen(QColor(0,0,255), 3.5*AppSetting->lineThickness()); + tickEnhanceTransparentPen = QPen(QColor(0,0,255,60), 3.5*AppSetting->lineThickness()); + drawEventTick(); + #endif + return; + } + if (chanType == schema::SPAN) { + #ifdef ENABLE_DISPLAY_SPAN_EVENTS_AS_BACKGROUND + drawSpanEvents(); + #endif + return; + } +} +#endif + + + +//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TEST DATA <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +#if defined(ENABLE_TEST_CPAP) || defined(ENABLE_TEST_SAWTOOTH) || defined(ENABLE_TEST_SINGLE) || defined(ENABLE_TEST_NODATA) +EventDataType test_inc=0.0; +EventDataType test_start; +EventDataType test_mid; +EventDataType test_end; +EventDataType test_value; +qint64 test_time; +qint64 test_time_inc; +int test_count; +qint64 test_ELFirst; + +bool testdata(int e, int ELsize, EventStoreType& raw, qint64& time ,qint64 minTime ,qint64 maxTime , EventDataType gain, EventList* EL) { + if (e==0) { + test_ELFirst=EL->first(); + test_start=(4.0f/gain); + test_mid=(7.07f/gain); + test_end=(15.0f/gain); + test_value=test_start; + test_inc=(test_end-test_start)/EventDataType(ELsize); + + test_time=time; + //test_time_inc=(maxTime-minTime)/ELsize; + test_time_inc=(EL->last()-EL->first())/ELsize; + test_count=0; + } + if (test_ELFirst!=EL->first()) { + test_ELFirst=EL->first(); + } + if (e>=ELsize) return false; + #if defined(ENABLE_TEST_CPAP) + raw= EventStoreType(test_mid); + return true; + #endif + #if defined(ENABLE_TEST_SINGLE) + int zz=ELsize-1; + if (e==zz) { + raw= (EventStoreType)test_mid; + time=(minTime+maxTime)/2; + return true; + } + return false; + #endif + #if defined(ENABLE_TEST_NODATA) + return false; + #endif + + // ENABLE_TEST_SAWTOOTH + if (test_value>=test_end) { + test_value=test_start; + } ; + raw=(EventStoreType)test_value;; + time=(qint64)test_time; + + test_time+=test_time_inc; + test_value+= test_inc; + return true; + + Q_UNUSED(test_count); + Q_UNUSED(e); + Q_UNUSED(ELsize); + Q_UNUSED(EL); + Q_UNUSED(raw); + Q_UNUSED(time); + Q_UNUSED(minTime); + Q_UNUSED(maxTime); +} +#endif + + diff --git a/oscar/Graphs/MinutesAtPressure.h b/oscar/Graphs/MinutesAtPressure.h index d7420ddf..7e2644df 100644 --- a/oscar/Graphs/MinutesAtPressure.h +++ b/oscar/Graphs/MinutesAtPressure.h @@ -9,48 +9,57 @@ #ifndef MINUTESATPRESSURE_H #define MINUTESATPRESSURE_H +#include #include "Graphs/layer.h" #include "SleepLib/day.h" +#include "SleepLib/schema.h" +#include "Graphs/gLineChart.h" class MinutesAtPressure; struct PressureInfo { - PressureInfo() - { - code = 0; - minx = maxx = 0; - peaktime = peakevents = 0; - min_pressure = max_pressure = 0; - } +public: + PressureInfo(); + PressureInfo(ChannelID code, qint64 minTime, qint64 maxTime) ; PressureInfo(PressureInfo ©) = default; - PressureInfo(ChannelID code, qint64 minx, qint64 maxx) : code(code), minx(minx), maxx(maxx) - { - times.resize(300); - } - void AddChannel(ChannelID c) - { - chans.append(c); - events[c].resize(300); - } - void AddChannels(QList & chans) - { - for (int i=0; i & chans); void finishCalcs(); + void setMachineTimes(EventDataType min,EventDataType max); ChannelID code; - qint64 minx, maxx; + schema::Channel chan; + qint64 minTime, maxTime; QVector times; int peaktime, peakevents; - int min_pressure, max_pressure; QHash > events; + QHash numEvents; QList chans; + QVector eventLists; + + void updateBucketsPerPressure(Session* sess); + int bucketsPerPressure = 1; + int numberXaxisDivisions =10; + + EventDataType rawToPressure ( EventStoreType raw,EventDataType gain); + EventStoreType rawToBucketId ( EventStoreType raw,EventDataType gain); + EventDataType minpressure = 0.0; + EventDataType maxpressure = 0.0; + qint64 totalDuration = 0; + + EventDataType machinePressureMin = 0.0; + EventDataType machinePressureMax = 0.0; + + int firstPlotBucket =0; + int lastPlotBucket =0; + +private: + void init(); }; + class RecalcMAP:public QRunnable { friend class MinutesAtPressure; @@ -58,15 +67,31 @@ public: explicit RecalcMAP(MinutesAtPressure * map) :map(map), m_quit(false), m_done(false) {} virtual ~RecalcMAP(); virtual void run(); - void quit(); + + protected: - void updateTimes(PressureInfo & info, Session * sess); MinutesAtPressure * map; volatile bool m_quit; volatile bool m_done; + +private: + void setSelectionRange(gGraph* graph); + qint64 minTime, maxTime; + ChannelID chanId; // required for debug. + + PressureInfo * ipap_info; + void updateTimes(PressureInfo & info); + void updateEvents(Session*sess,PressureInfo & info); + void updateTimesValues(qint64 d1,qint64 d2, int key,PressureInfo & info); + void updateEventsChannel(Session * sess,ChannelID id, QVector &background, PressureInfo & info ); + void updateFlagData(int ¤tLoc, int & currentEL,int& currentData,qint64 eventTime, QVector &dataArray, PressureInfo & info ) ; + void updateSpanData(int ¤tLoc, int & currentEL,int& currentData,qint64 startSpan, qint64 eventTime , QVector &dataArray, PressureInfo & info ) ; + }; + + class MinutesAtPressure:public Layer { friend class RecalcMAP; @@ -96,6 +121,10 @@ public: return map; } + +protected: + + int numCloned =0; void CloneInto(MinutesAtPressure * layer) { mutex.lock(); timelock.lock(); @@ -103,47 +132,142 @@ public: layer->m_minimum_height = m_minimum_height; layer->m_lastminx = m_lastminx; layer->m_lastmaxx = m_lastmaxx; - layer->times = times; - layer->chans = chans; - layer->events = events; - layer->maxtime = maxtime; - layer->maxevents = maxevents; - layer->m_presChannel = m_presChannel; - layer->m_minpressure = m_minpressure; - layer->m_maxpressure = m_maxpressure; - layer->max_mins = max_mins; - - layer->ahis = ahis; + layer->ipap = ipap; + layer->epap = epap; + layer->numCloned=numCloned+1; timelock.unlock(); + layer->m_enabled = m_enabled; mutex.unlock(); } -protected: - QMutex timelock; - QMutex mutex; - - bool m_empty; - int m_minimum_height; - - qint64 m_lastminx; - qint64 m_lastmaxx; - gGraph * m_graph; + bool isCLoned() {return numCloned!=0;}; RecalcMAP * m_remap; - QMap times; - QMap epap_times; - QList chans; - QHash > events; - int maxtime; - int maxevents; - ChannelID m_presChannel; - EventStoreType m_minpressure; - EventStoreType m_maxpressure; + bool initialized=false; + bool m_empty; + QMutex mutex; + QMutex timelock; + int m_minimum_height; + //QAtomicInteger m_recalcCount; + +private: PressureInfo epap, ipap; + void setEnabled(gGraph &graph); + QHash m_enabled; + gGraph * m_graph; + qint64 m_lastminx; + qint64 m_lastmaxx; + QPoint last_mouse=QPoint(0,0); + + EventDataType m_last_height=0; // re-calculate only when needed. + int m_last_peaktime=0; // re-calculate only when needed. + + bool isEnabled(ChannelID id) ; + QString topBarLabel; + +}; + + +class MapPainter +{ +public: + // environment - set in constructor + QPainter& painter; + gGraph& graph; + QRectF& drawingRect; + QRectF& boundingRect; + EventDataType lineThickness; + + MapPainter( + QPainter& painter, + gGraph& graph, + QRectF& drawingRect , + QRectF& boundingRect ) : + painter(painter), + graph(graph), + drawingRect(drawingRect) , + boundingRect(boundingRect) { + lineThickness= AppSetting->lineThickness(); + }; + + // mouse related + int mouseOverKey; + bool graphSelected; + void setMouse(int mouseOverKey,bool graphSelected) { + this->mouseOverKey=mouseOverKey; + this->graphSelected=graphSelected; + }; + + // for all graphs horizonatal + int startGraphBucket; + int endGraphBucket; + EventDataType pixelsPerBucket; + EventDataType minpressure; + int bucketsPerPressure; + void setHorizontal( EventDataType minpressure, EventDataType maxpressure,EventDataType pixelsPerBucket , int bucketsPerPressure, int catmullRomSplineNumberOfPoints) { + this->startGraphBucket = minpressure*bucketsPerPressure; + this->endGraphBucket = maxpressure*bucketsPerPressure; + this->pixelsPerBucket = pixelsPerBucket; + this->minpressure = minpressure; + this->bucketsPerPressure = bucketsPerPressure; + initCatmullRomSpline(pixelsPerBucket,catmullRomSplineNumberOfPoints); + }; + + void drawEvent(); + void drawSpanEvents(); + void drawEventTick(); + + + // Pen type for drawing - per graph + QPen linePen; + QPen pointSelectionPen; + QPen pointEnhancePen; + QPen tickPen; + QPen tickEnhancePen; + QPen tickEnhanceTransparentPen; + + //EventDataType bottom,top,height,left,right; + + schema::Channel* channel; + schema::ChanType chanType; + EventDataType yPixelsPerUnit; + QVector dataArray; + int startBucket; + int endBucket; + void setChannelInfo(ChannelID id, QVector dataArray, EventDataType yPixelsPerUnit ,int ,int) ; + + void initCatmullRomSpline(EventDataType pixelsPerBucket,int numberOfPoints); + EventDataType catmullRomSplineIncrement, catmullRomSplineInterval; + int catmullRomSplineNumberOfPoints; + EventDataType catmullRomSplineXstep; // based on pixelsPerBucket. + + void setPenColorAlpha(ChannelID channelId ,int opacity) ; + void setPenColorAlpha(ChannelID channelId ) ; + void drawPlot(); + EventDataType dataToYaxis(int value) ; + EventDataType verifyYaxis(EventDataType value); + void initCatmullRomSpline(int numberOfPoints); + void drawPoint(bool fill,int xp, int yp); + EventDataType drawSegment ( int i , EventDataType fromx,EventDataType fromy) ; + + EventDataType yPixelsPerMsec ; + EventDataType yPixelsPerEvent; + EventDataType pixelsPerPressure; + + EventDataType yPixelsPerStep ; + EventDataType yMinutesPerStep; + EventDataType peakMinutes; + + int singleCharWidth, textHeight; + void calculatePeakY(int peaktime ); + int drawYaxis(int peaktime); + + void drawEventYaxis(EventDataType peakEvents,int widest_YAxis); + void drawXaxis(int numberXaxisDivisions , int startGraphBucket , int endGraphBucket); + + void drawMetaData(QPoint& last_mouse , QString& topBarLabel,PressureInfo& ipap , PressureInfo& epap ,QHash& enabled , EventDataType minpressure , EventDataType maxpressure); - EventDataType max_mins; - QMap ahis; }; #endif // MINUTESATPRESSURE_H From e633a82de41359846e44e96d1bcc00d68fc01423 Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Sat, 24 Apr 2021 17:44:27 +1000 Subject: [PATCH 25/28] Consistent multi-file import for non-CPAP loaders (Viatom, Somnopose, Zeo, Dreem) --- Htmldocs/release_notes.html | 2 + .../SleepLib/loader_plugins/dreem_loader.cpp | 9 -- oscar/SleepLib/loader_plugins/dreem_loader.h | 3 +- .../loader_plugins/somnopose_loader.cpp | 34 +--- .../loader_plugins/somnopose_loader.h | 3 +- .../SleepLib/loader_plugins/viatom_loader.cpp | 48 +++--- oscar/SleepLib/loader_plugins/viatom_loader.h | 9 +- oscar/SleepLib/loader_plugins/zeo_loader.cpp | 25 --- oscar/SleepLib/loader_plugins/zeo_loader.h | 3 +- oscar/SleepLib/machine_loader.cpp | 23 +++ oscar/SleepLib/machine_loader.h | 9 ++ oscar/mainwindow.cpp | 148 ++++-------------- oscar/mainwindow.h | 1 + 13 files changed, 112 insertions(+), 205 deletions(-) diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index 24370a09..7206c5a3 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -27,6 +27,8 @@

    • [new] Add Bulgarian translation; update other languages.
    • [new] Improve Somnopose import options.
    • [new] Purge Current Selected Day allows purge of each machine type separately
    • +
    • [new] Multi-file import for non-CPAP loaders (Somnopose, Viatom, Zeo, Dreem)
    • +
    • [new] Weight, BMI and Zombie history appear in statistics
    • [fix] Correct calculation of average leak rate on Welcome page.
    • [fix] Correct installation of non-English Release Notes on Windows.
    • [fix] About/Credits page now offers Google translations to other languages.
    • diff --git a/oscar/SleepLib/loader_plugins/dreem_loader.cpp b/oscar/SleepLib/loader_plugins/dreem_loader.cpp index a6c032d6..0291b09e 100644 --- a/oscar/SleepLib/loader_plugins/dreem_loader.cpp +++ b/oscar/SleepLib/loader_plugins/dreem_loader.cpp @@ -43,15 +43,6 @@ DreemLoader::Detect(const QString & path) return false; } -int -DreemLoader::Open(const QString & dirpath) -{ - qDebug() << "DreemLoader::Open(" << dirpath << ")"; - // Dreem currently crams everything into a single file like Zeo did. - // See OpenFile. - return false; -} - int DreemLoader::OpenFile(const QString & filename) { if (!openCSV(filename)) { diff --git a/oscar/SleepLib/loader_plugins/dreem_loader.h b/oscar/SleepLib/loader_plugins/dreem_loader.h index d318d44e..e866bd2e 100644 --- a/oscar/SleepLib/loader_plugins/dreem_loader.h +++ b/oscar/SleepLib/loader_plugins/dreem_loader.h @@ -25,8 +25,9 @@ class DreemLoader : public MachineLoader virtual bool Detect(const QString & path); - virtual int Open(const QString & path); + virtual int Open(const QString & path) { Q_UNUSED(path); return 0; } // Only for CPAP virtual int OpenFile(const QString & path); + virtual QStringList getNameFilter() { return QStringList("Dreem CSV File (*.csv)"); } static void Register(); virtual int Version() { return dreem_data_version; } diff --git a/oscar/SleepLib/loader_plugins/somnopose_loader.cpp b/oscar/SleepLib/loader_plugins/somnopose_loader.cpp index 40f45de0..9f410d04 100644 --- a/oscar/SleepLib/loader_plugins/somnopose_loader.cpp +++ b/oscar/SleepLib/loader_plugins/somnopose_loader.cpp @@ -27,28 +27,6 @@ SomnoposeLoader::SomnoposeLoader() SomnoposeLoader::~SomnoposeLoader() { } -int SomnoposeLoader::Open(const QString & dirpath) -{ - QString newpath; - - QString dirtag = "somnopose"; - - QString path(dirpath); - path = path.replace("\\", "/"); - - if (path.toLower().endsWith("/" + dirtag)) { - return 0; - //newpath=path; - } else { - newpath = path + "/" + dirtag.toUpper(); - } - - //QString filename; - - // Somnopose folder structure detection stuff here. - - return 0; // number of machines affected -} int SomnoposeLoader::OpenFile(const QString & filename) { @@ -57,10 +35,10 @@ int SomnoposeLoader::OpenFile(const QString & filename) if (filename.toLower().endsWith(".csv")) { if (!file.open(QFile::ReadOnly)) { qDebug() << "Couldn't open Somnopose data file" << filename; - return 0; + return -1; } } else { - return 0; + return -1; } qDebug() << "Opening file" << filename; @@ -107,12 +85,12 @@ int SomnoposeLoader::OpenFile(const QString & filename) // Check we have all fields available if (col_timestamp < 0) { qDebug() << "Header missing timestamp"; - return 0; + return -1; } if ((col_inclination < 0) && (col_orientation < 0) && (col_movement < 0)) { qDebug() << "Header missing all of inclination, orientation, movement (at least one must be present)"; - return 0; + return -1; } QDateTime epoch(QDate(2001, 1, 1)); @@ -169,7 +147,7 @@ int SomnoposeLoader::OpenFile(const QString & filename) if (mach->SessionExists(sid)) { qDebug() << "File " << filename << " already loaded... skipping"; - return -1; // Already imported + return 0; // Already imported } sess = new Session(mach, sid); @@ -222,7 +200,7 @@ int SomnoposeLoader::OpenFile(const QString & filename) p_profile->StoreMachines(); } - return true; + return 1; } diff --git a/oscar/SleepLib/loader_plugins/somnopose_loader.h b/oscar/SleepLib/loader_plugins/somnopose_loader.h index da6effc1..58b9ee7b 100644 --- a/oscar/SleepLib/loader_plugins/somnopose_loader.h +++ b/oscar/SleepLib/loader_plugins/somnopose_loader.h @@ -26,8 +26,9 @@ class SomnoposeLoader : public MachineLoader virtual bool Detect(const QString & path) { Q_UNUSED(path); return false; } // bypass autoscanner - virtual int Open(const QString & path); + virtual int Open(const QString & path) { Q_UNUSED(path); return 0; } // Only for CPAP virtual int OpenFile(const QString & filename); + virtual QStringList getNameFilter() { return QStringList("Somnopose CSV File (*.csv)"); } static void Register(); virtual int Version() { return somnopose_data_version; } diff --git a/oscar/SleepLib/loader_plugins/viatom_loader.cpp b/oscar/SleepLib/loader_plugins/viatom_loader.cpp index 19147c4e..dbee9e85 100644 --- a/oscar/SleepLib/loader_plugins/viatom_loader.cpp +++ b/oscar/SleepLib/loader_plugins/viatom_loader.cpp @@ -34,33 +34,30 @@ ViatomLoader::Detect(const QString & path) } int -ViatomLoader::Open(const QString & dirpath) +ViatomLoader::Open(const QStringList & paths) { - qDebug() << "ViatomLoader::Open(" << dirpath << ")"; + qDebug() << "ViatomLoader::Open(" << paths.join("; ") << ")"; m_mach = nullptr; int imported = 0; int found = 0; s_unexpectedMessages.clear(); - if (QFileInfo(dirpath).isDir()) { - QDir dir(dirpath); - dir.setFilter(QDir::NoDotAndDotDot | QDir::Files | QDir::Hidden); - dir.setNameFilters(getNameFilter()); - dir.setSorting(QDir::Name); - - for (auto & fi : dir.entryInfoList()) { - if (OpenFile(fi.canonicalFilePath())) { - imported++; - } - found++; + int size = paths.size(); + for (int i=0; i < size; i++) { + if (isAborted()) { + break; } - } - else { // This filename has already been filtered by QFileDialog. - if (OpenFile(dirpath)) { + int ok = OpenFile(paths[i]); + if (ok > 0) { imported++; + } else if (ok < 0) { + // Stop on error... + break; } found++; + emit setProgressValue(i+1); + QCoreApplication::processEvents(); } if (!found) { @@ -91,25 +88,30 @@ ViatomLoader::Open(const QString & dirpath) } } - return imported; + return found; } -bool ViatomLoader::OpenFile(const QString & filename) +int ViatomLoader::OpenFile(const QString & filename) { Machine* mach = nullptr; + bool existing = false; - Session* sess = ParseFile(filename); + Session* sess = ParseFile(filename, &existing); if (sess) { SaveSessionToDatabase(sess); mach = sess->machine(); m_mach = mach; + return 1; } - return mach != nullptr; + return existing ? 0 : -1; // -1 = error } -Session* ViatomLoader::ParseFile(const QString & filename) +Session* ViatomLoader::ParseFile(const QString & filename, bool *existing) { + if (existing) { + *existing = false; + } QFile file(filename); if (!file.open(QFile::ReadOnly)) { qDebug() << "Couldn't open Viatom data file" << filename; @@ -136,6 +138,10 @@ Session* ViatomLoader::ParseFile(const QString & filename) if (mach->SessionExists(v.sessionid())) { // Skip already imported session //qDebug() << filename << "session already exists, skipping" << v.sessionid(); + if (existing) { + // Inform the caller (if they are interested) that this session was already imported + *existing = true; + } return nullptr; } diff --git a/oscar/SleepLib/loader_plugins/viatom_loader.h b/oscar/SleepLib/loader_plugins/viatom_loader.h index 7b326ac4..948f539d 100644 --- a/oscar/SleepLib/loader_plugins/viatom_loader.h +++ b/oscar/SleepLib/loader_plugins/viatom_loader.h @@ -28,8 +28,9 @@ class ViatomLoader : public MachineLoader virtual bool Detect(const QString & path); - virtual int Open(const QString & path); - Session* ParseFile(const QString & filename); + virtual int Open(const QString & path) { Q_UNUSED(path); return 0; } // Only for CPAP + virtual int Open(const QStringList & paths); + Session* ParseFile(const QString & filename, bool *existing=0); static void Register(); @@ -40,12 +41,12 @@ class ViatomLoader : public MachineLoader return MachineInfo(MT_OXIMETER, 0, viatom_class_name, QObject::tr("Viatom"), QString(), QString(), QString(), QObject::tr("Viatom Software"), QDateTime::currentDateTime(), viatom_data_version); } - QStringList getNameFilter(); + virtual QStringList getNameFilter(); //Machine *CreateMachine(); protected: - bool OpenFile(const QString & filename); + int OpenFile(const QString & filename); void SaveSessionToDatabase(Session* session); void AddEvent(ChannelID channel, qint64 t, EventDataType value); diff --git a/oscar/SleepLib/loader_plugins/zeo_loader.cpp b/oscar/SleepLib/loader_plugins/zeo_loader.cpp index bdaf4c4d..04f79b7c 100644 --- a/oscar/SleepLib/loader_plugins/zeo_loader.cpp +++ b/oscar/SleepLib/loader_plugins/zeo_loader.cpp @@ -32,31 +32,6 @@ ZEOLoader::~ZEOLoader() closeCSV(); } -int ZEOLoader::Open(const QString & dirpath) -{ - QString newpath; - - QString dirtag = "zeo"; - - // Could Scan the ZEO folder for a list of CSVs - - QString path(dirpath); - path = path.replace("\\", "/"); - - if (path.toLower().endsWith("/" + dirtag)) { - return 0; - //newpath=path; - } else { - newpath = path + "/" + dirtag.toUpper(); - } - - //QString filename; - - // ZEO folder structure detection stuff here. - - return 0; // number of machines affected -} - /*15233: "Sleep Date" 15234: "ZQ" 15236: "Total Z" diff --git a/oscar/SleepLib/loader_plugins/zeo_loader.h b/oscar/SleepLib/loader_plugins/zeo_loader.h index 12f1d6ad..f9ca3c72 100644 --- a/oscar/SleepLib/loader_plugins/zeo_loader.h +++ b/oscar/SleepLib/loader_plugins/zeo_loader.h @@ -27,8 +27,9 @@ class ZEOLoader : public MachineLoader virtual bool Detect(const QString &path) { Q_UNUSED(path); return false; } // bypass autoscanner - virtual int Open(const QString & path); + virtual int Open(const QString & path) { Q_UNUSED(path); return 0; } // Only for CPAP virtual int OpenFile(const QString & filename); + virtual QStringList getNameFilter() { return QStringList("Zeo CSV File (*.csv)"); } static void Register(); virtual int Version() { return zeo_data_version; } diff --git a/oscar/SleepLib/machine_loader.cpp b/oscar/SleepLib/machine_loader.cpp index 93448f1c..f295b99d 100644 --- a/oscar/SleepLib/machine_loader.cpp +++ b/oscar/SleepLib/machine_loader.cpp @@ -316,3 +316,26 @@ bool compressFile(QString infile, QString outfile) return true; } +int MachineLoader::Open(const QStringList & paths) +{ + int i, skipped = 0; + int size = paths.size(); + for (i=0; i < size; i++) { + if (isAborted()) { + break; + } + QString filename = paths[i]; + + int res = OpenFile(filename); + if (res < 0) { + break; + } + if (res == 0) { + // Should we report on skipped count? + skipped++; + } + emit setProgressValue(i+1); + QCoreApplication::processEvents(); + } + return i; +} diff --git a/oscar/SleepLib/machine_loader.h b/oscar/SleepLib/machine_loader.h index 1998208a..70de4c90 100644 --- a/oscar/SleepLib/machine_loader.h +++ b/oscar/SleepLib/machine_loader.h @@ -56,9 +56,18 @@ class MachineLoader: public QObject //! \brief Override this to scan path and detect new machine data virtual int Open(const QString & path) = 0; + //! \brief Load all of the given files and update dialog with progress (for non-CPAP devices) + virtual int Open(const QStringList & paths); + + //! \brief Load a specific (non-CPAP) file + virtual int OpenFile(const QString & path) { Q_UNUSED(path); return 0; } + //! \brief Override to returns the Version number of this MachineLoader virtual int Version() = 0; + //! \brief Name filter for files for this loader + virtual QStringList getNameFilter() { return QStringList(""); } + // !\\brief Used internally by loaders, override to return base MachineInfo record virtual MachineInfo newInfo() { return MachineInfo(); } diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index f49bc6ef..062e9e08 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -2311,64 +2311,14 @@ void MainWindow::doReprocessEvents() void MainWindow::on_actionImport_ZEO_Data_triggered() { - QFileDialog w; - w.setFileMode(QFileDialog::ExistingFiles); - w.setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); - w.setOption(QFileDialog::ShowDirsOnly, false); - w.setNameFilters(QStringList("Zeo CSV File (*.csv)")); - ZEOLoader zeo; - - if (w.exec() == QFileDialog::Accepted) { - QString filename = w.selectedFiles()[0]; - - qDebug() << "Loading ZEO data from" << filename; - int c = zeo.OpenFile(filename); - if (c > 0) { - Notify(tr("Imported %1 ZEO session(s) from\n\n%2").arg(c).arg(filename), tr("Import Success")); - qDebug() << "Imported" << c << "ZEO sessions"; - PopulatePurgeMenu(); - if (overview) overview->ReloadGraphs(); - if (welcome) welcome->refreshPage(); - } else if (c == 0) { - Notify(tr("Already up to date with ZEO data at\n\n%1").arg(filename), tr("Up to date")); - } else { - Notify(tr("Couldn't find any valid ZEO CSV data at\n\n%1").arg(filename),tr("Import Problem")); - } - - daily->LoadDate(daily->getDate()); - } + importNonCPAP(zeo); } void MainWindow::on_actionImport_Dreem_Data_triggered() { - QFileDialog w; - w.setFileMode(QFileDialog::ExistingFiles); - w.setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); - w.setOption(QFileDialog::ShowDirsOnly, false); - w.setNameFilters(QStringList("Dreem CSV File (*.csv)")); - DreemLoader dreem; - - if (w.exec() == QFileDialog::Accepted) { - QString filename = w.selectedFiles()[0]; - - qDebug() << "Loading Dreem data from" << filename; - int c = dreem.OpenFile(filename); - if (c > 0) { - Notify(tr("Imported %1 Dreem session(s) from\n\n%2").arg(c).arg(filename), tr("Import Success")); - qDebug() << "Imported" << c << "Dreem sessions"; - PopulatePurgeMenu(); - if (overview) overview->ReloadGraphs(); - if (welcome) welcome->refreshPage(); - } else if (c == 0) { - Notify(tr("Already up to date with Dreem data at\n\n%1").arg(filename), tr("Up to date")); - } else { - Notify(tr("Couldn't find any valid Dreem CSV data at\n\n%1").arg(filename),tr("Import Problem")); - } - - daily->LoadDate(daily->getDate()); - } + importNonCPAP(dreem); } void MainWindow::on_actionImport_RemStar_MSeries_Data_triggered() @@ -2434,102 +2384,70 @@ void MainWindow::on_actionChange_Data_Folder_triggered() RestartApplication(false, "-d"); } -void MainWindow::on_actionImport_Somnopose_Data_triggered() +void MainWindow::importNonCPAP(MachineLoader &loader) { QFileDialog w; w.setFileMode(QFileDialog::ExistingFiles); w.setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); w.setOption(QFileDialog::ShowDirsOnly, false); +#if defined(Q_OS_WIN) + // Windows can't handle Viatom name filter - use non-native for all non-CPAP loaders. w.setOption(QFileDialog::DontUseNativeDialog, true); - w.setNameFilters(QStringList("Somnopause CSV File (*.csv)")); +#endif + w.setNameFilters(loader.getNameFilter()); - SomnoposeLoader somno; // Display progress if we have more than 1 file to load... ProgressDialog progress(this); if (w.exec() == QFileDialog::Accepted) { - int i, skipped = 0; - int size = w.selectedFiles().size(); + QStringList files = w.selectedFiles(); + int size = files.size(); if (size > 1) { progress.setMessage(QObject::tr("Importing Sessions...")); progress.setProgressMax(size); progress.setProgressValue(0); + progress.addAbortButton(); progress.setWindowModality(Qt::ApplicationModal); + connect(&loader, SIGNAL(setProgressValue(int)), &progress, SLOT(setProgressValue(int))); + connect(&progress, SIGNAL(abortClicked()), &loader, SLOT(abortImport())); progress.open(); QCoreApplication::processEvents(); } - for (i=0; i < size; i++) { - QString filename = w.selectedFiles()[i]; - - int res = somno.OpenFile(filename); - if (!res) { - if (i == 0) { - Notify(tr("There was a problem opening Somnopose Data File: ") + filename); - return; - } else { - Notify(tr("Somnopause Data Import of %1 file(s) complete").arg(i) + "\n\n" + - tr("There was a problem opening Somnopose Data File: ") + filename, - tr("Somnopose Import Partial Success")); - break; - } - } - if (res < 0) { - // Should we report on skipped count? - skipped++; - } - progress.setProgressValue(i+1); + QString name = loader.loaderName(); + int res = loader.Open(files); + if (size > 1) { + disconnect(&loader, SIGNAL(setProgressValue(int)), &progress, SLOT(setProgressValue(int))); + disconnect(&progress, SIGNAL(abortClicked()), &loader, SLOT(abortImport())); + progress.close(); QCoreApplication::processEvents(); } - - if (i == size) { - Notify(tr("Somnopause Data Import complete")); + if (res == 0) { + Notify(tr("There was a problem opening %1 Data File: %2").arg(name, files[0])); + return; + } else if (res < size){ + Notify(tr("%1 Data Import of %2 file(s) complete").arg(name).arg(res) + "\n\n" + + tr("There was a problem opening %1 Data File: %2").arg(name, files[res]), + tr("%1 Import Partial Success").arg(name)); + } else { + Notify(tr("%1 Data Import complete").arg(name)); } PopulatePurgeMenu(); if (overview) overview->ReloadGraphs(); if (welcome) welcome->refreshPage(); daily->LoadDate(daily->getDate()); } +} +void MainWindow::on_actionImport_Somnopose_Data_triggered() +{ + SomnoposeLoader somno; + importNonCPAP(somno); } void MainWindow::on_actionImport_Viatom_Data_triggered() { ViatomLoader viatom; - - QFileDialog w; - w.setFileMode(QFileDialog::AnyFile); - w.setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); - w.setOption(QFileDialog::ShowDirsOnly, false); - w.setNameFilters(viatom.getNameFilter()); -#if defined(Q_OS_WIN) - // Windows can't handle this name filter. - w.setOption(QFileDialog::DontUseNativeDialog, true); - // And since the non-native dialog can't select both directories and files, - // it needs the following to enable selecting multiple files. - w.setFileMode(QFileDialog::ExistingFiles); -#endif - - if (w.exec() == QFileDialog::Accepted) { - QString filename = w.selectedFiles()[0]; - if (w.selectedFiles().size() > 1) { - // The user selected multiple files in a directory, so use the parent directory as the filename. - filename = QFileInfo(filename).absoluteDir().canonicalPath(); - } - - int c = viatom.Open(filename); - if (c > 0) { - Notify(tr("Imported %1 oximetry session(s) from\n\n%2").arg(c).arg(filename), tr("Import Success")); - PopulatePurgeMenu(); - if (overview) overview->ReloadGraphs(); - if (welcome) welcome->refreshPage(); - } else if (c == 0) { - Notify(tr("Already up to date with oximetry data at\n\n%1").arg(filename), tr("Up to date")); - } else { - Notify(tr("Couldn't find any valid data at\n\n%1").arg(filename),tr("Import Problem")); - } - - daily->LoadDate(daily->getDate()); - } + importNonCPAP(viatom); } void MainWindow::GenerateStatistics() diff --git a/oscar/mainwindow.h b/oscar/mainwindow.h index d2352277..ee48c7ec 100644 --- a/oscar/mainwindow.h +++ b/oscar/mainwindow.h @@ -378,6 +378,7 @@ private: void importCPAPDataCards(const QList & datacards); void addMachineToMenu(Machine* mach, QMenu* menu); void purgeDay(MachineType type); + void importNonCPAP(MachineLoader &loader); // QString getWelcomeHTML(); void FreeSessions(); From e20518e84cc8ca0f469705344ad8c5913de734c7 Mon Sep 17 00:00:00 2001 From: Phil Olynyk Date: Sat, 24 Apr 2021 16:52:25 -0400 Subject: [PATCH 26/28] Don't skip the first 40 seconds of ResMed low-rate pressure data. --- Htmldocs/release_notes.html | 1 + oscar/SleepLib/loader_plugins/resmed_loader.cpp | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index 7206c5a3..2c145d8f 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -44,6 +44,7 @@
    • [fix] Statistics headings will now be 99.5% or Max, depending on machine type and preference settings.
    • [fix] Mark exported Journal backup file as UTF-8.
    • [fix] Improve error message when unable to access OSCAR database.
    • +
    • [fix] Stop skipping the first 40 seconds of ResMed low-rate pressure data.

    Changes and fixes in OSCAR v1.2.0 diff --git a/oscar/SleepLib/loader_plugins/resmed_loader.cpp b/oscar/SleepLib/loader_plugins/resmed_loader.cpp index 2183be01..4696d886 100644 --- a/oscar/SleepLib/loader_plugins/resmed_loader.cpp +++ b/oscar/SleepLib/loader_plugins/resmed_loader.cpp @@ -3057,10 +3057,11 @@ void ResmedLoader::ToTimeDelta(Session *sess, ResMedEDFInfo &edf, EDFSignal &es, int startpos = 0; - if ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) { - startpos = 20; // Shave the first 20 seconds of pressure data - tt += rate * startpos; - } +// There's no reason to skip the first 40 seconds of slow data +// if ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) { +// startpos = 20; // Shave the first 40 seconds of pressure data +// tt += rate * startpos; +// } qint16 *sptr = es.dataArray; qint16 *eptr = sptr + recs; From 89edb3f077f207896d0a5c6fdfd06d3492e1ad5d Mon Sep 17 00:00:00 2001 From: kappa44 <6469032-kappa44@users.noreply.gitlab.com> Date: Mon, 26 Apr 2021 10:02:30 +1000 Subject: [PATCH 27/28] Correct Total Time and AHI in CSV Export when non-CPAP devices are used --- Htmldocs/release_notes.html | 1 + oscar/exportcsv.cpp | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index 2c145d8f..56808409 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -45,6 +45,7 @@

  • [fix] Mark exported Journal backup file as UTF-8.
  • [fix] Improve error message when unable to access OSCAR database.
  • [fix] Stop skipping the first 40 seconds of ResMed low-rate pressure data.
  • +
  • [fix] Correct Total Time and AHI in CSV Export when non-CPAP devices are used.
  • Changes and fixes in OSCAR v1.2.0 diff --git a/oscar/exportcsv.cpp b/oscar/exportcsv.cpp index a5a21e80..f28cab24 100644 --- a/oscar/exportcsv.cpp +++ b/oscar/exportcsv.cpp @@ -248,14 +248,13 @@ void ExportCSV::on_exportButton_clicked() data += sep + QString::number(day->size(), 10); data += sep + start.toString(Qt::ISODate); data += sep + end.toString(Qt::ISODate); - int time = day->total_time() / 1000L; + // Given this is a CPAP specific report, just report CPAP hours + int time = int(day->hours(MT_CPAP) * 3600L); int h = time / 3600; int m = int(time / 60) % 60; int s = int(time) % 60; data += sep + QString().sprintf("%02i:%02i:%02i", h, m, s); - float ahi = day->count(CPAP_Obstructive) + day->count(CPAP_Hypopnea) + day->count( - CPAP_Apnea) + day->count(CPAP_ClearAirway); - ahi /= day->hours(); + float ahi = day->calcAHI(); data += sep + QString::number(ahi, 'f', 3); for (int i = 0; i < countlist.size(); i++) { From 9cbd0d7b24f9f035d814176471497e1b4097a6e5 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Mon, 26 Apr 2021 20:10:37 -0400 Subject: [PATCH 28/28] Fix multiple regressions that broke compilation with clang. e633a82: overloaded virtual method errors 0022510: inconsistent missing override error e358d31: unused private fields 3591f11: unused private fields --- oscar/Graphs/MinutesAtPressure.h | 4 ++-- oscar/SleepLib/loader_plugins/intellipap_loader.cpp | 12 ++++++------ oscar/SleepLib/loader_plugins/prs1_loader.h | 4 ---- oscar/SleepLib/loader_plugins/resmed_loader.cpp | 2 +- oscar/SleepLib/loader_plugins/resmed_loader.h | 2 +- oscar/daily.h | 2 +- oscar/tests/resmedtests.cpp | 2 +- 7 files changed, 12 insertions(+), 16 deletions(-) diff --git a/oscar/Graphs/MinutesAtPressure.h b/oscar/Graphs/MinutesAtPressure.h index 7e2644df..effc1084 100644 --- a/oscar/Graphs/MinutesAtPressure.h +++ b/oscar/Graphs/MinutesAtPressure.h @@ -159,8 +159,8 @@ private: qint64 m_lastmaxx; QPoint last_mouse=QPoint(0,0); - EventDataType m_last_height=0; // re-calculate only when needed. - int m_last_peaktime=0; // re-calculate only when needed. + //EventDataType m_last_height=0; // re-calculate only when needed. + //int m_last_peaktime=0; // re-calculate only when needed. bool isEnabled(ChannelID id) ; QString topBarLabel; diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index 4a5a4717..74e72a92 100644 --- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp +++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp @@ -914,16 +914,16 @@ public: bool save(QByteArray dataBA); // save the next record in the file private: - DV6_HEADER hdr; // file header + //DV6_HEADER hdr; // file header QString filetype; QFile hFile; - int record_length; // Length of record block in incoming file - const int maxHistFileSize = 20*10e6; // Maximum size of file before we create a new file + //int record_length; // Length of record block in incoming file + //const int maxHistFileSize = 20*10e6; // Maximum size of file before we create a new file - int numWritten; // Number of records written - quint32 lastTimestamp; - unsigned int wrap_record; + //int numWritten; // Number of records written + //quint32 lastTimestamp; + //unsigned int wrap_record; }; bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) { diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index 8b8b6a65..536aca2b 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -478,10 +478,6 @@ class PRS1Loader : public CPAPLoader quint16 size, int family, int familyVersion); - - //! \brief Open a PRS1 data file, and break into data chunks, delivering them to the correct parser. - bool OpenFile(Machine *mach, const QString & filename); - QHash extra_session; //! \brief PRS1 Data files can store multiple sessions, so store them in this list for later processing. diff --git a/oscar/SleepLib/loader_plugins/resmed_loader.cpp b/oscar/SleepLib/loader_plugins/resmed_loader.cpp index 4696d886..cad613e4 100644 --- a/oscar/SleepLib/loader_plugins/resmed_loader.cpp +++ b/oscar/SleepLib/loader_plugins/resmed_loader.cpp @@ -251,7 +251,7 @@ void backupSTRfiles( const QString strpath, const QString importPath, const QStr MachineInfo & info, QMap & STRmap ); // forward ResMedEDFInfo * fetchSTRandVerify( QString filename, QString serialNumber ); // forward -int ResmedLoader::Open(const QString & dirpath, ResDaySaveCallback s) // alternate for unit testing +int ResmedLoader::OpenWithCallback(const QString & dirpath, ResDaySaveCallback s) // alternate for unit testing { ResDaySaveCallback origCallback = saveCallback; saveCallback = s; diff --git a/oscar/SleepLib/loader_plugins/resmed_loader.h b/oscar/SleepLib/loader_plugins/resmed_loader.h index ac8c82de..bf4da73c 100644 --- a/oscar/SleepLib/loader_plugins/resmed_loader.h +++ b/oscar/SleepLib/loader_plugins/resmed_loader.h @@ -134,7 +134,7 @@ class ResmedLoader : public CPAPLoader volatile int sessionCount; static void SaveSession(ResmedLoader* loader, Session* session); ResDaySaveCallback saveCallback; - int Open(const QString & dirpath, ResDaySaveCallback s); + int OpenWithCallback(const QString & dirpath, ResDaySaveCallback s); protected: //! \brief The STR.edf file is a unique edf file with many signals diff --git a/oscar/daily.h b/oscar/daily.h index 94aad87a..6ab473ed 100644 --- a/oscar/daily.h +++ b/oscar/daily.h @@ -304,7 +304,7 @@ private: */ void UpdateEventsTree(QTreeWidget * tree,Day *day); - virtual bool eventFilter(QObject *object, QEvent *event) override; + virtual bool eventFilter(QObject *object, QEvent *event); void updateCube(); diff --git a/oscar/tests/resmedtests.cpp b/oscar/tests/resmedtests.cpp index 68895a54..d3e059f6 100644 --- a/oscar/tests/resmedtests.cpp +++ b/oscar/tests/resmedtests.cpp @@ -57,7 +57,7 @@ static void parseAndEmitSessionYaml(const QString & path) // necessary for testing. Both are used for now in order to introduce the minimal // set of changes into the Resmed loader needed for testing. s_currentPath = path; - s_loader->Open(path, emitSessionYaml); + s_loader->OpenWithCallback(path, emitSessionYaml); } void ResmedTests::testSessionsToYaml()