/* KSysGuard, the KDE System Guard Copyright (c) 1999 - 2002 Chris Schlaeger This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. KSysGuard is currently maintained by Chris Schlaeger . Please do not commit any changes without consulting me first. Thanks! */ #include #include #include #include #include #include #include #include "SignalPlotter.h" SignalPlotter::SignalPlotter( TQWidget *parent, const char *name ) : TQWidget( parent, name ) { // Auto deletion does not work for pointer to arrays. mBeamData.setAutoDelete( false ); setBackgroundMode( NoBackground ); mShowThinFrame = true; mSamples = 0; mMinValue = mMaxValue = 0.0; mUseAutoRange = true; mGraphStyle = GRAPH_POLYGON; // Anything smaller than this does not make sense. setMinimumSize( 16, 16 ); tqsetSizePolicy( TQSizePolicy( TQSizePolicy::Expanding, TQSizePolicy::Expanding, false ) ); mShowVerticalLines = true; mVerticalLinesColor = KSGRD::Style->firstForegroundColor(); mVerticalLinesDistance = 30; mVerticalLinesScroll = true; mVerticalLinesOffset = 0; mHorizontalScale = 1; mShowHorizontalLines = true; mHorizontalLinesColor = KSGRD::Style->secondForegroundColor(); mHorizontalLinesCount = 5; mShowLabels = true; mShowTopBar = false; mFontSize = KSGRD::Style->fontSize(); mBackgroundColor = KSGRD::Style->backgroundColor(); } SignalPlotter::~SignalPlotter() { for ( double* p = mBeamData.first(); p; p = mBeamData.next() ) delete [] p; } bool SignalPlotter::addBeam( const TQColor &color ) { double* d = new double[ mSamples ]; memset( d, 0, sizeof(double) * mSamples ); mBeamData.append( d ); mBeamColor.append( color ); return true; } void SignalPlotter::addSample( const TQValueList& sampleBuf ) { if ( mBeamData.count() != sampleBuf.count() ) return; double* d; if ( mUseAutoRange ) { double sum = 0; for ( d = mBeamData.first(); d; d = mBeamData.next() ) { sum += d[ 0 ]; if ( sum < mMinValue ) mMinValue = sum; if ( sum > mMaxValue ) mMaxValue = sum; } } /* If the vertical lines are scrolling, increment the offset * so they move with the data. The vOffset / hScale confusion * is because v refers to Vertical Lines, and h to the horizontal * distance between the vertical lines. */ if ( mVerticalLinesScroll ) { mVerticalLinesOffset = ( mVerticalLinesOffset + mHorizontalScale) % mVerticalLinesDistance; } // Shift data buffers one sample down and insert new samples. TQValueList::ConstIterator s; for ( d = mBeamData.first(), s = sampleBuf.begin(); d; d = mBeamData.next(), ++s ) { memmove( d, d + 1, ( mSamples - 1 ) * sizeof( double ) ); d[ mSamples - 1 ] = *s; } update(); } void SignalPlotter::reorderBeams( const TQValueList& newOrder ) { if(newOrder.count() != mBeamData.count()) { kdDebug() << "Serious problem in move sample" << endl; return; } TQPtrList newBeamData; TQValueList newBeamColor; for(uint i = 0; i < newOrder.count(); i++) { int newIndex = newOrder[i]; newBeamData.append(mBeamData.at(newIndex)); newBeamColor.append(*mBeamColor.at(newIndex)); } mBeamData = newBeamData; mBeamColor = newBeamColor; } void SignalPlotter::changeRange( int beam, double min, double max ) { // Only the first beam affects range calculation. if ( beam > 1 ) return; mMinValue = min; mMaxValue = max; } TQValueList &SignalPlotter::beamColors() { return mBeamColor; } void SignalPlotter::removeBeam( uint pos ) { mBeamColor.remove( mBeamColor.at( pos ) ); double *p = mBeamData.take( pos ); delete [] p; } void SignalPlotter::setTitle( const TQString &title ) { mTitle = title; } TQString SignalPlotter::title() const { return mTitle; } void SignalPlotter::setUseAutoRange( bool value ) { mUseAutoRange = value; } bool SignalPlotter::useAutoRange() const { return mUseAutoRange; } void SignalPlotter::setMinValue( double min ) { mMinValue = min; } double SignalPlotter::minValue() const { return ( mUseAutoRange ? 0 : mMinValue ); } void SignalPlotter::setMaxValue( double max ) { mMaxValue = max; } double SignalPlotter::maxValue() const { return ( mUseAutoRange ? 0 : mMaxValue ); } void SignalPlotter::setGraphStyle( uint style ) { mGraphStyle = style; } uint SignalPlotter::graphStyle() const { return mGraphStyle; } void SignalPlotter::setHorizontalScale( uint scale ) { if (scale == mHorizontalScale) return; mHorizontalScale = scale; if (isVisible()) updateDataBuffers(); } int SignalPlotter::horizontalScale() const { return mHorizontalScale; } void SignalPlotter::setShowVerticalLines( bool value ) { mShowVerticalLines = value; } bool SignalPlotter::showVerticalLines() const { return mShowVerticalLines; } void SignalPlotter::setVerticalLinesColor( const TQColor &color ) { mVerticalLinesColor = color; } TQColor SignalPlotter::verticalLinesColor() const { return mVerticalLinesColor; } void SignalPlotter::setVerticalLinesDistance( int distance ) { mVerticalLinesDistance = distance; } int SignalPlotter::verticalLinesDistance() const { return mVerticalLinesDistance; } void SignalPlotter::setVerticalLinesScroll( bool value ) { mVerticalLinesScroll = value; } bool SignalPlotter::verticalLinesScroll() const { return mVerticalLinesScroll; } void SignalPlotter::setShowHorizontalLines( bool value ) { mShowHorizontalLines = value; } bool SignalPlotter::showHorizontalLines() const { return mShowHorizontalLines; } void SignalPlotter::setHorizontalLinesColor( const TQColor &color ) { mHorizontalLinesColor = color; } TQColor SignalPlotter::horizontalLinesColor() const { return mHorizontalLinesColor; } void SignalPlotter::setHorizontalLinesCount( int count ) { mHorizontalLinesCount = count; } int SignalPlotter::horizontalLinesCount() const { return mHorizontalLinesCount; } void SignalPlotter::setShowLabels( bool value ) { mShowLabels = value; } bool SignalPlotter::showLabels() const { return mShowLabels; } void SignalPlotter::setShowTopBar( bool value ) { mShowTopBar = value; } bool SignalPlotter::showTopBar() const { return mShowTopBar; } void SignalPlotter::setFontSize( int size ) { mFontSize = size; } int SignalPlotter::fontSize() const { return mFontSize; } void SignalPlotter::setBackgroundColor( const TQColor &color ) { mBackgroundColor = color; } TQColor SignalPlotter::backgroundColor() const { return mBackgroundColor; } void SignalPlotter::resizeEvent( TQResizeEvent* ) { Q_ASSERT( width() > 2 ); updateDataBuffers(); } void SignalPlotter::updateDataBuffers() { /* Since the data buffers for the beams are equal in size to the * width of the widget minus 2 we have to enlarge or shrink the * buffers accordingly when a resize occures. To have a nicer * display we try to keep as much data as possible. Data that is * lost due to shrinking the buffers cannot be recovered on * enlarging though. */ /* Determine new number of samples first. * +0.5 to ensure rounding up * +2 for extra data points so there is * 1) no wasted space and * 2) no loss of precision when drawing the first data point. */ uint newSampleNum = static_cast( ( ( width() - 2 ) / mHorizontalScale ) + 2.5 ); // overlap between the old and the new buffers. int overlap = kMin( mSamples, newSampleNum ); for ( uint i = 0; i < mBeamData.count(); ++i ) { double* nd = new double[ newSampleNum ]; // initialize new part of the new buffer if ( newSampleNum > (uint)overlap ) memset( nd, 0, sizeof( double ) * ( newSampleNum - overlap ) ); // copy overlap from old buffer to new buffer memcpy( nd + ( newSampleNum - overlap ), mBeamData.at( i ) + ( mSamples - overlap ), overlap * sizeof( double ) ); double *p = mBeamData.take( i ); delete [] p; mBeamData.insert( i, nd ); } mSamples = newSampleNum; } void SignalPlotter::paintEvent( TQPaintEvent* ) { uint w = width(); uint h = height(); /* Do not do tqrepaints when the widget is not yet setup properly. */ if ( w <= 2 ) return; TQPixmap pm( w, h ); TQPainter p; p.begin( &pm, this ); pm.fill( mBackgroundColor ); /* Draw white line along the bottom and the right side of the * widget to create a 3D like look. */ p.setPen( TQColor( tqcolorGroup().light() ) ); if(mShowThinFrame) { p.drawLine( 0, h - 1, w - 1, h - 1 ); p.drawLine( w - 1, 0, w - 1, h - 1 ); h--; w--; p.setClipRect( 0, 0, w, h ); } double range = mMaxValue - mMinValue; /* If the range is too small we will force it to 1.0 since it * looks a lot nicer. */ if ( range < 0.000001 ) range = 1.0; double minValue = mMinValue; if ( mUseAutoRange ) { if ( mMinValue != 0.0 ) { double dim = pow( 10, floor( log10( fabs( mMinValue ) ) ) ) / 2; if ( mMinValue < 0.0 ) minValue = dim * floor( mMinValue / dim ); else minValue = dim * ceil( mMinValue / dim ); range = mMaxValue - minValue; if ( range < 0.000001 ) range = 1.0; } // Massage the range so that the grid shows some nice values. double step = range / (mHorizontalLinesCount+1); double dim = pow( 10, floor( log10( step ) ) ) / 2; range = dim * ceil( step / dim ) * (mHorizontalLinesCount+1); } double maxValue = minValue + range; int top = 0; if ( mShowTopBar && h > ( mFontSize/*top bar size*/ + 2/*padding*/ +5/*smallest reasonable size for a graph*/ ) ) { /* Draw horizontal bar with current sensor values at top of display. */ p.setPen( mHorizontalLinesColor ); int x0 = w / 2; p.setFont( TQFont( p.font().family(), mFontSize ) ); top = p.fontMetrics().height(); h -= top; int h0 = top - 2; // h0 is our new top. It's at least 5 pixels high p.drawText(0, 0, x0, top - 2, Qt::AlignCenter, mTitle ); p.drawLine( x0 - 1, 1, x0 - 1, h0 ); p.drawLine( 0, top - 1, w - 2, top - 1 ); double bias = -minValue; double scaleFac = ( w - x0 - 2 ) / range; TQValueList::Iterator col; col = mBeamColor.begin(); for ( double* d = mBeamData.first(); d; d = mBeamData.next(), ++col ) { int start = x0 + (int)( bias * scaleFac ); int end = x0 + (int)( ( bias += d[ mSamples - 1 ] ) * scaleFac ); /* If the rect is wider than 2 pixels we draw only the last * pixels with the bright color. The rest is painted with * a 50% darker color. */ if ( end - start > 1 ) { p.setPen( (*col).dark( 150 ) ); p.setBrush( (*col).dark( 150 ) ); p.drawRect( start, 1, end - start, h0 ); p.setPen( *col ); p.drawLine( end, 1, end, h0 ); } else if ( start - end > 1 ) { p.setPen( (*col).dark( 150 ) ); p.setBrush( (*col).dark( 150 ) ); p.drawRect( end, 1, start - end, h0 ); p.setPen( *col ); p.drawLine( end, 1, end, h0 ); } else { p.setPen( *col ); p.drawLine( start, 1, start, h0 ); } } } /* Draw scope-like grid vertical lines */ if ( mShowVerticalLines && w > 60 ) { p.setPen( mVerticalLinesColor ); for ( uint x = mVerticalLinesOffset; x < ( w - 2 ); x += mVerticalLinesDistance ) p.drawLine( w - x, top, w - x, h + top - 2 ); } /* In autoRange mode we determine the range and plot the values in * one go. This is more efficiently than running through the * buffers twice but we do react on recently discarded samples as * well as new samples one plot too late. So the range is not * correct if the recently discarded samples are larger or smaller * than the current extreme values. But we can probably live with * this. */ if ( mUseAutoRange ) mMinValue = mMaxValue = 0.0; /* Plot stacked values */ double scaleFac = ( h - 2 ) / range; if ( mGraphStyle == GRAPH_ORIGINAL ) { int xPos = 0; for ( int i = 0; i < mSamples; i++, xPos += mHorizontalScale ) { double bias = -minValue; TQValueList::Iterator col; col = mBeamColor.begin(); double sum = 0.0; for ( double* d = mBeamData.first(); d; d = mBeamData.next(), ++col ) { if ( mUseAutoRange ) { sum += d[ i ]; if ( sum < mMinValue ) mMinValue = sum; if ( sum > mMaxValue ) mMaxValue = sum; } int start = top + h - 2 - (int)( bias * scaleFac ); int end = top + h - 2 - (int)( ( bias + d[ i ] ) * scaleFac ); bias += d[ i ]; /* If the line is longer than 2 pixels we draw only the last * 2 pixels with the bright color. The rest is painted with * a 50% darker color. */ if ( end - start > 2 ) { p.fillRect( xPos, start, mHorizontalScale, end - start - 1, (*col).dark( 150 ) ); p.fillRect( xPos, end - 1, mHorizontalScale, 2, *col ); } else if ( start - end > 2 ) { p.fillRect( xPos, start, mHorizontalScale, end - start + 1, (*col).dark( 150 ) ); p.fillRect( xPos, end + 1, mHorizontalScale, 2, *col ); } else p.fillRect( xPos, start, mHorizontalScale, end - start, *col ); } } } else if ( mGraphStyle == GRAPH_POLYGON ) { int *prevVals = new int[ mBeamData.count() ]; int hack[ 4 ]; hack[ 0 ] = hack[ 1 ] = hack[ 2 ] = hack[ 3 ] = 0; int x1 = w - ( ( mSamples + 1 ) * mHorizontalScale ); for ( int i = 0; i < mSamples; i++ ) { TQValueList::Iterator col; col = mBeamColor.begin(); double sum = 0.0; int y = top + h - 2; int oldY = top + h; int oldPrevY = oldY; int height = 0; int j = 0; int jMax = mBeamData.count() - 1; x1 += mHorizontalScale; int x2 = x1 + mHorizontalScale; for ( double* d = mBeamData.first(); d; d = mBeamData.next(), ++col, j++ ) { if ( mUseAutoRange ) { sum += d[ i ]; if ( sum < mMinValue ) mMinValue = sum; if ( sum > mMaxValue ) mMaxValue = sum; } height = (int)( ( d[ i ] - minValue ) * scaleFac ); y -= height; /* If the line is longer than 2 pixels we draw only the last * 2 pixels with the bright color. The rest is painted with * a 50% darker color. */ TQPen lastPen = TQPen( p.pen() ); p.setPen( (*col).dark( 150 ) ); p.setBrush( (*col).dark( 150 ) ); TQPointArray pa( 4 ); int prevY = ( i == 0 ) ? y : prevVals[ j ]; pa.putPoints( 0, 1, x1, prevY ); pa.putPoints( 1, 1, x2, y ); pa.putPoints( 2, 1, x2, oldY ); pa.putPoints( 3, 1, x1, oldPrevY ); p.drawPolygon( pa ); p.setPen( lastPen ); if ( jMax == 0 ) { // draw as normal, no deferred drawing req'd. p.setPen( *col ); p.drawLine( x1, prevY, x2, y ); } else if ( j == jMax ) { // draw previous values and current values p.drawLine( hack[ 0 ], hack[ 1 ], hack[ 2 ], hack[ 3 ] ); p.setPen( *col ); p.drawLine( x1, prevY, x2, y ); } else if ( j == 0 ) { // save values only hack[ 0 ] = x1; hack[ 1 ] = prevY; hack[ 2 ] = x2; hack[ 3 ] = y; p.setPen( *col ); } else { p.drawLine( hack[ 0 ], hack[ 1 ], hack[ 2 ], hack[ 3 ] ); hack[ 0 ] = x1; hack[ 1 ] = prevY; hack[ 2 ] = x2; hack[ 3 ] = y; p.setPen( *col ); } prevVals[ j ] = y; oldY = y; oldPrevY = prevY; } } delete[] prevVals; } /* Draw horizontal lines and values. Lines are always drawn. * Values are only draw when width is greater than 60 */ if ( mShowHorizontalLines ) { p.setPen( mHorizontalLinesColor ); p.setFont( TQFont( p.font().family(), mFontSize ) ); TQString val; /* top = 0 or font.height depending on whether there's a topbar or not * h = graphing area.height - i.e. the actual space we have to draw inside * * Note we are drawing from 0,0 as the top left corner. So we have to add on top to get to the top of where we are drawing * so top+h is the height of the widget */ for ( uint y = 1; y <= mHorizontalLinesCount; y++ ) { int y_coord = top + (y * h) / (mHorizontalLinesCount+1); //Make sure it's y*h first to avoid rounding bugs p.drawLine( 0, y_coord, w - 2, y_coord ); if ( mShowLabels && h > ( mFontSize + 1 ) * ( mHorizontalLinesCount + 1 ) && w > 60 ) { val = TQString::number(maxValue - (y * range) / (mHorizontalLinesCount+1 ) ); p.drawText( 6, y_coord - 1, val ); //draw the text one pixel raised above the line } } //Draw the bottom most (minimum) number as well if ( mShowLabels && h > ( mFontSize + 1 ) * ( mHorizontalLinesCount + 1 ) && w > 60 ) { val = TQString::number( minValue ); p.drawText( 6, top + h - 2, val ); } } p.end(); bitBlt( this, 0, 0, &pm ); } #include "SignalPlotter.moc"