/* gLineChart Implementation * * Copyright (c) 2019-2024 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 "Graphs/gLineChart.h" #include #include #include #include "Graphs/glcommon.h" #include "Graphs/gGraph.h" #include "Graphs/gGraphView.h" #include "SleepLib/profiles.h" #include "Graphs/gLineOverlay.h" #define EXTRA_ASSERTS 1 #if 0 QDataStream & operator<<(QDataStream & stream, const DottedLine & dot) { stream << dot.code; stream << dot.type; stream << dot.value; stream << dot.visible; stream << dot.available; return stream; } QDataStream & operator>>(QDataStream & stream, DottedLine & dot) { quint32 tmp; stream >> dot.code; stream >> tmp; stream >> dot.value; stream >> dot.visible; stream >> dot.available; dot.type = (ChannelCalcType)tmp; return stream; } #endif QColor darken(QColor color, float p) { int r = qMin(int(color.red() * p), 255); int g = qMin(int(color.green() * p), 255); int b = qMin(int(color.blue() * p), 255); return QColor(r,g,b, color.alpha()); } void gLineChart::resetGraphViewSettings() { #if !defined(ENABLE_ALWAYS_ON_ZERO_RED_LINE_FLOW_RATE) // always turn zero red line on as default value. if (m_code==CPAP_FlowRate) { m_dot_enabled[m_code][Calc_Zero] = true; } else #endif if (m_code==CPAP_Leak) { m_dot_enabled[m_code][Calc_UpperThresh] = true; } } gLineChart::gLineChart(ChannelID code, bool square_plot, bool disable_accel) : Layer(code), m_square_plot(square_plot), m_disable_accel(disable_accel) { addPlot(code, square_plot); m_report_empty = false; lines.reserve(50000); lasttime = 0; m_layertype = LT_LineChart; resetGraphViewSettings(); } gLineChart::~gLineChart() { for (auto fit = flags.begin(), end=flags.end(); fit != end; ++fit) { // destroy any overlay bar from previous day delete fit.value(); } flags.clear(); } bool gLineChart::isEmpty() { if (!m_day) { return true; } for (const auto code : m_codes) { for (int i=0, end=m_day->size(); i < end; i++) { Session *sess = m_day->sessions[i]; if (sess->channelExists(code)) { return false; } } } return true; } void gLineChart::SetDay(Day *d) { // Layer::SetDay(d); m_day = d; m_minx = 0, m_maxx = 0; m_miny = 0, m_maxy = 0; m_physminy = 0, m_physmaxy = 0; if (!d) { return; } qint64 t64; EventDataType tmp; bool first = true; for (auto & code : m_codes) { for (int i=0, end=d->size(); i < end; i++) { Session *sess = d->sessions[i]; if (!sess->enabled()) continue; // Don't use operator[] here or else it will insert a default-constructed entry // into sess->settings if it's not present. CPAPMode mode = (CPAPMode)sess->settings.value(CPAP_Mode).toInt(); if (mode >= MODE_BILEVEL_FIXED) { m_enabled[CPAP_Pressure] = true; // probably a confusion of Pressure and IPAP somewhere m_enabled[CPAP_IPAP] = true; m_enabled[CPAP_EPAP] = true; } if (code == CPAP_MaskPressure) { if (sess->channelExists(CPAP_MaskPressureHi)) { code = CPAP_MaskPressureHi; // verify setting m_codes[] m_enabled[code] = schema::channel[CPAP_MaskPressureHi].enabled(); goto skipcheck; // why not :P } } if (!sess->channelExists(code)) { continue; } skipcheck: if (first) { m_miny = sess->Min(code); m_maxy = sess->Max(code); m_physminy = sess->physMin(code); m_physmaxy = sess->physMax(code); m_minx = sess->first(code); m_maxx = sess->last(code); first = false; } else { tmp = sess->physMin(code); if (m_physminy > tmp) { m_physminy = tmp; } tmp = sess->physMax(code); if (m_physmaxy < tmp) { m_physmaxy = tmp; } tmp = sess->Min(code); if (m_miny > tmp) { m_miny = tmp; } tmp = sess->Max(code); if (m_maxy < tmp) { m_maxy = tmp; } t64 = sess->first(code); if (m_minx > t64) { m_minx = t64; } t64 = sess->last(code); if (m_maxx < t64) { m_maxx = t64; } } } } subtract_offset = 0; for (auto fit = flags.begin(), end=flags.end(); fit != end; ++fit) { // destroy any overlay bar from previous day delete fit.value(); } flags.clear(); quint32 z = schema::FLAG | schema::MINOR_FLAG | schema::SPAN; if (p_profile->general->showUnknownFlags()) z |= schema::UNKNOWN; QList available = m_day->getSortedMachineChannels(z); for (const auto & code : available) { if (!m_flags_enabled.contains(code)) { bool b = false; if (((m_codes[0] == CPAP_FlowRate) ||((m_codes[0] == CPAP_MaskPressureHi))) && (schema::channel[code].machtype() == MT_CPAP)) b = true; if ((m_codes[0] == CPAP_Leak) && (code == CPAP_LargeLeak)) b = true; m_flags_enabled[code] = b; } if (!m_day->channelExists(code)) continue; schema::Channel * chan = &schema::channel[code]; gLineOverlayBar * lob = nullptr; if (chan->type() == schema::FLAG) { lob = new gLineOverlayBar(code, chan->defaultColor(), chan->label(), FT_Bar); } else if ((chan->type() == schema::MINOR_FLAG) || (chan->type() == schema::UNKNOWN)) { lob = new gLineOverlayBar(code, chan->defaultColor(), chan->label(), FT_Dot); } else if (chan->type() == schema::SPAN) { lob = new gLineOverlayBar(code, chan->defaultColor(), chan->label(), FT_Span); } if (lob != nullptr) { lob->setOverlayDisplayType(((m_codes[0] == CPAP_FlowRate))? (OverlayDisplayType)AppSetting->overlayType() : ODT_TopAndBottom); lob->SetDay(m_day); flags[code] = lob; } } m_dotlines.clear(); for (const auto & code : m_codes) { const schema::Channel & chan = schema::channel[code]; if (code != CPAP_FlowRate) { addDotLine(DottedLine(code, Calc_Max,chan.calc[Calc_Max].enabled)); } if ((code != CPAP_FlowRate) && (code != CPAP_MaskPressure) && (code != CPAP_MaskPressureHi)) { addDotLine(DottedLine(code, Calc_Perc,chan.calc[Calc_Perc].enabled)); addDotLine(DottedLine(code, Calc_Middle, chan.calc[Calc_Middle].enabled)); } if ((code != CPAP_FlowRate) && (code != CPAP_Snore) && (code != CPAP_FlowLimit) && (code != CPAP_RDI) && (code != CPAP_AHI)) { addDotLine(DottedLine(code, Calc_Min, chan.calc[Calc_Min].enabled)); } } if (m_codes[0] == CPAP_Leak) { addDotLine(DottedLine(CPAP_Leak, Calc_UpperThresh, schema::channel[CPAP_Leak].calc[Calc_UpperThresh].enabled)); } else if (m_codes[0] == CPAP_FlowRate) { addDotLine(DottedLine(CPAP_FlowRate, Calc_Zero, schema::channel[CPAP_FlowRate].calc[Calc_Zero].enabled)); #if defined(ENABLE_ALWAYS_ON_ZERO_RED_LINE_FLOW_RATE) //on set day force red line on. m_dot_enabled[m_code][Calc_Zero] = true; #endif } else if (m_codes[0] == OXI_Pulse) { addDotLine(DottedLine(OXI_Pulse, Calc_UpperThresh, schema::channel[OXI_Pulse].calc[Calc_UpperThresh].enabled)); addDotLine(DottedLine(OXI_Pulse, Calc_LowerThresh, schema::channel[OXI_Pulse].calc[Calc_LowerThresh].enabled)); } else if (m_codes[0] == OXI_SPO2) { addDotLine(DottedLine(OXI_SPO2, Calc_LowerThresh, schema::channel[OXI_SPO2].calc[Calc_LowerThresh].enabled)); } if (m_day) { for (auto & dot : m_dotlines) { dot.calc(m_day); ChannelID code = dot.code; ChannelCalcType type = dot.type; bool b = false; // default value const auto & cit = m_dot_enabled.find(code); if (cit == m_dot_enabled.end()) { m_dot_enabled[code].insert(type, b); } else { const auto & it = cit.value().find(type); if (it == cit.value().end()) { cit.value().insert(type, b); } } } } } EventDataType gLineChart::Miny() { if (m_codes.size() == 0) return 0; if (!m_day) return 0; bool first = false; EventDataType min = 0, tmp; for (const auto code : m_codes) { if (!m_enabled[code] || !m_day->channelExists(code)) continue; tmp = m_day->Min(code); if (!first) { min = tmp; first = true; } else { min = qMin(tmp, min); } } if (!first) min = 0; m_miny = min; return min; } EventDataType gLineChart::Maxy() { if (m_codes.size() == 0) return 0; if (!m_day) return 0; bool first = false; EventDataType max = 0, tmp; for (const auto code : m_codes) { if (!m_enabled[code] || !m_day->channelExists(code)) continue; tmp = m_day->Max(code); if (!first) { max = tmp; first = true; } else { max = qMax(tmp, max); } } if (!first) max = 0; m_maxy = max; return max; // return Layer::Maxy() - subtract_offset; } bool gLineChart::mouseMoveEvent(QMouseEvent *event, gGraph *graph) { Q_UNUSED(event) Q_UNUSED(graph) graph->timedRedraw(0); return false; } QString gLineChart::getMetaString(qint64 time) { if (!m_day) return lasttext; lasttext = QString(); EventDataType val; EventDataType ipap = 0, epap = 0; bool addPS = false; for (const auto code : m_codes) { if (m_day->channelHasData(code)) { val = m_day->lookupValue(code, time, m_square_plot); lasttext += " "+QString("%1: %2").arg(schema::channel[code].label()).arg(val,0,'f',2); //.arg(schema::channel[code].units()); if (code == CPAP_IPAP || code == CPAP_IPAPSet) { ipap = val; addPS = true; } if (code == CPAP_EPAP || code == CPAP_EPAPSet) { epap = val; } } } if (addPS) { val = ipap - epap; lasttext += " "+QString("%1: %2").arg(schema::channel[CPAP_PS].label()).arg(val,0,'f',2);//.arg(schema::channel[CPAP_PS].units()); } lasttime = time; return lasttext; } // Time Domain Line Chart void gLineChart::paint(QPainter &painter, gGraph &w, const QRegion ®ion) { EventDataType actual_min_y, actual_max_y; QRectF rect = region.boundingRect(); rect.translate(0.0f, 0.001f); // TODO: Just use QRect directly. int left = rect.left(); int top = rect.top(); int width = rect.width(); int height = rect.height(); if (!m_visible) { return; } if (!m_day) { return; } //if (!m_day->channelExists(m_code)) return; if (width < 0) { return; } actual_min_y = (EventDataType)INT_MAX; actual_max_y = -(EventDataType)INT_MAX; top++; double minx, maxx; if (w.blockZoom()) { minx = w.rmin_x, maxx = w.rmax_x; } else { maxx = w.max_x, minx = w.min_x; } // hmmm.. subtract_offset.. EventDataType miny = m_physminy; EventDataType maxy = m_physmaxy; w.roundY(miny, maxy); //#define DEBUG_AUTOSCALER #ifdef DEBUG_AUTOSCALER QString a = QString::asprintf("%.2f - %.2f",miny, maxy); w.renderText(a,width/2,top-5); #endif // the middle of minx and maxy does not have to be the center... double logX = painter.device()->logicalDpiX(); double physX = painter.device()->physicalDpiX(); double ratioX = physX / logX * w.printScaleX(); double logY = painter.device()->logicalDpiY(); double physY = painter.device()->physicalDpiY(); double ratioY = physY / logY * w.printScaleY(); double xx = maxx - minx; double xmult = double(width) / xx; EventDataType yy = maxy - miny; EventDataType ymult = EventDataType(height - 3) / yy; // time to pixel conversion multiplier // Return on screwy min/max conditions if (xx < 0) { return; } if (yy <= 0) { if (miny == 0) { return; } } painter.setRenderHint(QPainter::Antialiasing, AppSetting->antiAliasing()); //bool mouseover = false; if (rect.contains(w.graphView()->currentMousePos())) { //mouseover = true; painter.fillRect(rect, QBrush(QColor(255,255,245,128))); } bool linecursormode = AppSetting->lineCursorMode(); //////////////////////////////////////////////////////////////////////// // Display Line Cursor //////////////////////////////////////////////////////////////////////// if (linecursormode) { double time = w.currentTime(); if ((time > minx) && (time < maxx)) { double xpos = (time - double(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); } if ((time != lasttime) || lasttext.isEmpty()) { getMetaString(time); } if (m_codes[0] != CPAP_FlowRate) { QString text = lasttext; int wid, h; GetTextExtent(text, wid, h); w.renderText(text, left , top-6); //(h+(4 * w.printScaleY()))); //+ width/2 - wid/2 } } double lastpx, lastpy; double px, py; int idx; bool done; double x0, xL; double sr = 0.0; int sam; int minz, maxz; // Draw bounding box painter.setPen(QColor(Qt::black)); painter.drawRect(rect); width--; height -= 2; int num_points = 0; //int visible_points = 0; int total_points = 0; //int total_visible = 0; bool square_plot, accel; qint64 clockdrift = qint64(p_profile->cpap->clockDrift()) * 1000L; qint64 drift = 0; QHash >::iterator ci; //m_line_color=schema::channel[m_code].defaultColor(); int legendx = left + width; int codepoints; painter.setClipRect(left, top, width, height+1); painter.setClipping(true); painter.setFont(*defaultfont); bool showDottedLines = true; // Unset Dotted lines visible status, so we only draw necessary labels later for (auto & dot : m_dotlines) { dot.visible = false; } ChannelID code; float lineThickness = AppSetting->lineThickness()+0.001F; for (int ic = 0; ic < m_codes.count(); ic++) { const auto & code = m_codes[ic]; square_plot = m_square[ic]; // set the mode per-channel const schema::Channel &chan = schema::channel[code]; //////////////////////////////////////////////////////////////////////// // Draw the Channel Threshold dotted lines, and flow waveform centreline //////////////////////////////////////////////////////////////////////// if (showDottedLines) { for (auto & dot : m_dotlines) { if ((dot.code != code) || (!m_dot_enabled[dot.code][dot.type]) || (!dot.available) || (!m_enabled[dot.code])) { continue; } //schema::Channel & chan = schema::channel[code]; dot.visible = true; QColor color = chan.calc[dot.type].color; color.setAlpha(200); painter.setPen(QPen(QBrush(color), lineThickness, Qt::DotLine)); EventDataType y=top + height + 1 - ((dot.value - miny) * ymult); painter.drawLine(left + 1, y, left + 1 + width, y); } } if (!m_enabled[code]) continue; lines.clear(); codepoints = 0; // For each session... for (const auto & sess : m_day->sessions) { if (!sess) { qWarning() << "gLineChart::Plot() nullptr Session Record.. This should not happen"; continue; } drift = (sess->type() == MT_CPAP) ? clockdrift : 0; if (!sess->enabled()) { continue; } schema::Channel ch = schema::channel[code]; bool fndbetter = false; for (const auto & c : ch.m_links) { ci = sess->eventlist.find(c->id()); if (ci != sess->eventlist.end()) { fndbetter = true; break; } } if (!fndbetter) { ci = sess->eventlist.find(code); if (ci == sess->eventlist.end()) { continue; } } num_points = 0; for (const auto & ni : ci.value()) { num_points += ni->count(); } total_points += num_points; codepoints += num_points; // Max number of samples taken from samples per pixel for better min/max values const int num_averages = 20; for (auto & ni : ci.value()) { EventList & el = (*ni); accel = (el.type() == EVL_Waveform); // Turn on acceleration if this is a waveform. if (accel) { sr = el.rate(); // Time distance between samples if (sr <= 0) { qWarning() << "qLineChart::Plot() assert(sr>0)"; continue; } } if (m_disable_accel) { accel = false; } //square_plot = m_square_plot; // now we set this per-channel above if (accel || num_points > 20000) { // Don't square plot if too many points or waveform square_plot = false; } int siz = el.count(); if (siz <= 1) { continue; } // Don't bother drawing 1 point or less. x0 = el.time(0) + drift; xL = el.time(siz - 1) + drift; if (maxx < x0) { continue; } if (xL < minx) { continue; } if (x0 > xL) { if (siz == 2) { // this happens on CPAP quint32 t = el.getTime()[0]; el.getTime()[0] = el.getTime()[1]; el.getTime()[1] = t; EventStoreType d = el.getData()[0]; el.getData()[0] = el.getData()[1]; el.getData()[1] = d; } else { qDebug() << "Reversed order sample fed to gLineChart - ignored."; continue; //assert(x1 0) { // sam = 1; // } if (ZW < num_averages) { sam = 1; accel = false; } else { sam = ZW / num_averages; if (sam < 1) { sam = 1; accel = false; } } // Prepare the min max y values if we still are accelerating this plot if (accel) { for (int i = 0; i < width; i++) { m_drawlist[i].setX(height); m_drawlist[i].setY(0); } minz = width; maxz = 0; } //total_visible += visible_points; } else { sam = 1; } // these calculations over estimate // The Z? values are much more accurate idx = 0; if (el.type() == EVL_Waveform) { // We can skip data previous to minx if this is a waveform if (minx > x0) { double j = minx - x0; // == starting min of first sample in this segment idx = (j / sr); //idx/=(sam*num_averages); //idx*=(sam*num_averages); // Loose the precision idx += sam - (idx % sam); } // else just start from the beginning } int xst = left + 1; int yst = top + height + 1; double time; EventDataType data; EventDataType gain = el.gain(); done = false; if (el.type() == EVL_Waveform) { // Waveform Plot if (idx > sam) { idx -= sam; } time = el.time(idx) + drift; double rate = double(sr) * double(sam); EventStoreType *ptr = el.rawData() + idx; if ((unsigned) siz > el.count()) siz = el.count(); if (accel) { ////////////////////////////////////////////////////////////////// // Accelerated Waveform Plot ////////////////////////////////////////////////////////////////// for (int i = idx; i <= siz; i += sam, ptr += sam) { time += rate; // This is much faster than QVector access. data = *ptr * gain; if (actual_min_y>data) { actual_min_y=data; } if (actual_max_y=0.5)?(int(px)+1):int(px); if (z < minz) { minz = z; // minz=First pixel } if (z > maxz) { maxz = z; // maxz=Last pixel } if (minz < 0) { qDebug() << "gLineChart::Plot() minz<0 should never happen!! minz =" << minz; minz = 0; } if (maxz > max_drawlist_size) { qDebug() << "gLineChart::Plot() maxz>max_drawlist_size!!!! maxz = " << maxz << " max_drawlist_size =" << max_drawlist_size; maxz = max_drawlist_size; } // Update the Y pixel bounds. if (py < m_drawlist[z].x()) { m_drawlist[z].setX(py); } if (py > m_drawlist[z].y()) { m_drawlist[z].setY(py); } if (time > maxx) { done = true; break; } } // Plot compressed accelerated vertex list if (maxz > width) { maxz = width; } float ax1, ay1; QPointF *drl = m_drawlist + minz; // Don't need to cap VertexBuffer here, as it's limited to max_drawlist_size anyway // Cap within VertexBuffer capacity, one vertex per line point // int np = (maxz - minz) * 2; for (int i = minz; i < maxz; i++, drl++) { ax1 = drl->x(); ay1 = drl->y(); lines.append(QLine(xst + i, yst - ax1, xst + i, yst - ay1)); } } else { // Zoomed in Waveform ////////////////////////////////////////////////////////////////// // Normal Waveform Plot ////////////////////////////////////////////////////////////////// // Prime first point data = (*ptr + el.offset()) * gain; lastpx = xst + ((time - minx) * xmult); lastpy = yst - ((data - miny) * ymult); siz--; for (int i = idx; i < siz; i += sam) { ptr += sam; time += rate; data = (*ptr + el.offset()) * gain; if (actual_min_y>data) { actual_min_y=data; } if (actual_max_y= maxx) { done = true; break; } } } if (w.printing() && AppSetting->monochromePrinting()) { painter.setPen(QPen(Qt::black, lineThickness + 0.5)); } else { painter.setPen(QPen(chan.defaultColor(), lineThickness)); } painter.drawLines(lines); w.graphView()->lines_drawn_this_frame += lines.count(); lines.clear(); } else { ////////////////////////////////////////////////////////////////// // Standard events/zoomed in Plot ////////////////////////////////////////////////////////////////// double start = el.first() + drift; quint32 *tptr = el.rawTime(); int idx = 0; if (siz > 15) { // Prime a bit... for (; idx < siz; ++idx) { time = start + *tptr++; if (time >= minx) { break; } } if (idx > 0) { idx--; } } // Step one backwards if possible (to draw through the left margin) EventStoreType *dptr = el.rawData() + idx; tptr = el.rawTime() + idx; time = start + *tptr++; data = (*dptr++ + el.offset()) * gain; if (actual_min_y>data) { actual_min_y=data; } if (actual_max_ydata) { actual_min_y=data; } if (actual_max_y xst + width) { px = xst + width; } } //else { // Letting the scissor do the dirty work for non horizontal lines // This really should be changed, as it might be cause that weird // display glitch on Linux.. //} if (square_plot) { lines.append(QLine(lastpx, lastpy, px, lastpy)); lines.append(QLine(px, lastpy, px, py)); } else { lines.append(QLine(lastpx, lastpy, px, py)); } lastpx = px; lastpy = py; if (time > maxx) { // Past right edge, abort further drawing.. done = true; break; } } if (w.printing() && AppSetting->monochromePrinting()) { painter.setPen(QPen(Qt::black, lineThickness + 0.5)); } else { painter.setPen(QPen(chan.defaultColor(), lineThickness)); } painter.drawLines(lines); w.graphView()->lines_drawn_this_frame+=lines.count(); lines.clear(); } if (done) { break; } } } // painter.setPen(QPen(m_colors[gi],lineThickness)); // painter.drawLines(lines); // w.graphView()->lines_drawn_this_frame+=lines.count(); // lines.clear(); //////////////////////////////////////////////////////////////////// // Draw Legends on the top line //////////////////////////////////////////////////////////////////// if ((codepoints > 0)) { QString text = schema::channel[code].label(); QRectF rec(0, rect.top()-3, 0,0); rec = painter.boundingRect(rec, Qt::AlignBottom | Qt::AlignLeft, text); rec.moveRight(legendx); legendx -= rec.width(); painter.setClipping(false); painter.setPen(Qt::black); painter.drawText(rec, Qt::AlignBottom | Qt::AlignRight, text); float ps = 2.0 * ratioY; ps = qMax(ps, 1.0f); painter.setPen(QPen(chan.defaultColor(), ps)); int linewidth = (10 * ratioX); int yp = rec.top()+(rec.height()/2); painter.drawLine(QLineF(rec.left()-linewidth, yp+0.001f , rec.left()-(2 * ratioX), yp)); painter.setClipping(true); legendx -= linewidth + (2*ratioX); } } painter.setClipping(false); //////////////////////////////////////////////////////////////////// // Draw Channel Threshold legend markers //////////////////////////////////////////////////////////////////// for (const auto & dot : m_dotlines) { if (!dot.visible) continue; code = dot.code; schema::Channel &chan = schema::channel[code]; int linewidth = (10 * ratioX); QRectF rec(0, rect.top()-3, 0,0); QString text = chan.calc[dot.type].label(); rec = painter.boundingRect(rec, Qt::AlignBottom | Qt::AlignLeft, text); rec.moveRight(legendx); legendx -= rec.width(); painter.setPen(Qt::black); painter.drawText(rec, Qt::AlignBottom | Qt::AlignRight, text); QColor color = chan.calc[dot.type].color; color.setAlpha(200); float ps = 2.0 * ratioY; ps = qMax(ps, 1.0f); painter.setPen(QPen(QBrush(color), ps,Qt::DotLine)); int yp = rec.top()+(rec.height()/2); painter.drawLine(QLineF(rec.left()-linewidth, yp+0.001f, rec.left()-(2 * ratioX), yp)); legendx -= linewidth + (2*ratioX); } painter.setClipping(true); if (!total_points) { // No Data? QString msg = QObject::tr("Plots Disabled"); int x, y; GetTextExtent(msg, x, y, bigfont); w.renderText(msg, rect, Qt::AlignCenter, 0, Qt::gray, bigfont); } painter.setClipping(false); // Calculate combined session times within selected area... double first, last; double time = 0; // Calculate the CPAP session time. for (auto & sess : m_day->sessions) { if (!sess->enabled() || (sess->type() != MT_CPAP)) continue; first = sess->first(); last = sess->last(); if (last < w.min_x) { continue; } if (first > w.max_x) { continue; } if (first < w.min_x) { first = w.min_x; } if (last > w.max_x) { last = w.max_x; } time += last - first; } time /= 1000; QList ahilist; for (int i = 0; i < ahiChannels.size(); i++) ahilist.push_back(ahiChannels.at(i)); // ahilist.push_back(CPAP_Hypopnea); // ahilist.push_back(CPAP_AllApnea); // ahilist.push_back(CPAP_Obstructive); // ahilist.push_back(CPAP_Apnea); // ahilist.push_back(CPAP_ClearAirway); QList extras; extras.push_back(CPAP_NRI); extras.push_back(CPAP_UserFlag1); extras.push_back(CPAP_UserFlag2); //double sum = 0; int cnt = 0; //Draw the linechart overlays (Event flags) independant of line Cursor mode //The problem was that turning lineCUrsor mode off (or Control L) also stopped flag event on most daily graphs. // The user didn't know what trigger the problem. Best quess is that Control L was typed by mistable. // this fix allows flag events to be normally displayed when the line Cursor mode is off. //was if (m_day /*&& (AppSetting->lineCursorMode() || (m_codes[0]==CPAP_FlowRate))*/) if (m_day) { bool blockhover = false; for (auto fit=flags.begin(), end=flags.end(); fit != end; ++fit) { code = fit.key(); if ((!m_flags_enabled[code]) || (!m_day->channelExists(code))) continue; gLineOverlayBar * lob = fit.value(); lob->setBlockHover(blockhover); lob->paint(painter, w, region); if (lob->hover()) blockhover = true; // did it render a hover over? if (ahilist.contains(code)) { //sum += lob->sum(); cnt += lob->count(); } } } if (m_codes[0] == CPAP_FlowRate) { float hours = time / 3600.0; int h = time / 3600; int m = int(time / 60) % 60; int s = int(time) % 60; double f = double(cnt) / hours; // / (sum / 3600.0); QString txt = QObject::tr("Duration %1:%2:%3").arg(h,2,10,QChar('0')).arg(m,2,10,QChar('0')).arg(s,2,10,QChar('0')) + " "+ QObject::tr("AHI %1").arg(f,0,'f',2);// +" " if (linecursormode) txt+=lasttext; w.renderText(txt,left,top-5); } // painter.setRenderHint(QPainter::Antialiasing, false); if (actual_max_y>0) { m_actual_min_y=actual_min_y; m_actual_max_y=actual_max_y; } }