/* gLineChart Implementation Copyright (c)2011 Mark Watkins <jedimark@users.sourceforge.net> License: GPL */ #include <math.h> #include <QString> #include <QDebug> #include <SleepLib/profiles.h> #include "gLineChart.h" #include "glcommon.h" #define EXTRA_ASSERTS 1 gLineChart::gLineChart(ChannelID code,QColor col,bool square_plot, bool disable_accel) :Layer(code),m_square_plot(square_plot),m_disable_accel(disable_accel) { addPlot(code,col,square_plot); m_line_color=col; m_report_empty=false; addVertexBuffer(lines=new gVertexBuffer(100000,GL_LINES)); lines->setColor(col); lines->setAntiAlias(true); lines->setBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } gLineChart::~gLineChart() { } bool gLineChart::isEmpty() { if (!m_day) return true; for (int j=0;j<m_codes.size();j++) { ChannelID code=m_codes[j]; for (int i=0;i<m_day->size();i++) { Session *sess=m_day->getSessions()[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; if (!d) return; qint64 t64; EventDataType min=99999999,max=-999999999,tmp; for (int j=0;j<m_codes.size();j++) { ChannelID code=m_codes[j]; for (int i=0;i<d->size();i++) { Session *sess=d->getSessions()[i]; if (!sess->channelExists(code)) continue; tmp=sess->Min(code); if (min > tmp) min=tmp; tmp=sess->Max(code); if (max < tmp) max=tmp; t64=sess->first(code); if (!m_minx || (m_minx > t64)) m_minx=t64; t64=sess->last(code); if (!m_maxx || (m_maxx < t64)) m_maxx=t64; } } m_miny=min; m_maxy=max; //if (m_code==CPAP_Leak) { // subtract_offset=profile.cpap.[IntentionalLeak].toDouble(); //} else subtract_offset=0; } EventDataType gLineChart::Miny() { int m=Layer::Miny(); if (subtract_offset>0) { m-=subtract_offset; if (m<0) m=0; } return m; } EventDataType gLineChart::Maxy() { return Layer::Maxy()-subtract_offset; } // Time Domain Line Chart void gLineChart::paint(gGraph & w,int left, int top, int width, int height) { if (!m_visible) return; if (!m_day) return; //if (!m_day->channelExists(m_code)) return; if (width<0) return; // lines=w.lines(); EventDataType miny,maxy; double minx,maxx; miny=w.min_y, maxy=w.max_y; if (w.blockZoom()) { minx=w.rmin_x, maxx=w.rmax_x; } else { maxx=w.max_x, minx=w.min_x; } // hmmm.. subtract_offset.. /*if (miny<0) { miny=-MAX(fabs(miny),fabs(maxy)); }*/ w.roundY(miny,maxy); 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; } EventDataType lastpx,lastpy; EventDataType px,py; int idx; bool done,first; double x0,xL; double sr; int sam; int minz,maxz; // Draw bounding box gVertexBuffer *outlines=w.lines(); GLuint blk=QColor(Qt::black).rgba(); outlines->add(left, top, left, top+height, blk); outlines->add(left, top+height, left+width,top+height, blk); outlines->add(left+width,top+height, left+width, top, blk); outlines->add(left+width, top, left,top, blk); width--; height-=2; int num_points=0; int visible_points=0; int total_points=0; int total_visible=0; bool square_plot,accel; QHash<ChannelID,QVector<EventList *> >::iterator ci; //m_line_color=schema::channel[m_code].defaultColor(); int legendx=left+width; int codepoints; //GLuint color; for (int gi=0;gi<m_codes.size();gi++) { ChannelID code=m_codes[gi]; //m_line_color=m_colors[gi]; lines->setColor(m_colors[gi]); //color=m_line_color.rgba(); codepoints=0; for (int svi=0;svi<m_day->size();svi++) { Session *sess=(*m_day)[svi]; if (!sess) { qWarning() << "gLineChart::Plot() NULL Session Record.. This should not happen"; continue; } if (!sess->enabled()) continue; schema::Channel ch=schema::channel[code]; bool fndbetter=false; for (QList<schema::Channel *>::iterator l=ch.m_links.begin();l!=ch.m_links.end();l++) { schema::Channel *c=*l; ci=(*m_day)[svi]->eventlist.find(c->id()); if (ci!=(*m_day)[svi]->eventlist.end()) { fndbetter=true; break; } } if (!fndbetter) { ci=(*m_day)[svi]->eventlist.find(code); if (ci==(*m_day)[svi]->eventlist.end()) continue; } QVector<EventList *> & evec=ci.value(); num_points=0; for (int i=0;i<evec.size();i++) num_points+=evec[i]->count(); total_points+=num_points; codepoints+=num_points; const int num_averages=20; // Max n umber of samples taken from samples per pixel for better min/max values for (int n=0;n<evec.size();n++) { // for each segment EventList & el=*evec[n]; 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; if (accel || num_points>20000) { // Don't square plot if too many points or waveform square_plot=false; } int siz=evec[n]->count(); if (siz<=1) continue; // Don't bother drawing 1 point or less. x0=el.time(0); xL=el.time(siz-1); 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<x2); } } if (accel) { //x1=el.time(1); double XR=xx/sr; double Z1=MAX(x0,minx); double Z2=MIN(xL,maxx); double ZD=Z2-Z1; double ZR=ZD/sr; double ZQ=ZR/XR; double ZW=ZR/(width*ZQ); visible_points+=ZR*ZQ; if (accel && n>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(); EventDataType nmult=ymult*gain; EventDataType ymin=EventDataType(miny)/gain; const QVector<EventStoreType> & dat=el.getData(); const QVector<quint32> & tim=el.getTime(); done=false; first=true; if (!accel) { lines->setSize(1.5); } else lines->setSize(1); bool firstpx=true; if (el.type()==EVL_Waveform) { // Waveform Plot if (idx>sam) idx-=sam; time=el.time(idx); double rate=double(sr)*double(sam); if (accel) { ////////////////////////////////////////////////////////////////// // Accelerated Waveform Plot ////////////////////////////////////////////////////////////////// for (int i=idx;i<siz;i+=sam) { time+=rate; //time=el.time(i); //if (time < minx) // continue; // Skip stuff before the start of our data window //data=el.data(i); data=dat[i];//*gain; px=((time - minx) * xmult); // Scale the time scale X to pixel scale X py=((data - ymin) * nmult); // Same for Y scale // In accel mode, each pixel has a min/max Y value. // m_drawlist's index is the pixel index for the X pixel axis. int z=round(px); // Hmmm... round may screw this up. 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; // Let this iteration finish.. (This point will be in far clipping) break; } } // Plot compressed accelerated vertex list if (maxz>width) { //qDebug() << "gLineChart::Plot() maxz exceeded graph width" << "maxz = " << maxz << "width =" << width; maxz=width; } float ax1,ay1; for (int i=minz;i<maxz;i++) { // ax1=(m_drawlist[i-1].x()+m_drawlist[i].x()+m_drawlist[i+1].x())/3.0; // ay1=(m_drawlist[i-1].y()+m_drawlist[i].y()+m_drawlist[i+1].y())/3.0; ax1=m_drawlist[i].x(); ay1=m_drawlist[i].y(); lines->add(xst+i,yst-ax1,xst+i,yst-ay1); if (lines->full()) break; } } else { // Zoomed in Waveform ////////////////////////////////////////////////////////////////// // Normal Waveform Plot ////////////////////////////////////////////////////////////////// if (idx>sam) { idx-=sam; time=el.time(idx); //double rate=double(sr)*double(sam); } for (int i=idx;i<siz;i+=sam) { time+=rate; //if (time < minx) // continue; // Skip stuff before the start of our data window data=dat[i];//el.data(i); px=xst+((time - minx) * xmult); // Scale the time scale X to pixel scale X py=yst-((data - ymin) * nmult); // Same for Y scale, with precomputed gain if (firstpx) { lastpx=px; lastpy=py; firstpx=false; continue; } lines->add(lastpx,lastpy,px,py); if (lines->full()) { done=true; break; } if (time > maxx) { //done=true; // Let this iteration finish.. (This point will be in far clipping) break; } lastpx=px; lastpy=py; } } } else { ////////////////////////////////////////////////////////////////// // Standard events/zoomed in Plot ////////////////////////////////////////////////////////////////// first=true; double start=el.first(); /*if (siz==2) { time=start+tim[0]; data=dat[0]*gain; data-=subtract_offset; lastpy=yst-((data - miny) * ymult); // Same for Y scale with precomputed gain lastpx=xst+((time - minx) * xmult); // Scale the time scale X to pixel scale X if (lastpx<xst-1) lastpx=xst-1; EventDataType data2=(dat[1]*gain)-subtract_offset; qint64 time2=start+tim[1]; py=yst-((data2 - miny) * ymult); px=xst+((time2 - minx) * xmult); if (px>xst+width) px=xst+width; lines->add(lastpx,lastpy,px,py); } else*/ for (int i=0;i<siz;i++) { time=start+tim[i]; if (first) { if (num_points>15 && (time < minx)) continue; // Skip stuff before the start of our data window first=false; if (i>0) i--; // Start with the previous sample (which will be in clipping area) time=start+tim[i]; } data=dat[i]*gain; // data-=subtract_offset; //data=el.data(i); // raw access is faster px=xst+((time - minx) * xmult); // Scale the time scale X to pixel scale X //py=yst+((data - ymin) * nmult); // Same for Y scale with precomputed gain py=yst-((data - miny) * ymult); // Same for Y scale with precomputed gain //if (px<left) px=left; //if (px>left+width) px=left+width; if (firstpx) { firstpx=false; } else { if (py==lastpy) { if (lastpx<xst) lastpx=xst; if (px>xst+width) px=xst+width; if (square_plot) { lines->add(lastpx,lastpy,px,lastpy,px,lastpy,px,py); } else { lines->add(lastpx,lastpy,px,py); } } else { if (square_plot) { lines->add(lastpx,lastpy,px,lastpy,px,lastpy,px,py); } else { lines->add(lastpx,lastpy,px,py); } } //lines->add(px,py,m_line_color); if (lines->full()) { done=true; break; } } lastpx=px; lastpy=py; //if (lastpx>start_px+width) done=true; if (time > maxx) { done=true; // Let this iteration finish.. (This point will be in far clipping) break; } } } if (done) break; } } if ((codepoints>0)) { //(m_codes.size()>1) && // Draw Legends for plots.. QString text=schema::channel[code].label(); int wid,hi; GetTextExtent(text,wid,hi); legendx-=wid; w.renderText(text,legendx,top-4); int bw=GetXHeight(); legendx-=bw/2; int tp=top-5-bw/2; w.quads()->add(legendx-bw,tp+bw/2,legendx,tp+bw/2,legendx,tp-bw/2,legendx-bw,tp-bw/2,m_line_color.rgba()); legendx-=hi+bw/2; } } if (!total_points) { // No Data? if (m_report_empty) { QString msg="No Waveform Available"; int x,y; GetTextExtent(msg,x,y,bigfont); //DrawText(w,msg,left+(width/2.0)-(x/2.0),scry-w.GetBottomMargin()-height/2.0+y/2.0,0,Qt::gray,bigfont); } } else { lines->scissor(left,w.flipY(top+height+2),width+1,height+1); //lines->draw(); } } AHIChart::AHIChart(const QColor col) :Layer(NoChannel),m_color(col) { m_miny=m_maxy=0; addVertexBuffer(lines=new gVertexBuffer(100000,GL_LINES)); lines->setColor(col); lines->setAntiAlias(true); lines->setSize(1.5); } AHIChart::~AHIChart() { } void AHIChart::paint(gGraph & w,int left, int top, int width, int height) { if (!m_visible) return; if (!m_day) return; // Draw bounding box gVertexBuffer *outlines=w.lines(); GLuint blk=QColor(Qt::black).rgba(); outlines->add(left, top, left, top+height, blk); outlines->add(left, top+height, left+width,top+height, blk); outlines->add(left+width,top+height, left+width, top, blk); outlines->add(left+width, top, left,top, blk); width--; height-=2; EventDataType miny,maxy; double minx,maxx; miny=w.min_y, maxy=w.max_y; maxx=w.max_x, minx=w.min_x; // hmmm.. subtract_offset.. w.roundY(miny,maxy); double xx=maxx-minx; double xmult=double(width)/xx; EventDataType yy=maxy-miny; EventDataType ymult=EventDataType(height-3)/yy; // time to pixel conversion multiplier bool first=true; double px,py; double lastpx,lastpy; double top1=top+height; bool done=false; GLuint color=m_color.rgba(); for (int i=0;i<m_time.size();i++) { qint64 ti=m_time[i]; EventDataType v=m_data[i]; if (ti<minx) continue; if (ti>maxx) done=true; if (first) { if (i>0) { ti=m_time[i-1]; v=m_data[i-1]; i--; } px=left+(double(ti-minx)*xmult); py=top1-(double(v-miny)*ymult); first=false; } else { px=left+(double(ti-minx)*xmult); py=top1-(double(v-miny)*ymult); lines->add(px,py,lastpx,lastpy); } lastpx=px; lastpy=py; if (done) break; } lines->scissor(left,w.flipY(top+height+2),width+1,height+1); } void AHIChart::SetDay(Day *d) { m_day=d; m_data.clear(); m_time.clear(); m_maxy=0; m_miny=0; if (!d) return; m_miny=9999; QVector<Session *>::iterator s; qint64 first=d->first(); qint64 last=d->last(); qint64 f; qint64 winsize=30000; // 30 second windows for (qint64 ti=first;ti<last;ti+=winsize) { f=ti-3600000L; //if (f<first) f=first; EventList *el[6]; EventDataType ahi=0; int cnt=0; bool fnd=false; for (s=d->begin();s!=d->end();s++) { if (!(*s)->enabled()) continue; Session *sess=*s; if ((ti<sess->first()) || (f>sess->last())) continue; // Drop off suddenly outside of sessions //if (ti>sess->last()) continue; fnd=true; if (sess->eventlist.contains(CPAP_Obstructive)) el[0]=sess->eventlist[CPAP_Obstructive][0]; else el[0]=NULL; if (sess->eventlist.contains(CPAP_Apnea)) el[1]=sess->eventlist[CPAP_Apnea][0]; else el[1]=NULL; if (sess->eventlist.contains(CPAP_Hypopnea)) el[2]=sess->eventlist[CPAP_Hypopnea][0]; else el[2]=NULL; if (sess->eventlist.contains(CPAP_ClearAirway)) el[3]=sess->eventlist[CPAP_ClearAirway][0]; else el[3]=NULL; if (sess->eventlist.contains(CPAP_NRI)) el[4]=sess->eventlist[CPAP_NRI][0]; else el[4]=NULL; int znt=5; if (PROFILE.general->calculateRDI()) { if (sess->eventlist.contains(CPAP_RERA)) {// What about ExP?? el[5]=sess->eventlist[CPAP_RERA][0]; znt++; } else el[5]=NULL; } qint64 t; for (int i=0;i<znt;i++) { if (!el[i]) continue; for (quint32 j=0;j<el[i]->count();j++) { t=el[i]->time(j); if ((t>=f) && (t<ti)) { cnt++; } } } } if (!fnd) cnt=0; double g=double(ti-f)/3600000.0; if (g>0) ahi=cnt/g; if (ahi<m_miny) m_miny=ahi; if (ahi>m_maxy) m_maxy=ahi; m_time.append(ti); m_data.append(ahi); } m_minx=first; m_maxx=last; }