/* 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! $Id: SignalPlotter.cc,v 1.7 2004/01/04 13:43:48 waba Exp $ */ #include #include #include #include #include #include #include "signalplotter.h" static inline int min( int a, int b ) { return ( a < b ? a : b ); } SignalPlotter::SignalPlotter( TQWidget *parent, const char *name ) : TQDialog( parent, name ), mPosInitialized( false ), mName( name ) { // Auto deletion does not work for pointer to arrays. mBeamData.setAutoDelete( false ); setBackgroundMode( NoBackground ); 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 = TQColor( 0x04FB1D ); mVerticalLinesDistance = 30; mVerticalLinesScroll = true; mVerticalLinesOffset = 0; mHorizontalScale = 1; mShowHorizontalLines = true; mHorizontalLinesColor = TQColor( 0x04FB1D ); mHorizontalLinesCount = 5; mShowLabels = true; mShowTopBar = false; mFontSize = 8; mBackgroundColor = TQColor( 0x313031 ); // Restore window size and position. KConfig* config = new KConfig( "knemorc", false ); if ( config->hasGroup( "Interface_" + mName ) ) { config->setGroup( "Interface_" + mName ); if ( config->hasKey( "PlotterX" ) && config->hasKey( "PlotterY" ) ) { mPos.setX( config->readNumEntry( "PlotterX" ) ); mPos.setY( config->readNumEntry( "PlotterY" ) ); mPosInitialized = true; } if ( config->hasKey( "PlotterWidth" ) && config->hasKey( "PlotterHeight" ) ) resize( config->readNumEntry( "PlotterWidth" ), config->readNumEntry( "PlotterHeight" ) ); } delete config; } SignalPlotter::~SignalPlotter() { for ( double* p = mBeamData.first(); p; p = mBeamData.next() ) delete [] p; // Store window size and position. KConfig* config = new KConfig( "knemorc", false ); if ( config->hasGroup( "Interface_" + mName ) ) { config->setGroup( "Interface_" + mName ); config->writeEntry( "PlotterX", x() ); config->writeEntry( "PlotterY", y() ); config->writeEntry( "PlotterWidth", width() ); config->writeEntry( "PlotterHeight", height() ); config->sync(); } delete config; } void SignalPlotter::hide() { mPos = pos(); mPosInitialized = true; TQDialog::hide(); } void SignalPlotter::show() { TQDialog::show(); /** * mPosInitialized should always be true, except when * starting KNemo for the very first time. */ if ( mPosInitialized ) move( mPos ); } 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 toQt::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::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 ) ); mBeamData.remove( pos ); } 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(); } uint 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 = min( 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 ) ); mBeamData.remove( i ); mBeamData.insert( i, nd ); } mSamples = newSampleNum; } void SignalPlotter::paintEvent( TQPaintEvent* ) { uint w = width(); uint h = height(); /* Do not do repaints when the widget is not yet setup properly. */ if ( w <= 2 ) return; TQPixmap pm( w, h ); TQPainter p; p.tqbegin( TQT_TQPAINTDEVICE(&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() ) ); p.drawLine( 0, h - 1, w - 1, h - 1 ); p.drawLine( w - 1, 0, w - 1, h - 1 ); p.setClipRect( 1, 1, w - 2, h - 2 ); 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; double dim = pow( 10, floor( log10( step ) ) ) / 2; range = dim * ceil( step / dim ) * mHorizontalLinesCount; } double maxValue = minValue + range; int top = 0; if ( mShowTopBar && h > ( mFontSize + 2 + mHorizontalLinesCount * 10 ) ) { /* 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; // JJ 2005-07-18: show numerical in/out values in the top bar ---> double *d1 = mBeamData.first(); double UploadSpeed = 0; if(d1) UploadSpeed = d1[ w - 3 ]; // read value from graph data double *d2 = mBeamData.next(); double DownloadSpeed = 0; if(d2) DownloadSpeed = d2[ w - 3 ]; // read value from graph data // The left side of the top bar is now divided into three sections: // - name of interface (original title) // - download speed (numerically, from the value shown by the bar on the right) // - upload speed (numerically, from the value shown by the bar on the right) // title p.drawText(0, 0, x0/3, top - 2, TQt::AlignCenter, mTitle ); TQValueList::Iterator col_speeds; col_speeds = mBeamColor.begin(); TQColor UploadColor = *(col_speeds++); TQColor DownloadColor = *(col_speeds); // download speed TQString DownloadSpeedText; DownloadSpeedText.sprintf("in: %0.2f KB/s", DownloadSpeed); p.setPen( DownloadColor ); p.drawText(x0/3, 0, x0/3, top - 2, TQt::AlignCenter, DownloadSpeedText ); // upload speed TQString UploadSpeedText; UploadSpeedText.sprintf("out: %0.2f KB/s", UploadSpeed); p.setPen( UploadColor ); p.drawText(2*x0/3, 0, x0/3, top - 2, TQt::AlignCenter, UploadSpeedText ); // restore correct pen color for the separator lines p.setPen( mHorizontalLinesColor ); // <--- JJ 2005-07-18 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[ w - 3 ] ) * 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 ] = { 0, 0, 0, 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 drawn when the * height is greater than 10 times hCount + 1, values are shown * when width is greater than 60 */ if ( mShowHorizontalLines && h > ( 10 * ( mHorizontalLinesCount + 1 ) ) ) { p.setPen( mHorizontalLinesColor ); p.setFont( TQFont( p.font().family(), mFontSize ) ); TQString val; for ( uint y = 1; y < mHorizontalLinesCount; y++ ) { p.drawLine( 0, top + y * ( h / mHorizontalLinesCount ), w - 2, top + y * ( h / mHorizontalLinesCount ) ); if ( mShowLabels && h > ( mFontSize + 1 ) * ( mHorizontalLinesCount + 1 ) && w > 60 ) { val = TQString( "%1" ).tqarg( maxValue - y * ( range / mHorizontalLinesCount ) ); p.drawText( 6, top + y * ( h / mHorizontalLinesCount ) - 1, val ); } } if ( mShowLabels && h > ( mFontSize + 1 ) * ( mHorizontalLinesCount + 1 ) && w > 60 ) { val = TQString( "%1" ).tqarg( minValue ); p.drawText( 6, top + h - 2, val ); } } p.end(); bitBlt( this, 0, 0, &pm ); } #include "signalplotter.moc"