Merge branch 'master' into prs1-split-parsing

This commit is contained in:
sawinglogz 2019-05-30 16:33:55 -04:00
commit bc3b93cf9c
17 changed files with 195 additions and 137 deletions

5
.gitignore vendored
View File

@ -46,4 +46,7 @@ Makefile*
*~ *~
#Doxygen output does not go in repository #Doxygen output does not go in repository
doxydoc doxydoc
#SourceTrail files
*.json

View File

@ -2,25 +2,25 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>OSCAR</string> <string>OSCAR</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Created by Qt/QMake</string> <string>Created by Qt/QMake</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>${ASSETCATALOG_COMPILER_APPICON_NAME}</string> <string>${ASSETCATALOG_COMPILER_APPICON_NAME}</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string> <string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}</string> <string>${MACOSX_DEPLOYMENT_TARGET}</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>NSRequiresAquaSystemAppearance</key> <key>NSRequiresAquaSystemAppearance</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>

View File

@ -50,7 +50,7 @@ PROJECT_NUMBER = 1.0.x
# for a project that appears at the top of each page and should give viewer # for a project that appears at the top of each page and should give viewer
# a quick idea about the purpose of the project. Keep the description short. # a quick idea about the purpose of the project. Keep the description short.
PROJECT_BRIEF = OpenSource CPAP Analysis Reporter PROJECT_BRIEF = Open_Source_CPAP_Analysis_Reporter
# With the PROJECT_LOGO tag one can specify an logo or icon that is # With the PROJECT_LOGO tag one can specify an logo or icon that is
# included in the documentation. The maximum height of the logo should not # included in the documentation. The maximum height of the logo should not
@ -1643,7 +1643,7 @@ INCLUDED_BY_GRAPH = YES
# the time of a run. So in most cases it will be better to enable call graphs # the time of a run. So in most cases it will be better to enable call graphs
# for selected functions only using the \callgraph command. # for selected functions only using the \callgraph command.
CALL_GRAPH = NO CALL_GRAPH = YES
# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then # If the CALLER_GRAPH and HAVE_DOT tags are set to YES then
# doxygen will generate a caller dependency graph for every global function # doxygen will generate a caller dependency graph for every global function
@ -1651,7 +1651,7 @@ CALL_GRAPH = NO
# the time of a run. So in most cases it will be better to enable caller # the time of a run. So in most cases it will be better to enable caller
# graphs for selected functions only using the \callergraph command. # graphs for selected functions only using the \callergraph command.
CALLER_GRAPH = NO CALLER_GRAPH = YES
# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen # If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen
# will generate a graphical hierarchy of all classes instead of a textual one. # will generate a graphical hierarchy of all classes instead of a textual one.
@ -1721,7 +1721,7 @@ DOT_TRANSPARENT = NO
# makes dot run faster, but since only newer versions of dot (>1.8.10) # makes dot run faster, but since only newer versions of dot (>1.8.10)
# support this, this feature is disabled by default. # support this, this feature is disabled by default.
DOT_MULTI_TARGETS = NO DOT_MULTI_TARGETS = YES
# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will # If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will
# generate a legend page explaining the meaning of the various boxes and # generate a legend page explaining the meaning of the various boxes and

View File

@ -10,7 +10,3 @@ TEMPLATE = subdirs
SUBDIRS += oscar SUBDIRS += oscar
CONFIG += ordered CONFIG += ordered
macx: {
QMAKE_INFO_PLIST = Building/MacOS/Info.plist.in
}

4
makedoxy.bat Normal file
View File

@ -0,0 +1,4 @@
setlocal
set path="c:\Program Files (x86)\Graphviz2.38\bin";%path%
doxygen
endlocal

View File

@ -1037,7 +1037,11 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin
// Parse the data chunks and read the files.. // Parse the data chunks and read the files..
if (fi.canonicalFilePath().isEmpty()) { if (fi.canonicalFilePath().isEmpty()) {
qWarning() << fi; #if QT_VERSION < QT_VERSION_CHECK(5,12,0)
qWarning() << fi.fileName() << "canonicalFilePath is empty";
#else
qWarning() << fi << "cannonicalFilePath is empty";
#endif
} }
QList<PRS1DataChunk *> Chunks = ParseFile(fi.canonicalFilePath()); QList<PRS1DataChunk *> Chunks = ParseFile(fi.canonicalFilePath());

View File

@ -46,7 +46,8 @@ qint64 timezoneOffset();
enum SummaryType { ST_CNT, ST_SUM, ST_AVG, ST_WAVG, ST_PERC, ST_90P, ST_MIN, ST_MAX, ST_MID, ST_CPH, ST_SPH, ST_FIRST, ST_LAST, ST_HOURS, ST_SESSIONS, ST_SETMIN, ST_SETAVG, ST_SETMAX, ST_SETWAVG, ST_SETSUM, ST_SESSIONID, ST_DATE }; enum SummaryType { ST_CNT, ST_SUM, ST_AVG, ST_WAVG, ST_PERC, ST_90P, ST_MIN, ST_MAX, ST_MID, ST_CPH, ST_SPH, ST_FIRST, ST_LAST, ST_HOURS, ST_SESSIONS, ST_SETMIN, ST_SETAVG, ST_SETMAX, ST_SETWAVG, ST_SETSUM, ST_SESSIONID, ST_DATE };
/*! \enum MachineType /*! \enum MachineType
\brief Generalized type of a machine \brief Generalized type of a machine. MT_CPAP is any type of xPAP machine, MT_OXIMETER any type of Oximeter
\brief MT_SLEEPSTAGE stage of sleep detector (ZEO importer), MT_JOURNAL for optional notes, MT_POSITION for sleep position detector (Somnopose)
*/ */
enum MachineType { MT_UNKNOWN = 0, MT_CPAP, MT_OXIMETER, MT_SLEEPSTAGE, MT_JOURNAL, MT_POSITION, MT_UNCATEGORIZED = 99}; enum MachineType { MT_UNKNOWN = 0, MT_CPAP, MT_OXIMETER, MT_SLEEPSTAGE, MT_JOURNAL, MT_POSITION, MT_UNCATEGORIZED = 99};
//void InitMapsWithoutAwesomeInitializerLists(); //void InitMapsWithoutAwesomeInitializerLists();

View File

@ -581,15 +581,17 @@ void Profile::UnloadMachineData()
return; return;
} }
for (auto & mach : m_machlist) {
mach->saveSessionInfo();
mach->sessionlist.clear();
mach->day.clear();
}
for (auto & day : daylist) { for (auto & day : daylist) {
delete day; delete day;
} }
daylist.clear(); daylist.clear();
for (auto & mach : m_machlist) {
mach->sessionlist.clear();
mach->day.clear();
}
removeLock(); removeLock();
} }
void Profile::LoadMachineData(ProgressDialog *progress) void Profile::LoadMachineData(ProgressDialog *progress)
@ -1159,6 +1161,7 @@ QList<Day *> Profile::getDays(MachineType mt, QDate start, QDate end)
return list; return list;
} }
// Counts number of days in range with data for specified machine type
int Profile::countDays(MachineType mt, QDate start, QDate end) int Profile::countDays(MachineType mt, QDate start, QDate end)
{ {
if (!start.isValid()) { if (!start.isValid()) {
@ -1225,6 +1228,7 @@ int Profile::countCompliantDays(MachineType mt, QDate start, QDate end)
} }
// Count number of events of type code in period
EventDataType Profile::calcCount(ChannelID code, MachineType mt, QDate start, QDate end) EventDataType Profile::calcCount(ChannelID code, MachineType mt, QDate start, QDate end)
{ {
if (!start.isValid()) { if (!start.isValid()) {

View File

@ -367,6 +367,11 @@ const QString STR_US_ShowUnknownFlags = "ShowUnknownFlags";
const QString STR_US_StatReportMode = "StatReportMode"; const QString STR_US_StatReportMode = "StatReportMode";
const QString STR_US_LastOverviewRange = "LastOverviewRange"; const QString STR_US_LastOverviewRange = "LastOverviewRange";
// Values for StatReportMode
const int STAT_MODE_STANDARD = 0;
const int STAT_MODE_MONTHLY = 1;
const int STAT_MODE_RANGE = 2;
class DoctorInfo : public PrefSettings class DoctorInfo : public PrefSettings
{ {
public: public:

View File

@ -63,6 +63,7 @@ class Session
Session(Machine *, SessionID); Session(Machine *, SessionID);
virtual ~Session(); virtual ~Session();
//! \brief Checks whether the supplied time is within the bounds of this session (ms since epoch)
inline bool checkInside(qint64 time) { inline bool checkInside(qint64 time) {
return ((time >= s_first) && (time <= s_last)); return ((time >= s_first) && (time <= s_last));
} }
@ -113,13 +114,16 @@ class Session
//! \brief Sets whether or not session is being used. //! \brief Sets whether or not session is being used.
void setEnabled(bool b); void setEnabled(bool b);
//! \brief Return the earliest time in session (in milliseconds since epoch)
inline qint64 realFirst() const { return s_first; } inline qint64 realFirst() const { return s_first; }
//! \brief Return the latest time in session (in milliseconds since epoch)
inline qint64 realLast() const { return s_last; } inline qint64 realLast() const { return s_last; }
//! \brief Return the start of this sessions time range (in milliseconds since epoch) //! \brief Return the start of this sessions time range, adjusted for clock drift (in milliseconds since epoch)
qint64 first(); qint64 first();
//! \brief Return the end of this sessions time range (in milliseconds since epoch) //! \brief Return the end of this sessions time range, adjusted for clock drift (in milliseconds since epoch)
qint64 last(); qint64 last();
//! \brief Return the millisecond length of this session //! \brief Return the millisecond length of this session
@ -156,10 +160,13 @@ class Session
//! \brief Just set the end of the timerange without comparing //! \brief Just set the end of the timerange without comparing
void really_set_last(qint64 d) { s_last = d; } void really_set_last(qint64 d) { s_last = d; }
//! \brief Set time to lower of 'd' and existing s_first
void set_first(qint64 d) { void set_first(qint64 d) {
if (!s_first) { s_first = d; } if (!s_first) { s_first = d; }
else if (d < s_first) { s_first = d; } else if (d < s_first) { s_first = d; }
} }
//! \brief Set last time to higher of 'd' and existing s_last. Throw warning if 'd' less than s_first.
void set_last(qint64 d) { void set_last(qint64 d) {
if (d <= s_first) { if (d <= s_first) {
qWarning() << "Session::set_last() d<=s_first"; qWarning() << "Session::set_last() d<=s_first";
@ -387,11 +394,16 @@ class Session
//! \brief Returns true if session only contains summary data //! \brief Returns true if session only contains summary data
inline bool summaryOnly() { return s_summaryOnly; } inline bool summaryOnly() { return s_summaryOnly; }
//! \brief Returns true if there are no settings for this session
inline bool noSettings() { return s_noSettings; } inline bool noSettings() { return s_noSettings; }
//! \brief Mark this session as containing summary data only or not (true, false)
inline void setSummaryOnly(bool b) { s_summaryOnly = b; } inline void setSummaryOnly(bool b) { s_summaryOnly = b; }
//! \brief Mark this ssession as having settings data or not (true, false)
inline void setNoSettings(bool b) { s_noSettings = b; } inline void setNoSettings(bool b) { s_noSettings = b; }
//! \brief Mark whether this session has loaded data (true, false)
void setOpened(bool b = true) { void setOpened(bool b = true) {
s_events_loaded = b; s_events_loaded = b;
s_summary_loaded = b; s_summary_loaded = b;
@ -411,6 +423,7 @@ class Session
QString eventFile() const; QString eventFile() const;
//! \brief Returns MachineType for this session
MachineType type() { return s_machtype; } MachineType type() { return s_machtype; }
@ -423,13 +436,17 @@ protected:
SessionID s_session; SessionID s_session;
Machine *s_machine; Machine *s_machine;
//! \brief Time session begins (in ms since epoch)
qint64 s_first; qint64 s_first;
//! \brief Time session ends (in ms since epoch)
qint64 s_last; qint64 s_last;
bool s_changed; bool s_changed;
bool s_lonesession; bool s_lonesession;
bool s_evchecksum_checked; bool s_evchecksum_checked;
bool _first_session; bool _first_session;
bool s_summaryOnly; bool s_summaryOnly;
//! \brief True if there are no settings for this session
bool s_noSettings; bool s_noSettings;
bool s_summary_loaded; bool s_summary_loaded;

View File

@ -964,12 +964,11 @@ QString Daily::getMachineSettings(Day * day) {
ChannelID cpapmode = loader->CPAPModeChannel(); ChannelID cpapmode = loader->CPAPModeChannel();
schema::Channel & chan = schema::channel[cpapmode]; schema::Channel & chan = schema::channel[cpapmode];
first[cpapmode] = QString("<tr><td><p tiltle='%2'>%1</p></td><td colspan=4>%3</td></tr>") first[cpapmode] = QString("<tr><td><p title='%2'>%1</p></td><td colspan=4>%3</td></tr>")
.arg(chan.label()) .arg(chan.label())
.arg(chan.description()) .arg(chan.description())
.arg(day->getCPAPMode()); .arg(day->getCPAPMode());
if (sess) for (; it != it_end; ++it) { if (sess) for (; it != it_end; ++it) {
ChannelID code = it.key(); ChannelID code = it.key();
@ -991,12 +990,14 @@ QString Daily::getMachineSettings(Day * day) {
data = it.value().toString() + " "+ chan.units(); data = it.value().toString() + " "+ chan.units();
} }
if (code ==0xe202) // Format EPR relief correctly
data = formatRelief(data);
QString tmp = QString("<tr><td><p title='%2'>%1</p></td><td colspan=4>%3</td></tr>") QString tmp = QString("<tr><td><p title='%2'>%1</p></td><td colspan=4>%3</td></tr>")
.arg(schema::channel[code].label()) .arg(schema::channel[code].label())
.arg(schema::channel[code].description()) .arg(schema::channel[code].description())
.arg(data); .arg(data);
if ((code == CPAP_IPAP) if ((code == CPAP_IPAP)
|| (code == CPAP_EPAP) || (code == CPAP_EPAP)
|| (code == CPAP_IPAPHi) || (code == CPAP_IPAPHi)

View File

@ -457,7 +457,11 @@ int main(int argc, char *argv[]) {
qDebug() << "Using " + GetAppData() + " as OSCAR data folder"; qDebug() << "Using " + GetAppData() + " as OSCAR data folder";
QDir newDir(GetAppData()); QDir newDir(GetAppData());
if ( ! newDir.exists() || newDir.isEmpty() ) { // directoy doesn't exist yet or is empty, try to migrate old data #if QT_VERSION < QT_VERSION_CHECK(5,9,0)
if ( ! newDir.exists() || newDir.count() == 0 ) { // directoy doesn't exist yet or is empty, try to migrate old data
#else
if ( ! newDir.exists() || newDir.isEmpty() ) { // directoy doesn't exist yet or is empty, try to migrate old data
#endif
if (QMessageBox::question(nullptr, QObject::tr("Migrate SleepyHead Data?"), if (QMessageBox::question(nullptr, QObject::tr("Migrate SleepyHead Data?"),
QObject::tr("On the next screen OSCAR will ask you to select a folder with SleepyHead data") +"\n" + QObject::tr("On the next screen OSCAR will ask you to select a folder with SleepyHead data") +"\n" +
QObject::tr("Click [OK] to go to the next screen or [No] if you do not wish to use any SleepyHead data."), QObject::tr("Click [OK] to go to the next screen or [No] if you do not wish to use any SleepyHead data."),

View File

@ -1345,10 +1345,12 @@ void MainWindow::on_actionCheck_for_Updates_triggered()
bool toolbox_visible = false; bool toolbox_visible = false;
void MainWindow::on_action_Screenshot_triggered() void MainWindow::on_action_Screenshot_triggered()
{ {
setUpdatesEnabled(false);
if (daily) if (daily)
daily->hideSpaceHogs(); daily->hideSpaceHogs();
toolbox_visible = ui->toolBox->isVisible(); toolbox_visible = ui->toolBox->isVisible();
ui->toolBox->hide(); ui->toolBox->hide();
setUpdatesEnabled(true);
QTimer::singleShot(250, this, SLOT(DelayedScreenshot())); QTimer::singleShot(250, this, SLOT(DelayedScreenshot()));
} }
@ -1381,10 +1383,12 @@ void MainWindow::DelayedScreenshot()
} else { } else {
Notify(tr("Screenshot saved to file \"%1\"").arg(QDir::toNativeSeparators(a))); Notify(tr("Screenshot saved to file \"%1\"").arg(QDir::toNativeSeparators(a)));
} }
setUpdatesEnabled(false);
if (daily) if (daily)
daily->showSpaceHogs(); daily->showSpaceHogs();
ui->toolBox->setVisible(toolbox_visible); ui->toolBox->setVisible(toolbox_visible);
setUpdatesEnabled(true);
} }
void MainWindow::on_actionView_Oximetry_triggered() void MainWindow::on_actionView_Oximetry_triggered()

View File

@ -47,9 +47,14 @@ class MainWindow;
This document is an attempt to provide a little technical insight into OSCAR's program internals. This document is an attempt to provide a little technical insight into OSCAR's program internals.
\section project_info Further Information \section project_info Further Information
The project was hosted on sourceforge, and it's original project page can be reached at <a href="http://sourceforge.net/projects/sleepyhead">http://sourceforge.net/projects/sleepyhead</a>. OSCAR is hosted on <a href="https://gitlab.com/pholy/OSCAR-code">Gitlab</a> with full source code available there.
There was also a <a href="http://sourceforge.net/apps/mediawiki/sleepyhead/index.php?title=Main_Page">SleepyHead Wiki</a> containing further information Help for users can be found in the <a href="http://www.apneaboard.com/wiki/index.php?title=OSCAR_Help">OSCAR Help Wiki</a>.
Data structures are described in a <a href="http://www.apneaboard.com/wiki/index.php?title=OSCAR_Data_Information">OSCAR Data Information Wiki</a>.
The SleepyHead project was hosted on sourceforge, and it's original project page can be reached at <a href="http://sourceforge.net/projects/sleepyhead">http://sourceforge.net/projects/sleepyhead</a>.
There was also a <a href="http://sourceforge.net/apps/mediawiki/sleepyhead/index.php?title=Main_Page">SleepyHead Wiki</a> containing further information.
\section structure Program Structure \section structure Program Structure
OSCAR is written in C++ using Qt Toolkit library, and comprises of 3 main components OSCAR is written in C++ using Qt Toolkit library, and comprises of 3 main components

View File

@ -457,3 +457,7 @@ test {
tests/sessiontests.h tests/sessiontests.h
} }
# On macOS put a custom Info.plist into the bundle that disables dark mode on Mojave
macx {
QMAKE_INFO_PLIST = "../Building/MacOS/Info.plist.in"
}

View File

@ -497,6 +497,10 @@ void Statistics::updateRXChanges()
} }
// Statistics constructor is responsible for creating list of rows that will on the Statistics page
// and skeletons of column 1 text that correspond to each calculation type.
// Actual column 1 text is combination of skeleton for the row's calculation time and the text of the row.
// Also creates "machine" names for machine types.
Statistics::Statistics(QObject *parent) : Statistics::Statistics(QObject *parent) :
QObject(parent) QObject(parent)
{ {
@ -575,10 +579,8 @@ Statistics::Statistics(QObject *parent) :
} }
const QString table_width = "width=99%"; // Get the user information block for displaying at top of page
QString Statistics::htmlHeader(bool showheader) QString Statistics::getUserInfo () {
{
QString address = p_profile->user->address(); QString address = p_profile->user->address();
address.replace("\n", "<br/>"); address.replace("\n", "<br/>");
@ -600,61 +602,45 @@ QString Statistics::htmlHeader(bool showheader)
} }
} }
return userinfo;
}
const QString table_width = "width=99%";
// Create the page header in HTML. Includes everything from <head> through <body>
QString Statistics::htmlHeader(bool showheader)
{
QString html = QString("<html><head>")+ QString html = QString("<html><head>")+
"</head>" "</head>"
"<style type='text/css'>"
"p,a,td,body { font-family: '"+QApplication::font().family()+"'; }"
"p,a,td,body { font-size: "+QString::number(QApplication::font().pointSize() + 2)+"px; }"
"table.curved {"
"border: 1px solid gray;"
"border-radius:10px;"
"-moz-border-radius:10px;"
"-webkit-border-radius:10px;"
"page-break-after:auto;"
"-fs-table-paginate: paginate;"
"}"
"tr.datarow:nth-child(even) {"
"background-color: #f8f8f8;"
"}"
"table { page-break-after:auto; -fs-table-paginate: paginate; }"
"tr { page-break-inside:avoid; page-break-after:auto }"
"td { page-break-inside:avoid; page-break-after:auto }"
"thead { display:table-header-group; }"
"tfoot { display:table-footer-group; }"
"</style>"
"<link rel='stylesheet' type='text/css' href='qrc:/docs/tooltips.css' />"
"<script type='text/javascript'>"
"function ChangeColor(tableRow, highLight)"
"{ tableRow.style.backgroundColor = highLight; }"
"function Go(url) { throw(url); }"
"</script>"
"</head>"
"<body leftmargin=0 topmargin=5 rightmargin=0>"; "<body leftmargin=0 topmargin=5 rightmargin=0>";
QPixmap logoPixmap(":/icons/logo-md.png"); QPixmap logoPixmap(":/icons/logo-lg.png");
if (showheader) { if (showheader) {
html += "<div align=center>" html += "<div align=center><table class=curved width='99%'>"
+resizeHTMLPixmap(logoPixmap,64,64)+"<br/>" "<tr>"
"<font size='+3'>" + STR_TR_OSCAR + "</font><br/>" "<td align='left' valign='middle'>" + getUserInfo() + "</td>"
"<font size='+2'>" + tr("Usage Statistics") + "</font><br/>"; "<td align='right' valign='middle' width='150'>"
// "<font size='+1' title=\"" + "<font size='+2'>" + STR_TR_OSCAR + "&nbsp;&nbsp;&nbsp;</font><br/>"
// tr("For legal reasons without manufacturer documentation, OSCAR is unsuitable as a compliance/medical reporting tool.") + "<font size='+1'>" + QObject::tr("Usage Statistics") + "&nbsp;&nbsp;&nbsp;</font>"
// "\">" + tr("(NOT approved for compliance or medical reporting purposes)")+"</font><br/>"; "</td>"
if (!userinfo.isEmpty()) "<td align='right' valign='middle' width='150'>" + resizeHTMLPixmap(logoPixmap,120,120)+"&nbsp;&nbsp;&nbsp;<br/>"
html += "<br/>"+userinfo; "</td>"
html += "</div><br/>"; "</tr>"
} "</table>"
"</div><br/>";
}
return html; return html;
} }
// HTML for page footer
QString Statistics::htmlFooter(bool showinfo) QString Statistics::htmlFooter(bool showinfo)
{ {
QString html; QString html;
if (showinfo) { if (showinfo) {
html += "<hr/><div align=center><font size='-1'><i>"; html += "<hr><div align=center><font size='-1'><i>";
html += tr("This report was generated by OSCAR v%1").arg(ShortVersionString) + "<br/>" html += tr("This report was generated by OSCAR v%1").arg(ShortVersionString) + "<br/>"
+tr("OSCAR is free open-source CPAP report software"); +tr("OSCAR is free open-source CPAP report software");
html += "</i></font></div>"; html += "</i></font></div>";
@ -665,7 +651,8 @@ QString Statistics::htmlFooter(bool showinfo)
} }
// Calculate AHI for a period as total # of events / total hours used
// Add RERA if calculating RDI instead of just AHI
EventDataType calcAHI(QDate start, QDate end) EventDataType calcAHI(QDate start, QDate end)
{ {
EventDataType val = (p_profile->calcCount(CPAP_Obstructive, MT_CPAP, start, end) EventDataType val = (p_profile->calcCount(CPAP_Obstructive, MT_CPAP, start, end)
@ -688,6 +675,7 @@ EventDataType calcAHI(QDate start, QDate end)
return val; return val;
} }
// Calculate flow limits per hour
EventDataType calcFL(QDate start, QDate end) EventDataType calcFL(QDate start, QDate end)
{ {
EventDataType val = (p_profile->calcCount(CPAP_FlowLimit, MT_CPAP, start, end)); EventDataType val = (p_profile->calcCount(CPAP_FlowLimit, MT_CPAP, start, end));
@ -702,6 +690,7 @@ EventDataType calcFL(QDate start, QDate end)
return val; return val;
} }
// Calculate ...(what are these?)
EventDataType calcSA(QDate start, QDate end) EventDataType calcSA(QDate start, QDate end)
{ {
EventDataType val = (p_profile->calcCount(CPAP_SensAwake, MT_CPAP, start, end)); EventDataType val = (p_profile->calcCount(CPAP_SensAwake, MT_CPAP, start, end));
@ -716,7 +705,7 @@ EventDataType calcSA(QDate start, QDate end)
return val; return val;
} }
// Structure for recording Prescription Changes (now called Machine Settings Changes)
struct RXChange { struct RXChange {
RXChange() { highlight = 0; machine = nullptr; } RXChange() { highlight = 0; machine = nullptr; }
RXChange(const RXChange &copy) { RXChange(const RXChange &copy) {
@ -767,6 +756,7 @@ struct UsageData {
EventDataType ahi; EventDataType ahi;
EventDataType hours; EventDataType hours;
}; };
bool operator <(const UsageData &c1, const UsageData &c2) bool operator <(const UsageData &c1, const UsageData &c2)
{ {
if (c1.ahi < c2.ahi) { if (c1.ahi < c2.ahi) {
@ -872,7 +862,7 @@ QString Statistics::GenerateRXChanges()
QString html = "<div align=center><br/>"; QString html = "<div align=center><br/>";
html += QString("<table class=curved style=\"page-break-before:always;\" "+table_width+" width=100%>"); html += QString("<table class=curved style=\"page-break-before:always;\" "+table_width+" width=100%>");
html += "<thead>"; html += "<thead>";
html += "<tr bgcolor='"+heading_color+"'><th colspan=9 align=center><font size=+2>" + tr("Changes to Prescription Settings") + "</font></th></tr>"; html += "<tr bgcolor='"+heading_color+"'><th colspan=9 align=center><font size=+2>" + tr("Changes to Machine Settings") + "</font></th></tr>";
// QString extratxt; // QString extratxt;
@ -947,6 +937,25 @@ QString Statistics::GenerateRXChanges()
return html; return html;
} }
// Report no data available
QString Statistics::htmlNoData()
{
QString html = "<div align=center>";
html += QString( "<p><font size=\"+3\"><br />" + tr("I can haz data?!?") + "</font></p>"+
"<p><img src='qrc:/icons/logo-lm.png' width=\"100\" height=\"100\"></p>"
"<p><i>"+tr("Oscar has no data to report :(")+"</i></p>");
return html;
}
// Get RDI or AHI text depending on user preferences
QString Statistics::getRDIorAHIText() {
if (p_profile->general->calculateRDI()) {
return STR_TR_RDI;
}
return STR_TR_AHI;
}
// Create the HTML that will be the Statistics page.
QString Statistics::GenerateHTML() QString Statistics::GenerateHTML()
{ {
QList<Machine *> cpap_machines = p_profile->GetMachines(MT_CPAP); QList<Machine *> cpap_machines = p_profile->GetMachines(MT_CPAP);
@ -956,6 +965,7 @@ QString Statistics::GenerateHTML()
mach.append(cpap_machines); mach.append(cpap_machines);
mach.append(oximeters); mach.append(oximeters);
// Go through all CPAP and Oximeter machines and see if any data is present
bool havedata = false; bool havedata = false;
for (int i=0; i < mach.size(); ++i) { for (int i=0; i < mach.size(); ++i) {
int daysize = mach[i]->day.size(); int daysize = mach[i]->day.size();
@ -965,69 +975,52 @@ QString Statistics::GenerateHTML()
} }
} }
// Create HTML header and <body> statement
QString html = htmlHeader(havedata); QString html = htmlHeader(havedata);
// return ""; // If we don't have any data, return HTML that says that and we are done
if (!havedata) {
html += htmlNoData();
html += htmlFooter(havedata);
return html;
}
// Find first and last days with valid CPAP data // Find first and last days with valid CPAP data
QDate lastcpap = p_profile->LastGoodDay(MT_CPAP); QDate lastcpap = p_profile->LastGoodDay(MT_CPAP);
QDate firstcpap = p_profile->FirstGoodDay(MT_CPAP); QDate firstcpap = p_profile->FirstGoodDay(MT_CPAP);
// Get dates for standard report (last week, month, 6 months, year)
QDate cpapweek = lastcpap.addDays(-6); QDate cpapweek = lastcpap.addDays(-6);
QDate cpapmonth = lastcpap.addDays(-29); QDate cpapmonth = lastcpap.addDays(-29);
QDate cpap6month = lastcpap.addMonths(-6); QDate cpap6month = lastcpap.addMonths(-6);
QDate cpapyear = lastcpap.addMonths(-12); QDate cpapyear = lastcpap.addMonths(-12);
if (cpapweek < firstcpap) { cpapweek = firstcpap; } // but not before the first available date of course
if (cpapmonth < firstcpap) { cpapmonth = firstcpap; } if (cpapweek < firstcpap) { cpapweek = firstcpap; }
if (cpapmonth < firstcpap) { cpapmonth = firstcpap; }
if (cpap6month < firstcpap) { cpap6month = firstcpap; } if (cpap6month < firstcpap) { cpap6month = firstcpap; }
if (cpapyear < firstcpap) { cpapyear = firstcpap; } if (cpapyear < firstcpap) { cpapyear = firstcpap; }
if (!havedata) { QString ahitxt = getRDIorAHIText();
// html += "<div align=center><table class=curved height=100% "+table_width+">";
html += "<div align=center>";
// html += QString("<tr><td align=center>") +
html += QString( "<p><font size=\"+3\"><br />" + tr("I can haz data?!?") + "</font></p>"+
"<p><img src='qrc:/icons/logo-lm.png' width=\"100\" height=\"100\"></p>"
"<p><i>"+tr("Oscar has no data to report :(")+"</i></p>");
// "</table></div>"; // Prepare top of table
html += htmlFooter(havedata);
return html;
}
// int cpapdays = p_profile->countDays(MT_CPAP, firstcpap, lastcpap);
// CPAPMode cpapmode = (CPAPMode)(int)p_profile->calcSettingsMax(CPAP_Mode, MT_CPAP, firstcpap, lastcpap);
// float percentile = p_profile->general->prefCalcPercentile() / 100.0;
// int mididx=p_profile->general->prefCalcMiddle();
// SummaryType ST_mid;
// if (mididx==0) ST_mid=ST_PERC;
// if (mididx==1) ST_mid=ST_WAVG;
// if (mididx==2) ST_mid=ST_AVG;
QString ahitxt;
if (p_profile->general->calculateRDI()) {
ahitxt = STR_TR_RDI;
} else {
ahitxt = STR_TR_AHI;
}
// int decimals = 2;
html += "<div align=center>"; html += "<div align=center>";
html += "<table class=curved "+table_width+">"; html += "<table class=curved "+table_width+">";
// Compute number of monthly periods for a monthly rather than standard time distribution
int number_periods = 0; int number_periods = 0;
if (p_profile->general->statReportMode() == 1) { if (p_profile->general->statReportMode() == STAT_MODE_MONTHLY) {
number_periods = p_profile->FirstDay().daysTo(p_profile->LastDay()) / 30; QDate beginDate = qMax(firstcpap, lastcpap.addYears(-1));
int beginMonth = beginDate.month();
int lastMonth = lastcpap.month();
if (lastMonth < beginMonth) lastMonth += 12; // handle time extending to next year
number_periods = lastMonth - beginMonth + 1;
if (number_periods < 1) {
qDebug() << "*** Begin" << beginDate << "beginMonth" << beginMonth << "lastMonth" << lastMonth << "periods" << number_periods;
number_periods = 1;
}
// But not more than one year
if (number_periods > 12) { if (number_periods > 12) {
number_periods = 12; number_periods = 12;
} }
@ -1037,8 +1030,8 @@ QString Statistics::GenerateHTML()
QList<Period> periods; QList<Period> periods;
bool skipsection = false;; bool skipsection = false;;
// Loop through all rows of the Statistics report
for (QList<StatisticsRow>::iterator i = rows.begin(); i != rows.end(); ++i) { for (QList<StatisticsRow>::iterator i = rows.begin(); i != rows.end(); ++i) {
StatisticsRow &row = (*i); StatisticsRow &row = (*i);
QString name; QString name;
@ -1047,14 +1040,15 @@ QString Statistics::GenerateHTML()
last = p_profile->LastGoodDay(row.type); last = p_profile->LastGoodDay(row.type);
first = p_profile->FirstGoodDay(row.type); first = p_profile->FirstGoodDay(row.type);
// Clear the periods (columns)
periods.clear(); periods.clear();
if (p_profile->general->statReportMode() == 0) { if (p_profile->general->statReportMode() == STAT_MODE_STANDARD) {
periods.push_back(Period(last,last,tr("Most Recent"))); periods.push_back(Period(last,last,tr("Most Recent")));
periods.push_back(Period(qMax(last.addDays(-6), first), last, tr("Last Week"))); periods.push_back(Period(qMax(last.addDays(-6), first), last, tr("Last Week")));
periods.push_back(Period(qMax(last.addDays(-29),first), last, tr("Last 30 Days"))); periods.push_back(Period(qMax(last.addDays(-29),first), last, tr("Last 30 Days")));
periods.push_back(Period(qMax(last.addMonths(-6), first), last, tr("Last 6 Months"))); periods.push_back(Period(qMax(last.addMonths(-6), first), last, tr("Last 6 Months")));
periods.push_back(Period(qMax(last.addMonths(-12), first), last, tr("Last Year"))); periods.push_back(Period(qMax(last.addMonths(-12), first), last, tr("Last Year")));
} else { } else { // STAT_MODE_MONTHLY or STAT_MODE_RANGE
QDate l=last,s=last; QDate l=last,s=last;
periods.push_back(Period(last,last,tr("Last Session"))); periods.push_back(Period(last,last,tr("Last Session")));
@ -1157,7 +1151,7 @@ QString Statistics::GenerateHTML()
int np = periods.size(); int np = periods.size();
int width; int width;
for (int j=0; j < np; j++) { for (int j=0; j < np; j++) {
if (p_profile->general->statReportMode() == 1) { if (p_profile->general->statReportMode() == STAT_MODE_MONTHLY) {
width = j < np-1 ? 6 : 100 - (25 + 6*(np-1)); width = j < np-1 ? 6 : 100 - (25 + 6*(np-1));
} else { } else {
width = 75/np; width = 75/np;
@ -1498,7 +1492,9 @@ QString StatisticsRow::value(QDate start, QDate end)
value = QString("%1").arg(formatTime(p_profile->calcHours(type, start, end) / days)); value = QString("%1").arg(formatTime(p_profile->calcHours(type, start, end) / days));
} else if (calc == SC_COMPLIANCE) { } else if (calc == SC_COMPLIANCE) {
float c = p_profile->countCompliantDays(type, start, end); float c = p_profile->countCompliantDays(type, start, end);
float p = (100.0 / days) * c; // float p = (100.0 / days) * c;
float realDays = qAbs(start.daysTo(end)) + 1;
float p = (100.0 / realDays) * c;
value = QString("%1%").arg(p, 0, 'f', 0); value = QString("%1%").arg(p, 0, 'f', 0);
} else if (calc == SC_DAYS) { } else if (calc == SC_DAYS) {
value = QString("%1").arg(p_profile->countDays(type, start, end)); value = QString("%1").arg(p_profile->countDays(type, start, end));

View File

@ -15,10 +15,14 @@
#include "SleepLib/schema.h" #include "SleepLib/schema.h"
#include "SleepLib/machine.h" #include "SleepLib/machine.h"
//! \brief Type of calculation on one statistics row
enum StatCalcType { enum StatCalcType {
SC_UNDEFINED=0, SC_COLUMNHEADERS, SC_HEADING, SC_SUBHEADING, SC_MEDIAN, SC_AVG, SC_WAVG, SC_90P, SC_MIN, SC_MAX, SC_CPH, SC_SPH, SC_AHI, SC_HOURS, SC_COMPLIANCE, SC_DAYS, SC_ABOVE, SC_BELOW SC_UNDEFINED=0, SC_COLUMNHEADERS, SC_HEADING, SC_SUBHEADING, SC_MEDIAN, SC_AVG, SC_WAVG, SC_90P, SC_MIN, SC_MAX, SC_CPH, SC_SPH, SC_AHI, SC_HOURS, SC_COMPLIANCE, SC_DAYS, SC_ABOVE, SC_BELOW
}; };
/*! \struct StatisticsRow
\brief Describes a single row on the statistics page
*/
struct StatisticsRow { struct StatisticsRow {
StatisticsRow() { calc=SC_UNDEFINED; } StatisticsRow() { calc=SC_UNDEFINED; }
StatisticsRow(QString src, QString calc, QString type) { StatisticsRow(QString src, QString calc, QString type) {
@ -40,6 +44,7 @@ struct StatisticsRow {
StatCalcType calc; StatCalcType calc;
MachineType type; MachineType type;
//! \brief Looks up calculation type for this row
StatCalcType lookupCalc(QString calc) StatCalcType lookupCalc(QString calc)
{ {
if (calc.compare("avg",Qt::CaseInsensitive)==0) { if (calc.compare("avg",Qt::CaseInsensitive)==0) {
@ -74,6 +79,7 @@ struct StatisticsRow {
return SC_UNDEFINED; return SC_UNDEFINED;
} }
//! \brief Look up machine type
MachineType lookupType(QString type) MachineType lookupType(QString type)
{ {
if (type.compare("cpap", Qt::CaseInsensitive)==0) { if (type.compare("cpap", Qt::CaseInsensitive)==0) {
@ -94,6 +100,7 @@ struct StatisticsRow {
QString value(QDate start, QDate end); QString value(QDate start, QDate end);
}; };
//! \class Prescription (Machine) setting
class RXItem { class RXItem {
public: public:
RXItem() { RXItem() {
@ -156,6 +163,8 @@ class Statistics : public QObject
void saveRXChanges(); void saveRXChanges();
void updateRXChanges(); void updateRXChanges();
QString getUserInfo();
QString getRDIorAHIText();
QString GenerateHTML(); QString GenerateHTML();
QString GenerateMachineList(); QString GenerateMachineList();
QString GenerateRXChanges(); QString GenerateRXChanges();
@ -164,6 +173,7 @@ class Statistics : public QObject
protected: protected:
QString htmlNoData();
QString htmlHeader(bool showheader); QString htmlHeader(bool showheader);
QString htmlFooter(bool showinfo=true); QString htmlFooter(bool showinfo=true);