/******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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, see <http://www.gnu.org/licenses/> ******************************************************************************/ package spectrum; import java.awt.Color; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.geom.Line2D; import java.awt.image.ColorModel; import java.awt.image.MemoryImageSource; import java.text.DecimalFormat; import javax.swing.JPanel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import settings.ColorSetting; import settings.ColorSetting.ColorSettingName; import settings.Setting; import settings.SettingChangeListener; import settings.SettingsManager; public class WaterfallPanel extends JPanel implements DFTResultsListener, Pausable, SettingChangeListener { private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger( WaterfallPanel.class ); private static DecimalFormat CURSOR_FORMAT = new DecimalFormat( "0.00000" ); private static final String PAUSED = "PAUSED"; private byte[] mPixels; private byte[] mPausedPixels; private int mDFTSize = 4096; private int mImageHeight = 700; private MemoryImageSource mMemoryImageSource; private ColorModel mColorModel = WaterfallColorModel.getDefaultColorModel(); private Color mColorSpectrumCursor; private Image mWaterfallImage; private Point mCursorLocation = new Point( 0, 0 ); private boolean mCursorVisible = false; private long mCursorFrequency = 0; private boolean mPaused = false; private int mZoom = 0; private int mDFTZoomWindowOffset = 0; private SettingsManager mSettingsManager; /** * Displays a scrolling window of multiple DFT frequency bin outputs over * time. Maps DFT frequency bin decibel values into a 256 bucket color map * for display. * * @param settingsManager */ public WaterfallPanel( SettingsManager settingsManager ) { super(); mSettingsManager = settingsManager; mSettingsManager.addListener( this ); mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR ); reset(); } /** * Prepares this instance for disposal */ public void dispose() { if( mSettingsManager != null ) { mSettingsManager.removeListener( this ); } mSettingsManager = null; mMemoryImageSource = null; } /** * Resets the memory image source and byte backing array when the DFT point * size has changed */ private void reset() { mPixels = new byte[ mDFTSize * mImageHeight ]; mMemoryImageSource = new MemoryImageSource( mDFTSize, mImageHeight, mColorModel, mPixels, 0, mDFTSize ); mMemoryImageSource.setAnimated( true ); mWaterfallImage = createImage( mMemoryImageSource ); repaint(); } /** * Pausable interface - pauses updates to the waterfall */ public void setPaused( boolean paused ) { if( paused ) { mPausedPixels = mPixels.clone(); } mPaused = paused; repaint(); } /** * Returns current pause state * @return true if paused, false otherwise */ public boolean isPaused() { return mPaused; } /** * Sets the current zoom level (2^zoom) * * 0 No Zoom * 1 2x Zoom * 2 4x Zoom * 3 8x Zoom * 4 16x Zoom * 5 32x Zoom * 6 64x Zoom * * @param zoom level, 0 - 6. * @param offset into the DFT bins for display */ public void setZoom( int zoom ) { mZoom = zoom; } /** * Multiplier for the current zoom level */ private int getZoomMultiplier() { return (int)Math.pow( 2.0, mZoom ); } /** * Sets the zoom window offset from zero * @param offset in DFT bins */ public void setZoomWindowOffset( int offset ) { mDFTZoomWindowOffset = offset; } /** * Fetches a named color setting from the settings manager. If the setting * doesn't exist, creates the setting using the defaultColor */ private Color getColor( ColorSettingName name ) { ColorSetting setting = mSettingsManager.getColorSetting( name ); return setting.getColor(); } /** * Monitors for setting changes. Colors can be changed by external actions * and will automatically update in this class */ @Override public void settingChanged( Setting setting ) { if( setting instanceof ColorSetting ) { ColorSetting colorSetting = (ColorSetting)setting; if( ( (ColorSetting) setting ).getColorSettingName() == ColorSettingName.SPECTRUM_CURSOR ) { mColorSpectrumCursor = colorSetting.getColor(); } } } @Override public void settingDeleted( Setting setting ) { /* Not implemented */ } /** * Sets the display location of the cursor. Cursor location monitoring is * handled external to this class. * * @param point */ public void setCursorLocation( Point point ) { mCursorLocation = point; repaint(); } /** * Sets the current cursor display frequency. Cursor location frequency * monitoring is handled external to this class. * * @param frequency */ public void setCursorFrequency( long frequency ) { mCursorFrequency = frequency; } /** * Toggles the visibility of the cursor * @param visible */ public void setCursorVisible( boolean visible ) { mCursorVisible = visible; repaint(); } /** * Calculates the x-axis pixel offset from zero where to start rendering the * waterfall image * * @param multiplier - current zoom multiplier * @return x-axis pixel offset */ private double getPixelOffset( int multiplier ) { double offset = 0; if( mZoom != 0 ) { double binPixelWidth = getBinPixelWidth( multiplier ); offset = -binPixelWidth * (double)( mDFTZoomWindowOffset ); } return offset; } private double getBinPixelWidth( int multiplier ) { return ( (double)getWidth() * (double)multiplier ) / (double)mDFTSize; } /** * Renders the screen at each refresh */ public void paintComponent( Graphics g ) { super.paintComponent( g ); int multiplier = getZoomMultiplier(); double binPixelWidth = getBinPixelWidth( multiplier ); int offset = (int)( getPixelOffset( multiplier ) - binPixelWidth ); g.drawImage( mWaterfallImage, offset, 0, ( getWidth() * multiplier ) + (int)binPixelWidth, mImageHeight, this ); Graphics2D graphics = (Graphics2D) g; graphics.setColor( mColorSpectrumCursor ); if( mCursorVisible ) { graphics.draw( new Line2D.Float( mCursorLocation.x, 0, mCursorLocation.x, (float)(getSize().getHeight() ) ) ); String frequency = CURSOR_FORMAT.format( mCursorFrequency / 1000000.0D ); graphics.drawString( frequency , mCursorLocation.x + 5, mCursorLocation.y ); } if( mPaused ) { graphics.drawString( PAUSED, 20, 20 ); } paintZoomIndicator( graphics ); graphics.dispose(); } /** * When zoom level is greater than zero, paints a small indicator at the * bottom center of the screen showing the location of the zoom window * within the overall DFT results window */ private void paintZoomIndicator( Graphics2D graphics ) { if( mZoom != 0 ) { int width = getWidth() / 4; int x = ( getWidth() / 2 ) - ( width / 2 ); //Draw the outer window graphics.drawRect( x, getHeight() - 12, width, 10 ); int zoomWidth = width / getZoomMultiplier(); int windowOffset = 0; if( mDFTZoomWindowOffset != 0 ) { windowOffset = (int)( ( (double)mDFTZoomWindowOffset / (double)mDFTSize ) * width ); } //Draw the zoom window graphics.fillRect( x + windowOffset, getHeight() - 12, zoomWidth, 10 ); //Draw the zoom text graphics.drawString( "Zoom: " + getZoomMultiplier() + "x", x + width + 3, getHeight() - 2 ); } } /** * Implements the DFT results listener interface method. This is the * primary method for receiving new frequency bin results. */ @Override public void receive( float[] update ) { //If our FFT size changes, reset our pixel map and image source if( mDFTSize != update.length ) { mDFTSize = update.length; reset(); } //Move the pixels down a row to make room for the new results System.arraycopy( mPixels, 0, mPixels, mDFTSize, mPixels.length - mDFTSize ); /** * Find the average value and scale the display to it */ double sum = 0.0d; for( int x = 0; x < update.length - 1; x++ ) { sum += update[ x ]; } float average = (float)( sum / (double)update.length - 1 ); float scale = 256.0f / average; for( int x = 0; x < update.length - 1; x++ ) { float value = ( average - update[ x ] ) * scale; if( value < 0 ) { mPixels[ x ] = 0; } else if( value > 255 ) { mPixels[ x ] = (byte)255; } else { mPixels[ x ] = (byte)value; } } //Task the swing event thread to update the display EventQueue.invokeLater( new Runnable() { @Override public void run() { if( mMemoryImageSource != null ) { if( mPaused ) { mMemoryImageSource.newPixels( mPausedPixels, mColorModel, 0, mDFTSize ); } else { mMemoryImageSource.newPixels( mPixels, mColorModel, 0, mDFTSize ); } } } } ); } }