diff --git a/Translations/Hebrew.he.ts b/Translations/Hebrew.he.ts index 27545fc6..f82e515c 100644 --- a/Translations/Hebrew.he.ts +++ b/Translations/Hebrew.he.ts @@ -32,7 +32,7 @@ Show data folder - + הצג את מחיצת הנתונים @@ -75,12 +75,12 @@ Could not find the oximeter file: - + קובץ נתוני אוקסימטר לא קיים: Could not open the oximeter file: - + כשל בפתיחת קובץ אוקסימטר: @@ -98,12 +98,12 @@ Could not find the oximeter file: - + קובץ נתוני אוקסימטר לא קיים: Could not open the oximeter file: - + כשל בפתיחת קובץ אוקסימטר: @@ -111,7 +111,7 @@ Checking for newer OSCAR versions - + בודק האם גירסה חדשה זמינה @@ -124,7 +124,7 @@ Show or hide the calender - הראה או הסתר את לוח השנה + הצג או הסתר את לוח השנה @@ -165,7 +165,7 @@ i - + @@ -186,17 +186,17 @@ I'm feeling ... - + ההרגשה שלי... If height is greater than zero in Preferences Dialog, setting weight here will show Body Mass Index (BMI) value - + משקל גוף משמש לחישוב BMI Flags - + דגלים @@ -206,7 +206,7 @@ Show/hide available graphs. - + החלף נראות גרפים. @@ -301,12 +301,12 @@ Unable to display Pie Chart on this system - + אין אפשרות להציג גרף עוגה This CPAP device does NOT record detailed data - + המכשיר לא רושם נתונים מפורטים @@ -369,27 +369,28 @@ UF1 - + UF2 - + Time at Pressure - + Keep chart titles in English so they can be posted to an English forum + Session Start Times - + זמני התחלת שימוש Session End Times - + זמני סיום שימוש @@ -404,97 +405,98 @@ Unknown Session - + שימוש כללי Duration - + משך Click to %1 this session. - + ללחוץ כדי %1 שימוש זה. disable - + להסתיר enable - + לכלול %1 Session #%2 - + %1 שימוש %2 %1h %2m %3s - + Device Settings - + הגדרות המכשיר (Mode and Pressure settings missing; yesterday's shown.) - + (חסרות הגדרות מצב ולחץ: מראה של אתמול) 10 of 10 Event Types - + This bookmark is in a currently disabled area.. - + סימניה באזור שכרגע מוסתר. 10 of 10 Graphs + What is this for? Model %1 - %2 - + מודל %1 - %2 PAP Mode: %1 - + מצב פאפ: %1 This day just contains summary data, only limited information is available. - + עבור יום זה קיימים רק נתוני סיכום. Total time in apnea - + סה"כ זמן בדום נשימה Time over leak redline - + משך חריגה מרמת דליפה תקינה Total ramp time - + סה"כ זמן ראמפ Time outside of ramp - + זמן מחוץ לראמפ @@ -509,7 +511,7 @@ "Nothing's here!" - + לא קיימים נתונים @@ -529,17 +531,17 @@ no data :( - + חסרים נתונים :( Sorry, this device only provides compliance data. - + המכשיר לא רושם נתונים מפורטים. No data is available for this day. - + אין נתונים ליום זה. @@ -558,34 +560,36 @@ ERROR The start date MUST be before the end date - + תאריך התחלה חייבת להיות לפני תאריך סיום The entered start date %1 is after the end date %2 - + תאריך התחלה %1 הוא אחרי תאריך סיום %2 Hint: Change the end date first - + +כדאי קודם לשנות את תאריך הסיום The entered end date %1 - + תאריך סיום %1 is before the start date %1 - + לפני תאריך התחלה %1 Hint: Change the start date first - + +כדאי קודם לשנות את תאריך ההתחלה @@ -791,17 +795,17 @@ Hint: Change the start date first Import Error - + שגיאה ביבוא This device Record cannot be imported in this profile. - + אי אפשר לייבא את הרשומה לתוך הפרופיל הנוכחי. The Day records overlap with already existing content. - + כבר קיימים נתונים לאותו יום. @@ -809,72 +813,72 @@ Hint: Change the start date first Hide this message - + הסתר Search Topic: - + חפש: Help Files are not yet available for %1 and will display in %2. - + עדיין חסר עזרה בשפה %1. מציג בשפה %2. Help files do not appear to be present. - + חסר קובץ טקסט עזרה. HelpEngine did not set up correctly - + כשל באיתחול HelpEngine HelpEngine could not register documentation correctly. - + HelpEngine לא הצליחה לרשום את קובץ התיעוד. Contents - + תוכן Index - + אינדקס Search - + חיפוש No documentation available - + תיעוד לא קיים Please wait a bit.. Indexing still in progress - + עדיין מכין את קובץ העזרה No - + לא %1 result(s) for "%2" - + %1 תוצאות עבור %2 clear - + נקה @@ -882,12 +886,12 @@ Hint: Change the start date first Could not find the oximeter file: - + קובץ נתוני אוקסימטר לא קיים: Could not open the oximeter file: - + כשל בפתיחת קובץ אוקסימטר: @@ -900,33 +904,33 @@ Hint: Change the start date first Report Mode - + סוג דוח Standard - + רגיל Monthly - + חודשי Date Range - + תאריכים Navigation - + מסכים Profiles - + פרופילים @@ -961,7 +965,7 @@ Hint: Change the start date first Records - + רשומות @@ -971,7 +975,7 @@ Hint: Change the start date first Troubleshooting - + פתרונות @@ -991,22 +995,22 @@ Hint: Change the start date first Exit - + יציאה Show Daily view - + מבט יומי Show Overview view - + מבט על &About OSCAR - + &אודות OSCAR @@ -1021,12 +1025,12 @@ Hint: Change the start date first Reset Graph &Heights - + איפוס &גובה הגרפים Reset sizes of graphs - + איפוס גודל הגרפים @@ -1081,22 +1085,22 @@ Hint: Change the start date first Show Right Sidebar - + הצג סרגל ימני View S&tatistics - + &סטטיסטיקה View Statistics - + סטטיסטיקה Show Statistics view - + הצג סטטיסטיקה @@ -1106,12 +1110,12 @@ Hint: Change the start date first Change &Language - + שינוי &שפה Change &Data Folder - + שינוי &מחיצת נתונים @@ -1151,47 +1155,47 @@ Hint: Change the start date first Show &Pie Chart - + הצג גרף &עוגה Show Pie Chart on Daily page - + הצג גרף עוגה במסך היומי Standard graph order, good for CPAP, APAP, Bi-Level - + סדר גרפים רגיל - טוב לרוב המקרים Advanced - + מתקדם Advanced graph order, good for ASV, AVAPS - + סדר גרפים מתקדם - טוב ל ASV, AVAPS Show Personal Data - + הצג נתונים אישיים Check For &Updates - + בדוק האם גירסה &חדשה זמינה Daily Sidebar - + סרגל צד יומי &Import CPAP Card Data - + &ייבוא נתוני כרטיס סיפאפ @@ -1201,22 +1205,22 @@ Hint: Change the start date first Show Daily Left Sidebar - + הצג סרגל יומי שמאלי Daily Calendar - + לוח יומי Show Daily Calendar - + הצג לוח יומי Backup &Journal - + גיבוי יומן @@ -1226,17 +1230,17 @@ Hint: Change the start date first CSV Export Wizard - + אשף יצוא CSV Export for Review - + יצוא לבדיקה Report an Issue - + @@ -1246,7 +1250,7 @@ Hint: Change the start date first Exp&ort Data - + יצוא &נתונים @@ -1306,12 +1310,12 @@ Hint: Change the start date first Show Debug Pane - הראה חלון דיבוג + Take &Screenshot - &תמונת מסך + &צילום מסך @@ -1341,7 +1345,7 @@ Hint: Change the start date first Right &Sidebar - תצוגת צד &ימני + תצוגת סרגל &ימני @@ -1367,7 +1371,7 @@ Hint: Change the start date first Welcome - ברוך הבא + כניסה @@ -1387,87 +1391,91 @@ Hint: Change the start date first Help Browser - + הצג עזרה Loading profile "%1" - + מציג פרופיל "%1" %1 (Profile: %2) - + %1 (פרופיל: %2) Imported %1 CPAP session(s) from %2 - + נקראו %1 רשומות שימוש מ- + +%2 Import Success - + הייבוא הצליח Already up to date with CPAP data at %1 - + כבר מעודכן בנתונים ב- + +%1 Up to date - + מעודכן Import Problem - + בעיה בייבוא נתונים Please wait, importing from backup folder(s)... - + רק רגע, מייבא מחיצות גיבוי... Please insert your CPAP data card... - + להכניס את כרטיס הנתונים... Choose a folder - + לבחור מחיצה No profile has been selected for Import. - + אף פרופיל לא נבחר. Import is already running in the background. - + ייבוא כבר רץ ברקע. A %1 file structure for a %2 was located at: - + נתונים מתאימים ל-%1 %2 נמצאו ב: A %1 file structure was located at: - + נתונים מתאימים ל-%1 נמצאו ב: CPAP Data Located - + נמצאו נתונים @@ -1478,52 +1486,52 @@ Hint: Change the start date first There was a problem opening %1 Data File: %2 - + כשל בפתיחת קובץ %1: %2 %1 Data Import of %2 file(s) complete - + %1 ייבוא של קבצים %2 הצליח %1 Import Partial Success - + %1 הצלחה חלקית בייבוא הנתונים %1 Data Import complete - + %1 הסתיים ייבוא הנתונים Would you like to import from this location? - + לייבא נתונים מפה? Specify - + בחירה Import Reminder - + תזכורת לייבא נתונים Please open a profile first. - + קודם לבחור פרופיל. There was an error saving screenshot to file "%1" - + כשל בשמירת צילום מסך לקובץ "%1" Screenshot saved to file "%1" - + צילום מסך נשמר לקובץ "%1" @@ -1553,12 +1561,12 @@ Hint: Change the start date first Find your CPAP data card - + לבחור את כרטיס הנתונים Choose where to save screenshot - + איפה לשמור את צילום המסך @@ -1601,7 +1609,7 @@ Hint: Change the start date first Couldn't find any valid Device Data at %1 - + אין נתונים תקינים ב-%1 @@ -1678,12 +1686,12 @@ Hint: Change the start date first %1's Journal - + היומן של %1 Choose where to save journal - + איפה לשמור את היומן @@ -1735,12 +1743,12 @@ Hint: Change the start date first OSCAR Information - + אודות התוכנה Bookmarks - סימניות + סימניות @@ -1841,12 +1849,12 @@ Hint: Change the start date first about:blank - + Very weak password protection and not recommended if security is required. - + מנגנון הסיסמה בתוכנה מספקת רמת הגנת מידע מאוד חלשה. @@ -1871,7 +1879,7 @@ Hint: Change the start date first It's totally ok to fib or skip this, but your rough age is needed to enhance accuracy of certain calculations. - + לא חובה, אבל משהו בערך יכול לדייק חלק מהחישובים. @@ -1886,7 +1894,7 @@ Hint: Change the start date first Gender - מגדר + מין @@ -1906,12 +1914,12 @@ Hint: Change the start date first Metric - + ס"מ English - + רגל ואינצ @@ -2026,7 +2034,7 @@ Hint: Change the start date first Welcome to the Open Source CPAP Analysis Reporter - + OSCAR - תוכנה להצגת נתוני סיפאפ @@ -2041,42 +2049,42 @@ Hint: Change the start date first PLEASE READ CAREFULLY - + חשוב!!! OSCAR is intended merely as a data viewer, and definitely not a substitute for competent medical guidance from your Doctor. - + התוכנה בכלל איננה תחליף לייעוץ רפואי מוסמך! היא מיועדת להצגת נתונים בלבד. Accuracy of any data displayed is not and can not be guaranteed. - + אין כל אחראיות לגבי נכונות מה שמוצג בתוכנה זו. Any reports generated are for PERSONAL USE ONLY, and NOT IN ANY WAY fit for compliance or medical diagnostic purposes. - + כל מה שמוצג על ידי תוכנה זו הוא לשימושך האישי בלבד, לא למעקב אחרי הטיפול, לא לצורכי אבחון ולא לשום צורך רפואי אחר. The authors will not be held liable for <u>anything</u> related to the use or misuse of this software. - + מייצרי התוכנה לא יהיו אחראיים לכל תוצאה שהיא הקשורה לשימוש, שימוש לרעה, או אי-שימוש בתוכנה. Use of this software is entirely at your own risk. - + השימוש בתוכנה זו היא לגמרי על אחראיות המשתמש. OSCAR is copyright &copy;2011-2018 Mark Watkins and portions &copy;2019-2022 The OSCAR Team - + Please provide a username for this profile - + שם של משתמש הפרופיל הזה @@ -2101,7 +2109,7 @@ Hint: Change the start date first &Close this window - + &סגור @@ -2159,7 +2167,7 @@ Hint: Change the start date first Snapshot - + תמונת מצב @@ -2219,13 +2227,14 @@ Index Total Time in Apnea - + סה"כ זמן בדום נשימה Total Time in Apnea (Minutes) - + סה"כ זמן בדום נשימה +(דקות) @@ -2255,17 +2264,18 @@ Index 10 of 10 Charts + NO IDEA WHAT THIS MEANS Show all graphs - הראה את כל הגרפים + הצג את כל הגרפים Hide all graphs - הסתר כל הגרפים + הסתר את כל הגרפים @@ -2400,7 +2410,7 @@ Index Duration - + משך @@ -2811,7 +2821,7 @@ Index Preferences - + העדפות @@ -2993,7 +3003,7 @@ Defaults to 60 minutes.. Highly recommend it's left at this value. Search - + חיפוש @@ -3008,7 +3018,7 @@ Defaults to 60 minutes.. Highly recommend it's left at this value. Pulse - דופק + דופק @@ -3040,7 +3050,7 @@ Defaults to 60 minutes.. Highly recommend it's left at this value. &General - + &כללי @@ -3118,7 +3128,7 @@ as this is the only value available on summary-only days. Median - חציון + חציון @@ -3183,7 +3193,7 @@ as this is the only value available on summary-only days. &Appearance - + &תצוגה @@ -3351,12 +3361,12 @@ If you've got a new computer with a small solid state disk, this is a good Hours - שעות + שעות Seconds - + שניות @@ -3586,19 +3596,19 @@ If you've got a new computer with a small solid state disk, this is a good Profile - פרופיל + פרופיל Welcome - ברוך הבא + כניסה Daily - יומי + יומי @@ -3606,13 +3616,13 @@ If you've got a new computer with a small solid state disk, this is a good Overview - מבט על + מבט על Statistics - סטטיסטיקה + סטטיסטיקה @@ -3787,12 +3797,12 @@ Try it and see if you like it. Font - + גופן Size - גודל + גודל @@ -3807,34 +3817,34 @@ Try it and see if you like it. Application - + כללי Graph Text - + גרפים Graph Titles - + כותרות הגרפים Big Text - + טקסט גדול Details - פרטים + פרטים &Cancel - &בטל + &בטל @@ -3864,7 +3874,7 @@ Try it and see if you like it. No CPAP devices detected - + לא נמצא מכשיר @@ -3879,24 +3889,24 @@ Try it and see if you like it. Never - + אף פעם Name - שם + שם Color - צבע + צבע Flag Type - + סוג דגל @@ -3907,12 +3917,12 @@ Try it and see if you like it. CPAP Events - + ארועי סיפאפ Oximeter Events - + ארועי אוקסימטר @@ -3927,7 +3937,7 @@ Try it and see if you like it. Unknown Events - + אירועים אחרים @@ -4039,14 +4049,16 @@ Are you sure you want to make these changes? Restart Required - נחוץ אתחול + נחוץ אתחול One or more of the changes you have made will require this application to be restarted, in order for these changes to come into effect. Would you like do this now? - + כדי להחיל את השינויים, התוכנה צריכה לאתחל מחדש. + +לאתחל עכשיו? @@ -4101,22 +4113,22 @@ Would you like do this now? Version - גרסה + גרסה &Open Profile - + &פתח פרופיל &Edit Profile - &ערוך פרופיל + &ערוך פרופיל &New Profile - + פרופיל &חדש @@ -4131,37 +4143,37 @@ Would you like do this now? Destroy Profile - + מחק פרופיל Profile - פרופיל + פרופיל Ventilator Brand - + יצרן המכשיר Ventilator Model - + מודל Other Data - + נתונים נוספים Last Imported - + ייבוא אחרון Name - שם + שם @@ -4172,7 +4184,7 @@ Would you like do this now? Enter Password for %1 - הזן סיסמה עבור %1 + הזן סיסמה עבור %1 @@ -4337,7 +4349,7 @@ Would you like do this now? Abort - + עצור @@ -4459,7 +4471,7 @@ Would you like do this now? Min %1 - + מינימום %1 @@ -4550,17 +4562,17 @@ TTIA: %1 Min IPAP - + IPAP מינימלי Max IPAP - IPAP מקסימלי + IPAP מקסימלי Device - + מכשיר @@ -4648,7 +4660,7 @@ TTIA: %1 Seconds - + שניות @@ -4713,12 +4725,12 @@ TTIA: %1 Question - שאלה + שאלה Information - מידע + מידע @@ -4733,7 +4745,7 @@ TTIA: %1 No Data Available - + אין נתונים @@ -4758,7 +4770,7 @@ TTIA: %1 &Cancel - &בטל + &בטל @@ -4798,7 +4810,7 @@ TTIA: %1 Profile - פרופיל + פרופיל @@ -4842,12 +4854,12 @@ TTIA: %1 Min EPAP - EPAP מינימלי + EPAP מינימלי Max EPAP - + EPAP מקסימלי @@ -4871,7 +4883,7 @@ TTIA: %1 AVAPS - + @@ -4919,7 +4931,7 @@ TTIA: %1 SA - + @@ -5074,13 +5086,14 @@ TTIA: %1 Insp. Time - משך נשימה + Exp. Time - משך נשיפה + Keep chart titles in English so they can be posted to an English forum + @@ -5125,33 +5138,38 @@ TTIA: %1 Minute Vent. - https://he.wikipedia.org/wiki/%D7%A4%D7%99%D7%96%D7%99%D7%95%D7%9C%D7%95%D7%92%D7%99%D7%94_%D7%A9%D7%9C_%D7%9E%D7%A2%D7%A8%D7%9B%D7%AA_%D7%94%D7%A0%D7%A9%D7%99%D7%9E%D7%94 - אוורור דקתי + Keep chart titles in English so they can be posted to an English forum +https://he.wikipedia.org/wiki/%D7%A4%D7%99%D7%96%D7%99%D7%95%D7%9C%D7%95%D7%92%D7%99%D7%94_%D7%A9%D7%9C_%D7%9E%D7%A2%D7%A8%D7%9B%D7%AA_%D7%94%D7%A0%D7%A9%D7%99%D7%9E%D7%94 + Tidal Volume - https://he.wikipedia.org/wiki/%D7%A4%D7%99%D7%96%D7%99%D7%95%D7%9C%D7%95%D7%92%D7%99%D7%94_%D7%A9%D7%9C_%D7%9E%D7%A2%D7%A8%D7%9B%D7%AA_%D7%94%D7%A0%D7%A9%D7%99%D7%9E%D7%94 - נפח חלופי + Keep chart titles in English so they can be posted to an English forum +https://he.wikipedia.org/wiki/%D7%A4%D7%99%D7%96%D7%99%D7%95%D7%9C%D7%95%D7%92%D7%99%D7%94_%D7%A9%D7%9C_%D7%9E%D7%A2%D7%A8%D7%9B%D7%AA_%D7%94%D7%A0%D7%A9%D7%99%D7%9E%D7%94 + Resp. Rate - קצב נשימה + Keep chart titles in English so they can be posted to an English forum + Snore - נחירה + Keep chart titles in English so they can be posted to an English forum + Leak - דליפה + Keep chart titles in English so they can be posted to an English forum + @@ -5161,13 +5179,13 @@ TTIA: %1 Large Leak - + דליפה גדולה LL - + @@ -5183,19 +5201,22 @@ TTIA: %1 MaskPressure + Keep chart titles in English so they can be posted to an English forum Flow Rate - קצב זרימה + Keep chart titles in English so they can be posted to an English forum + Sleep Stage - שלב שינה + Keep chart titles in English so they can be posted to an English forum + @@ -5244,7 +5265,7 @@ TTIA: %1 Series - + סדרה Machine @@ -5370,12 +5391,12 @@ TTIA: %1 Yes - + כן No - + לא @@ -5390,7 +5411,7 @@ TTIA: %1 Med - חציון + חציון @@ -5417,7 +5438,8 @@ TTIA: %1 Pressure - לחץ + Keep chart titles in English so they can be posted to an English forum + @@ -5437,7 +5459,8 @@ TTIA: %1 Event Flags - דגלי אירועים + Keep chart titles in English so they can be posted to an English forum + @@ -5657,12 +5680,12 @@ popout window, delete it, then pop out this graph again. Duration - + משך Events - אירועים + אירועים @@ -5754,7 +5777,7 @@ popout window, delete it, then pop out this graph again. Machine Information - מידע מכונה + מידע על המכשיר @@ -5849,44 +5872,44 @@ popout window, delete it, then pop out this graph again. CMS50D+ - + CMS50E/F - + Contec - + CMS50 - + CMS50F3.7 - + CMS50F - + Fisher & Paykel - + ICON - + @@ -5903,7 +5926,7 @@ popout window, delete it, then pop out this graph again. SmartFlex Mode - + @@ -5941,17 +5964,17 @@ popout window, delete it, then pop out this graph again. SN - + DeVilbiss - + Intellipap - + @@ -5961,27 +5984,27 @@ popout window, delete it, then pop out this graph again. ChoiceMMed - + MD300 - + Respironics - + M-Series - + System One - + @@ -6393,48 +6416,48 @@ popout window, delete it, then pop out this graph again. CPAP-Check - + AutoCPAP - + Auto-Trial - + AutoBiLevel - + S - + S/T - + S/T - AVAPS - + PC - AVAPS - + Flex Mode - + @@ -6444,22 +6467,22 @@ popout window, delete it, then pop out this graph again. C-Flex - + C-Flex+ - + A-Flex - + P-Flex - + @@ -6471,18 +6494,18 @@ popout window, delete it, then pop out this graph again. Bi-Flex - + Flex - + Flex Level - + @@ -6492,7 +6515,7 @@ popout window, delete it, then pop out this graph again. Passover - + @@ -6573,12 +6596,12 @@ popout window, delete it, then pop out this graph again. SmartRamp - + Ramp+ - + @@ -6644,7 +6667,7 @@ popout window, delete it, then pop out this graph again. EZ-Start - + @@ -6805,7 +6828,7 @@ popout window, delete it, then pop out this graph again. BND - + @@ -6821,12 +6844,12 @@ popout window, delete it, then pop out this graph again. TB - + Philips Respironics - + @@ -6851,43 +6874,43 @@ popout window, delete it, then pop out this graph again. CPAP Mode - אופן עבודה סיפאפ + אופן עבודה סיפאפ VPAPauto - + ASVAuto - + iVAPS - + PAC - + Auto for Her - + EPR - + ResMed Exhale Pressure Relief - + @@ -6913,7 +6936,7 @@ popout window, delete it, then pop out this graph again. Response - + @@ -6938,7 +6961,7 @@ popout window, delete it, then pop out this graph again. SmartStart - + @@ -7009,12 +7032,12 @@ popout window, delete it, then pop out this graph again. Essentials - + Plus - + @@ -7024,39 +7047,39 @@ popout window, delete it, then pop out this graph again. Manual - + Soft - + Standard - + BiPAP-T - + BiPAP-S - + BiPAP-S/T - + SmartStop - + @@ -7066,12 +7089,12 @@ popout window, delete it, then pop out this graph again. Simple - + Advanced - + @@ -7083,90 +7106,90 @@ popout window, delete it, then pop out this graph again. Auto - + Mask - + ResMed Mask Setting - + Pillows - + Full Face - + Nasal - + Ramp - + Ramp Enable - + ResMed - + S9 - + EPR: - + Somnopose - + Somnopose Software - + Weinmann - + SOMNOsoft2 - + Zeo - + Personal Sleep Coach - + @@ -7196,7 +7219,7 @@ popout window, delete it, then pop out this graph again. Please Wait... - + רק רגע... @@ -7241,7 +7264,7 @@ popout window, delete it, then pop out this graph again. PS Min - PS מינימלי + PS מינימלי @@ -7251,7 +7274,7 @@ popout window, delete it, then pop out this graph again. PS Max - PS מקסימלי + PS מקסימלי @@ -7352,7 +7375,7 @@ popout window, delete it, then pop out this graph again. CSR - + @@ -7390,7 +7413,7 @@ popout window, delete it, then pop out this graph again. UA - + @@ -7420,7 +7443,7 @@ popout window, delete it, then pop out this graph again. LF - + @@ -7492,7 +7515,7 @@ popout window, delete it, then pop out this graph again. SD - + @@ -7781,7 +7804,7 @@ popout window, delete it, then pop out this graph again. RERA (RE) - + @@ -7892,7 +7915,7 @@ popout window, delete it, then pop out this graph again. Height - גובה + גובה @@ -7902,7 +7925,7 @@ popout window, delete it, then pop out this graph again. Notes - הערות + הערות @@ -7942,7 +7965,7 @@ popout window, delete it, then pop out this graph again. Journal - יומן + יומן @@ -8047,7 +8070,7 @@ popout window, delete it, then pop out this graph again. Zeo ZQ - + @@ -8057,7 +8080,7 @@ popout window, delete it, then pop out this graph again. ZEO ZQ - + @@ -8338,76 +8361,76 @@ popout window, delete it, then pop out this graph again. There are no graphs visible to print - אין גרפים נראים להדפסה + אין גרפים נראים להדפסה Would you like to show bookmarked areas in this report? - האם ברצונך להראות אזורים מסומנים בדו"ח הזה? + האם ברצונך להראות אזורים מסומנים בדו"ח הזה? Printing %1 Report - מדפיס דו"ח %1 + מדפיס דו"ח %1 %1 Report - דו"ח %1 + דו"ח %1 : %1 hours, %2 minutes, %3 seconds - : %1 שעות, %2 דקות, %3 שניות + : %1 שעות, %2 דקות, %3 שניות RDI %1 - + AHI %1 - + AI=%1 HI=%2 CAI=%3 - + REI=%1 VSI=%2 FLI=%3 PB/CSR=%4%% - + UAI=%1 - + NRI=%1 LKI=%2 EPI=%3 - + AI=%1 - + Reporting from %1 to %2 - מדווח מ %1 עד %2 + מדווח מ %1 עד %2 Entire Day's Flow Waveform - צורת גל ליום שלם + צורת גל ליום שלם @@ -8417,12 +8440,12 @@ popout window, delete it, then pop out this graph again. Entire Day - יום שלם + יום שלם Page %1 of %2 - דף %1 מתוך %2 + דף %1 מתוך %2 @@ -8477,7 +8500,7 @@ popout window, delete it, then pop out this graph again. Dreem - + @@ -8492,12 +8515,12 @@ popout window, delete it, then pop out this graph again. Viatom - + Viatom Software - + @@ -8574,7 +8597,7 @@ popout window, delete it, then pop out this graph again. SleepStyle - + @@ -8617,12 +8640,12 @@ popout window, delete it, then pop out this graph again. %1h %2m - + No Sessions Present - + אין רשומות שימושים @@ -8630,17 +8653,17 @@ popout window, delete it, then pop out this graph again. Import Error - + שגיאה ביבוא This device Record cannot be imported in this profile. - + אי אפשר לייבא את הרשומה לתוך הפרופיל הנוכחי. The Day records overlap with already existing content. - + כבר קיימים נתונים לאותו יום. @@ -8648,69 +8671,69 @@ popout window, delete it, then pop out this graph again. CPAP Statistics - + סטטיסטיקות סיפאפ CPAP Usage - + שימוש במכשיר סיפאפ Average Hours per Night - + משך שימוש ממוצע ללילה (שעות) Therapy Efficacy - + אפקטיביות הטיפול Leak Statistics - + נתוני דליפה Pressure Statistics - + נתוני לחץ Oximeter Statistics - + נתוני אוקסימטריה Blood Oxygen Saturation - + נתוני ריווי חמצן Pulse Rate - דופק + דופק %1 Median - + חציון %1 Average %1 - ממוצע %1 + ממוצע %1 Min %1 - + מינימום %1 Max %1 - + מקסימום %1 @@ -8769,27 +8792,27 @@ popout window, delete it, then pop out this graph again. First Use - שימוש ראשון + שימוש ראשון Last Use - שימוש אחרון + שימוש אחרון Days - ימים + ימים Pressure Relief - + הפחתת לחץ Pressure Settings - + הגדרות לחץ @@ -8804,12 +8827,12 @@ popout window, delete it, then pop out this graph again. Device Information - + אודות המכשיר Changes to Device Settings - + היסטוריית הגדרות המכשיר @@ -8824,37 +8847,37 @@ popout window, delete it, then pop out this graph again. Most Recent - אחרון + שימוש אחרון Last Week - שבוע אחרון + שבוע אחרון Last 30 Days - 30 ימים אחרונים + 30 ימים אחרונים Last 6 Months - ששה חודשים אחרונים + ששה חודשים אחרונים Last Year - שנה אחרונה + שנה אחרונה Last Session - + שימוש אחרון Details - פרטים + פרטים @@ -8869,7 +8892,7 @@ popout window, delete it, then pop out this graph again. %1 days of %2 Data, between %3 and %4 - + %1 ימי נתוני %2, מ-%3 ועד %4 @@ -8991,7 +9014,7 @@ popout window, delete it, then pop out this graph again. Best RX Setting - הגדרות המרשם הטובות ביותר + הגדרות המרשם הטובות ביותר @@ -9014,7 +9037,7 @@ popout window, delete it, then pop out this graph again. Worst RX Setting - הגדרות המרשם הגרועות ביותר + הגדרות המרשם הגרועות ביותר @@ -9022,37 +9045,37 @@ popout window, delete it, then pop out this graph again. Welcome to the Open Source CPAP Analysis Reporter - + OSCAR - תוכנה להצגת נתוני סיפאפ What would you like to do? - + אפשרויות CPAP Importer - + ייבוא נתוני סיפאפ Oximetry Wizard - + אשף אוקסימטריה Daily View - + מבט יומי Overview - מבט על + מבט על Statistics - סטטיסטיקה + סטטיסטיקה @@ -9077,37 +9100,37 @@ popout window, delete it, then pop out this graph again. First import can take a few minutes. - יבוא ראשון יכול לקחת כמה דקות. + יבוא ראשון יכול לקחת כמה דקות. The last time you used your %1... - + השימוש האחרון ב-%1... last night - + אמש today - + היום %2 days ago - + לפני %2 ימים was %1 (on %2) - + היה %1 (ב-%2) %1 hours, %2 minutes and %3 seconds - + %1 שעות, %2 דקות, ו %3 שניות @@ -9117,32 +9140,32 @@ popout window, delete it, then pop out this graph again. under - + מתחת ל over - + גבוה מה reasonably close to - + קרוב ל equal to - + שווה ל You had an AHI of %1, which is %2 your %3 day average of %4. - + ה-AHI היה %1, %2ממוצע של %4 על פני %3 ימים. Your pressure was under %1 %2 for %3% of the time. - + לחץ האוויר היה מתחת ל-%1 %2 במשך %3% מהזמן. @@ -9153,47 +9176,47 @@ popout window, delete it, then pop out this graph again. Your IPAP pressure was under %1 %2 for %3% of the time. - + לחץ IPAP היה מתחת ל-%1 %2 במשך %3% מהזמן. Your EPAP pressure was under %1 %2 for %3% of the time. - + לחץ EPAP היה מתחת ל-%1 %2 במשך %3% מהזמן. 1 day ago - + לפני יום Your device was on for %1. - + המכשיר פעל במשך %1. Your CPAP device used a constant %1 %2 of air - + המכשיר השתמש באפן קבוע ב-%1 %2 אוויר Your device used a constant %1-%2 %3 of air. - + המכשיר השתמש באפן קבוע ב-%2-%1 %3 אוויר. Your device was under %1-%2 %3 for %4% of the time. - + המכשיר פעל מתחת ל-%2-%1 %3 במשך %4% מהזמן. Your average leaks were %1 %2, which is %3 your %4 day average of %5. - + דליפות: %1 %2, שהם %3 מתוך הממוצע ל-%4 ימים: %5. No CPAP data has been imported yet. - + יש לייבא נתונים. diff --git a/Translations/Nederlands.nl.ts b/Translations/Nederlands.nl.ts index 2a583bd2..1e8b5f05 100644 --- a/Translations/Nederlands.nl.ts +++ b/Translations/Nederlands.nl.ts @@ -257,7 +257,7 @@ In verband met de koppeling met Bladwijzers, lijkt me 'Notities' beter Flags - Markeringen + Incident markeringen @@ -3091,7 +3091,7 @@ anders is het geen AHI/uur meer. CPAP Clock Drift - Correctie afwijking klok CPAP + Correctie afwijking CPAP-klok @@ -3316,7 +3316,7 @@ Werkt vooral bij importeren. Enable Unknown Events Channels - Zet de kanalen van onbekende gebeurtenissen aan + Kanalen van onbekende gebeurtenissen aan @@ -3554,7 +3554,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Overview Linecharts - Overzicht lijngrafieken + Soort grafieken in overzicht @@ -3569,7 +3569,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Overlay Flags - Markeringen + Incident markeringen @@ -3581,7 +3581,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Standard Bars - Standaardbalken + Lange markeringen @@ -3636,7 +3636,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Calculate Unintentional Leaks When Not Present - Bereken de onbedoelde lekkage als deze niet door het apparaat gegeven wordt + Bereken de onbedoelde lekkage als deze niet gegeven wordt @@ -3656,7 +3656,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Tooltip Timeout - Tooltip timeout + Tijdsduur tooltips @@ -3666,7 +3666,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Top Markers - Top markeringen + Aanduiding aan bovenzijde @@ -3681,7 +3681,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Automatically load last used profile on start-up - Laadt automatisch het laatste profiel bij opstarten + Laad automatisch het laatste profiel bij opstarten @@ -3775,7 +3775,7 @@ Als U een nieuwe computer met SSD hebt, is dit een goede keuze. Switch Tabs - Wissel tabbladen + Kies tabbladen diff --git a/oscar/Graphs/gAHIChart.cpp b/oscar/Graphs/gAHIChart.cpp new file mode 100644 index 00000000..e80692da --- /dev/null +++ b/oscar/Graphs/gAHIChart.cpp @@ -0,0 +1,184 @@ +/* gAHUChart Implementation + * + * Copyright (c) 2019-2022 The Oscar Team + * Copyright (c) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#define TEST_MACROS_ENABLEDoff +#include "test_macros.h" + +#include +#include +#include + +#include "mainwindow.h" +#include "SleepLib/profiles.h" +#include "SleepLib/machine_common.h" +#include "gAHIChart.h" + +#include "gYAxis.h" + +extern MainWindow * mainwin; + +//extern short SummaryCalcItem::midcalc; + +//////////////////////////////////////////////////////////////////////////// +/// AHI Chart Stuff +//////////////////////////////////////////////////////////////////////////// +void gAHIChart::preCalc() +{ + gSummaryChart::preCalc(); + + ahi_wavg = 0; + ahi_avg = 0; + total_days = 0; + total_hours = 0; + min_ahi = 99999; + max_ahi = -99999; + + ahi_data.clear(); + ahi_data.reserve(idx_end-idx_start); +} +void gAHIChart::customCalc(Day *day, QVector &list) +{ + int size = list.size(); + if (size == 0) return; + EventDataType hours = day->hours(m_machtype); + EventDataType ahi_cnt = 0; + + for (auto & slice : list) { + SummaryCalcItem * calc = slice.calc; + + EventDataType value = slice.value; + float valh = value/ hours; + + switch (midcalc) { + case 0: + calc->median_data.append(valh); + break; + case 1: + calc->wavg_sum += value; + calc->divisor += hours; + break; + case 2: + calc->avg_sum += value; + calc->cnt++; + break; + } + + calc->min = qMin(valh, calc->min); + calc->max = qMax(valh, calc->max); + + ahi_cnt += value; + } + min_ahi = qMin(ahi_cnt / hours, min_ahi); + max_ahi = qMax(ahi_cnt / hours, max_ahi); + + ahi_data.append(ahi_cnt / hours); + + ahi_wavg += ahi_cnt; + ahi_avg += ahi_cnt; + total_hours += hours; + total_days++; +} +void gAHIChart::afterDraw(QPainter & /*painter */, gGraph &graph, QRectF rect) +{ + if (totaldays == nousedays) return; + + //int size = idx_end - idx_start; + + bool skip = true; + float med = 0; + switch (midcalc) { + case 0: + if (ahi_data.size() > 0) { + med = median(ahi_data.begin(), ahi_data.end()); + skip = false; + } + break; + case 1: // wavg + if (total_hours > 0) { + med = ahi_wavg / total_hours; + skip = false; + } + break; + case 2: // avg + if (total_days > 0) { + med = ahi_avg / total_days; + skip = false; + } + break; + } + + QStringList txtlist; + if (!skip) txtlist.append(QString("%1 %2 / %3 / %4").arg(STR_TR_AHI).arg(min_ahi, 0, 'f', 2).arg(med, 0, 'f', 2).arg(max_ahi, 0, 'f', 2)); + + int i = calcitems.size(); + while (i > 0) { + i--; + ChannelID code = calcitems[i].code; + schema::Channel & chan = schema::channel[code]; + float mid = 0; + skip = true; + switch (midcalc) { + case 0: + if (calcitems[i].median_data.size() > 0) { + mid = median(calcitems[i].median_data.begin(), calcitems[i].median_data.end()); + skip = false; + } + break; + case 1: + if (calcitems[i].divisor > 0) { + mid = calcitems[i].wavg_sum / calcitems[i].divisor; + skip = false; + } + break; + case 2: + if (calcitems[i].cnt > 0) { + mid = calcitems[i].avg_sum / calcitems[i].cnt; + skip = false; + } + break; + } + + if (!skip) txtlist.append(QString("%1 %2 / %3 / %4").arg(chan.label()).arg(calcitems[i].min, 0, 'f', 2).arg(mid, 0, 'f', 2).arg(calcitems[i].max, 0, 'f', 2)); + } + QString txt = txtlist.join(", "); + graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); +} + +void gAHIChart::populate(Day *day, int idx) +{ + QVector & slices = cache[idx]; + + float hours = day->hours(m_machtype); + + for (auto & calc : calcitems) { + ChannelID code = calc.code; + if (!day->hasData(code, ST_CNT)) continue; + + schema::Channel *chan = schema::channel.channels.find(code).value(); + + float c = day->count(code); + slices.append(SummaryChartSlice(&calc, c, c / hours, chan->label(), calc.color)); + } +} +QString gAHIChart::tooltipData(Day *day, int idx) +{ + QVector & slices = cache[idx]; + float total = 0; + float hour = day->hours(m_machtype); + QString txt; + int i = slices.size(); + while (i > 0) { + i--; + total += slices[i].value; + txt += QString("\n%1: %2").arg(slices[i].name).arg(float(slices[i].value) / hour, 0, 'f', 2); + } + return QString("\n%1: %2").arg(STR_TR_AHI).arg(float(total) / hour,0,'f',2)+txt; +} + + diff --git a/oscar/Graphs/gAHIChart.h b/oscar/Graphs/gAHIChart.h new file mode 100644 index 00000000..b5ab4c79 --- /dev/null +++ b/oscar/Graphs/gAHIChart.h @@ -0,0 +1,78 @@ +/* gAHIChart Header + * + * Copyright (c) 2019-2022 The Oscar Team + * Copyright (C) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#ifndef GAHICHART_H +#define GAHICHART_H + +#include "SleepLib/day.h" +#include "SleepLib/profiles.h" +#include "Graphs/gGraphView.h" + +#include "Graphs/gSummaryChart.h" + + +class gAHIChart : public gSummaryChart +{ +public: + gAHIChart() + :gSummaryChart("AHIChart", MT_CPAP) { + for (int i = 0; i < ahiChannels.size(); i++) + addCalc(ahiChannels.at(i), ST_CPH); + +// addCalc(CPAP_ClearAirway, ST_CPH); +// addCalc(CPAP_AllApnea, ST_CPH); +// addCalc(CPAP_Obstructive, ST_CPH); +// addCalc(CPAP_Apnea, ST_CPH); +// addCalc(CPAP_Hypopnea, ST_CPH); + if (p_profile->general->calculateRDI()) + addCalc(CPAP_RERA, ST_CPH); + } + virtual ~gAHIChart() {} + + virtual void preCalc(); + virtual void customCalc(Day *, QVector &); + virtual void afterDraw(QPainter &, gGraph &, QRectF); + + virtual void populate(Day *, int idx); + + virtual QString tooltipData(Day * day, int); + + virtual Layer * Clone() { + gAHIChart * sc = new gAHIChart(); + gSummaryChart::CloneInto(sc); + CloneInto(sc); + return sc; + } + + void CloneInto(gAHIChart * /* layer */) { +// layer->ahicalc = ahicalc; +// layer->ahi_wavg = ahi_wavg; +// layer->ahi_avg = ahi_avg; +// layer->total_hours = total_hours; +// layer->max_ahi = max_ahi; +// layer->min_ahi = min_ahi; +// layer->total_days = total_days; +// layer->ahi_data = ahi_data; + } + + // SummaryCalcItem ahicalc; + double ahi_wavg; + double ahi_avg; + + double total_hours; + float max_ahi; + float min_ahi; + + int total_days; + QList ahi_data; +}; + + +#endif // GAHICHART_H + diff --git a/oscar/Graphs/gGraphView.cpp b/oscar/Graphs/gGraphView.cpp index 61adab78..2b3d94ab 100644 --- a/oscar/Graphs/gGraphView.cpp +++ b/oscar/Graphs/gGraphView.cpp @@ -38,8 +38,10 @@ #include "mainwindow.h" #include "Graphs/glcommon.h" #include "Graphs/gLineChart.h" +#ifndef REMOVE_FITNESS +#include "Graphs/gOverviewGraph.h" +#endif #include "Graphs/gSummaryChart.h" -#include "Graphs/gSessionTimesChart.h" #include "Graphs/gYAxis.h" #include "Graphs/gFlagsLine.h" #include "SleepLib/profiles.h" @@ -2199,11 +2201,18 @@ void gGraphView::populateMenu(gGraph * graph) font.setPointSize(font.pointSize() + 3); gLineChart * lc = dynamic_cast(findLayer(graph,LT_LineChart)); - SummaryChart * sc = dynamic_cast(findLayer(graph,LT_SummaryChart)); + #ifndef REMOVE_FITNESS + gOverviewGraph * sc = dynamic_cast(findLayer(graph,LT_SummaryChart)); + #endif gSummaryChart * stg = dynamic_cast(findLayer(graph,LT_Overview)); limits_menu->clear(); - if (lc || sc || stg) { + #ifndef REMOVE_FITNESS + if (lc || sc || stg ) + #else + if (lc || stg ) + #endif + { QWidgetAction * widget = new QWidgetAction(this); MinMaxWidget * minmax = new MinMaxWidget(graph, this); diff --git a/oscar/Graphs/gOverviewGraph.cpp b/oscar/Graphs/gOverviewGraph.cpp new file mode 100644 index 00000000..310b3643 --- /dev/null +++ b/oscar/Graphs/gOverviewGraph.cpp @@ -0,0 +1,1308 @@ +/* gOverviewGraph Implementation + * + * Copyright (c) 2019-2022 The OSCAR Team + * Copyright (c) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#include +#include +#include +#include "gYAxis.h" +#include "gOverviewGraph.h" + +#ifndef REMOVE_FITNESS +/* To enable this module change the REMOTE_FITNESS define in appsettings.h +*/ + +gOverviewGraph::gOverviewGraph(QString label, GraphType type) + : Layer(NoChannel), m_label(label), m_graphtype(type) +{ + m_empty = true; + hl_day = -1; + m_machinetype = MT_CPAP; + + QDateTime d1 = QDateTime::currentDateTime(); + QDateTime d2 = d1; + d1.setTimeSpec(Qt::UTC); + tz_offset = d2.secsTo(d1); + tz_hours = tz_offset / 3600.0; + m_layertype = LT_SummaryChart; +} +gOverviewGraph::~gOverviewGraph() +{ +} +void gOverviewGraph::SetDay(Day * nullday) +{ + Q_UNUSED(nullday) + Day *day = nullptr; + Layer::SetDay(day); + + m_values.clear(); + m_times.clear(); + m_days.clear(); + m_hours.clear(); + m_goodcodes.clear(); + m_miny = 999999999.0F; + m_maxy = -999999999.0F; + m_physmaxy = 0; + m_physminy = 0; + m_minx = 0; + m_maxx = 0; + + + int dn; + EventDataType tmp, total; + ChannelID code; + CPAPMode cpapmode = (CPAPMode)(int)p_profile->calcSettingsMax(CPAP_Mode, MT_CPAP, + p_profile->FirstDay(MT_CPAP), p_profile->LastDay(MT_CPAP)); + + + ////////////////////////////////////////////////////////// + // Setup for dealing with different CPAP Pressure types + ////////////////////////////////////////////////////////// + if (m_label == STR_TR_Pressure) { + m_codes.clear(); + m_colors.clear(); + m_type.clear(); + m_typeval.clear(); + + float perc = p_profile->general->prefCalcPercentile() / 100.0; + int mididx = p_profile->general->prefCalcMiddle(); + SummaryType mid; + + if (mididx == 0) { mid = ST_PERC; } + else if (mididx == 1) { mid = ST_WAVG; } + else mid = ST_AVG; + + + if (cpapmode >= MODE_ASV) { + addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN); + addSlice(CPAP_IPAPLo, QColor("light blue"), ST_SETMIN); + addSlice(CPAP_IPAP, QColor("cyan"), mid, 0.5); + addSlice(CPAP_IPAP, QColor("dark cyan"), ST_PERC, perc); + //addSlice(CPAP_IPAP,QColor("light blue"),ST_PERC,0.95); + addSlice(CPAP_IPAPHi, QColor("blue"), ST_SETMAX); + } else if (cpapmode >= MODE_BILEVEL_AUTO_FIXED_PS) { + addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN); + addSlice(CPAP_IPAP, QColor("light cyan"), mid, 0.5); + addSlice(CPAP_IPAP, QColor("light blue"), ST_PERC, perc); + addSlice(CPAP_PSMin, QColor("blue"), ST_SETMIN, perc); + addSlice(CPAP_PSMax, QColor("red"), ST_SETMAX, perc); + + } else if (cpapmode >= MODE_BILEVEL_FIXED) { + addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN); + addSlice(CPAP_EPAP, QColor("light green"), ST_PERC, perc); + addSlice(CPAP_IPAP, QColor("light cyan"), mid, 0.5); + addSlice(CPAP_IPAP, QColor("light blue"), ST_PERC, perc); + addSlice(CPAP_IPAPHi, QColor("blue"), ST_SETMAX); + } else if (cpapmode >= MODE_APAP) { + addSlice(CPAP_PressureMin, QColor("orange"), ST_SETMIN); + addSlice(CPAP_Pressure, QColor("dark green"), mid, 0.5f); + addSlice(CPAP_Pressure, QColor("grey"), ST_PERC, perc); + addSlice(CPAP_PressureMax, QColor("red"), ST_SETMAX); + } else { + addSlice(CPAP_Pressure, QColor("dark green"), ST_SETWAVG); + } + } + + // Initialize goodcodes (which identified which legends are drawn) to all off + m_goodcodes.resize(m_codes.size()); + for (int i = 0; i < m_codes.size(); i++) { + m_goodcodes[i] = false; + } + + m_fday = 0; + qint64 tt; + m_empty = true; + + if (m_graphtype == GT_SESSIONS) { + // No point drawing anything if no real data on record + if (p_profile->countDays(MT_CPAP, p_profile->FirstDay(MT_CPAP), p_profile->LastDay(MT_CPAP)) == 0) { + return; + } + } + bool first = true; + + int suboffset; + SummaryType type; + + // For each day in the main profile daylist + + for (auto d=p_profile->daylist.begin(), dend=p_profile->daylist.end(); d!=dend; ++d) { + Day * day = d.value(); + + // get the timestamp of this day. + tt = QDateTime(d.key(), QTime(0, 0, 0), Qt::UTC).toTime_t(); + + // calculate day number + dn = tt / 86400; + + // to ms since epoch. + tt *= 1000L; + + // update min and max for this timestamp + if (!m_minx || tt < m_minx) { m_minx = tt; } + if (!m_maxx || tt > m_maxx) { m_maxx = tt; } + + total = 0; + bool fnd = false; + + ////////////////////////////////////////////////////////// + // Setup for Sessions Time display chart + ////////////////////////////////////////////////////////// + if (m_graphtype == GT_SESSIONS) { + qint64 zt; + EventDataType tmp2; + // Turn all legends on + for (int i = 0; i < m_codes.size(); i++) { + m_goodcodes[i] = true; + } + + // for each day object on record for this date + + // skip any empty or irrelevant day records + if (!day || (day->machine(m_machinetype) == nullptr)) { continue; } + + //int ft = qint64(day->first()) / 1000L; + //ft += tz_offset; // convert to local time + + //int dz2 = ft / 86400; + //dz2 *= 86400; + // ft = first sessions time, rounded back to midnight.. + + // For each session in this day record + for (int s=0, size=day->size(); s < size; s++) { + Session *sess = (*day)[s]; + + if (!sess->enabled()) { continue; } + + // Get session duration + tmp = sess->hours(); + m_values[dn][s] = tmp; + + total += tmp; + + // Get session start timestamp + zt = qint64(sess->first()) / 1000L; + zt += tz_offset; + + // Calculate the starting hour + tmp2 = zt - dn * 86400; + tmp2 /= 3600.0; + + m_times[dn][s] = tmp2; + + // Update min & max Y values + if (first) { + m_miny = tmp2; + m_maxy = tmp2 + tmp; + first = false; + } else { + if (tmp2 < m_miny) { + m_miny = tmp2; + } + + if (tmp2 + tmp > m_maxy) { + m_maxy = tmp2 + tmp; + } + } + } // for each session + + // if total hours for all sessions more than 0, register the day as valid + if (total > 0) { + m_days[dn] = day; + m_hours[dn] = total; + m_empty = false; + } + } else + { + ////////////////////////////////////////////////////////////////////////////// + // Data Channel summary charts + ////////////////////////////////////////////////////////////////////////////// + + // For each Channel + for (int j = 0; j < m_codes.size(); j++) { + code = m_codes[j]; + suboffset = 0; + type = m_type[j]; + EventDataType typeval = m_typeval[j]; + + day = d.value(); + + CPAPMode mode = (CPAPMode)(int)day->settings_max(CPAP_Mode); + + // ignore irrelevent day objects + if (day->machine(m_machinetype) == nullptr) { continue; } + + bool hascode = //day->channelHasData(code) || + (type == ST_HOURS) || + (type == ST_SESSIONS) || + day->settingExists(code) || + day->hasData(code, type); + + + if (code == CPAP_Pressure) { + if ((cpapmode > MODE_CPAP) && (mode == MODE_CPAP)) { + hascode = false; + + if ((type == ST_WAVG) || (type == ST_AVG) || ((type == ST_PERC) && (typeval == 0.5))) { + type = ST_SETWAVG; + hascode = true; + } + } else { + type = m_type[j]; + } + } + + //if (code==CPAP_Hypopnea) { // Make sure at least one of the CPAP data gets through with 0 + // hascode=true; + //} + if (hascode) { + m_days[dn] = day; + + switch (type) { + case ST_AVG: + tmp = day->avg(code); + break; + + case ST_SUM: + tmp = day->sum(code); + break; + + case ST_WAVG: + tmp = day->wavg(code); + break; + + case ST_90P: + tmp = day->p90(code); + break; + + case ST_PERC: + tmp = day->percentile(code, typeval); + break; + + case ST_MIN: + tmp = day->Min(code); + break; + + case ST_MAX: + tmp = day->Max(code); + break; + + case ST_CNT: + tmp = day->count(code); + break; + + case ST_CPH: + tmp = day->count(code) / day->hours(m_machinetype); + break; + + case ST_SPH: + tmp = day->sph(code); + break; + + case ST_HOURS: + tmp = day->hours(m_machinetype); + break; + + case ST_SESSIONS: + tmp = day->size(); + break; + + case ST_SETMIN: + tmp = day->settings_min(code); + break; + + case ST_SETMAX: + tmp = day->settings_max(code); + break; + + case ST_SETAVG: + tmp = day->settings_avg(code); + break; + + case ST_SETWAVG: + tmp = day->settings_wavg(code); + break; + + case ST_SETSUM: + tmp = day->settings_sum(code); + break; + + default: + tmp = 0; + break; + } + + if (suboffset > 0) { + tmp -= suboffset; + + if (tmp < 0) { tmp = 0; } + } + + total += tmp; + m_values[dn][j + 1] = tmp; + + if (tmp < m_miny) { m_miny = tmp; } + + if (tmp > m_maxy) { m_maxy = tmp; } + + m_goodcodes[j] = true; + fnd = true; + } + + } + + if (fnd) { + if (!m_fday) { m_fday = dn; } + + m_values[dn][0] = total; + m_hours[dn] = day->hours(m_machinetype); + + if (m_graphtype == GT_BAR) { + if (total < m_miny) { m_miny = total; } + + if (total > m_maxy) { m_maxy = total; } + } + } + } + } + + m_empty = true; + + for (const auto & goodcode : m_goodcodes) { + if (goodcode) { + m_empty = false; + break; + } + } + + if (m_graphtype == GT_BAR) { + m_miny = 0; + } + + // m_minx=qint64(QDateTime(p_profile->FirstDay(),QTime(0,0,0),Qt::UTC).toTime_t())*1000L; + m_maxx = qint64(QDateTime(p_profile->LastDay(), QTime(23, 59, 0), Qt::UTC).toTime_t()) * 1000L; + m_physmaxy = m_maxy; + m_physminy = m_miny; +} + +void gOverviewGraph::paint(QPainter &painter, gGraph &w, const QRegion ®ion) +{ + int left = region.boundingRect().left(); + int top = region.boundingRect().top(); + int width = region.boundingRect().width(); + int height = region.boundingRect().height(); + + if (!m_visible) { return; } + + GraphType graphtype = m_graphtype; + + if (graphtype == GT_LINE || graphtype == GT_POINTS) { + bool pts = AppSetting->overviewLinechartMode() == OLC_Lines; + graphtype = pts ? GT_POINTS : GT_LINE; + } + + rtop = top; + + painter.setPen(QColor(Qt::black)); + painter.drawLine(left, top, left, top+height); + painter.drawLine(left, top+height, left+width, top+height); + painter.drawLine(left+width, top+height, left+width, top); + painter.drawLine( left+width, top, left, top); + + qint64 minx = w.min_x, maxx = w.max_x; + + int days = ceil(double(maxx-minx) / 86400000.0); + + bool buttuglydaysteps = false ; //!p_profile->appearance->animations(); + + double lcursor = w.graphView()->currentTime(); + if (days >= 1) { + + double b = w.max_x - w.min_x; + double a = lcursor - w.min_x; + double c = a / b; + + if (buttuglydaysteps) { + // this kills the beautiful smooth scrolling and makes days stop on day boundaries :( + minx = floor(double(minx)/86400000.0); + minx *= 86400000L; + + maxx = minx + 86400000L * qint64(days)-1; + } + + b = maxx - minx; + double d = c * b; + lcursor = d + minx; + + + } + + + qint64 xx = maxx - minx; + + + + EventDataType miny = m_physminy; + EventDataType maxy = m_physmaxy; + + w.roundY(miny, maxy); + + EventDataType yy = maxy - miny; + EventDataType ymult = float(height - 2) / yy; + + barw = (float(width) / float(days)); + + // graph = &w; + float px;// = left; + l_left = w.marginLeft() + gYAxis::Margin; + l_top = w.marginTop(); + l_width = width; + l_height = height; + float py; + EventDataType total; + + int daynum = 0; + EventDataType h, tmp; + + + l_offset = (minx) % 86400000L; + offset = float(l_offset) / 86400000.0; + + offset *= barw; + px = left - offset; + l_minx = minx; + l_maxx = maxx + 86400000L; + + int total_days = 0; + double total_val = 0; + double total_hours = 0; + bool lastdaygood = false; + QVector totalcounts; + QVector totalvalues; + QVector lastX; + QVector lastY; + int numcodes = m_codes.size(); + totalcounts.resize(numcodes); + totalvalues.resize(numcodes); + lastX.resize(numcodes); + lastY.resize(numcodes); + int zd = minx / 86400000L; + zd--; + auto d = m_values.find(zd); + + QVector goodcodes; + goodcodes.resize(m_goodcodes.size()); + + lastdaygood = true; + + // Display Line Cursor + if (AppSetting->lineCursorMode()) { + qint64 time = lcursor; + double xmult = double(width) / xx; + + if ((time > minx) && (time < maxx)) { + double xpos = (time - minx) * xmult; + painter.setPen(QPen(QBrush(QColor(0,255,0,255)),1)); + painter.drawLine(left+xpos, top-w.marginTop()-3, left+xpos, top+height+w.bottom-1); + } + + + +// QDateTime dt=QDateTime::fromMSecsSinceEpoch(time,Qt::UTC); + +// QString text = dt.date().toString(Qt::SystemLocaleLongDate); + +// int wid, h; + // GetTextExtent(text, wid, h); + // w.renderText(text, left + width/2 - wid/2, top-h+5); + + } + + for (int i = 0; i < numcodes; i++) { + totalcounts[i] = 0; + + // Set min and max to the opposite largest starting value + if ((m_type[i] == ST_MIN) || (m_type[i] == ST_SETMIN)) { + totalvalues[i] = maxy; + } else if ((m_type[i] == ST_MAX) || (m_type[i] == ST_SETMAX)) { + totalvalues[i] = miny; + } else { + totalvalues[i] = 0; + } + + // Turn off legend display.. It will only display if it's turned back on during draw. + goodcodes[i] = false; + + if (!m_goodcodes[i]) { continue; } + + lastX[i] = px; + + if (d != m_values.end() && d.value().contains(i + 1)) { + + tmp = d.value()[i + 1]; + h = tmp * ymult; + } else { + lastdaygood = false; + h = 0; + } + + lastY[i] = top + height - 1 - h; + } + + float compliance_hours = 0; + + if (p_profile->cpap->showComplianceInfo()) { + compliance_hours = p_profile->cpap->complianceHours(); + } + + int incompliant = 0; + Day *day; + EventDataType hours; + + //quint32 * tptr; + //EventStoreType * dptr; + + short px2, py2; + const qint64 ms_per_day = 86400000L; + + + painter.setClipRect(left, top, width, height); + painter.setClipping(true); + + QColor summaryColor = QColor("dark gray"); + + float lineThickness = AppSetting->lineThickness(); + + + for (qint64 Q = minx; Q <= maxx + ms_per_day; Q += ms_per_day) { + zd = Q / ms_per_day; + d = m_values.find(zd); + + if (Q < minx) { + goto jumpnext; + } + + if (d != m_values.end()) { + day = m_days[zd]; + bool summary_only = day && day->summaryOnly(); + + + if (!m_hours.contains(zd)) { + goto jumpnext; + } + + hours = m_hours[zd]; + + int x1 = px; + //x1-=(barw/2.0); + int x2 = px + barw; + + //if (x1 < left) { x1 = left; } + + if (x2 > left + width) { x2 = left + width; } + + // if (x2add(x1 - 1, top, x1 - 1, top + height, x2, top + height, x2, top, col.rgba()); + } else { + painter.fillRect((x1+barw/2)-5, top, barw, height, QBrush(col)); +// quads->add((x1 + barw / 2) - 5, top, (x1 + barw / 2) - 5, top + height, (x2 - barw / 2) + 5, +// top + height, (x2 - barw / 2) + 5, top, col.rgba()); + } + } + + if (graphtype == GT_SESSIONS) { + int j; + auto times = m_times.find(zd); + QColor col = m_colors[0]; + //if (hourssetColor(Qt::black); + + int np = d.value().size(); + + if (np > 0) { + for (auto & goodcode : goodcodes) { + goodcode = true; + } + } + + for (j = 0; j < np; j++) { + EventDataType tmp2 = times.value()[j] - miny; + py = top + height - (tmp2 * ymult); + + tmp = d.value()[j]; // length + + //tmp-=miny; + h = tmp * ymult; + + QLinearGradient gradient(x1, py-h, x1+barw, py-h); + gradient.setColorAt(0,col1); + gradient.setColorAt(1,col2); + painter.fillRect(x1, py-h, barw, h, QBrush(gradient)); +// quads->add(x1, py, x1, py - h, x2, py - h, x2, py, col1, col2); + + if ((h > 0) && (barw > 2)) { + painter.setPen(QColor(Qt::black)); + painter.drawLine(x1, py, x1, py - h); + painter.drawLine(x1, py - h, x2, py - h); + painter.drawLine(x1, py, x2, py); + painter.drawLine(x2, py, x2, py - h); + } + + totalvalues[0] += hours * tmp; + } + + totalcounts[0] += hours; + totalvalues[1] += j; + totalcounts[1]++; + total_val += hours; + total_hours += hours; + total_days++; + } else + { + if (!d.value().contains(0)) { + goto jumpnext; + } + + total = d.value()[0]; + + //if (total>0) { + if (day) { + EventDataType hours = m_hours[zd]; + total_val += total * hours; + total_hours += hours; + total_days++; + } + + py = top + height; + + //} + bool good; + SummaryType type; + + for (auto g=d.value().begin(), dend=d.value().end(); g != dend; g++) { + short j = g.key(); + + if (!j) { continue; } + + j--; + good = m_goodcodes[j]; + + if (!good) { + continue; + } + + type = m_type[j]; + // code was actually used (to signal the display of the legend summary) + goodcodes[j] = good; + + tmp = g.value(); + + QColor col = m_colors[j]; + + if (type == ST_HOURS) { + if (tmp < compliance_hours) { + col = QColor("#f04040"); + incompliant++; + } else if (summary_only) { + col = summaryColor; + } + } + + if (zd == hl_day) { + col = COLOR_Gold; + } + + //if (!tmp) continue; + if ((type == ST_MAX) || (type == ST_SETMAX)) { + if (totalvalues[j] < tmp) { + totalvalues[j] = tmp; + } + } else if ((type == ST_MIN) || (type == ST_SETMIN)) { + if (totalvalues[j] > tmp) { + totalvalues[j] = tmp; + } + } else { + totalvalues[j] += tmp * hours; + } + + //if (tmp) { + totalcounts[j] += hours; + //} + tmp -= miny; + h = tmp * ymult; // height in pixels + + if (graphtype == GT_BAR) { + QColor col1 = col; + QColor col2 = brighten(col,2.5); + + QLinearGradient gradient(x1, py-h, x1+barw, py-h); + gradient.setColorAt(0,col1); + gradient.setColorAt(1,col2); + painter.fillRect(x1, py-h, barw, h, QBrush(gradient)); + +// quads->add(x1, py, x1, py - h, col1); +// quads->add(x2, py - h, x2, py, col2); + + if (h > 0 && barw > 2) { + painter.setPen(QColor(Qt::black)); + painter.drawLine(x1, py, x1, py - h); + painter.drawLine(x1, py - h, x2, py - h); + painter.drawLine(x1, py, x2, py); + painter.drawLine(x2, py, x2, py - h); + } // if (bar + + py -= h; + } else if (graphtype == GT_LINE) { // if (m_graphtype==GT_BAR + QColor col1 = col; + QColor col2 = m_colors[j]; + px2 = px + barw; + py2 = (top + height - 1) - h; + + // If more than 1 day between records, skip the vertical crud. + if ((px2 - lastX[j]) > barw + 1) { + lastdaygood = false; + } + + if (lastdaygood) { + if (lastY[j] != py2) { // vertical line + painter.setPen(QPen(col2, lineThickness)); + painter.drawLine(lastX[j], lastY[j], px, py2); + } + + painter.setPen(QPen(col1, lineThickness)); + painter.drawLine(px, py2, px2, py2); + } else { + painter.setPen(QPen(col1, lineThickness)); + painter.drawLine(x1, py2, x2, py2); + } + + lastX[j] = px2; + lastY[j] = py2; + } else if (graphtype == GT_POINTS) { + QColor col1 = col; + QColor col2 = m_colors[j]; + px2 = px + barw; + py2 = (top + height - 2) - h; + + // If more than 1 day between records, skip the vertical crud. + if ((px2 - lastX[j]) > barw + 1) { + lastdaygood = false; + } + + if (zd == hl_day) { + painter.setPen(QPen(brighten(col2),10)); + painter.drawPoint(px2 - barw / 2, py2); + } + + if (lastdaygood) { + painter.setPen(QPen(col2, lineThickness)); + painter.drawLine(lastX[j] - barw / 2, lastY[j], px2 - barw / 2, py2); + } else { + painter.setPen(QPen(col1, lineThickness)); + painter.drawLine(px + barw / 2 - 1, py2, px + barw / 2 + 1, py2); + } + + lastX[j] = px2; + lastY[j] = py2; + } + } // for(QHashmaxx+extra) break; + } else { + if (Q < maxx) { + incompliant++; + } + + lastdaygood = false; + } + +jumpnext: + + if (px >= left + width + barw) { + break; + } + + px += barw; + + daynum++; + //lastQ=Q; + } + painter.setClipping(false); + + // Draw Ledgend + px = left + width - 3; + py = top - 5; + int legendx = px; + QString a, b; + int x, y; + + QFontMetrics fm(*defaultfont); + int bw = fm.width('X'); + int bh = fm.height() / 1.8; + + bool ishours = false; + int good = 0; + + for (int j = 0; j < m_codes.size(); j++) { + if (!goodcodes[j]) { continue; } + + good++; + SummaryType type = m_type[j]; + ChannelID code = m_codes[j]; + EventDataType tval = m_typeval[j]; + + switch (type) { + case ST_WAVG: + b = "Avg"; + break; + + case ST_AVG: + b = "Avg"; + break; + + case ST_90P: + b = "90%"; + break; + + case ST_PERC: + if (tval >= 0.99) { b = STR_TR_Max; } + else if (tval == 0.5) { b = STR_TR_Med; } + else { b = QString("%1%").arg(tval * 100.0, 0, 'f', 0); } + + break; + + //b=QString("%1%").arg(tval*100.0,0,'f',0); break; + case ST_MIN: + b = STR_TR_Min; + break; + + case ST_MAX: + b = STR_TR_Max; + break; + + case ST_SETMIN: + b = STR_TR_Min; + break; + + case ST_SETMAX: + b = STR_TR_Max; + break; + + case ST_CPH: + b = ""; + break; + + case ST_SPH: + b = "%"; + break; + + case ST_HOURS: + b = STR_UNIT_Hours; + break; + + case ST_SESSIONS: + b = STR_TR_Sessions; + break; + + default: + b = ""; + break; + } + + a = schema::channel[code].label(); + + if (a == w.title() && !b.isEmpty()) { a = b; } + else { a += " " + b; } + + QString val; + float f = 0; + + if (totalcounts[j] > 0) { + if ((type == ST_MIN) || (type == ST_MAX) || (type == ST_SETMIN) || (type == ST_SETMAX)) { + f = totalvalues[j]; + } else { + f = totalvalues[j] / totalcounts[j]; + } + } + + if (type == ST_HOURS) { + int h = f; + int m = int(f * 60) % 60; + val.sprintf("%02i:%02i", h, m); + ishours = true; + } else { + val = QString::number(f, 'f', 2); + } + + a += ": " + val; + //GetTextExtent(a,x,y); + //float wt=20*w.printScaleX(); + //px-=wt+x; + //w.renderText(a,px+wt,py+1); + //quads->add(px+wt-y/4-y,py-y,px+wt-y/4,py-y,px+wt-y/4,py+1,px+wt-y/4-y,py+1,m_colors[j].rgba()); + + + //QString text=schema::channel[code].label(); + + int wid, hi; + GetTextExtent(a, wid, hi); + legendx -= wid; + w.renderText(a, legendx, top - 4); + // legendx-=bw/2; + + painter.fillRect(legendx - bw-4, top-w.marginTop()-1, bh, w.marginTop(), QBrush(m_colors[j])); + legendx -= bw * 2; + + + //lines->add(px,py,px+20,py,m_colors[j]); + //lines->add(px,py+1,px+20,py+1,m_colors[j]); + } + + if ((m_graphtype == GT_BAR) && (good > 0)) { + + if (m_type.size() > 1) { + float val = total_val / float(total_hours); + a = m_label + ": " + QString::number(val, 'f', 2) + " "; + GetTextExtent(a, x, y); + legendx -= x; + w.renderText(a, legendx, py + 1); + } + } + + a = ""; + /*if (m_graphtype==GT_BAR) { + if (m_type.size()>1) { + float val=total_val/float(total_days); + a+=m_label+": "+QString::number(val,'f',2)+" "; + // + } + }*/ + a += QString(QObject::tr("Days: %1")).arg(total_days, 0); + + if (p_profile->cpap->showComplianceInfo()) { + if (ishours && incompliant > 0) { + a += " "+QString(QObject::tr("Low Usage Days: %1")).arg(incompliant, 0)+ + " "+QString(QObject::tr("(%1% compliant, defined as > %2 hours)")). + arg((1.0 / daynum) * (total_days - incompliant) * 100.0, 0, 'f', 2).arg(compliance_hours, 0, 'f', 1); + } + } + + + //GetTextExtent(a,x,y); + //legendx-=30+x; + //w.renderText(a,px+24,py+5); + w.renderText(a, left, py + 1); +} + +QString formatTime(EventDataType v, bool show_seconds = false, bool duration = false, + bool show_12hr = false) +{ + int h = int(v); + + if (!duration) { + h %= 24; + } else { show_12hr = false; } + + int m = int(v * 60) % 60; + int s = int(v * 3600) % 60; + + char pm[3] = {"am"}; + + if (show_12hr) { + h >= 12 ? pm[0] = 'p' : pm[0] = 'a'; + h %= 12; + + if (h == 0) { h = 12; } + + } else { + pm[0] = 0; + } + + if (show_seconds) { + return QString().sprintf("%i:%02i:%02i%s", h, m, s, pm); + } else { + return QString().sprintf("%i:%02i%s", h, m, pm); + } +} + +bool gOverviewGraph::mouseMoveEvent(QMouseEvent *event, gGraph *graph) +{ + graph->timedRedraw(0); + int xposLeft = event->x(); + int yPosTop = event->y(); + + if (!m_rect.contains(xposLeft, yPosTop)) { + // if ((x<0 || y<0 || x>l_width || y>l_height)) { + hl_day = -1; + //graph->timedRedraw(2000); + return false; + } + + xposLeft -= m_rect.left(); + yPosTop -= m_rect.top(); + + Q_UNUSED(yPosTop) + + double xx = l_maxx - l_minx; + + double xmult = xx / double(l_width + barw); + + qint64 mx = ceil(xmult * double(xposLeft - offset)); + mx += l_minx; + mx = mx + l_offset; //-86400000L; + int zd = mx / 86400000L; + + Day *day; + //if (hl_day!=zd) // This line is an optimization + + { + hl_day = zd; + graph->Trigger(2000); + + auto d = m_values.find(hl_day); + + QMap &valhash = d.value(); + + xposLeft += m_rect.left(); //gYAxis::Margin+gGraphView::titleWidth; //graph->m_marginleft+ + int y = event->y() - m_rect.top() + rtop - 15; + //QDateTime dt1=QDateTime::fromTime_t(hl_day*86400).toLocalTime(); + QDateTime dt2 = QDateTime::fromTime_t(hl_day * 86400).toUTC(); +// QDateTime dt2 = QDateTime::fromTime_t(hl_day * 86400).toLocalTime(); + + //QTime t1=dt1.time(); + //QTime t2=dt2.time(); + + QDate dt = dt2.date(); + day = m_days[zd]; + + if ((d != m_values.end()) && (day != nullptr)) { + bool summary_only = day->summaryOnly(); + + QString strTooltip = dt.toString(Qt::SystemLocaleShortDate); + + // Day * day=m_days[hl_day]; + //EventDataType val; + QString val; + + if (m_graphtype == GT_SESSIONS) { + if (m_type[0] == ST_HOURS) { + + int t = m_hours[zd] * 3600.0; + int h = t / 3600; + int m = (t / 60) % 60; + //int s=t % 60; + val.sprintf("%02i:%02i", h, m); + } else { + val = QString::number(d.value()[0], 'f', 2); + } + + strTooltip += "\r\n" + m_label + ": " + val; + + if (m_type[1] == ST_SESSIONS) { + strTooltip += " "+QString(QObject::tr("(Sess: %1)")).arg(day->size(), 0); + } + + EventDataType v = m_times[zd][0]; + int lastt = m_times[zd].size() - 1; + + if (lastt < 0) { lastt = 0; } + + strTooltip += "\r\n"+QString(QObject::tr("Bedtime: %1")).arg(formatTime(v, false, false, true)); + v = m_times[zd][lastt] + m_values[zd][lastt]; + strTooltip += "\r\n"+QString(QObject::tr("Waketime: %1")).arg(formatTime(v, false, false, true)); + + } else + if (m_graphtype == GT_BAR) { + if (m_type[0] == ST_HOURS) { + int t = d.value()[0] * 3600.0; + int h = t / 3600; + int m = (t / 60) % 60; + //int s=t % 60; + val.sprintf("%02i:%02i", h, m); + } else { + val = QString::number(d.value()[0], 'f', 2); + } + + strTooltip += "\r\n" + m_label + ": " + val; + //z+="\r\nMode="+QString::number(day->settings_min("FlexSet"),'f',0); + + } else { + QString strDataType; + + for (int i = 0; i < m_type.size(); i++) { + if (!m_goodcodes[i]) { + continue; + } + + if (!valhash.contains(i + 1)) { + continue; + } + + EventDataType tval = m_typeval[i]; + + switch (m_type[i]) { + case ST_WAVG: + strDataType = STR_TR_WAvg; + break; + + case ST_AVG: + strDataType = STR_TR_Avg; + break; + + case ST_90P: + strDataType = QString("90%"); + break; + + case ST_PERC: + if (tval >= 0.99) { strDataType = STR_TR_Max; } + else if (tval == 0.5) { strDataType = STR_TR_Med; } + else { strDataType = QString("%1%").arg(tval * 100.0, 0, 'f', 0); } + + break; + + case ST_MIN: + strDataType = STR_TR_Min; + break; + + case ST_MAX: + strDataType = STR_TR_Max; + break; + + case ST_CPH: + strDataType = ""; + break; + + case ST_SPH: + strDataType = "%"; + break; + + case ST_HOURS: + strDataType = STR_UNIT_Hours; + break; + + case ST_SESSIONS: + strDataType = STR_TR_Sessions; + break; + + case ST_SETMIN: + strDataType = STR_TR_Min; + break; + + case ST_SETMAX: + strDataType = STR_TR_Max; + break; + + default: + strDataType = ""; + break; + } + + if (m_type[i] == ST_SESSIONS) { + val = QString::number(d.value()[i + 1], 'f', 0); + strTooltip += "\r\n" + strDataType + ": " + val; + } else { + //if (day && (day->channelExists(m_codes[i]) || day->settingExists(m_codes[i]))) { + schema::Channel &chan = schema::channel[m_codes[i]]; + EventDataType v; + + if (valhash.contains(i + 1)) { + v = valhash[i + 1]; + } else { v = 0; } + + if (m_codes[i] == Journal_Weight) { + val = weightString(v, p_profile->general->unitSystem()); + } else { + val = QString::number(v, 'f', 2); + } + + strTooltip += "\r\n" + chan.label() + " " + strDataType + ": " + val; + //} + } + } + + } + if (summary_only) { + strTooltip += "\r\n"+QObject::tr("(Summary Only)"); + } + + graph->ToolTip(strTooltip, xposLeft, y - 15); + return false; + } else { + QString z = dt.toString(Qt::SystemLocaleShortDate) + "\r\n"+QObject::tr("No Data"); + graph->ToolTip(z, xposLeft, y - 15); + return false; + } + } + return false; +} + +bool gOverviewGraph::mousePressEvent(QMouseEvent *event, gGraph *graph) +{ + if (event->modifiers() & Qt::ShiftModifier) { + //qDebug() << "Jump to daily view?"; + return true; + } + + Q_UNUSED(graph) + return false; +} + +bool gOverviewGraph::keyPressEvent(QKeyEvent *event, gGraph *graph) +{ + Q_UNUSED(event) + Q_UNUSED(graph) + //qDebug() << "Summarychart Keypress"; + return false; +} + +#include "mainwindow.h" +extern MainWindow *mainwin; +bool gOverviewGraph::mouseReleaseEvent(QMouseEvent *event, gGraph *graph) +{ + if (event->modifiers() & Qt::ShiftModifier) { + if (hl_day < 0) { + mouseMoveEvent(event, graph); + } + + if (hl_day > 0) { + QDateTime d = QDateTime::fromTime_t(hl_day * 86400).toUTC(); +// QDateTime d = QDateTime::fromTime_t(hl_day * 86400).toLocalTime(); + mainwin->getDaily()->LoadDate(d.date()); + mainwin->JumpDaily(); + //qDebug() << "Jump to daily view?" << d; + return true; + } + } + + Q_UNUSED(event) + hl_day = -1; + graph->timedRedraw(2000); + return false; +} +#endif diff --git a/oscar/Graphs/gOverviewGraph.h b/oscar/Graphs/gOverviewGraph.h new file mode 100644 index 00000000..c6b36669 --- /dev/null +++ b/oscar/Graphs/gOverviewGraph.h @@ -0,0 +1,170 @@ +/* gOverviewGraph Header + * + * Copyright (c) 2019-2022 The OSCAR Team + * Copyright (C) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#ifndef GOVERVIEWGRAPH_H +#define GOVERVIEWGRAPH_H + +#include +#include "gGraphView.h" +#include "gXAxis.h" +#include "SleepLib/appsettings.h" + +#ifndef REMOVE_FITNESS + +/* +BMI, Weight and Zombie graphs are are hard coded in Overview.cpp +These graph require special handling in class gOverviewGraph. + +Currently there are 4 graphs types, one of which is not used. +GT_BAR Used by CPAP graph to make bar graphs for each day +GT_LINE ? Used for making a line ? +GT_POINT ? Used to display points instead of lines ? +GT_SESSION ?? NOT USED. + +BMI, Weight and Zombie graphs current use GT_LINE and not GT_BAR +The Overview Linecharts preference allows points to be displayed instead of lines. +*/ + +/*! \enum GraphType + \value GT_BAR Display as a BarGraph + \value GT_LINE Display as a line plot + */ +enum GraphType { GT_BAR, GT_LINE, GT_POINTS , GT_SESSIONS }; + + +/*! \class gOverviewGraph + \brief The main overall chart type layer used in Overview page + */ +class gOverviewGraph: public Layer +{ + public: + //! \brief Constructs a gOverviewGraph with QString label, of GraphType type + gOverviewGraph(QString label, GraphType type = GT_BAR); + virtual ~gOverviewGraph(); + + //! \brief Renders the graph to the QPainter object + virtual void paint(QPainter &painter, gGraph &w, const QRegion ®ion); + + //! \brief Precalculation code prior to drawing. Day object is not needed here, it's just here for Layer compatability. + virtual void SetDay(Day *day = nullptr); + + //! \brief Returns true if no data was found for this day during SetDay + virtual bool isEmpty() { return m_empty; } + + //! \brief Adds a layer to the gOverviewGraph (When in Bar mode, it becomes culminative, eg, the AHI chart) + void addSlice(ChannelID code, QColor color, SummaryType type, EventDataType tval = 0.00f) { + m_codes.push_back(code); + m_colors.push_back(color); + m_type.push_back(type); + //m_zeros.push_back(ignore_zeros); + m_typeval.push_back(tval); + } + + //! \brief Deselect highlighting (the gold bar) + virtual void deselect() { + hl_day = -1; + } + + //! \brief Returns true if currently selected.. + virtual bool isSelected() { return hl_day >= 0; } + + + //! \brief Sets the MachineType this gOverviewGraph is interested in + void setMachineType(MachineType type) { m_machinetype = type; } + + //! \brief Returns the MachineType this gOverviewGraph is interested in + MachineType machineType() { return m_machinetype; } + + virtual Layer * Clone() { + gOverviewGraph * sc = new gOverviewGraph(m_label); + Layer::CloneInto(sc); + CloneInto(sc); + return sc; + } + + void CloneInto(gOverviewGraph * layer) { + layer->m_orientation = m_orientation; + layer->m_colors = m_colors; + layer->m_codes = m_codes; + layer->m_goodcodes = m_goodcodes; + layer->m_type = m_type; + layer->m_typeval = m_typeval; + layer->m_values = m_values; + layer->m_times = m_times; + layer->m_hours = m_hours; + layer->m_days = m_days; + + layer->m_empty = m_empty; + layer->m_fday = m_fday; + layer->m_label = m_label; + layer->barw = barw; + layer->l_offset = l_offset; + layer->offset = offset; + layer->l_left = l_left; + layer->l_top = l_top; + layer->l_width= l_width; + layer->l_height = l_height; + layer->rtop = rtop; + layer->l_minx = l_minx; + layer->l_maxx = l_maxx; + layer->hl_day = hl_day; + layer->m_graphtype = m_graphtype; + layer->m_machinetype = m_machinetype; + layer->tz_offset = tz_offset; + layer->tz_hours = tz_hours; + + } + + + protected: + Qt::Orientation m_orientation; + + QVector m_colors; + QVector m_codes; + QVector m_goodcodes; + //QVector m_zeros; + QVector m_type; + QVector m_typeval; + QHash > m_values; + QHash > m_times; + QHash m_hours; + QHash m_days; + + bool m_empty; + int m_fday; + QString m_label; + + float barw; // bar width from last draw + qint64 l_offset; // last offset + float offset; // in pixels; + int l_left, l_top, l_width, l_height; + int rtop; + qint64 l_minx, l_maxx; + int hl_day; + //gGraph *graph; + GraphType m_graphtype; + MachineType m_machinetype; + int tz_offset; + float tz_hours; + + //! \brief Key was pressed that effects this layer + virtual bool keyPressEvent(QKeyEvent *event, gGraph *graph); + + //! \brief Mouse moved over this layers area (shows the hover-over tooltips here) + virtual bool mouseMoveEvent(QMouseEvent *event, gGraph *graph); + + //! \brief Mouse Button was pressed over this area + virtual bool mousePressEvent(QMouseEvent *event, gGraph *graph); + + //! \brief Mouse Button was released over this area. (jumps to daily view here) + virtual bool mouseReleaseEvent(QMouseEvent *event, gGraph *graph); +}; +#endif + +#endif // GOVERVIEWGRAPH_H diff --git a/oscar/Graphs/gPressureChart.h b/oscar/Graphs/gPressureChart.h index 498aca58..c784f81f 100644 --- a/oscar/Graphs/gPressureChart.h +++ b/oscar/Graphs/gPressureChart.h @@ -10,7 +10,7 @@ #ifndef GPRESSURECHART_H #define GPRESSURECHART_H -#include "gSessionTimesChart.h" +#include "gSummaryChart.h" class gPressureChart : public gSummaryChart { diff --git a/oscar/Graphs/gSessionTimesChart.cpp b/oscar/Graphs/gSessionTimesChart.cpp index 1a51495e..868a4949 100644 --- a/oscar/Graphs/gSessionTimesChart.cpp +++ b/oscar/Graphs/gSessionTimesChart.cpp @@ -23,746 +23,7 @@ extern MainWindow * mainwin; -short SummaryCalcItem::midcalc; - -gSummaryChart::gSummaryChart(QString label, MachineType machtype) - :Layer(NoChannel), m_label(label), m_machtype(machtype) -{ - m_layertype = LT_Overview; - QDateTime d1 = QDateTime::currentDateTime(); - QDateTime d2 = d1; - d1.setTimeSpec(Qt::UTC); // CHECK: Does this deal with DST? - tz_offset = d2.secsTo(d1); - tz_hours = tz_offset / 3600.0; - expected_slices = 5; - - idx_end = 0; - idx_start = 0; -} - -gSummaryChart::gSummaryChart(ChannelID code, MachineType machtype) - :Layer(code), m_machtype(machtype) -{ - m_layertype = LT_Overview; - QDateTime d1 = QDateTime::currentDateTime(); - QDateTime d2 = d1; - d1.setTimeSpec(Qt::UTC); // CHECK: Does this deal with DST? - tz_offset = d2.secsTo(d1); - tz_hours = tz_offset / 3600.0; - expected_slices = 5; - - addCalc(code, ST_MIN, brighten(schema::channel[code].defaultColor() ,0.60f)); - addCalc(code, ST_MID, brighten(schema::channel[code].defaultColor() ,1.20f)); - addCalc(code, ST_90P, brighten(schema::channel[code].defaultColor() ,1.70f)); - addCalc(code, ST_MAX, brighten(schema::channel[code].defaultColor() ,2.30f)); - - idx_end = 0; - idx_start = 0; -} - -gSummaryChart::~gSummaryChart() -{ -} - -void gSummaryChart::SetDay(Day *unused_day) -{ - cache.clear(); - - Q_UNUSED(unused_day) - Layer::SetDay(nullptr); - - if (m_machtype != MT_CPAP) { - // Channels' machine types are not terribly reliable: oximetry channels can be reported by a CPAP, - // and position channels can be reported by an oximeter. So look for any days with data. - firstday = p_profile->FirstDay(); - lastday = p_profile->LastDay(); - } else { - // But CPAP channels (like pressure settings) can only be reported by a CPAP. - firstday = p_profile->FirstDay(m_machtype); - lastday = p_profile->LastDay(m_machtype); - } - - dayindex.clear(); - daylist.clear(); - - if (!firstday.isValid() || !lastday.isValid()) return; - // daylist.reserve(firstday.daysTo(lastday)+1); - QDate date = firstday; - int idx = 0; - do { - auto di = p_profile->daylist.find(date); - Day * day = nullptr; - if (di != p_profile->daylist.end()) { - day = di.value(); - } - daylist.append(day); - dayindex[date] = idx; - idx++; - date = date.addDays(1); - } while (date <= lastday); - - m_minx = QDateTime(firstday, QTime(0,0,0), Qt::LocalTime).toMSecsSinceEpoch(); - m_maxx = QDateTime(lastday, QTime(23,59,59), Qt::LocalTime).toMSecsSinceEpoch(); - m_miny = 0; - m_maxy = 20; - - m_empty = false; - m_emptyPrev = true; - -} - - -int gSummaryChart::addCalc(ChannelID code, SummaryType type, QColor color) -{ - calcitems.append(SummaryCalcItem(code, type, color)); - return calcitems.size() - 1; // return the index of the newly appended calc -} - -int gSummaryChart::addCalc(ChannelID code, SummaryType type) -{ - return addCalc(code, type, schema::channel[code].defaultColor()); -} - - -bool gSummaryChart::keyPressEvent(QKeyEvent *event, gGraph *graph) -{ - Q_UNUSED(event) - Q_UNUSED(graph) - return false; -} - -bool gSummaryChart::mouseMoveEvent(QMouseEvent *event, gGraph *graph) -{ - Q_UNUSED(event) - Q_UNUSED(graph) - return false; -} - -bool gSummaryChart::mousePressEvent(QMouseEvent *event, gGraph *graph) -{ - Q_UNUSED(event) - Q_UNUSED(graph) - return false; -} - -bool gSummaryChart::mouseReleaseEvent(QMouseEvent *event, gGraph *graph) -{ - if (!(event->modifiers() & Qt::ShiftModifier)) return false; - - float x = event->x() - m_rect.left(); - float y = event->y() - m_rect.top(); - qDebug() << x << y; - - EventDataType miny; - EventDataType maxy; - - graph->roundY(miny, maxy); - - QDate date = QDateTime::fromMSecsSinceEpoch(m_minx, Qt::LocalTime).date(); - - int days = ceil(double(m_maxx - m_minx) / 86400000.0); - - float barw = float(m_rect.width()) / float(days); - - float idx = x/barw; - - date = date.addDays(idx); - - auto it = dayindex.find(date); - if (it != dayindex.end()) { - Day * day = daylist.at(it.value()); - if (day) { - mainwin->getDaily()->LoadDate(date); - mainwin->JumpDaily(); - } - } - - return true; -} - -void gSummaryChart::preCalc() -{ - midcalc = p_profile->general->prefCalcMiddle(); - - for (auto & calc : calcitems) { - calc.reset(idx_end - idx_start, midcalc); - } -} - -void gSummaryChart::customCalc(Day *day, QVector & slices) -{ - int size = slices.size(); - if (size != calcitems.size()) { - return; - } - float hour = day->hours(m_machtype); - - for (int i=0; i < size; ++i) { - const SummaryChartSlice & slice = slices.at(i); - SummaryCalcItem & calc = calcitems[i]; - - calc.update(slice.value, hour); - } -} - -void gSummaryChart::afterDraw(QPainter &painter, gGraph &graph, QRectF rect) -{ - if (totaldays == nousedays) return; - - if (calcitems.size() == 0) return; - - QStringList strlist; - QString txt; - - int midcalc = p_profile->general->prefCalcMiddle(); - QString midstr; - if (midcalc == 0) { - midstr = QObject::tr("Med."); - } else if (midcalc == 1) { - midstr = QObject::tr("W-Avg"); - } else { - midstr = QObject::tr("Avg"); - } - - - float perc = p_profile->general->prefCalcPercentile(); - QString percstr = QString("%1%").arg(perc, 0, 'f',0); - - schema::Channel & chan = schema::channel[calcitems.at(0).code]; - - for (auto & calc : calcitems) { - - if (calcitems.size() == 1) { - float val = calc.min; - if (val < 99998) - strlist.append(QObject::tr("Min: %1").arg(val,0,'f',2)); - } - - float mid = 0; - switch (midcalc) { - case 0: - if (calc.median_data.size() > 0) { - mid = median(calc.median_data.begin(), calc.median_data.end()); - } - break; - case 1: - mid = calc.wavg_sum / calc.divisor; - break; - case 2: - mid = calc.avg_sum / calc.cnt; - break; - } - - float val = 0; - switch (calc.type) { - case ST_CPH: - val = mid; - txt = midstr+": "; - break; - case ST_SPH: - val = mid; - txt = midstr+": "; - break; - case ST_MIN: - val = calc.min; - if (val >= 99998) continue; - txt = QObject::tr("Min: "); - break; - case ST_MAX: - val = calc.max; - if (val <= -99998) continue; - txt = QObject::tr("Max: "); - break; - case ST_SETMIN: - val = calc.min; - if (val >= 99998) continue; - txt = QObject::tr("Min: "); - break; - case ST_SETMAX: - val = calc.max; - if (val <= -99998) continue; - txt = QObject::tr("Max: "); - break; - case ST_MID: - val = mid; - txt = QString("%1: ").arg(midstr); - break; - case ST_90P: - val = mid; - txt = QString("%1: ").arg(percstr); - break; - default: - val = mid; - txt = QString("???: "); - break; - } - strlist.append(QString("%1%2").arg(txt).arg(val,0,'f',2)); - if (calcitems.size() == 1) { - val = calc.max; - if (val > -99998) - strlist.append(QObject::tr("Max: %1").arg(val,0,'f',2)); - } - } - - QString str; - if (totaldays > 1) { - str = QObject::tr("%1 (%2 days): ").arg(chan.fullname()).arg(totaldays); - } else { - str = QObject::tr("%1 (%2 day): ").arg(chan.fullname()).arg(totaldays); - } - str += " "+strlist.join(", "); - - QRectF rec(rect.left(), rect.top(), 0,0); - painter.setFont(*defaultfont); - rec = painter.boundingRect(rec, Qt::AlignTop, str); - rec.moveBottom(rect.top()-3*graph.printScaleY()); - painter.drawText(rec, Qt::AlignTop, str); - -// graph.renderText(str, rect.left(), rect.top()-5*graph.printScaleY(), 0); - - -} - -QString gSummaryChart::tooltipData(Day *, int idx) -{ - QString txt; - const auto & slices = cache[idx]; - int i = slices.size(); - while (i > 0) { - i--; - txt += QString("\n%1: %2").arg(slices[i].name).arg(float(slices[i].value), 0, 'f', 2); - } - return txt; -} - -void gSummaryChart::populate(Day * day, int idx) -{ - - bool good = false; - for (const auto & item : calcitems) { - if (day->hasData(item.code, item.type)) { - good = true; - break; - } - } - if (!good) return; - - auto & slices = cache[idx]; - - float hours = day->hours(m_machtype); - if ((hours==0) && (m_machtype != MT_CPAP)) hours = day->hours(); - float base = 0; - - for (auto & item : calcitems) { - ChannelID code = item.code; - schema::Channel & chan = schema::channel[code]; - float value = 0; - QString name; - QColor color; - switch (item.type) { - case ST_CPH: - value = day->count(code) / hours; - name = chan.label(); - color = item.color; - slices.append(SummaryChartSlice(&item, value, value, name, color)); - break; - case ST_SPH: - value = (100.0 / hours) * (day->sum(code) / 3600.0); - name = QObject::tr("% in %1").arg(chan.label()); - color = item.color; - slices.append(SummaryChartSlice(&item, value, value, name, color)); - break; - case ST_HOURS: - value = hours; - name = QObject::tr("Hours"); - color = COLOR_LightBlue; - slices.append(SummaryChartSlice(&item, value, hours, name, color)); - break; - case ST_MIN: - value = day->Min(code); - name = QObject::tr("Min %1").arg(chan.label()); - color = item.color; - slices.append(SummaryChartSlice(&item, value, value - base, name, color)); - base = value; - break; - case ST_MID: - value = day->calcMiddle(code); - name = day->calcMiddleLabel(code); - color = item.color; - slices.append(SummaryChartSlice(&item, value, value - base, name, color)); - base = value; - break; - case ST_90P: - value = day->calcPercentile(code); - name = day->calcPercentileLabel(code); - color = item.color; - slices.append(SummaryChartSlice(&item, value, value - base, name, color)); - base = value; - break; - case ST_MAX: - value = day->calcMax(code); - name = day->calcMaxLabel(code); - color = item.color; - slices.append(SummaryChartSlice(&item, value, value - base, name, color)); - base = value; - break; - default: - break; - } - } -} - -void gSummaryChart::paint(QPainter &painter, gGraph &graph, const QRegion ®ion) -{ - QRectF rect = region.boundingRect(); - - rect.translate(0.0f, 0.001f); - - painter.setPen(QColor(Qt::black)); - painter.drawRect(rect); - - rect.moveBottom(rect.bottom()+1); - - m_minx = graph.min_x; - m_maxx = graph.max_x; - - QDateTime date2 = QDateTime::fromMSecsSinceEpoch(m_minx, Qt::LocalTime); - QDateTime enddate2 = QDateTime::fromMSecsSinceEpoch(m_maxx, Qt::LocalTime); - - QDate date = date2.date(); - QDate enddate = enddate2.date(); - - int days = ceil(double(m_maxx - m_minx) / 86400000.0); - - //float lasty1 = rect.bottom(); - - auto it = dayindex.find(date); - idx_start = 0; - if (it == dayindex.end()) { - it = dayindex.begin(); - } else { - idx_start = it.value(); - } - - int idx = idx_start; - - // Determine how many days after the first day of the chart that this data is to begin - int numDaysOffset = 0; - if (firstday > date) { // date = beginning date of chart; firstday = beginning date of data - numDaysOffset = date.daysTo(firstday); - } - - // Determine how many days before the last day of the chart that this data is to end - int numDaysAfter = 0; - if (enddate > lastday) { // enddate = last date of chart; lastday = last date of data - numDaysAfter = lastday.daysTo(enddate); - } - if (numDaysAfter > days) // Nothing to do if this data is off the left edge of the chart - return; - - auto ite = dayindex.find(enddate); - idx_end = daylist.size()-1; - if (ite != dayindex.end()) { - idx_end = ite.value(); - } - - QPoint mouse = graph.graphView()->currentMousePos(); - - nousedays = 0; - totaldays = 0; - - QRectF hl_rect; - QDate hl_date; - Day * hl_day = nullptr; - int hl_idx = -1; - bool hl = false; - - if ((daylist.size() == 0) || (it == dayindex.end())) - return; - - //Day * lastday = nullptr; - - // int dc = 0; -// for (int i=idx; i<=idx_end; ++i) { -// Day * day = daylist.at(i); -// if (day || lastday) { -// dc++; -// } -// lastday = day; -// } -// days = dc; -// lastday = nullptr; - float barw = float(rect.width()) / float(days); - - QString hl2_text = ""; - - QVector outlines; - int size = idx_end - idx; - outlines.reserve(size * expected_slices); - - // Virtual call to setup any custom graph stuff - preCalc(); - - float lastx1 = rect.left(); - lastx1 += numDaysOffset * barw; - float right_edge = (rect.left()+rect.width()+1); - - - ///////////////////////////////////////////////////////////////////// - /// Calculate Graph Peaks - ///////////////////////////////////////////////////////////////////// - peak_value = 0; - for (int i=idx; i <= idx_end; ++i, lastx1 += barw) { - Day * day = daylist.at(i); - - if ((lastx1 + barw) > right_edge) - break; - - if (!day) { - continue; - } - - day->OpenSummary(); - - auto cit = cache.find(i); - - if (cit == cache.end()) { - populate(day, i); - cit = cache.find(i); - } - - if (cit != cache.end()) { - float base = 0, val; - for (const auto & slice : cit.value()) { - val = slice.height; - base += val; - } - peak_value = qMax(peak_value, base); - } - } - m_miny = 0; - m_maxy = ceil(peak_value); - - ///////////////////////////////////////////////////////////////////// - /// Y-Axis scaling - ///////////////////////////////////////////////////////////////////// - - EventDataType miny; - EventDataType maxy; - - graph.roundY(miny, maxy); - float ymult = float(rect.height()) / (maxy-miny); - - lastx1 = rect.left(); - lastx1 += numDaysOffset * barw; - - ///////////////////////////////////////////////////////////////////// - /// Main drawing loop - ///////////////////////////////////////////////////////////////////// - do { - Day * day = daylist.at(idx); - - if ((lastx1 + barw) > right_edge) - break; - - totaldays++; - - if (!day) - { - // lasty1 = rect.bottom(); - lastx1 += barw; - it++; - nousedays++; - //lastday = day; - continue; - } - - //lastday = day; - - float x1 = lastx1 + barw; - - day->OpenSummary(); - QRectF hl2_rect; - - bool hlday = false; - QRectF rec2(lastx1, rect.top(), barw, rect.height()); - if (rec2.contains(mouse)) { - hl_rect = rec2; - hl_day = day; - hl_date = it.key(); - hl_idx = idx; - - hl = true; - hlday = true; - } - - auto cit = cache.find(idx); - - if (cit == cache.end()) { - populate(day, idx); - cit = cache.find(idx); - } - - float lastval = 0, val, y1,y2; - if (cit != cache.end()) { - ///////////////////////////////////////////////////////////////////////////////////// - /// Draw pressure settings - ///////////////////////////////////////////////////////////////////////////////////// - QVector & list = cit.value(); - customCalc(day, list); - - QLinearGradient gradient(lastx1, 0, lastx1 + barw, 0); //rect.bottom(), barw, rect.bottom()); - - for (const auto & slice : list) { - val = slice.height; - y1 = ((lastval-miny) * ymult); - y2 = (val * ymult); - QColor color = slice.color; - - QRectF rec = QRectF(lastx1, rect.bottom() - y1, barw, -y2).intersected(rect); - - if (hlday) { - if (rec.contains(mouse.x(), mouse.y())) { - color = Qt::yellow; - hl2_rect = rec; - } - } - - if (barw <= 3) { - painter.fillRect(rec, QBrush(color)); - } else if (barw > 8) { - gradient.setColorAt(0,color); - gradient.setColorAt(1,brighten(color, 2.0)); - painter.fillRect(rec, QBrush(gradient)); -// painter.fillRect(rec, slice.brush); - outlines.append(rec); - } else { - painter.fillRect(rec, brighten(color, 1.25)); - outlines.append(rec); - } - - lastval += val; - } - } - - lastx1 = x1; - it++; - } while (++idx <= idx_end); - painter.setPen(QPen(Qt::black,1)); - painter.drawRects(outlines); - - if (hl) { - QColor col2(255,0,0,64); - painter.fillRect(hl_rect, QBrush(col2)); - - QString txt = hl_date.toString(Qt::SystemLocaleShortDate)+" "; - if (hl_day) { - // grab extra tooltip data - txt += tooltipData(hl_day, hl_idx); - if (!hl2_text.isEmpty()) { - QColor col = Qt::yellow; - col.setAlpha(255); - // painter.fillRect(hl2_rect, QBrush(col)); - txt += hl2_text; - } - } - - graph.ToolTip(txt, mouse.x()-15, mouse.y()+5, TT_AlignRight); - } - try { - afterDraw(painter, graph, rect); - } catch(...) { - qDebug() << "Bad median call in" << m_label; - } - - - // This could be turning off graphs prematurely.. - if (cache.size() == 0) { - m_empty = true; - m_emptyPrev = true; - graph.graphView()->updateScale(); - emit summaryChartEmpty(this,m_minx,m_maxx,true); - } else if (m_emptyPrev) { - m_emptyPrev = false; - emit summaryChartEmpty(this,m_minx,m_maxx,false); - } - -} - -QString gUsageChart::tooltipData(Day * day, int) -{ - return QObject::tr("\nHours: %1").arg(day->hours(m_machtype), 0, 'f', 2); -} - -void gUsageChart::populate(Day *day, int idx) -{ - QVector & slices = cache[idx]; - - float hours = day->hours(m_machtype); - - QColor cpapcolor = day->summaryOnly() ? QColor(128,128,128) : calcitems[0].color; - bool haveoxi = day->hasMachine(MT_OXIMETER); - - QColor goodcolor = haveoxi ? QColor(128,255,196) : cpapcolor; - - QColor color = (hours < compliance_threshold) ? QColor(255,64,64) : goodcolor; - slices.append(SummaryChartSlice(&calcitems[0], hours, hours, QObject::tr("Hours"), color)); -} - -void gUsageChart::preCalc() -{ - midcalc = p_profile->general->prefCalcMiddle(); - - compliance_threshold = p_profile->cpap->complianceHours(); - incompdays = 0; - - SummaryCalcItem & calc = calcitems[0]; - calc.reset(idx_end - idx_start, midcalc); -} - -void gUsageChart::customCalc(Day *, QVector &list) -{ - if (list.size() == 0) { - incompdays++; - return; - } - - SummaryChartSlice & slice = list[0]; - SummaryCalcItem & calc = calcitems[0]; - - if (slice.value < compliance_threshold) incompdays++; - - calc.update(slice.value, 1); -} - -void gUsageChart::afterDraw(QPainter &, gGraph &graph, QRectF rect) -{ - if (totaldays == nousedays) return; - - if (totaldays > 1) { - float comp = 100.0 - ((float(incompdays + nousedays) / float(totaldays)) * 100.0); - - int midcalc = p_profile->general->prefCalcMiddle(); - float mid = 0; - SummaryCalcItem & calc = calcitems[0]; - switch (midcalc) { - case 0: // median - mid = median(calc.median_data.begin(), calc.median_data.end()); - break; - case 1: // w-avg - mid = calc.wavg_sum / calc.divisor; - break; - case 2: - mid = calc.avg_sum / calc.cnt; - break; - } - - QString txt = QObject::tr("%1 low usage, %2 no usage, out of %3 days (%4% compliant.) Length: %5 / %6 / %7"). - arg(incompdays).arg(nousedays).arg(totaldays).arg(comp,0,'f',1).arg(calc.min, 0, 'f', 2).arg(mid, 0, 'f', 2).arg(calc.max, 0, 'f', 2);; - graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); - } -} - +/// short SummaryCalcItem::midcalc; void gSessionTimesChart::preCalc() { @@ -1098,230 +359,3 @@ void gSessionTimesChart::paint(QPainter &painter, gGraph &graph, const QRegion & afterDraw(painter, graph, rect); } - -//////////////////////////////////////////////////////////////////////////// -/// Total Time in Apnea Chart Stuff -//////////////////////////////////////////////////////////////////////////// - -void gTTIAChart::preCalc() -{ - gSummaryChart::preCalc(); -} - -void gTTIAChart::customCalc(Day *, QVector & slices) -{ - if (slices.size() == 0) return; - const SummaryChartSlice & slice = slices.at(0); - - calcitems[0].update(slice.value, slice.value); -} - -void gTTIAChart::afterDraw(QPainter &, gGraph &graph, QRectF rect) -{ - QStringList txtlist; - - for (auto & calc : calcitems) { - //ChannelID code = calc.code; - //schema::Channel & chan = schema::channel[code]; - float mid = 0; - switch (midcalc) { - case 0: - if (calc.median_data.size() > 0) { - mid = median(calc.median_data.begin(), calc.median_data.end()); - } - break; - case 1: - if (calc.divisor > 0) { - mid = calc.wavg_sum / calc.divisor; - } - break; - case 2: - if (calc.cnt > 0) { - mid = calc.avg_sum / calc.cnt; - } - break; - } - - txtlist.append(QString("%1 %2 / %3 / %4").arg(QObject::tr("TTIA:")).arg(calc.min, 0, 'f', 2).arg(mid, 0, 'f', 2).arg(calc.max, 0, 'f', 2)); - } - QString txt = txtlist.join(", "); - graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); -} - -void gTTIAChart::populate(Day *day, int idx) -{ - QVector & slices = cache[idx]; -// float ttia = day->sum(CPAP_AllApnea) + day->sum(CPAP_Obstructive) + day->sum(CPAP_ClearAirway) + day->sum(CPAP_Apnea) + day->sum(CPAP_Hypopnea); - float ttia = day->sum(AllAhiChannels); - - int h = ttia / 3600; - int m = int(ttia) / 60 % 60; - int s = int(ttia) % 60; - slices.append(SummaryChartSlice(&calcitems[0], ttia / 60.0, ttia / 60.0, QObject::tr("\nTTIA: %1").arg(QString().sprintf("%02i:%02i:%02i",h,m,s)), QColor(255,147,150))); -} - -QString gTTIAChart::tooltipData(Day *, int idx) -{ - QVector & slices = cache[idx]; - if (slices.size() == 0) return QString(); - - const SummaryChartSlice & slice = slices.at(0); - return slice.name; -} - -//////////////////////////////////////////////////////////////////////////// -/// AHI Chart Stuff -//////////////////////////////////////////////////////////////////////////// -void gAHIChart::preCalc() -{ - gSummaryChart::preCalc(); - - ahi_wavg = 0; - ahi_avg = 0; - total_days = 0; - total_hours = 0; - min_ahi = 99999; - max_ahi = -99999; - - ahi_data.clear(); - ahi_data.reserve(idx_end-idx_start); -} -void gAHIChart::customCalc(Day *day, QVector &list) -{ - int size = list.size(); - if (size == 0) return; - EventDataType hours = day->hours(m_machtype); - EventDataType ahi_cnt = 0; - - for (auto & slice : list) { - SummaryCalcItem * calc = slice.calc; - - EventDataType value = slice.value; - float valh = value/ hours; - - switch (midcalc) { - case 0: - calc->median_data.append(valh); - break; - case 1: - calc->wavg_sum += value; - calc->divisor += hours; - break; - case 2: - calc->avg_sum += value; - calc->cnt++; - break; - } - - calc->min = qMin(valh, calc->min); - calc->max = qMax(valh, calc->max); - - ahi_cnt += value; - } - min_ahi = qMin(ahi_cnt / hours, min_ahi); - max_ahi = qMax(ahi_cnt / hours, max_ahi); - - ahi_data.append(ahi_cnt / hours); - - ahi_wavg += ahi_cnt; - ahi_avg += ahi_cnt; - total_hours += hours; - total_days++; -} -void gAHIChart::afterDraw(QPainter & /*painter */, gGraph &graph, QRectF rect) -{ - if (totaldays == nousedays) return; - - //int size = idx_end - idx_start; - - bool skip = true; - float med = 0; - switch (midcalc) { - case 0: - if (ahi_data.size() > 0) { - med = median(ahi_data.begin(), ahi_data.end()); - skip = false; - } - break; - case 1: // wavg - if (total_hours > 0) { - med = ahi_wavg / total_hours; - skip = false; - } - break; - case 2: // avg - if (total_days > 0) { - med = ahi_avg / total_days; - skip = false; - } - break; - } - - QStringList txtlist; - if (!skip) txtlist.append(QString("%1 %2 / %3 / %4").arg(STR_TR_AHI).arg(min_ahi, 0, 'f', 2).arg(med, 0, 'f', 2).arg(max_ahi, 0, 'f', 2)); - - int i = calcitems.size(); - while (i > 0) { - i--; - ChannelID code = calcitems[i].code; - schema::Channel & chan = schema::channel[code]; - float mid = 0; - skip = true; - switch (midcalc) { - case 0: - if (calcitems[i].median_data.size() > 0) { - mid = median(calcitems[i].median_data.begin(), calcitems[i].median_data.end()); - skip = false; - } - break; - case 1: - if (calcitems[i].divisor > 0) { - mid = calcitems[i].wavg_sum / calcitems[i].divisor; - skip = false; - } - break; - case 2: - if (calcitems[i].cnt > 0) { - mid = calcitems[i].avg_sum / calcitems[i].cnt; - skip = false; - } - break; - } - - if (!skip) txtlist.append(QString("%1 %2 / %3 / %4").arg(chan.label()).arg(calcitems[i].min, 0, 'f', 2).arg(mid, 0, 'f', 2).arg(calcitems[i].max, 0, 'f', 2)); - } - QString txt = txtlist.join(", "); - graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); -} - -void gAHIChart::populate(Day *day, int idx) -{ - QVector & slices = cache[idx]; - - float hours = day->hours(m_machtype); - - for (auto & calc : calcitems) { - ChannelID code = calc.code; - if (!day->hasData(code, ST_CNT)) continue; - - schema::Channel *chan = schema::channel.channels.find(code).value(); - - float c = day->count(code); - slices.append(SummaryChartSlice(&calc, c, c / hours, chan->label(), calc.color)); - } -} -QString gAHIChart::tooltipData(Day *day, int idx) -{ - QVector & slices = cache[idx]; - float total = 0; - float hour = day->hours(m_machtype); - QString txt; - int i = slices.size(); - while (i > 0) { - i--; - total += slices[i].value; - txt += QString("\n%1: %2").arg(slices[i].name).arg(float(slices[i].value) / hour, 0, 'f', 2); - } - return QString("\n%1: %2").arg(STR_TR_AHI).arg(float(total) / hour,0,'f',2)+txt; -} - diff --git a/oscar/Graphs/gSessionTimesChart.h b/oscar/Graphs/gSessionTimesChart.h index 053a1a24..da65457d 100644 --- a/oscar/Graphs/gSessionTimesChart.h +++ b/oscar/Graphs/gSessionTimesChart.h @@ -12,261 +12,9 @@ #include "SleepLib/day.h" #include "SleepLib/profiles.h" -#include "gGraphView.h" - - -struct TimeSpan -{ -public: - TimeSpan():begin(0), end(0) {} - TimeSpan(float b, float e) : begin(b), end(e) {} - TimeSpan(const TimeSpan & copy) { - begin = copy.begin; - end = copy.end; - } - ~TimeSpan() {} - float begin; - float end; -}; - -struct SummaryCalcItem { - SummaryCalcItem() { - code = 0; - type = ST_CNT; - color = Qt::black; - wavg_sum = 0; - avg_sum = 0; - cnt = 0; - divisor = 0; - min = 0; - max = 0; - } - SummaryCalcItem(const SummaryCalcItem & copy) { - code = copy.code; - type = copy.type; - color = copy.color; - - wavg_sum = 0; - avg_sum = 0; - cnt = 0; - divisor = 0; - min = 0; - max = 0; - midcalc = p_profile->general->prefCalcMiddle(); - - } - - SummaryCalcItem(ChannelID code, SummaryType type, QColor color) - :code(code), type(type), color(color) { - } - float mid() - { - float val = 0; - switch (midcalc) { - case 0: - if (median_data.size() > 0) - val = median(median_data.begin(), median_data.end()); - break; - case 1: - if (divisor > 0) - val = wavg_sum / divisor; - break; - case 2: - if (cnt > 0) - val = avg_sum / cnt; - } - return val; - } - - - inline void update(float value, float weight) { - if (midcalc == 0) { - median_data.append(value); - } - - avg_sum += value; - cnt++; - wavg_sum += value * weight; - divisor += weight; - min = qMin(min, value); - max = qMax(max, value); - } - - void reset(int reserve, short mc) { - midcalc = mc; - - wavg_sum = 0; - avg_sum = 0; - divisor = 0; - cnt = 0; - min = 99999; - max = -99999; - median_data.clear(); - if (midcalc == 0) { - median_data.reserve(reserve); - } - } - ChannelID code; - SummaryType type; - QColor color; - - double wavg_sum; - double divisor; - double avg_sum; - int cnt; - EventDataType min; - EventDataType max; - static short midcalc; - - QList median_data; - -}; - -struct SummaryChartSlice { - SummaryChartSlice() { - calc = nullptr; - height = 0; - value = 0; - name = ST_CNT; - color = Qt::black; - } - SummaryChartSlice(const SummaryChartSlice & copy) { - calc = copy.calc; - value = copy.value; - height = copy.height; - name = copy.name; - color = copy.color; -// brush = copy.brush; - } - - SummaryChartSlice(SummaryCalcItem * calc, EventDataType value, EventDataType height, QString name, QColor color) - :calc(calc), value(value), height(height), name(name), color(color) { -// QLinearGradient gradient(0, 0, 1, 0); -// gradient.setCoordinateMode(QGradient::ObjectBoundingMode); -// gradient.setColorAt(0,color); -// gradient.setColorAt(1,brighten(color)); -// brush = QBrush(gradient); - } - SummaryCalcItem * calc; - EventDataType value; - EventDataType height; - QString name; - QColor color; -// QBrush brush; -}; - -class gSummaryChart : public QObject , public Layer -{ - Q_OBJECT; -public: - gSummaryChart(QString label, MachineType machtype); - gSummaryChart(ChannelID code, MachineType machtype); - virtual ~gSummaryChart(); - - //! \brief Renders the graph to the QPainter object - virtual void paint(QPainter &, gGraph &, const QRegion &); - - //! \brief Called whenever data model changes underneath. Day object is not needed here, it's just here for Layer compatability. - virtual void SetDay(Day *day = nullptr); - - //! \brief Returns true if no data was found for this day during SetDay - virtual bool isEmpty() { return m_empty; } - - //! \brief Allows chart to recalculate empty flag. - void reCalculate() {m_empty=false;}; - - virtual void populate(Day *, int idx); - - //! \brief Override to setup custom stuff before main loop - virtual void preCalc(); - - //! \brief Override to call stuff in main loop - virtual void customCalc(Day *, QVector &); - - //! \brief Override to call stuff after draw is complete - virtual void afterDraw(QPainter &, gGraph &, QRectF); - - //! \brief Return any extra data to show beneath the date in the hover over tooltip - virtual QString tooltipData(Day *, int); - - virtual void dataChanged() { - cache.clear(); - } - - virtual int addCalc(ChannelID code, SummaryType type, QColor color); - virtual int addCalc(ChannelID code, SummaryType type); - - virtual Layer * Clone() { - gSummaryChart * sc = new gSummaryChart(m_label, m_machtype); - Layer::CloneInto(sc); - CloneInto(sc); - - // copy this here, because only base summary charts need it - sc->calcitems = calcitems; - - return sc; - } - - void CloneInto(gSummaryChart * layer) { - layer->m_empty = m_empty; - layer->firstday = firstday; - layer->lastday = lastday; - layer->expected_slices = expected_slices; - layer->nousedays = nousedays; - layer->totaldays = totaldays; - layer->peak_value = peak_value; - layer->idx_start = idx_start; - layer->idx_end = idx_end; - layer->cache.clear(); - layer->dayindex = dayindex; - layer->daylist = daylist; - } -signals: - void summaryChartEmpty(gSummaryChart*,qint64,qint64,bool); - -protected: - //! \brief Key was pressed that effects this layer - virtual bool keyPressEvent(QKeyEvent *event, gGraph *graph); - - //! \brief Mouse moved over this layers area (shows the hover-over tooltips here) - virtual bool mouseMoveEvent(QMouseEvent *event, gGraph *graph); - - //! \brief Mouse Button was pressed over this area - virtual bool mousePressEvent(QMouseEvent *event, gGraph *graph); - - //! \brief Mouse Button was released over this area. (jumps to daily view here) - virtual bool mouseReleaseEvent(QMouseEvent *event, gGraph *graph); - - QString m_label; - MachineType m_machtype; - bool m_empty; - bool m_emptyPrev; - int hl_day; - int tz_offset; - float tz_hours; - QDate firstday; - QDate lastday; - - QMap dayindex; - QList daylist; - - QHash > cache; - QVector calcitems; - - int expected_slices; - - int nousedays; - int totaldays; - - EventDataType peak_value; - EventDataType min_value; - - int idx_start; - int idx_end; - - short midcalc; -}; +#include "Graphs/gGraphView.h" +#include "Graphs/gSummaryChart.h" /*! \class gSessionTimesChart @@ -318,123 +66,4 @@ public: }; -class gUsageChart : public gSummaryChart -{ -public: - gUsageChart() - :gSummaryChart("Usage", MT_CPAP) { - addCalc(NoChannel, ST_HOURS, QColor(64,128,255)); - } - virtual ~gUsageChart() {} - - virtual void preCalc(); - virtual void customCalc(Day *, QVector &); - virtual void afterDraw(QPainter &, gGraph &, QRectF); - virtual void populate(Day *day, int idx); - - virtual QString tooltipData(Day * day, int); - - - virtual Layer * Clone() { - gUsageChart * sc = new gUsageChart(); - gSummaryChart::CloneInto(sc); - CloneInto(sc); - return sc; - } - - void CloneInto(gUsageChart * layer) { - layer->incompdays = incompdays; - layer->compliance_threshold = compliance_threshold; - } - -private: - int incompdays; - EventDataType compliance_threshold; -}; - -class gTTIAChart : public gSummaryChart -{ -public: - gTTIAChart() - :gSummaryChart("TTIA", MT_CPAP) { - addCalc(NoChannel, ST_CNT, QColor(255,147,150)); - } - virtual ~gTTIAChart() {} - - virtual void preCalc(); - virtual void customCalc(Day *, QVector &); - virtual void afterDraw(QPainter &, gGraph &, QRectF); - virtual void populate(Day *day, int idx); - virtual QString tooltipData(Day * day, int); - - virtual Layer * Clone() { - gTTIAChart * sc = new gTTIAChart(); - gSummaryChart::CloneInto(sc); - CloneInto(sc); - return sc; - } - - void CloneInto(gTTIAChart * /* layer*/) { - } - -private: -}; - -class gAHIChart : public gSummaryChart -{ -public: - gAHIChart() - :gSummaryChart("AHIChart", MT_CPAP) { - for (int i = 0; i < ahiChannels.size(); i++) - addCalc(ahiChannels.at(i), ST_CPH); - -// addCalc(CPAP_ClearAirway, ST_CPH); -// addCalc(CPAP_AllApnea, ST_CPH); -// addCalc(CPAP_Obstructive, ST_CPH); -// addCalc(CPAP_Apnea, ST_CPH); -// addCalc(CPAP_Hypopnea, ST_CPH); - if (p_profile->general->calculateRDI()) - addCalc(CPAP_RERA, ST_CPH); - } - virtual ~gAHIChart() {} - - virtual void preCalc(); - virtual void customCalc(Day *, QVector &); - virtual void afterDraw(QPainter &, gGraph &, QRectF); - - virtual void populate(Day *, int idx); - - virtual QString tooltipData(Day * day, int); - - virtual Layer * Clone() { - gAHIChart * sc = new gAHIChart(); - gSummaryChart::CloneInto(sc); - CloneInto(sc); - return sc; - } - - void CloneInto(gAHIChart * /* layer */) { -// layer->ahicalc = ahicalc; -// layer->ahi_wavg = ahi_wavg; -// layer->ahi_avg = ahi_avg; -// layer->total_hours = total_hours; -// layer->max_ahi = max_ahi; -// layer->min_ahi = min_ahi; -// layer->total_days = total_days; -// layer->ahi_data = ahi_data; - } - - // SummaryCalcItem ahicalc; - double ahi_wavg; - double ahi_avg; - - double total_hours; - float max_ahi; - float min_ahi; - - int total_days; - QList ahi_data; -}; - - #endif // GSESSIONTIMESCHART_H diff --git a/oscar/Graphs/gSummaryChart.cpp b/oscar/Graphs/gSummaryChart.cpp index 0ca75b35..e4347553 100644 --- a/oscar/Graphs/gSummaryChart.cpp +++ b/oscar/Graphs/gSummaryChart.cpp @@ -1,1299 +1,690 @@ -/* gSummaryChart Implementation +/* gSummaryChart Implementation * - * Copyright (c) 2019-2022 The OSCAR Team + * Copyright (c) 2019-2022 The Oscar Team * Copyright (c) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public * License. See the file COPYING in the main directory of the source code * for more details. */ +#define TEST_MACROS_ENABLEDoff +#include "test_macros.h" + #include #include #include -#include "gYAxis.h" -#include "gSummaryChart.h" - -SummaryChart::SummaryChart(QString label, GraphType type) - : Layer(NoChannel), m_label(label), m_graphtype(type) -{ - m_empty = true; - hl_day = -1; - m_machinetype = MT_CPAP; - - QDateTime d1 = QDateTime::currentDateTime(); - QDateTime d2 = d1; - d1.setTimeSpec(Qt::UTC); - tz_offset = d2.secsTo(d1); - tz_hours = tz_offset / 3600.0; - m_layertype = LT_SummaryChart; -} -SummaryChart::~SummaryChart() -{ -} -void SummaryChart::SetDay(Day * nullday) -{ - Q_UNUSED(nullday) - Day *day = nullptr; - Layer::SetDay(day); - - m_values.clear(); - m_times.clear(); - m_days.clear(); - m_hours.clear(); - m_goodcodes.clear(); - m_miny = 999999999.0F; - m_maxy = -999999999.0F; - m_physmaxy = 0; - m_physminy = 0; - m_minx = 0; - m_maxx = 0; - - - int dn; - EventDataType tmp, tmp2, total; - ChannelID code; - CPAPMode cpapmode = (CPAPMode)(int)p_profile->calcSettingsMax(CPAP_Mode, MT_CPAP, - p_profile->FirstDay(MT_CPAP), p_profile->LastDay(MT_CPAP)); - - - ////////////////////////////////////////////////////////// - // Setup for dealing with different CPAP Pressure types - ////////////////////////////////////////////////////////// - if (m_label == STR_TR_Pressure) { - m_codes.clear(); - m_colors.clear(); - m_type.clear(); - m_typeval.clear(); - - float perc = p_profile->general->prefCalcPercentile() / 100.0; - int mididx = p_profile->general->prefCalcMiddle(); - SummaryType mid; - - if (mididx == 0) { mid = ST_PERC; } - else if (mididx == 1) { mid = ST_WAVG; } - else mid = ST_AVG; - - - if (cpapmode >= MODE_ASV) { - addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN); - addSlice(CPAP_IPAPLo, QColor("light blue"), ST_SETMIN); - addSlice(CPAP_IPAP, QColor("cyan"), mid, 0.5); - addSlice(CPAP_IPAP, QColor("dark cyan"), ST_PERC, perc); - //addSlice(CPAP_IPAP,QColor("light blue"),ST_PERC,0.95); - addSlice(CPAP_IPAPHi, QColor("blue"), ST_SETMAX); - } else if (cpapmode >= MODE_BILEVEL_AUTO_FIXED_PS) { - addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN); - addSlice(CPAP_IPAP, QColor("light cyan"), mid, 0.5); - addSlice(CPAP_IPAP, QColor("light blue"), ST_PERC, perc); - addSlice(CPAP_PSMin, QColor("blue"), ST_SETMIN, perc); - addSlice(CPAP_PSMax, QColor("red"), ST_SETMAX, perc); - - } else if (cpapmode >= MODE_BILEVEL_FIXED) { - addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN); - addSlice(CPAP_EPAP, QColor("light green"), ST_PERC, perc); - addSlice(CPAP_IPAP, QColor("light cyan"), mid, 0.5); - addSlice(CPAP_IPAP, QColor("light blue"), ST_PERC, perc); - addSlice(CPAP_IPAPHi, QColor("blue"), ST_SETMAX); - } else if (cpapmode >= MODE_APAP) { - addSlice(CPAP_PressureMin, QColor("orange"), ST_SETMIN); - addSlice(CPAP_Pressure, QColor("dark green"), mid, 0.5f); - addSlice(CPAP_Pressure, QColor("grey"), ST_PERC, perc); - addSlice(CPAP_PressureMax, QColor("red"), ST_SETMAX); - } else { - addSlice(CPAP_Pressure, QColor("dark green"), ST_SETWAVG); - } - } - - // Initialize goodcodes (which identified which legends are drawn) to all off - m_goodcodes.resize(m_codes.size()); - for (int i = 0; i < m_codes.size(); i++) { - m_goodcodes[i] = false; - } - - m_fday = 0; - qint64 tt, zt; - m_empty = true; - - if (m_graphtype == GT_SESSIONS) { - // No point drawing anything if no real data on record - if (p_profile->countDays(MT_CPAP, p_profile->FirstDay(MT_CPAP), p_profile->LastDay(MT_CPAP)) == 0) { - return; - } - } - - int suboffset; - SummaryType type; - bool first = true; - - // For each day in the main profile daylist - - for (auto d=p_profile->daylist.begin(), dend=p_profile->daylist.end(); d!=dend; ++d) { - Day * day = d.value(); - - // get the timestamp of this day. - tt = QDateTime(d.key(), QTime(0, 0, 0), Qt::UTC).toTime_t(); - - // calculate day number - dn = tt / 86400; - - // to ms since epoch. - tt *= 1000L; - - // update min and max for this timestamp - if (!m_minx || tt < m_minx) { m_minx = tt; } - if (!m_maxx || tt > m_maxx) { m_maxx = tt; } - - total = 0; - bool fnd = false; - - ////////////////////////////////////////////////////////// - // Setup for Sessions Time display chart - ////////////////////////////////////////////////////////// - if (m_graphtype == GT_SESSIONS) { - // Turn all legends on - for (int i = 0; i < m_codes.size(); i++) { - m_goodcodes[i] = true; - } - - // for each day object on record for this date - - // skip any empty or irrelevant day records - if (!day || (day->machine(m_machinetype) == nullptr)) { continue; } - - //int ft = qint64(day->first()) / 1000L; - //ft += tz_offset; // convert to local time - - //int dz2 = ft / 86400; - //dz2 *= 86400; - // ft = first sessions time, rounded back to midnight.. - - // For each session in this day record - for (int s=0, size=day->size(); s < size; s++) { - Session *sess = (*day)[s]; - - if (!sess->enabled()) { continue; } - - // Get session duration - tmp = sess->hours(); - m_values[dn][s] = tmp; - - total += tmp; - - // Get session start timestamp - zt = qint64(sess->first()) / 1000L; - zt += tz_offset; - - // Calculate the starting hour - tmp2 = zt - dn * 86400; - tmp2 /= 3600.0; - - m_times[dn][s] = tmp2; - - // Update min & max Y values - if (first) { - m_miny = tmp2; - m_maxy = tmp2 + tmp; - first = false; - } else { - if (tmp2 < m_miny) { - m_miny = tmp2; - } - - if (tmp2 + tmp > m_maxy) { - m_maxy = tmp2 + tmp; - } - } - } // for each session - - // if total hours for all sessions more than 0, register the day as valid - if (total > 0) { - m_days[dn] = day; - m_hours[dn] = total; - m_empty = false; - } - } else { - ////////////////////////////////////////////////////////////////////////////// - // Data Channel summary charts - ////////////////////////////////////////////////////////////////////////////// - - // For each Channel - for (int j = 0; j < m_codes.size(); j++) { - code = m_codes[j]; - suboffset = 0; - type = m_type[j]; - EventDataType typeval = m_typeval[j]; - - day = d.value(); - - CPAPMode mode = (CPAPMode)(int)day->settings_max(CPAP_Mode); - - // ignore irrelevent day objects - if (day->machine(m_machinetype) == nullptr) { continue; } - - bool hascode = //day->channelHasData(code) || - (type == ST_HOURS) || - (type == ST_SESSIONS) || - day->settingExists(code) || - day->hasData(code, type); - - - if (code == CPAP_Pressure) { - if ((cpapmode > MODE_CPAP) && (mode == MODE_CPAP)) { - hascode = false; - - if ((type == ST_WAVG) || (type == ST_AVG) || ((type == ST_PERC) && (typeval == 0.5))) { - type = ST_SETWAVG; - hascode = true; - } - } else { - type = m_type[j]; - } - } - - //if (code==CPAP_Hypopnea) { // Make sure at least one of the CPAP data gets through with 0 - // hascode=true; - //} - if (hascode) { - m_days[dn] = day; - - switch (type) { - case ST_AVG: - tmp = day->avg(code); - break; - - case ST_SUM: - tmp = day->sum(code); - break; - - case ST_WAVG: - tmp = day->wavg(code); - break; - - case ST_90P: - tmp = day->p90(code); - break; - - case ST_PERC: - tmp = day->percentile(code, typeval); - break; - - case ST_MIN: - tmp = day->Min(code); - break; - - case ST_MAX: - tmp = day->Max(code); - break; - - case ST_CNT: - tmp = day->count(code); - break; - - case ST_CPH: - tmp = day->count(code) / day->hours(m_machinetype); - break; - - case ST_SPH: - tmp = day->sph(code); - break; - - case ST_HOURS: - tmp = day->hours(m_machinetype); - break; - - case ST_SESSIONS: - tmp = day->size(); - break; - - case ST_SETMIN: - tmp = day->settings_min(code); - break; - - case ST_SETMAX: - tmp = day->settings_max(code); - break; - - case ST_SETAVG: - tmp = day->settings_avg(code); - break; - - case ST_SETWAVG: - tmp = day->settings_wavg(code); - break; - - case ST_SETSUM: - tmp = day->settings_sum(code); - break; - - default: - tmp = 0; - break; - } - - if (suboffset > 0) { - tmp -= suboffset; - - if (tmp < 0) { tmp = 0; } - } - - total += tmp; - m_values[dn][j + 1] = tmp; - - if (tmp < m_miny) { m_miny = tmp; } - - if (tmp > m_maxy) { m_maxy = tmp; } - - m_goodcodes[j] = true; - fnd = true; - } - - } - - if (fnd) { - if (!m_fday) { m_fday = dn; } - - m_values[dn][0] = total; - m_hours[dn] = day->hours(m_machinetype); - - if (m_graphtype == GT_BAR) { - if (total < m_miny) { m_miny = total; } - - if (total > m_maxy) { m_maxy = total; } - } - } - } - } - - m_empty = true; - - for (const auto & goodcode : m_goodcodes) { - if (goodcode) { - m_empty = false; - break; - } - } - - if (m_graphtype == GT_BAR) { - m_miny = 0; - } - - // m_minx=qint64(QDateTime(p_profile->FirstDay(),QTime(0,0,0),Qt::UTC).toTime_t())*1000L; - m_maxx = qint64(QDateTime(p_profile->LastDay(), QTime(23, 59, 0), Qt::UTC).toTime_t()) * 1000L; - m_physmaxy = m_maxy; - m_physminy = m_miny; -} - -void SummaryChart::paint(QPainter &painter, gGraph &w, const QRegion ®ion) -{ - int left = region.boundingRect().left(); - int top = region.boundingRect().top(); - int width = region.boundingRect().width(); - int height = region.boundingRect().height(); - - if (!m_visible) { return; } - - GraphType graphtype = m_graphtype; - - if (graphtype == GT_LINE || graphtype == GT_POINTS) { - bool pts = AppSetting->overviewLinechartMode() == OLC_Lines; - graphtype = pts ? GT_POINTS : GT_LINE; - } - - rtop = top; - - painter.setPen(QColor(Qt::black)); - painter.drawLine(left, top, left, top+height); - painter.drawLine(left, top+height, left+width, top+height); - painter.drawLine(left+width, top+height, left+width, top); - painter.drawLine( left+width, top, left, top); - - qint64 minx = w.min_x, maxx = w.max_x; - - int days = ceil(double(maxx-minx) / 86400000.0); - - bool buttuglydaysteps = false ; //!p_profile->appearance->animations(); - - double lcursor = w.graphView()->currentTime(); - if (days >= 1) { - - double b = w.max_x - w.min_x; - double a = lcursor - w.min_x; - double c = a / b; - - if (buttuglydaysteps) { - // this kills the beautiful smooth scrolling and makes days stop on day boundaries :( - minx = floor(double(minx)/86400000.0); - minx *= 86400000L; - - maxx = minx + 86400000L * qint64(days)-1; - } - - b = maxx - minx; - double d = c * b; - lcursor = d + minx; - - - } - - - qint64 xx = maxx - minx; - - - - EventDataType miny = m_physminy; - EventDataType maxy = m_physmaxy; - - w.roundY(miny, maxy); - - EventDataType yy = maxy - miny; - EventDataType ymult = float(height - 2) / yy; - - barw = (float(width) / float(days)); - - // graph = &w; - float px;// = left; - l_left = w.marginLeft() + gYAxis::Margin; - l_top = w.marginTop(); - l_width = width; - l_height = height; - float py; - EventDataType total; - - int daynum = 0; - EventDataType h, tmp, tmp2; - - - l_offset = (minx) % 86400000L; - offset = float(l_offset) / 86400000.0; - - offset *= barw; - px = left - offset; - l_minx = minx; - l_maxx = maxx + 86400000L; - - int total_days = 0; - double total_val = 0; - double total_hours = 0; - bool lastdaygood = false; - QVector totalcounts; - QVector totalvalues; - QVector lastX; - QVector lastY; - int numcodes = m_codes.size(); - totalcounts.resize(numcodes); - totalvalues.resize(numcodes); - lastX.resize(numcodes); - lastY.resize(numcodes); - int zd = minx / 86400000L; - zd--; - auto d = m_values.find(zd); - - QVector goodcodes; - goodcodes.resize(m_goodcodes.size()); - - lastdaygood = true; - - // Display Line Cursor - if (AppSetting->lineCursorMode()) { - qint64 time = lcursor; - double xmult = double(width) / xx; - - if ((time > minx) && (time < maxx)) { - double xpos = (time - minx) * xmult; - painter.setPen(QPen(QBrush(QColor(0,255,0,255)),1)); - painter.drawLine(left+xpos, top-w.marginTop()-3, left+xpos, top+height+w.bottom-1); - } - - - -// QDateTime dt=QDateTime::fromMSecsSinceEpoch(time,Qt::UTC); - -// QString text = dt.date().toString(Qt::SystemLocaleLongDate); - -// int wid, h; - // GetTextExtent(text, wid, h); - // w.renderText(text, left + width/2 - wid/2, top-h+5); - - } - - for (int i = 0; i < numcodes; i++) { - totalcounts[i] = 0; - - // Set min and max to the opposite largest starting value - if ((m_type[i] == ST_MIN) || (m_type[i] == ST_SETMIN)) { - totalvalues[i] = maxy; - } else if ((m_type[i] == ST_MAX) || (m_type[i] == ST_SETMAX)) { - totalvalues[i] = miny; - } else { - totalvalues[i] = 0; - } - - // Turn off legend display.. It will only display if it's turned back on during draw. - goodcodes[i] = false; - - if (!m_goodcodes[i]) { continue; } - - lastX[i] = px; - - if (d != m_values.end() && d.value().contains(i + 1)) { - - tmp = d.value()[i + 1]; - h = tmp * ymult; - } else { - lastdaygood = false; - h = 0; - } - - lastY[i] = top + height - 1 - h; - } - - float compliance_hours = 0; - - if (p_profile->cpap->showComplianceInfo()) { - compliance_hours = p_profile->cpap->complianceHours(); - } - - int incompliant = 0; - Day *day; - EventDataType hours; - - //quint32 * tptr; - //EventStoreType * dptr; - - short px2, py2; - const qint64 ms_per_day = 86400000L; - - - painter.setClipRect(left, top, width, height); - painter.setClipping(true); - - QColor summaryColor = QColor("dark gray"); - - float lineThickness = AppSetting->lineThickness(); - - - for (qint64 Q = minx; Q <= maxx + ms_per_day; Q += ms_per_day) { - zd = Q / ms_per_day; - d = m_values.find(zd); - - if (Q < minx) { - goto jumpnext; - } - - if (d != m_values.end()) { - day = m_days[zd]; - bool summary_only = day && day->summaryOnly(); - - - if (!m_hours.contains(zd)) { - goto jumpnext; - } - - hours = m_hours[zd]; - - int x1 = px; - //x1-=(barw/2.0); - int x2 = px + barw; - - //if (x1 < left) { x1 = left; } - - if (x2 > left + width) { x2 = left + width; } - - // if (x2add(x1 - 1, top, x1 - 1, top + height, x2, top + height, x2, top, col.rgba()); - } else { - painter.fillRect((x1+barw/2)-5, top, barw, height, QBrush(col)); -// quads->add((x1 + barw / 2) - 5, top, (x1 + barw / 2) - 5, top + height, (x2 - barw / 2) + 5, -// top + height, (x2 - barw / 2) + 5, top, col.rgba()); - } - } - - if (graphtype == GT_SESSIONS) { - int j; - auto times = m_times.find(zd); - QColor col = m_colors[0]; - //if (hourssetColor(Qt::black); - - int np = d.value().size(); - - if (np > 0) { - for (auto & goodcode : goodcodes) { - goodcode = true; - } - } - - for (j = 0; j < np; j++) { - tmp2 = times.value()[j] - miny; - py = top + height - (tmp2 * ymult); - - tmp = d.value()[j]; // length - - //tmp-=miny; - h = tmp * ymult; - - QLinearGradient gradient(x1, py-h, x1+barw, py-h); - gradient.setColorAt(0,col1); - gradient.setColorAt(1,col2); - painter.fillRect(x1, py-h, barw, h, QBrush(gradient)); -// quads->add(x1, py, x1, py - h, x2, py - h, x2, py, col1, col2); - - if ((h > 0) && (barw > 2)) { - painter.setPen(QColor(Qt::black)); - painter.drawLine(x1, py, x1, py - h); - painter.drawLine(x1, py - h, x2, py - h); - painter.drawLine(x1, py, x2, py); - painter.drawLine(x2, py, x2, py - h); - } - - totalvalues[0] += hours * tmp; - } - - totalcounts[0] += hours; - totalvalues[1] += j; - totalcounts[1]++; - total_val += hours; - total_hours += hours; - total_days++; - } else { - if (!d.value().contains(0)) { - goto jumpnext; - } - - total = d.value()[0]; - - //if (total>0) { - if (day) { - EventDataType hours = m_hours[zd]; - total_val += total * hours; - total_hours += hours; - total_days++; - } - - py = top + height; - - //} - bool good; - SummaryType type; - - for (auto g=d.value().begin(), dend=d.value().end(); g != dend; g++) { - short j = g.key(); - - if (!j) { continue; } - - j--; - good = m_goodcodes[j]; - - if (!good) { - continue; - } - - type = m_type[j]; - // code was actually used (to signal the display of the legend summary) - goodcodes[j] = good; - - tmp = g.value(); - - QColor col = m_colors[j]; - - if (type == ST_HOURS) { - if (tmp < compliance_hours) { - col = QColor("#f04040"); - incompliant++; - } else if (summary_only) { - col = summaryColor; - } - } - - if (zd == hl_day) { - col = COLOR_Gold; - } - - //if (!tmp) continue; - if ((type == ST_MAX) || (type == ST_SETMAX)) { - if (totalvalues[j] < tmp) { - totalvalues[j] = tmp; - } - } else if ((type == ST_MIN) || (type == ST_SETMIN)) { - if (totalvalues[j] > tmp) { - totalvalues[j] = tmp; - } - } else { - totalvalues[j] += tmp * hours; - } - - //if (tmp) { - totalcounts[j] += hours; - //} - tmp -= miny; - h = tmp * ymult; // height in pixels - - if (graphtype == GT_BAR) { - QColor col1 = col; - QColor col2 = brighten(col,2.5); - - QLinearGradient gradient(x1, py-h, x1+barw, py-h); - gradient.setColorAt(0,col1); - gradient.setColorAt(1,col2); - painter.fillRect(x1, py-h, barw, h, QBrush(gradient)); - -// quads->add(x1, py, x1, py - h, col1); -// quads->add(x2, py - h, x2, py, col2); - - if (h > 0 && barw > 2) { - painter.setPen(QColor(Qt::black)); - painter.drawLine(x1, py, x1, py - h); - painter.drawLine(x1, py - h, x2, py - h); - painter.drawLine(x1, py, x2, py); - painter.drawLine(x2, py, x2, py - h); - } // if (bar - - py -= h; - } else if (graphtype == GT_LINE) { // if (m_graphtype==GT_BAR - QColor col1 = col; - QColor col2 = m_colors[j]; - px2 = px + barw; - py2 = (top + height - 1) - h; - - // If more than 1 day between records, skip the vertical crud. - if ((px2 - lastX[j]) > barw + 1) { - lastdaygood = false; - } - - if (lastdaygood) { - if (lastY[j] != py2) { // vertical line - painter.setPen(QPen(col2, lineThickness)); - painter.drawLine(lastX[j], lastY[j], px, py2); - } - - painter.setPen(QPen(col1, lineThickness)); - painter.drawLine(px, py2, px2, py2); - } else { - painter.setPen(QPen(col1, lineThickness)); - painter.drawLine(x1, py2, x2, py2); - } - - lastX[j] = px2; - lastY[j] = py2; - } else if (graphtype == GT_POINTS) { - QColor col1 = col; - QColor col2 = m_colors[j]; - px2 = px + barw; - py2 = (top + height - 2) - h; - - // If more than 1 day between records, skip the vertical crud. - if ((px2 - lastX[j]) > barw + 1) { - lastdaygood = false; - } - - if (zd == hl_day) { - painter.setPen(QPen(brighten(col2),10)); - painter.drawPoint(px2 - barw / 2, py2); - } - - if (lastdaygood) { - painter.setPen(QPen(col2, lineThickness)); - painter.drawLine(lastX[j] - barw / 2, lastY[j], px2 - barw / 2, py2); - } else { - painter.setPen(QPen(col1, lineThickness)); - painter.drawLine(px + barw / 2 - 1, py2, px + barw / 2 + 1, py2); - } - - lastX[j] = px2; - lastY[j] = py2; - } - } // for(QHashmaxx+extra) break; - } else { - if (Q < maxx) { - incompliant++; - } - - lastdaygood = false; - } - -jumpnext: - - if (px >= left + width + barw) { - break; - } - - px += barw; - - daynum++; - //lastQ=Q; - } - painter.setClipping(false); - - // Draw Ledgend - px = left + width - 3; - py = top - 5; - int legendx = px; - QString a, b; - int x, y; - - QFontMetrics fm(*defaultfont); - int bw = fm.width('X'); - int bh = fm.height() / 1.8; - - bool ishours = false; - int good = 0; - - for (int j = 0; j < m_codes.size(); j++) { - if (!goodcodes[j]) { continue; } - - good++; - SummaryType type = m_type[j]; - ChannelID code = m_codes[j]; - EventDataType tval = m_typeval[j]; - - switch (type) { - case ST_WAVG: - b = "Avg"; - break; - - case ST_AVG: - b = "Avg"; - break; - - case ST_90P: - b = "90%"; - break; - - case ST_PERC: - if (tval >= 0.99) { b = STR_TR_Max; } - else if (tval == 0.5) { b = STR_TR_Med; } - else { b = QString("%1%").arg(tval * 100.0, 0, 'f', 0); } - - break; - - //b=QString("%1%").arg(tval*100.0,0,'f',0); break; - case ST_MIN: - b = STR_TR_Min; - break; - - case ST_MAX: - b = STR_TR_Max; - break; - - case ST_SETMIN: - b = STR_TR_Min; - break; - - case ST_SETMAX: - b = STR_TR_Max; - break; - - case ST_CPH: - b = ""; - break; - - case ST_SPH: - b = "%"; - break; - - case ST_HOURS: - b = STR_UNIT_Hours; - break; - - case ST_SESSIONS: - b = STR_TR_Sessions; - break; - - default: - b = ""; - break; - } - - a = schema::channel[code].label(); - - if (a == w.title() && !b.isEmpty()) { a = b; } - else { a += " " + b; } - - QString val; - float f = 0; - - if (totalcounts[j] > 0) { - if ((type == ST_MIN) || (type == ST_MAX) || (type == ST_SETMIN) || (type == ST_SETMAX)) { - f = totalvalues[j]; - } else { - f = totalvalues[j] / totalcounts[j]; - } - } - - if (type == ST_HOURS) { - int h = f; - int m = int(f * 60) % 60; - val.sprintf("%02i:%02i", h, m); - ishours = true; - } else { - val = QString::number(f, 'f', 2); - } - - a += ": " + val; - //GetTextExtent(a,x,y); - //float wt=20*w.printScaleX(); - //px-=wt+x; - //w.renderText(a,px+wt,py+1); - //quads->add(px+wt-y/4-y,py-y,px+wt-y/4,py-y,px+wt-y/4,py+1,px+wt-y/4-y,py+1,m_colors[j].rgba()); - - - //QString text=schema::channel[code].label(); - - int wid, hi; - GetTextExtent(a, wid, hi); - legendx -= wid; - w.renderText(a, legendx, top - 4); - // legendx-=bw/2; - - painter.fillRect(legendx - bw-4, top-w.marginTop()-1, bh, w.marginTop(), QBrush(m_colors[j])); - legendx -= bw * 2; - - - //lines->add(px,py,px+20,py,m_colors[j]); - //lines->add(px,py+1,px+20,py+1,m_colors[j]); - } - - if ((m_graphtype == GT_BAR) && (good > 0)) { - - if (m_type.size() > 1) { - float val = total_val / float(total_hours); - a = m_label + ": " + QString::number(val, 'f', 2) + " "; - GetTextExtent(a, x, y); - legendx -= x; - w.renderText(a, legendx, py + 1); - } - } - - a = ""; - /*if (m_graphtype==GT_BAR) { - if (m_type.size()>1) { - float val=total_val/float(total_days); - a+=m_label+": "+QString::number(val,'f',2)+" "; - // - } - }*/ - a += QString(QObject::tr("Days: %1")).arg(total_days, 0); - - if (p_profile->cpap->showComplianceInfo()) { - if (ishours && incompliant > 0) { - a += " "+QString(QObject::tr("Low Usage Days: %1")).arg(incompliant, 0)+ - " "+QString(QObject::tr("(%1% compliant, defined as > %2 hours)")). - arg((1.0 / daynum) * (total_days - incompliant) * 100.0, 0, 'f', 2).arg(compliance_hours, 0, 'f', 1); - } - } - - - //GetTextExtent(a,x,y); - //legendx-=30+x; - //w.renderText(a,px+24,py+5); - w.renderText(a, left, py + 1); -} - -QString formatTime(EventDataType v, bool show_seconds = false, bool duration = false, - bool show_12hr = false) -{ - int h = int(v); - - if (!duration) { - h %= 24; - } else { show_12hr = false; } - - int m = int(v * 60) % 60; - int s = int(v * 3600) % 60; - - char pm[3] = {"am"}; - - if (show_12hr) { - h >= 12 ? pm[0] = 'p' : pm[0] = 'a'; - h %= 12; - - if (h == 0) { h = 12; } - - } else { - pm[0] = 0; - } - - if (show_seconds) { - return QString().sprintf("%i:%02i:%02i%s", h, m, s, pm); - } else { - return QString().sprintf("%i:%02i%s", h, m, pm); - } -} - -bool SummaryChart::mouseMoveEvent(QMouseEvent *event, gGraph *graph) -{ - graph->timedRedraw(0); - int xposLeft = event->x(); - int yPosTop = event->y(); - - if (!m_rect.contains(xposLeft, yPosTop)) { - // if ((x<0 || y<0 || x>l_width || y>l_height)) { - hl_day = -1; - //graph->timedRedraw(2000); - return false; - } - - xposLeft -= m_rect.left(); - yPosTop -= m_rect.top(); - - Q_UNUSED(yPosTop) - - double xx = l_maxx - l_minx; - - double xmult = xx / double(l_width + barw); - - qint64 mx = ceil(xmult * double(xposLeft - offset)); - mx += l_minx; - mx = mx + l_offset; //-86400000L; - int zd = mx / 86400000L; - - Day *day; - //if (hl_day!=zd) // This line is an optimization - - { - hl_day = zd; - graph->Trigger(2000); - - auto d = m_values.find(hl_day); - - QMap &valhash = d.value(); - - xposLeft += m_rect.left(); //gYAxis::Margin+gGraphView::titleWidth; //graph->m_marginleft+ - int y = event->y() - m_rect.top() + rtop - 15; - //QDateTime dt1=QDateTime::fromTime_t(hl_day*86400).toLocalTime(); - QDateTime dt2 = QDateTime::fromTime_t(hl_day * 86400).toUTC(); -// QDateTime dt2 = QDateTime::fromTime_t(hl_day * 86400).toLocalTime(); - - //QTime t1=dt1.time(); - //QTime t2=dt2.time(); - - QDate dt = dt2.date(); - day = m_days[zd]; - - if ((d != m_values.end()) && (day != nullptr)) { - bool summary_only = day->summaryOnly(); - - QString strTooltip = dt.toString(Qt::SystemLocaleShortDate); - - // Day * day=m_days[hl_day]; - //EventDataType val; - QString val; - - if (m_graphtype == GT_SESSIONS) { - if (m_type[0] == ST_HOURS) { - - int t = m_hours[zd] * 3600.0; - int h = t / 3600; - int m = (t / 60) % 60; - //int s=t % 60; - val.sprintf("%02i:%02i", h, m); - } else { - val = QString::number(d.value()[0], 'f', 2); - } - - strTooltip += "\r\n" + m_label + ": " + val; - - if (m_type[1] == ST_SESSIONS) { - strTooltip += " "+QString(QObject::tr("(Sess: %1)")).arg(day->size(), 0); - } - - EventDataType v = m_times[zd][0]; - int lastt = m_times[zd].size() - 1; - - if (lastt < 0) { lastt = 0; } - - strTooltip += "\r\n"+QString(QObject::tr("Bedtime: %1")).arg(formatTime(v, false, false, true)); - v = m_times[zd][lastt] + m_values[zd][lastt]; - strTooltip += "\r\n"+QString(QObject::tr("Waketime: %1")).arg(formatTime(v, false, false, true)); - - } else if (m_graphtype == GT_BAR) { - if (m_type[0] == ST_HOURS) { - int t = d.value()[0] * 3600.0; - int h = t / 3600; - int m = (t / 60) % 60; - //int s=t % 60; - val.sprintf("%02i:%02i", h, m); - } else { - val = QString::number(d.value()[0], 'f', 2); - } - - strTooltip += "\r\n" + m_label + ": " + val; - //z+="\r\nMode="+QString::number(day->settings_min("FlexSet"),'f',0); - - } else { - QString strDataType; - - for (int i = 0; i < m_type.size(); i++) { - if (!m_goodcodes[i]) { - continue; - } - - if (!valhash.contains(i + 1)) { - continue; - } - - EventDataType tval = m_typeval[i]; - - switch (m_type[i]) { - case ST_WAVG: - strDataType = STR_TR_WAvg; - break; - - case ST_AVG: - strDataType = STR_TR_Avg; - break; - - case ST_90P: - strDataType = QString("90%"); - break; - - case ST_PERC: - if (tval >= 0.99) { strDataType = STR_TR_Max; } - else if (tval == 0.5) { strDataType = STR_TR_Med; } - else { strDataType = QString("%1%").arg(tval * 100.0, 0, 'f', 0); } - - break; - - case ST_MIN: - strDataType = STR_TR_Min; - break; - - case ST_MAX: - strDataType = STR_TR_Max; - break; - - case ST_CPH: - strDataType = ""; - break; - - case ST_SPH: - strDataType = "%"; - break; - - case ST_HOURS: - strDataType = STR_UNIT_Hours; - break; - - case ST_SESSIONS: - strDataType = STR_TR_Sessions; - break; - - case ST_SETMIN: - strDataType = STR_TR_Min; - break; - - case ST_SETMAX: - strDataType = STR_TR_Max; - break; - - default: - strDataType = ""; - break; - } - - if (m_type[i] == ST_SESSIONS) { - val = QString::number(d.value()[i + 1], 'f', 0); - strTooltip += "\r\n" + strDataType + ": " + val; - } else { - //if (day && (day->channelExists(m_codes[i]) || day->settingExists(m_codes[i]))) { - schema::Channel &chan = schema::channel[m_codes[i]]; - EventDataType v; - - if (valhash.contains(i + 1)) { - v = valhash[i + 1]; - } else { v = 0; } - - if (m_codes[i] == Journal_Weight) { - val = weightString(v, p_profile->general->unitSystem()); - } else { - val = QString::number(v, 'f', 2); - } - - strTooltip += "\r\n" + chan.label() + " " + strDataType + ": " + val; - //} - } - } - - } - if (summary_only) { - strTooltip += "\r\n"+QObject::tr("(Summary Only)"); - } - - graph->ToolTip(strTooltip, xposLeft, y - 15); - return false; - } else { - QString z = dt.toString(Qt::SystemLocaleShortDate) + "\r\n"+QObject::tr("No Data"); - graph->ToolTip(z, xposLeft, y - 15); - return false; - } - } - return false; -} - -bool SummaryChart::mousePressEvent(QMouseEvent *event, gGraph *graph) -{ - if (event->modifiers() & Qt::ShiftModifier) { - //qDebug() << "Jump to daily view?"; - return true; - } - - Q_UNUSED(graph) - return false; -} - -bool SummaryChart::keyPressEvent(QKeyEvent *event, gGraph *graph) -{ - Q_UNUSED(event) - Q_UNUSED(graph) - //qDebug() << "Summarychart Keypress"; - return false; -} #include "mainwindow.h" -extern MainWindow *mainwin; -bool SummaryChart::mouseReleaseEvent(QMouseEvent *event, gGraph *graph) -{ - if (event->modifiers() & Qt::ShiftModifier) { - if (hl_day < 0) { - mouseMoveEvent(event, graph); - } +#include "SleepLib/profiles.h" +#include "SleepLib/machine_common.h" +#include "gSummaryChart.h" - if (hl_day > 0) { - QDateTime d = QDateTime::fromTime_t(hl_day * 86400).toUTC(); -// QDateTime d = QDateTime::fromTime_t(hl_day * 86400).toLocalTime(); - mainwin->getDaily()->LoadDate(d.date()); - mainwin->JumpDaily(); - //qDebug() << "Jump to daily view?" << d; - return true; - } +#include "gYAxis.h" + +extern MainWindow * mainwin; + +short SummaryCalcItem::midcalc; + +gSummaryChart::gSummaryChart(QString label, MachineType machtype) + :Layer(NoChannel), m_label(label), m_machtype(machtype) +{ + m_layertype = LT_Overview; + QDateTime d1 = QDateTime::currentDateTime(); + QDateTime d2 = d1; + d1.setTimeSpec(Qt::UTC); // CHECK: Does this deal with DST? + tz_offset = d2.secsTo(d1); + tz_hours = tz_offset / 3600.0; + expected_slices = 5; + + idx_end = 0; + idx_start = 0; +} + +gSummaryChart::gSummaryChart(ChannelID code, MachineType machtype) + :Layer(code), m_machtype(machtype) +{ + m_layertype = LT_Overview; + QDateTime d1 = QDateTime::currentDateTime(); + QDateTime d2 = d1; + d1.setTimeSpec(Qt::UTC); // CHECK: Does this deal with DST? + tz_offset = d2.secsTo(d1); + tz_hours = tz_offset / 3600.0; + expected_slices = 5; + + addCalc(code, ST_MIN, brighten(schema::channel[code].defaultColor() ,0.60f)); + addCalc(code, ST_MID, brighten(schema::channel[code].defaultColor() ,1.20f)); + addCalc(code, ST_90P, brighten(schema::channel[code].defaultColor() ,1.70f)); + addCalc(code, ST_MAX, brighten(schema::channel[code].defaultColor() ,2.30f)); + + idx_end = 0; + idx_start = 0; +} + +gSummaryChart::~gSummaryChart() +{ +} + +void gSummaryChart::SetDay(Day *unused_day) +{ + cache.clear(); + + Q_UNUSED(unused_day) + Layer::SetDay(nullptr); + + if (m_machtype != MT_CPAP) { + // Channels' machine types are not terribly reliable: oximetry channels can be reported by a CPAP, + // and position channels can be reported by an oximeter. So look for any days with data. + firstday = p_profile->FirstDay(); + lastday = p_profile->LastDay(); + } else { + // But CPAP channels (like pressure settings) can only be reported by a CPAP. + firstday = p_profile->FirstDay(m_machtype); + lastday = p_profile->LastDay(m_machtype); } + dayindex.clear(); + daylist.clear(); + + if (!firstday.isValid() || !lastday.isValid()) return; + // daylist.reserve(firstday.daysTo(lastday)+1); + QDate date = firstday; + int idx = 0; + do { + auto di = p_profile->daylist.find(date); + Day * day = nullptr; + if (di != p_profile->daylist.end()) { + day = di.value(); + } + daylist.append(day); + dayindex[date] = idx; + idx++; + date = date.addDays(1); + } while (date <= lastday); + + m_minx = QDateTime(firstday, QTime(0,0,0), Qt::LocalTime).toMSecsSinceEpoch(); + m_maxx = QDateTime(lastday, QTime(23,59,59), Qt::LocalTime).toMSecsSinceEpoch(); + m_miny = 0; + m_maxy = 20; + + m_empty = false; + m_emptyPrev = true; + +} + + +int gSummaryChart::addCalc(ChannelID code, SummaryType type, QColor color) +{ + calcitems.append(SummaryCalcItem(code, type, color)); + return calcitems.size() - 1; // return the index of the newly appended calc +} + +int gSummaryChart::addCalc(ChannelID code, SummaryType type) +{ + return addCalc(code, type, schema::channel[code].defaultColor()); +} + + +bool gSummaryChart::keyPressEvent(QKeyEvent *event, gGraph *graph) +{ Q_UNUSED(event) - hl_day = -1; - graph->timedRedraw(2000); + Q_UNUSED(graph) return false; } +bool gSummaryChart::mouseMoveEvent(QMouseEvent *event, gGraph *graph) +{ + Q_UNUSED(event) + Q_UNUSED(graph) + return false; +} + +bool gSummaryChart::mousePressEvent(QMouseEvent *event, gGraph *graph) +{ + Q_UNUSED(event) + Q_UNUSED(graph) + return false; +} + +bool gSummaryChart::mouseReleaseEvent(QMouseEvent *event, gGraph *graph) +{ + if (!(event->modifiers() & Qt::ShiftModifier)) return false; + + float x = event->x() - m_rect.left(); + float y = event->y() - m_rect.top(); + qDebug() << x << y; + + EventDataType miny; + EventDataType maxy; + + graph->roundY(miny, maxy); + + QDate date = QDateTime::fromMSecsSinceEpoch(m_minx, Qt::LocalTime).date(); + + int days = ceil(double(m_maxx - m_minx) / 86400000.0); + + float barw = float(m_rect.width()) / float(days); + + float idx = x/barw; + + date = date.addDays(idx); + + auto it = dayindex.find(date); + if (it != dayindex.end()) { + Day * day = daylist.at(it.value()); + if (day) { + mainwin->getDaily()->LoadDate(date); + mainwin->JumpDaily(); + } + } + + return true; +} + +void gSummaryChart::preCalc() +{ + midcalc = p_profile->general->prefCalcMiddle(); + + for (auto & calc : calcitems) { + calc.reset(idx_end - idx_start, midcalc); + } +} + +void gSummaryChart::customCalc(Day *day, QVector & slices) +{ + int size = slices.size(); + if (size != calcitems.size()) { + return; + } + float hour = day->hours(m_machtype); + + for (int i=0; i < size; ++i) { + const SummaryChartSlice & slice = slices.at(i); + SummaryCalcItem & calc = calcitems[i]; + + calc.update(slice.value, hour); + } +} + +void gSummaryChart::afterDraw(QPainter &painter, gGraph &graph, QRectF rect) +{ + if (totaldays == nousedays) return; + + if (calcitems.size() == 0) return; + + QStringList strlist; + QString txt; + + int midcalc = p_profile->general->prefCalcMiddle(); + QString midstr; + if (midcalc == 0) { + midstr = QObject::tr("Med."); + } else if (midcalc == 1) { + midstr = QObject::tr("W-Avg"); + } else { + midstr = QObject::tr("Avg"); + } + + + float perc = p_profile->general->prefCalcPercentile(); + QString percstr = QString("%1%").arg(perc, 0, 'f',0); + + schema::Channel & chan = schema::channel[calcitems.at(0).code]; + + for (auto & calc : calcitems) { + + if (calcitems.size() == 1) { + float val = calc.min; + if (val < 99998) + strlist.append(QObject::tr("Min: %1").arg(val,0,'f',2)); + } + + float mid = 0; + switch (midcalc) { + case 0: + if (calc.median_data.size() > 0) { + mid = median(calc.median_data.begin(), calc.median_data.end()); + } + break; + case 1: + mid = calc.wavg_sum / calc.divisor; + break; + case 2: + mid = calc.avg_sum / calc.cnt; + break; + } + + float val = 0; + switch (calc.type) { + case ST_CPH: + val = mid; + txt = midstr+": "; + break; + case ST_SPH: + val = mid; + txt = midstr+": "; + break; + case ST_MIN: + val = calc.min; + if (val >= 99998) continue; + txt = QObject::tr("Min: "); + break; + case ST_MAX: + val = calc.max; + if (val <= -99998) continue; + txt = QObject::tr("Max: "); + break; + case ST_SETMIN: + val = calc.min; + if (val >= 99998) continue; + txt = QObject::tr("Min: "); + break; + case ST_SETMAX: + val = calc.max; + if (val <= -99998) continue; + txt = QObject::tr("Max: "); + break; + case ST_MID: + val = mid; + txt = QString("%1: ").arg(midstr); + break; + case ST_90P: + val = mid; + txt = QString("%1: ").arg(percstr); + break; + default: + val = mid; + txt = QString("???: "); + break; + } + strlist.append(QString("%1%2").arg(txt).arg(val,0,'f',2)); + if (calcitems.size() == 1) { + val = calc.max; + if (val > -99998) + strlist.append(QObject::tr("Max: %1").arg(val,0,'f',2)); + } + } + + QString str; + if (totaldays > 1) { + str = QObject::tr("%1 (%2 days): ").arg(chan.fullname()).arg(totaldays); + } else { + str = QObject::tr("%1 (%2 day): ").arg(chan.fullname()).arg(totaldays); + } + str += " "+strlist.join(", "); + + QRectF rec(rect.left(), rect.top(), 0,0); + painter.setFont(*defaultfont); + rec = painter.boundingRect(rec, Qt::AlignTop, str); + rec.moveBottom(rect.top()-3*graph.printScaleY()); + painter.drawText(rec, Qt::AlignTop, str); + +// graph.renderText(str, rect.left(), rect.top()-5*graph.printScaleY(), 0); + + +} + +QString gSummaryChart::tooltipData(Day *, int idx) +{ + QString txt; + const auto & slices = cache[idx]; + int i = slices.size(); + while (i > 0) { + i--; + txt += QString("\n%1: %2").arg(slices[i].name).arg(float(slices[i].value), 0, 'f', 2); + } + return txt; +} + +void gSummaryChart::populate(Day * day, int idx) +{ + + bool good = false; + for (const auto & item : calcitems) { + if (day->hasData(item.code, item.type)) { + good = true; + break; + } + } + if (!good) return; + + auto & slices = cache[idx]; + + float hours = day->hours(m_machtype); + if ((hours==0) && (m_machtype != MT_CPAP)) hours = day->hours(); + float base = 0; + + for (auto & item : calcitems) { + ChannelID code = item.code; + schema::Channel & chan = schema::channel[code]; + float value = 0; + QString name; + QColor color; + switch (item.type) { + case ST_CPH: + value = day->count(code) / hours; + name = chan.label(); + color = item.color; + slices.append(SummaryChartSlice(&item, value, value, name, color)); + break; + case ST_SPH: + value = (100.0 / hours) * (day->sum(code) / 3600.0); + name = QObject::tr("% in %1").arg(chan.label()); + color = item.color; + slices.append(SummaryChartSlice(&item, value, value, name, color)); + break; + case ST_HOURS: + value = hours; + name = QObject::tr("Hours"); + color = COLOR_LightBlue; + slices.append(SummaryChartSlice(&item, value, hours, name, color)); + break; + case ST_MIN: + value = day->Min(code); + name = QObject::tr("Min %1").arg(chan.label()); + color = item.color; + slices.append(SummaryChartSlice(&item, value, value - base, name, color)); + base = value; + break; + case ST_MID: + value = day->calcMiddle(code); + name = day->calcMiddleLabel(code); + color = item.color; + slices.append(SummaryChartSlice(&item, value, value - base, name, color)); + base = value; + break; + case ST_90P: + value = day->calcPercentile(code); + name = day->calcPercentileLabel(code); + color = item.color; + slices.append(SummaryChartSlice(&item, value, value - base, name, color)); + base = value; + break; + case ST_MAX: + value = day->calcMax(code); + name = day->calcMaxLabel(code); + color = item.color; + slices.append(SummaryChartSlice(&item, value, value - base, name, color)); + base = value; + break; + default: + break; + } + } +} + +void gSummaryChart::paint(QPainter &painter, gGraph &graph, const QRegion ®ion) +{ + QRectF rect = region.boundingRect(); + + rect.translate(0.0f, 0.001f); + + painter.setPen(QColor(Qt::black)); + painter.drawRect(rect); + + rect.moveBottom(rect.bottom()+1); + + m_minx = graph.min_x; + m_maxx = graph.max_x; + + QDateTime date2 = QDateTime::fromMSecsSinceEpoch(m_minx, Qt::LocalTime); + QDateTime enddate2 = QDateTime::fromMSecsSinceEpoch(m_maxx, Qt::LocalTime); + + QDate date = date2.date(); + QDate enddate = enddate2.date(); + + int days = ceil(double(m_maxx - m_minx) / 86400000.0); + + //float lasty1 = rect.bottom(); + + auto it = dayindex.find(date); + idx_start = 0; + if (it == dayindex.end()) { + it = dayindex.begin(); + } else { + idx_start = it.value(); + } + + int idx = idx_start; + + // Determine how many days after the first day of the chart that this data is to begin + int numDaysOffset = 0; + if (firstday > date) { // date = beginning date of chart; firstday = beginning date of data + numDaysOffset = date.daysTo(firstday); + } + + // Determine how many days before the last day of the chart that this data is to end + int numDaysAfter = 0; + if (enddate > lastday) { // enddate = last date of chart; lastday = last date of data + numDaysAfter = lastday.daysTo(enddate); + } + if (numDaysAfter > days) // Nothing to do if this data is off the left edge of the chart + return; + + auto ite = dayindex.find(enddate); + idx_end = daylist.size()-1; + if (ite != dayindex.end()) { + idx_end = ite.value(); + } + + QPoint mouse = graph.graphView()->currentMousePos(); + + nousedays = 0; + totaldays = 0; + + QRectF hl_rect; + QDate hl_date; + Day * hl_day = nullptr; + int hl_idx = -1; + bool hl = false; + + if ((daylist.size() == 0) || (it == dayindex.end())) + return; + + //Day * lastday = nullptr; + + // int dc = 0; +// for (int i=idx; i<=idx_end; ++i) { +// Day * day = daylist.at(i); +// if (day || lastday) { +// dc++; +// } +// lastday = day; +// } +// days = dc; +// lastday = nullptr; + float barw = float(rect.width()) / float(days); + + QString hl2_text = ""; + + QVector outlines; + int size = idx_end - idx; + outlines.reserve(size * expected_slices); + + // Virtual call to setup any custom graph stuff + preCalc(); + + float lastx1 = rect.left(); + lastx1 += numDaysOffset * barw; + float right_edge = (rect.left()+rect.width()+1); + + + ///////////////////////////////////////////////////////////////////// + /// Calculate Graph Peaks + ///////////////////////////////////////////////////////////////////// + peak_value = 0; + for (int i=idx; i <= idx_end; ++i, lastx1 += barw) { + Day * day = daylist.at(i); + + if ((lastx1 + barw) > right_edge) + break; + + if (!day) { + continue; + } + + day->OpenSummary(); + + auto cit = cache.find(i); + + if (cit == cache.end()) { + populate(day, i); + cit = cache.find(i); + } + + if (cit != cache.end()) { + float base = 0, val; + for (const auto & slice : cit.value()) { + val = slice.height; + base += val; + } + peak_value = qMax(peak_value, base); + } + } + m_miny = 0; + m_maxy = ceil(peak_value); + + ///////////////////////////////////////////////////////////////////// + /// Y-Axis scaling + ///////////////////////////////////////////////////////////////////// + + EventDataType miny; + EventDataType maxy; + + graph.roundY(miny, maxy); + float ymult = float(rect.height()) / (maxy-miny); + + lastx1 = rect.left(); + lastx1 += numDaysOffset * barw; + + ///////////////////////////////////////////////////////////////////// + /// Main drawing loop + ///////////////////////////////////////////////////////////////////// + do { + Day * day = daylist.at(idx); + + if ((lastx1 + barw) > right_edge) + break; + + totaldays++; + + if (!day) + { + // lasty1 = rect.bottom(); + lastx1 += barw; + it++; + nousedays++; + //lastday = day; + continue; + } + + //lastday = day; + + float x1 = lastx1 + barw; + + day->OpenSummary(); + QRectF hl2_rect; + + bool hlday = false; + QRectF rec2(lastx1, rect.top(), barw, rect.height()); + if (rec2.contains(mouse)) { + hl_rect = rec2; + hl_day = day; + hl_date = it.key(); + hl_idx = idx; + + hl = true; + hlday = true; + } + + auto cit = cache.find(idx); + + if (cit == cache.end()) { + populate(day, idx); + cit = cache.find(idx); + } + + float lastval = 0, val, y1,y2; + if (cit != cache.end()) { + ///////////////////////////////////////////////////////////////////////////////////// + /// Draw pressure settings + ///////////////////////////////////////////////////////////////////////////////////// + QVector & list = cit.value(); + customCalc(day, list); + + QLinearGradient gradient(lastx1, 0, lastx1 + barw, 0); //rect.bottom(), barw, rect.bottom()); + + for (const auto & slice : list) { + val = slice.height; + y1 = ((lastval-miny) * ymult); + y2 = (val * ymult); + QColor color = slice.color; + + QRectF rec = QRectF(lastx1, rect.bottom() - y1, barw, -y2).intersected(rect); + + if (hlday) { + if (rec.contains(mouse.x(), mouse.y())) { + color = Qt::yellow; + hl2_rect = rec; + } + } + + if (barw <= 3) { + painter.fillRect(rec, QBrush(color)); + } else if (barw > 8) { + gradient.setColorAt(0,color); + gradient.setColorAt(1,brighten(color, 2.0)); + painter.fillRect(rec, QBrush(gradient)); +// painter.fillRect(rec, slice.brush); + outlines.append(rec); + } else { + painter.fillRect(rec, brighten(color, 1.25)); + outlines.append(rec); + } + + lastval += val; + } + } + + lastx1 = x1; + it++; + } while (++idx <= idx_end); + painter.setPen(QPen(Qt::black,1)); + painter.drawRects(outlines); + + if (hl) { + QColor col2(255,0,0,64); + painter.fillRect(hl_rect, QBrush(col2)); + + QString txt = hl_date.toString(Qt::SystemLocaleShortDate)+" "; + if (hl_day) { + // grab extra tooltip data + txt += tooltipData(hl_day, hl_idx); + if (!hl2_text.isEmpty()) { + QColor col = Qt::yellow; + col.setAlpha(255); + // painter.fillRect(hl2_rect, QBrush(col)); + txt += hl2_text; + } + } + + graph.ToolTip(txt, mouse.x()-15, mouse.y()+5, TT_AlignRight); + } + try { + afterDraw(painter, graph, rect); + } catch(...) { + qDebug() << "Bad median call in" << m_label; + } + + + // This could be turning off graphs prematurely.. + if (cache.size() == 0) { + m_empty = true; + m_emptyPrev = true; + graph.graphView()->updateScale(); + emit summaryChartEmpty(this,m_minx,m_maxx,true); + } else if (m_emptyPrev) { + m_emptyPrev = false; + emit summaryChartEmpty(this,m_minx,m_maxx,false); + } + +} diff --git a/oscar/Graphs/gSummaryChart.h b/oscar/Graphs/gSummaryChart.h index 4745f476..35203bb2 100644 --- a/oscar/Graphs/gSummaryChart.h +++ b/oscar/Graphs/gSummaryChart.h @@ -1,142 +1,231 @@ -/* gSummaryChart Header +/* gSessionTimesChart Header * - * Copyright (c) 2019-2022 The OSCAR Team + * Copyright (c) 2019-2022 The Oscar Team * Copyright (C) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public * License. See the file COPYING in the main directory of the source code * for more details. */ +#if 1 +#ifndef GSUMMARYCHART_H +#define GSUMMARYCHART_H -#ifndef GBARCHART_H -#define GBARCHART_H - -#include -#include "gGraphView.h" -#include "gXAxis.h" - -/*! \enum GraphType - \value GT_BAR Display as a BarGraph - \value GT_LINE Display as a line plot - \value GT_SESSIONS Display type for session times chart - */ -enum GraphType { GT_BAR, GT_LINE, GT_POINTS, GT_SESSIONS }; +#include "SleepLib/day.h" +#include "SleepLib/profiles.h" +#include "Graphs/gGraphView.h" +#include "SleepLib/appsettings.h" -/*! \class SummaryChart - \brief The main overall chart type layer used in Overview page - */ -class SummaryChart: public Layer +struct TimeSpan { - public: - //! \brief Constructs a SummaryChart with QString label, of GraphType type - SummaryChart(QString label, GraphType type = GT_BAR); - virtual ~SummaryChart(); +public: + TimeSpan():begin(0), end(0) {} + TimeSpan(float b, float e) : begin(b), end(e) {} + TimeSpan(const TimeSpan & copy) { + begin = copy.begin; + end = copy.end; + } + ~TimeSpan() {} + float begin; + float end; +}; + +struct SummaryCalcItem { + SummaryCalcItem() { + code = 0; + type = ST_CNT; + color = Qt::black; + wavg_sum = 0; + avg_sum = 0; + cnt = 0; + divisor = 0; + min = 0; + max = 0; + } + SummaryCalcItem(const SummaryCalcItem & copy) { + code = copy.code; + type = copy.type; + color = copy.color; + + wavg_sum = 0; + avg_sum = 0; + cnt = 0; + divisor = 0; + min = 0; + max = 0; + midcalc = p_profile->general->prefCalcMiddle(); + + } + + SummaryCalcItem(ChannelID code, SummaryType type, QColor color) + :code(code), type(type), color(color) { + } + float mid() + { + float val = 0; + switch (midcalc) { + case 0: + if (median_data.size() > 0) + val = median(median_data.begin(), median_data.end()); + break; + case 1: + if (divisor > 0) + val = wavg_sum / divisor; + break; + case 2: + if (cnt > 0) + val = avg_sum / cnt; + } + return val; + } + + + inline void update(float value, float weight) { + if (midcalc == 0) { + median_data.append(value); + } + + avg_sum += value; + cnt++; + wavg_sum += value * weight; + divisor += weight; + min = qMin(min, value); + max = qMax(max, value); + } + + void reset(int reserve, short mc) { + midcalc = mc; + + wavg_sum = 0; + avg_sum = 0; + divisor = 0; + cnt = 0; + min = 99999; + max = -99999; + median_data.clear(); + if (midcalc == 0) { + median_data.reserve(reserve); + } + } + ChannelID code; + SummaryType type; + QColor color; + + double wavg_sum; + double divisor; + double avg_sum; + int cnt; + EventDataType min; + EventDataType max; + static short midcalc; + + QList median_data; + +}; + +struct SummaryChartSlice { + SummaryChartSlice() { + calc = nullptr; + height = 0; + value = 0; + name = ST_CNT; + color = Qt::black; + } + SummaryChartSlice(const SummaryChartSlice & copy) { + calc = copy.calc; + value = copy.value; + height = copy.height; + name = copy.name; + color = copy.color; +// brush = copy.brush; + } + + SummaryChartSlice(SummaryCalcItem * calc, EventDataType value, EventDataType height, QString name, QColor color) + :calc(calc), value(value), height(height), name(name), color(color) { +// QLinearGradient gradient(0, 0, 1, 0); +// gradient.setCoordinateMode(QGradient::ObjectBoundingMode); +// gradient.setColorAt(0,color); +// gradient.setColorAt(1,brighten(color)); +// brush = QBrush(gradient); + } + SummaryCalcItem * calc; + EventDataType value; + EventDataType height; + QString name; + QColor color; +// QBrush brush; +}; + +class gSummaryChart : public QObject , public Layer +{ + Q_OBJECT; +public: + gSummaryChart(QString label, MachineType machtype); + gSummaryChart(ChannelID code, MachineType machtype); + virtual ~gSummaryChart(); //! \brief Renders the graph to the QPainter object - virtual void paint(QPainter &painter, gGraph &w, const QRegion ®ion); + virtual void paint(QPainter &, gGraph &, const QRegion &); - //! \brief Precalculation code prior to drawing. Day object is not needed here, it's just here for Layer compatability. + //! \brief Called whenever data model changes underneath. Day object is not needed here, it's just here for Layer compatability. virtual void SetDay(Day *day = nullptr); //! \brief Returns true if no data was found for this day during SetDay virtual bool isEmpty() { return m_empty; } - //! \brief Adds a layer to the summaryChart (When in Bar mode, it becomes culminative, eg, the AHI chart) - void addSlice(ChannelID code, QColor color, SummaryType type, EventDataType tval = 0.00f) { - m_codes.push_back(code); - m_colors.push_back(color); - m_type.push_back(type); - //m_zeros.push_back(ignore_zeros); - m_typeval.push_back(tval); + //! \brief Allows chart to recalculate empty flag. + void reCalculate() {m_empty=false;}; + + virtual void populate(Day *, int idx); + + //! \brief Override to setup custom stuff before main loop + virtual void preCalc(); + + //! \brief Override to call stuff in main loop + virtual void customCalc(Day *, QVector &); + + //! \brief Override to call stuff after draw is complete + virtual void afterDraw(QPainter &, gGraph &, QRectF); + + //! \brief Return any extra data to show beneath the date in the hover over tooltip + virtual QString tooltipData(Day *, int); + + virtual void dataChanged() { + cache.clear(); } - //! \brief Deselect highlighting (the gold bar) - virtual void deselect() { - hl_day = -1; - } - - //! \brief Returns true if currently selected.. - virtual bool isSelected() { return hl_day >= 0; } - - - //! \brief Sets the MachineType this SummaryChart is interested in - void setMachineType(MachineType type) { m_machinetype = type; } - - //! \brief Returns the MachineType this SummaryChart is interested in - MachineType machineType() { return m_machinetype; } + virtual int addCalc(ChannelID code, SummaryType type, QColor color); + virtual int addCalc(ChannelID code, SummaryType type); virtual Layer * Clone() { - SummaryChart * sc = new SummaryChart(m_label); + gSummaryChart * sc = new gSummaryChart(m_label, m_machtype); Layer::CloneInto(sc); CloneInto(sc); + + // copy this here, because only base summary charts need it + sc->calcitems = calcitems; + return sc; } - void CloneInto(SummaryChart * layer) { - layer->m_orientation = m_orientation; - layer->m_colors = m_colors; - layer->m_codes = m_codes; - layer->m_goodcodes = m_goodcodes; - layer->m_type = m_type; - layer->m_typeval = m_typeval; - layer->m_values = m_values; - layer->m_times = m_times; - layer->m_hours = m_hours; - layer->m_days = m_days; - + void CloneInto(gSummaryChart * layer) { layer->m_empty = m_empty; - layer->m_fday = m_fday; - layer->m_label = m_label; - layer->barw = barw; - layer->l_offset = l_offset; - layer->offset = offset; - layer->l_left = l_left; - layer->l_top = l_top; - layer->l_width= l_width; - layer->l_height = l_height; - layer->rtop = rtop; - layer->l_minx = l_minx; - layer->l_maxx = l_maxx; - layer->hl_day = hl_day; - layer->m_graphtype = m_graphtype; - layer->m_machinetype = m_machinetype; - layer->tz_offset = tz_offset; - layer->tz_hours = tz_hours; - + layer->firstday = firstday; + layer->lastday = lastday; + layer->expected_slices = expected_slices; + layer->nousedays = nousedays; + layer->totaldays = totaldays; + layer->peak_value = peak_value; + layer->idx_start = idx_start; + layer->idx_end = idx_end; + layer->cache.clear(); + layer->dayindex = dayindex; + layer->daylist = daylist; } +signals: + void summaryChartEmpty(gSummaryChart*,qint64,qint64,bool); - - protected: - Qt::Orientation m_orientation; - - QVector m_colors; - QVector m_codes; - QVector m_goodcodes; - //QVector m_zeros; - QVector m_type; - QVector m_typeval; - QHash > m_values; - QHash > m_times; - QHash m_hours; - QHash m_days; - - bool m_empty; - int m_fday; - QString m_label; - - float barw; // bar width from last draw - qint64 l_offset; // last offset - float offset; // in pixels; - int l_left, l_top, l_width, l_height; - int rtop; - qint64 l_minx, l_maxx; - int hl_day; - //gGraph *graph; - GraphType m_graphtype; - MachineType m_machinetype; - int tz_offset; - float tz_hours; - +protected: //! \brief Key was pressed that effects this layer virtual bool keyPressEvent(QKeyEvent *event, gGraph *graph); @@ -148,7 +237,39 @@ class SummaryChart: public Layer //! \brief Mouse Button was released over this area. (jumps to daily view here) virtual bool mouseReleaseEvent(QMouseEvent *event, gGraph *graph); + + QString m_label; + MachineType m_machtype; + bool m_empty; + bool m_emptyPrev; + int hl_day; + int tz_offset; + float tz_hours; + QDate firstday; + QDate lastday; + + QMap dayindex; + QList daylist; + + QHash > cache; + QVector calcitems; + + int expected_slices; + + int nousedays; + int totaldays; + + EventDataType peak_value; + EventDataType min_value; + + int idx_start; + int idx_end; + + short midcalc; }; -#endif // GBARCHART_H + +#endif // GSUMMARYCHART_H + +#endif diff --git a/oscar/Graphs/gTTIAChart.cpp b/oscar/Graphs/gTTIAChart.cpp new file mode 100644 index 00000000..8bd6ec15 --- /dev/null +++ b/oscar/Graphs/gTTIAChart.cpp @@ -0,0 +1,98 @@ +/* gTTIAChart Implementation + * + * Copyright (c) 2019-2022 The Oscar Team + * Copyright (c) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#define TEST_MACROS_ENABLEDoff +#include "test_macros.h" + +#include +#include +#include + +#include "mainwindow.h" +#include "SleepLib/profiles.h" +#include "SleepLib/machine_common.h" +#include "gTTIAChart.h" + +#include "gYAxis.h" + +extern MainWindow * mainwin; + +// short SummaryCalcItem::midcalc; + +//////////////////////////////////////////////////////////////////////////// +/// Total Time in Apnea Chart Stuff +//////////////////////////////////////////////////////////////////////////// + +void gTTIAChart::preCalc() +{ + gSummaryChart::preCalc(); +} + +void gTTIAChart::customCalc(Day *, QVector & slices) +{ + if (slices.size() == 0) return; + const SummaryChartSlice & slice = slices.at(0); + + calcitems[0].update(slice.value, slice.value); +} + +void gTTIAChart::afterDraw(QPainter &, gGraph &graph, QRectF rect) +{ + QStringList txtlist; + + for (auto & calc : calcitems) { + //ChannelID code = calc.code; + //schema::Channel & chan = schema::channel[code]; + float mid = 0; + switch (midcalc) { + case 0: + if (calc.median_data.size() > 0) { + mid = median(calc.median_data.begin(), calc.median_data.end()); + } + break; + case 1: + if (calc.divisor > 0) { + mid = calc.wavg_sum / calc.divisor; + } + break; + case 2: + if (calc.cnt > 0) { + mid = calc.avg_sum / calc.cnt; + } + break; + } + + txtlist.append(QString("%1 %2 / %3 / %4").arg(QObject::tr("TTIA:")).arg(calc.min, 0, 'f', 2).arg(mid, 0, 'f', 2).arg(calc.max, 0, 'f', 2)); + } + QString txt = txtlist.join(", "); + graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); +} + +void gTTIAChart::populate(Day *day, int idx) +{ + QVector & slices = cache[idx]; +// float ttia = day->sum(CPAP_AllApnea) + day->sum(CPAP_Obstructive) + day->sum(CPAP_ClearAirway) + day->sum(CPAP_Apnea) + day->sum(CPAP_Hypopnea); + float ttia = day->sum(AllAhiChannels); + + int h = ttia / 3600; + int m = int(ttia) / 60 % 60; + int s = int(ttia) % 60; + slices.append(SummaryChartSlice(&calcitems[0], ttia / 60.0, ttia / 60.0, QObject::tr("\nTTIA: %1").arg(QString().sprintf("%02i:%02i:%02i",h,m,s)), QColor(255,147,150))); +} + +QString gTTIAChart::tooltipData(Day *, int idx) +{ + QVector & slices = cache[idx]; + if (slices.size() == 0) return QString(); + + const SummaryChartSlice & slice = slices.at(0); + return slice.name; +} + + diff --git a/oscar/Graphs/gTTIAChart.h b/oscar/Graphs/gTTIAChart.h new file mode 100644 index 00000000..43ddbe30 --- /dev/null +++ b/oscar/Graphs/gTTIAChart.h @@ -0,0 +1,46 @@ +/* gTTIAChart Header + * + * Copyright (c) 2019-2022 The Oscar Team + * Copyright (C) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#ifndef GTTIACHART_H +#define GTTIACHART_H + +#include "SleepLib/day.h" +#include "SleepLib/profiles.h" +#include "Graphs/gGraphView.h" +#include "Graphs/gSummaryChart.h" + + +class gTTIAChart : public gSummaryChart +{ +public: + gTTIAChart() + :gSummaryChart("TTIA", MT_CPAP) { + addCalc(NoChannel, ST_CNT, QColor(255,147,150)); + } + virtual ~gTTIAChart() {} + + virtual void preCalc(); + virtual void customCalc(Day *, QVector &); + virtual void afterDraw(QPainter &, gGraph &, QRectF); + virtual void populate(Day *day, int idx); + virtual QString tooltipData(Day * day, int); + + virtual Layer * Clone() { + gTTIAChart * sc = new gTTIAChart(); + gSummaryChart::CloneInto(sc); + CloneInto(sc); + return sc; + } + + void CloneInto(gTTIAChart * /* layer*/) { + } + +private: +}; +#endif // GTTIACHART_H diff --git a/oscar/Graphs/gUsageChart.cpp b/oscar/Graphs/gUsageChart.cpp new file mode 100644 index 00000000..95eb6562 --- /dev/null +++ b/oscar/Graphs/gUsageChart.cpp @@ -0,0 +1,102 @@ +/* gUsageChart Implementation + * + * Copyright (c) 2019-2022 The Oscar Team + * Copyright (c) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ +#if 1 +#define TEST_MACROS_ENABLEDoff +#include "test_macros.h" + +#include +#include +#include + +#include "mainwindow.h" +#include "SleepLib/profiles.h" +#include "SleepLib/machine_common.h" +#include "gUsageChart.h" + +#include "gYAxis.h" + +extern MainWindow * mainwin; + +// short SummaryCalcItem::midcalc; + +QString gUsageChart::tooltipData(Day * day, int) +{ + return QObject::tr("\nHours: %1").arg(day->hours(m_machtype), 0, 'f', 2); +} + +void gUsageChart::populate(Day *day, int idx) +{ + QVector & slices = cache[idx]; + + float hours = day->hours(m_machtype); + + QColor cpapcolor = day->summaryOnly() ? QColor(128,128,128) : calcitems[0].color; + bool haveoxi = day->hasMachine(MT_OXIMETER); + + QColor goodcolor = haveoxi ? QColor(128,255,196) : cpapcolor; + + QColor color = (hours < compliance_threshold) ? QColor(255,64,64) : goodcolor; + slices.append(SummaryChartSlice(&calcitems[0], hours, hours, QObject::tr("Hours"), color)); +} + +void gUsageChart::preCalc() +{ + midcalc = p_profile->general->prefCalcMiddle(); + + compliance_threshold = p_profile->cpap->complianceHours(); + incompdays = 0; + + SummaryCalcItem & calc = calcitems[0]; + calc.reset(idx_end - idx_start, midcalc); +} + +void gUsageChart::customCalc(Day *, QVector &list) +{ + if (list.size() == 0) { + incompdays++; + return; + } + + SummaryChartSlice & slice = list[0]; + SummaryCalcItem & calc = calcitems[0]; + + if (slice.value < compliance_threshold) incompdays++; + + calc.update(slice.value, 1); +} + +void gUsageChart::afterDraw(QPainter &, gGraph &graph, QRectF rect) +{ + if (totaldays == nousedays) return; + + if (totaldays > 1) { + float comp = 100.0 - ((float(incompdays + nousedays) / float(totaldays)) * 100.0); + + int midcalc = p_profile->general->prefCalcMiddle(); + float mid = 0; + SummaryCalcItem & calc = calcitems[0]; + switch (midcalc) { + case 0: // median + mid = median(calc.median_data.begin(), calc.median_data.end()); + break; + case 1: // w-avg + mid = calc.wavg_sum / calc.divisor; + break; + case 2: + mid = calc.avg_sum / calc.cnt; + break; + } + + QString txt = QObject::tr("%1 low usage, %2 no usage, out of %3 days (%4% compliant.) Length: %5 / %6 / %7"). + arg(incompdays).arg(nousedays).arg(totaldays).arg(comp,0,'f',1).arg(calc.min, 0, 'f', 2).arg(mid, 0, 'f', 2).arg(calc.max, 0, 'f', 2);; + graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); + } +} + +#endif diff --git a/oscar/Graphs/gUsageChart.h b/oscar/Graphs/gUsageChart.h new file mode 100644 index 00000000..c2401b5f --- /dev/null +++ b/oscar/Graphs/gUsageChart.h @@ -0,0 +1,54 @@ +/* gUsageChart Header + * + * Copyright (c) 2019-2022 The Oscar Team + * Copyright (C) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ +#if 1 +#ifndef GUSAGECHART_H +#define GUSAGECHART_H + +#include "SleepLib/day.h" +#include "SleepLib/profiles.h" +#include "Graphs/gGraphView.h" +#include "Graphs/gSummaryChart.h" + + +class gUsageChart : public gSummaryChart +{ +public: + gUsageChart() + :gSummaryChart("Usage", MT_CPAP) { + addCalc(NoChannel, ST_HOURS, QColor(64,128,255)); + } + virtual ~gUsageChart() {} + + virtual void preCalc(); + virtual void customCalc(Day *, QVector &); + virtual void afterDraw(QPainter &, gGraph &, QRectF); + virtual void populate(Day *day, int idx); + + virtual QString tooltipData(Day * day, int); + + + virtual Layer * Clone() { + gUsageChart * sc = new gUsageChart(); + gSummaryChart::CloneInto(sc); + CloneInto(sc); + return sc; + } + + void CloneInto(gUsageChart * layer) { + layer->incompdays = incompdays; + layer->compliance_threshold = compliance_threshold; + } + +private: + int incompdays; + EventDataType compliance_threshold; +}; + +#endif // GUSAGECHART_H +#endif diff --git a/oscar/SleepLib/appsettings.cpp b/oscar/SleepLib/appsettings.cpp index 721007e1..26af4018 100644 --- a/oscar/SleepLib/appsettings.cpp +++ b/oscar/SleepLib/appsettings.cpp @@ -34,7 +34,9 @@ AppWideSetting::AppWideSetting(Preferences *pref) : PrefSettings(pref) m_graphTooltips = initPref(STR_AS_GraphTooltips, true).toBool(); m_usePixmapCaching = initPref(STR_AS_UsePixmapCaching, false).toBool(); m_odt = (OverlayDisplayType)initPref(STR_AS_OverlayType, (int)ODT_Bars).toInt(); +#ifndef REMOVE_FITNESS m_olm = (OverviewLinechartModes)initPref(STR_AS_OverviewLinechartMode, (int)OLC_Bartop).toInt(); +#endif m_lineThickness=initPref(STR_AS_LineThickness, 1.0).toFloat(); m_lineCursorMode = initPref(STR_AS_LineCursorMode, true).toBool(); initPref(STR_AS_RightSidebarVisible, false); diff --git a/oscar/SleepLib/appsettings.h b/oscar/SleepLib/appsettings.h index 23b343c7..b6f75e6c 100644 --- a/oscar/SleepLib/appsettings.h +++ b/oscar/SleepLib/appsettings.h @@ -18,7 +18,12 @@ class Preferences; +#define REMOVE_FITNESS +/* valid values are REMOVE_FITNESS or REMOVE_FITNESS_OFF */ + +#ifndef REMOVE_FITNESS enum OverviewLinechartModes { OLC_Bartop, OLC_Lines }; +#endif // ApplicationWideSettings Strings @@ -33,7 +38,9 @@ const QString STR_AS_ShowPieChart = "EnablePieChart"; const QString STR_AS_Animations = "AnimationsAndTransitions"; const QString STR_AS_SquareWave = "SquareWavePlots"; const QString STR_AS_OverlayType = "OverlayType"; +#ifndef REMOVE_FITNESS const QString STR_AS_OverviewLinechartMode = "OverviewLinechartMode"; +#endif const QString STR_AS_UsePixmapCaching = "UsePixmapCaching"; const QString STR_AS_AllowYAxisScaling = "AllowYAxisScaling"; const QString STR_AS_IncludeSerial = "IncludeSerial"; @@ -82,7 +89,9 @@ public: float m_lineThickness; OverlayDisplayType m_odt; +#ifndef REMOVE_FITNESS OverviewLinechartModes m_olm; +#endif QString m_profileName, m_language; QString versionString() const { return getPref(STR_PREF_VersionString).toString(); } @@ -137,8 +146,10 @@ public: bool rightSidebarVisible() const { return getPref(STR_AS_RightSidebarVisible).toBool(); } //! \brief Returns the type of overlay flags (which are displayed over the Flow Waveform) inline OverlayDisplayType overlayType() const { return m_odt; } +#ifndef REMOVE_FITNESS //! \brief Returns the display type of Overview pages linechart inline OverviewLinechartModes overviewLinechartMode() const { return m_olm; } +#endif bool userEventPieChart() const { return getPref(STR_CS_UserEventPieChart).toBool(); } bool showSerialNumbers() const { return getPref(STR_US_ShowSerialNumbers).toBool(); } int openTabAtStart() const { return getPref(STR_US_OpenTabAtStart).toInt(); } @@ -187,7 +198,9 @@ public: //! \brief Sets whether to allow double clicking on Y-Axis labels to change vertical scaling mode void setGraphTooltips(bool b) { setPref(STR_AS_GraphTooltips, m_graphTooltips=b); } //! \brief Sets the type of overlay flags (which are displayed over the Flow Waveform) +#ifndef REMOVE_FITNESS void setOverviewLinechartMode(OverviewLinechartModes olm) { setPref(STR_AS_OverviewLinechartMode, (int)(m_olm=olm)); } +#endif //! \brief Set the pen width of line plots. void setLineThickness(float size) { setPref(STR_AS_LineThickness, m_lineThickness=size); } //! \brief Sets whether to display Line Cursor diff --git a/oscar/SleepLib/profiles.cpp b/oscar/SleepLib/profiles.cpp index 95a1947a..64217a31 100644 --- a/oscar/SleepLib/profiles.cpp +++ b/oscar/SleepLib/profiles.cpp @@ -1087,7 +1087,11 @@ int CleanupProfile(Profile *prof) << STR_AS_AntiAliasing << STR_AS_LineThickness << STR_AS_UsePixmapCaching << STR_AS_SquareWave << STR_AS_RightPanelWidth << STR_US_TooltipTimeout << STR_AS_Animations << STR_AS_AllowYAxisScaling << STR_AS_GraphTooltips - << STR_CS_UserEventPieChart << STR_AS_OverlayType << STR_AS_OverviewLinechartMode; + << STR_CS_UserEventPieChart << STR_AS_OverlayType + #ifndef REMOVE_FITNESS + << STR_AS_OverviewLinechartMode + #endif + ; int cnt = 0; for (auto & prf :migrateList) { diff --git a/oscar/daily.cpp b/oscar/daily.cpp index 3c401cd4..488727fe 100644 --- a/oscar/daily.cpp +++ b/oscar/daily.cpp @@ -136,7 +136,6 @@ Daily::Daily(QWidget *parent,gGraphView * shared) // Remove Incomplete Extras Tab //ui->tabWidget->removeTab(3); - ZombieMeterMoved=false; BookmarksChanged=false; lastcpapday=nullptr; @@ -503,6 +502,9 @@ Daily::Daily(QWidget *parent,gGraphView * shared) ui->splitter->setVisible(false); +#ifndef REMOVE_FITNESS + ZombieMeterMoved=false; + if (p_profile->general->unitSystem()==US_English) { ui->weightSpinBox->setSuffix(STR_UNIT_POUND); ui->weightSpinBox->setDecimals(0); @@ -513,6 +515,12 @@ Daily::Daily(QWidget *parent,gGraphView * shared) ui->weightSpinBox->setDecimals(1); ui->weightSpinBox->setSuffix(STR_UNIT_KG); } +#else // REMOVE_FITNESS + // hide the parent widget to make the gridlayout invisible + QWidget *myparent ; + myparent = ui->gridLayout->parentWidget(); + if(myparent) myparent->hide(); +#endif GraphView->setEmptyImage(QPixmap(":/icons/logo-md.png")); GraphView->setEmptyText(STR_Empty_NoData); @@ -926,9 +934,12 @@ void Daily::on_ReloadDay() // GraphView->fadeOut(); Unload(previous_date); } + unload_time=time.restart(); //bool fadedir=previous_date < ui->calendar->selectedDate(); +#ifndef REMOVE_FITNESS ZombieMeterMoved=false; +#endif Load(ui->calendar->selectedDate()); load_time=time.restart(); @@ -937,6 +948,9 @@ void Daily::on_ReloadDay() ui->calButton->setText(ui->calendar->selectedDate().toString(MedDateFormat)); ui->calendar->setFocus(Qt::ActiveWindowFocusReason); +#ifndef REMOVE_FITNESS + ZombieMeterMoved=false; + if (p_profile->general->unitSystem()==US_English) { ui->weightSpinBox->setSuffix(STR_UNIT_POUND); ui->weightSpinBox->setDecimals(0); @@ -947,6 +961,7 @@ void Daily::on_ReloadDay() ui->weightSpinBox->setDecimals(1); ui->weightSpinBox->setSuffix(STR_UNIT_KG); } +#endif this->setCursor(Qt::ArrowCursor); other_time=time.restart(); @@ -1882,6 +1897,7 @@ void Daily::Load(QDate date) //sl.append(tr("Starts")); //sl.append(tr("Notes")); ui->bookmarkTable->setHorizontalHeaderLabels(sl); +#ifndef REMOVE_FITNESS ui->ZombieMeter->blockSignals(true); ui->weightSpinBox->blockSignals(true); ui->ouncesSpinBox->blockSignals(true); @@ -1895,16 +1911,18 @@ void Daily::Load(QDate date) ui->BMI->display(0); ui->BMI->setVisible(false); ui->BMIlabel->setVisible(false); +#endif ui->toggleGraphs->setVisible(false); ui->toggleEvents->setVisible(false); BookmarksChanged=false; Session *journal=GetJournalSession(date); if (journal) { - bool ok; if (journal->settings.contains(Journal_Notes)) ui->JournalNotes->setHtml(journal->settings[Journal_Notes].toString()); +#ifndef REMOVE_FITNESS + bool ok; if (journal->settings.contains(Journal_Weight)) { double kg=journal->settings[Journal_Weight].toDouble(&ok); @@ -1947,6 +1965,7 @@ void Daily::Load(QDate date) ui->ZombieMeter->setValue(journal->settings[Journal_ZombieMeter].toDouble(&ok)); ui->ZombieMeter->blockSignals(false); } +#endif if (journal->settings.contains(Bookmark_Start)) { QVariantList start=journal->settings[Bookmark_Start].toList(); @@ -1984,6 +2003,7 @@ void Daily::Load(QDate date) void Daily::UnitsChanged() { +#ifndef REMOVE_FITNESS double kg; if (p_profile->general->unitSystem()==US_English) { kg=ui->weightSpinBox->value(); @@ -2005,6 +2025,7 @@ void Daily::UnitsChanged() ui->ouncesSpinBox->setVisible(false); ui->weightSpinBox->setSuffix(STR_UNIT_KG); } +#endif } void Daily::clearLastDay() @@ -2491,6 +2512,7 @@ void Daily::on_removeBookmarkButton_clicked() } mainwin->updateFavourites(); } +#ifndef REMOVE_FITNESS void Daily::on_ZombieMeter_valueChanged(int action) { Q_UNUSED(action); @@ -2503,6 +2525,7 @@ void Daily::on_ZombieMeter_valueChanged(int action) journal->SetChanged(true); mainwin->updateOverview(); } +#endif void Daily::on_bookmarkTable_itemChanged(QTableWidgetItem *item) { @@ -2510,6 +2533,7 @@ void Daily::on_bookmarkTable_itemChanged(QTableWidgetItem *item) update_Bookmarks(); } +#ifndef REMOVE_FITNESS void Daily::on_weightSpinBox_valueChanged(double arg1) { // This is called if up/down arrows are used, in which case editingFinished is @@ -2596,6 +2620,7 @@ void Daily::on_ouncesSpinBox_editingFinished() // This is functionally identical to the weightSpinBox_editingFinished, so just call that this->on_weightSpinBox_editingFinished(); } +#endif QString Daily::GetDetailsText() { diff --git a/oscar/daily.h b/oscar/daily.h index 315ae0e0..71198971 100644 --- a/oscar/daily.h +++ b/oscar/daily.h @@ -10,7 +10,6 @@ #ifndef DAILY_H #define DAILY_H - #include #include #include @@ -25,12 +24,12 @@ #include "SleepLib/profiles.h" #include "mainwindow.h" -#include "Graphs/gSummaryChart.h" #include "Graphs/gGraphView.h" #include "Graphs/gLineChart.h" #include "sessionbar.h" #include "mytextbrowser.h" + namespace Ui { class Daily; } @@ -230,7 +229,12 @@ private slots: */ void on_bookmarkTable_itemChanged(QTableWidgetItem *item); + void on_graphCombo_activated(int index); + void on_toggleGraphs_clicked(bool checked); + + +#ifndef REMOVE_FITNESS /*! \fn on_ouncesSpinBox_valueChanged(int arg1); \brief Called when the zombie slider has been moved.. Updates the BMI dislpay and journal objects. @@ -238,10 +242,6 @@ private slots: */ void on_ZombieMeter_valueChanged(int value); - void on_graphCombo_activated(int index); - - void on_toggleGraphs_clicked(bool checked); - /*! \fn on_weightSpinBox_editingFinished(); \brief Called when weight has changed.. Updates the BMI dislpay and journal objects. @@ -260,6 +260,7 @@ private slots: void on_ouncesSpinBox_valueChanged(int arg1); void on_weightSpinBox_valueChanged(double arg1); +#endif void doToggleSession(Session *); @@ -354,7 +355,9 @@ private: gLineChart *leakchart; +#ifndef REMOVE_FITNESS bool ZombieMeterMoved; +#endif bool BookmarksChanged; }; diff --git a/oscar/oscar.pro b/oscar/oscar.pro index 38a7718a..2f908712 100644 --- a/oscar/oscar.pro +++ b/oscar/oscar.pro @@ -280,6 +280,12 @@ SOURCES += \ Graphs/gspacer.cpp \ Graphs/gStatsLine.cpp \ Graphs/gSummaryChart.cpp \ + Graphs/gAHIChart.cpp \ + Graphs/gTTIAChart.cpp \ + Graphs/gUsageChart.cpp \ + Graphs/gSessionTimesChart.cpp \ + Graphs/gPressureChart.cpp \ + Graphs/gOverviewGraph.cpp \ Graphs/gXAxis.cpp \ Graphs/gYAxis.cpp \ Graphs/layer.cpp \ @@ -323,8 +329,6 @@ SOURCES += \ SleepLib/xmlreplay.cpp \ SleepLib/serialoximeter.cpp \ SleepLib/loader_plugins/md300w1_loader.cpp \ - Graphs/gSessionTimesChart.cpp \ - Graphs/gPressureChart.cpp \ logger.cpp \ SleepLib/machine_common.cpp \ SleepLib/loader_plugins/weinmann_loader.cpp \ @@ -382,6 +386,12 @@ HEADERS += \ Graphs/gspacer.h \ Graphs/gStatsLine.h \ Graphs/gSummaryChart.h \ + Graphs/gAHIChart.h \ + Graphs/gTTIAChart.h \ + Graphs/gUsageChart.h \ + Graphs/gSessionTimesChart.h \ + Graphs/gPressureChart.h \ + Graphs/gOverviewGraph.h \ Graphs/gXAxis.h \ Graphs/gYAxis.h \ Graphs/layer.h \ @@ -428,8 +438,6 @@ HEADERS += \ SleepLib/xmlreplay.h \ SleepLib/serialoximeter.h \ SleepLib/loader_plugins/md300w1_loader.h \ - Graphs/gSessionTimesChart.h \ - Graphs/gPressureChart.h \ logger.h \ SleepLib/loader_plugins/weinmann_loader.h \ Graphs/gdailysummary.h \ diff --git a/oscar/overview.cpp b/oscar/overview.cpp index e650c27e..a2e6907a 100644 --- a/oscar/overview.cpp +++ b/oscar/overview.cpp @@ -29,7 +29,11 @@ #include "Graphs/gXAxis.h" #include "Graphs/gLineChart.h" #include "Graphs/gYAxis.h" +#include "Graphs/gSessionTimesChart.h" #include "Graphs/gPressureChart.h" +#include "Graphs/gAHIChart.h" +#include "Graphs/gUsageChart.h" +#include "Graphs/gTTIAChart.h" #include "cprogressbar.h" #include "mainwindow.h" @@ -263,7 +267,8 @@ void Overview::CreateAllGraphs() { //chartsToBeMonitored.insert(ahi,AHI); UC = createGraph(STR_GRAPH_Usage, tr("Usage"), tr("Usage\n(hours)")); - UC->AddLayer(uc = new gUsageChart()); + uc = new gUsageChart(); + UC->AddLayer(uc); //chartsToBeMonitored.insert(uc,UC); STG = createGraph("New Session", tr("Session Times"), tr("Session Times"), YT_Time); @@ -311,30 +316,34 @@ void Overview::CreateAllGraphs() { sc->addCalc(code, ST_CPH, schema::channel[code].defaultColor()); G->AddLayer(sc); chartsToBeMonitored.insert(sc,G); - } + } if (sc!= nullptr) { sc ->reCalculate(); } } // if showInOverview() } // for chit - // Note The following don not use gSummaryChart. They use SummaryChart instead. and can not be monitored. + #ifndef REMOVE_FITNESS + /* To enable these changes: change the REMOTE_FITNESS define in appsettings.h */ + + // Note The following do not use gSummaryChart. They use gOverviewGraph instead. and can not be monitored. WEIGHT = createGraph(STR_GRAPH_Weight, STR_TR_Weight, STR_TR_Weight, YT_Weight); - weight = new SummaryChart("Weight", GT_LINE); + weight = new gOverviewGraph("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 = new gOverviewGraph("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 = new gOverviewGraph("Zombie", GT_LINE); zombie->setMachineType(MT_JOURNAL); zombie->addSlice(Journal_ZombieMeter, QColor("black"), ST_SETAVG); ZOMBIE->AddLayer(zombie); + #endif connectgSummaryCharts(); } @@ -532,10 +541,10 @@ void Overview::on_XBoundsChanged(qint64 start,qint64 end) bool largerRange=false; if (displayStartDate>maxRangeEndDate || minRangeStartDate>displayEndDate) { - // have non-overlaping ranges + // have non-overlaping ranges // Only occurs when custom mode is switched to/from a latest mode. custom mode to/from last week. // All other displays expand the existing range. - // reset all empty flags to not empty + // reset all empty flags to not empty largerRange=true; chartsEmpty = QHash( chartsToBeMonitored ); minRangeStartDate = displayStartDate; @@ -737,7 +746,7 @@ void Overview::on_rangeCombo_activated(int index) DateErrorDisplay::DateErrorDisplay (Overview* overview) - : m_overview(overview) + : m_overview(overview) { m_visible=false; m_timer = new QTimer(); @@ -837,7 +846,7 @@ void Overview::setRange(QDate& start, QDate& end, bool updateGraphs/*zoom*/) uiEndDate = end; //bool nextSamePage= start.daysTo(end)<=31; - bool nextSamePage= (start.year() == end.year() && start.month() == end.month()) ; + bool nextSamePage= (start.year() == end.year() && start.month() == end.month()) ; if (samePage>0 ||nextSamePage) { // The widgets do not signal pageChanged on opening - since the page hasn't changed. // however the highlighting may need to be changed. diff --git a/oscar/overview.h b/oscar/overview.h index f01fa71d..a5497b4a 100644 --- a/oscar/overview.h +++ b/oscar/overview.h @@ -19,8 +19,10 @@ #include #include "SleepLib/profiles.h" #include "Graphs/gGraphView.h" +#ifndef REMOVE_FITNESS +#include "Graphs/gOverviewGraph.h" +#endif #include "Graphs/gSummaryChart.h" -#include "Graphs/gSessionTimesChart.h" namespace Ui { class Overview; @@ -96,8 +98,10 @@ class Overview : public QWidget gGraph *createGraph(QString code, QString name, QString units = "", YTickerType yttype = YT_Number); gGraph *AHI, *AHIHR, *UC, *FL, *SA, *US, *PR, *LK, *NPB, *SET, *SES, *RR, *MV, *TV, *PTB, *PULSE, *SPO2, *NLL, *WEIGHT, *ZOMBIE, *BMI, *TGMV, *TOTLK, *STG, *SN, *TTIA; - SummaryChart *bc, *sa, *us, *pr, *set, *ses, *ptb, *pulse, *spo2, +#ifndef REMOVE_FITNESS + gOverviewGraph *bc, *sa, *us, *pr, *set, *ses, *ptb, *pulse, *spo2, *weight, *zombie, *bmi, *ahihr, *tgmv, *totlk; +#endif gSummaryChart * stg, *uc, *ahi, * pres, *lk, *npb, *rr, *mv, *tv, *nll, *sn, *ttia; @@ -173,7 +177,7 @@ class Overview : public QWidget QDate displayStartDate; QDate displayEndDate; - // min / max dates of the graph Range + // min / max dates of the graph Range QDate minRangeStartDate; QDate maxRangeEndDate; diff --git a/oscar/preferencesdialog.cpp b/oscar/preferencesdialog.cpp index aae0bda2..a2f5ea19 100644 --- a/oscar/preferencesdialog.cpp +++ b/oscar/preferencesdialog.cpp @@ -290,13 +290,18 @@ PreferencesDialog::PreferencesDialog(QWidget *parent, Profile *_profile) : ui->updateCheckEvery->setValue(AppSetting->updateCheckFrequency()); if (AppSetting->updatesLastChecked().isValid()) { RefreshLastChecked(); - } else { + } else { ui->updateLastChecked->setText(tr("Never")); } #endif ui->overlayFlagsCombo->setCurrentIndex(AppSetting->overlayType()); + #ifndef REMOVE_FITNESS ui->overviewLinecharts->setCurrentIndex(AppSetting->overviewLinechartMode()); + #else + ui->overviewLinecharts->hide(); + ui->overviewLinechartsLabel->hide(); + #endif ui->ahiGraphWindowSize->setEnabled(false); ui->ahiGraphWindowSize->setValue(profile->cpap->AHIWindow()); @@ -883,7 +888,9 @@ bool PreferencesDialog::Save() profile->cpap->setClockDrift(s); AppSetting->setOverlayType((OverlayDisplayType)ui->overlayFlagsCombo->currentIndex()); + #ifndef REMOVE_FITNESS AppSetting->setOverviewLinechartMode((OverviewLinechartModes)ui->overviewLinecharts->currentIndex()); + #endif profile->oxi->setSpO2DropPercentage(ui->spo2Drop->value()); profile->oxi->setSpO2DropDuration(ui->spo2DropTime->value()); diff --git a/oscar/preferencesdialog.ui b/oscar/preferencesdialog.ui index fa76507d..48f0c49b 100644 --- a/oscar/preferencesdialog.ui +++ b/oscar/preferencesdialog.ui @@ -2632,7 +2632,7 @@ Mainly affects the importer. - + Overview Linecharts