* Mouse scrolling and zooming are supported and the waterfall display can * be paused. *
* Complex sample buffers are processed by a DFTProcessor and the output of * the DFT is translated to decibels for display in the spectrum and * waterfall components. */ public SpectralDisplayPanel(PlaylistManager playlistManager, SettingsManager settingsManager, DiscoveredTunerModel discoveredTunerModel) { mChannelModel = playlistManager.getChannelModel(); mChannelProcessingManager = playlistManager.getChannelProcessingManager(); mSettingsManager = settingsManager; mDiscoveredTunerModel = discoveredTunerModel; mSpectrumPanel = new SpectrumPanel(mSettingsManager); mOverlayPanel = new OverlayPanel(mSettingsManager, mChannelModel, mChannelProcessingManager); mWaterfallPanel = new WaterfallPanel(mSettingsManager); init(); loadSettings(); } private void loadSettings() { SystemProperties properties = SystemProperties.getInstance(); String rawSize = properties.get(FFT_SIZE_PROPERTY, DFTSize.FFT04096.name()); DFTSize size = null; if(rawSize != null) { try { size = DFTSize.valueOf(rawSize); } catch(Exception e) { //Do nothing } } if(size == null) { size = DFTSize.FFT04096; } setDFTSize(size, false); } public void dispose() { /* De-register from receiving samples when the window closes */ clearTuner(); mSettingsManager = null; mComplexDftProcessor.dispose(); mComplexDftProcessor = null; mDFTConverter.dispose(); mDFTConverter = null; mSpectrumPanel.dispose(); mSpectrumPanel = null; mWaterfallPanel.dispose(); mWaterfallPanel = null; mOverlayPanel.dispose(); mOverlayPanel = null; mTuner = null; } /** * Queues an FFT size change request. The scheduled executor will apply * the change when it runs. */ public void setDFTSize(DFTSize size, boolean save) { mComplexDftProcessor.setDFTSize(size); mOverlayPanel.setDFTSize(size); mDFTSize = size; if(save) { SystemProperties.getInstance().set(FFT_SIZE_PROPERTY, size.name()); } setZoom(0, 0, 0); } public void setDFTSize(DFTSize size) { setDFTSize(size, true); } @Override public DFTSize getDFTSize() { return mDFTSize; } public int getZoom() { return mZoom; } /** * Sets the current zoom level which will be 2 to the power of zoom (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 frequency under the mouse to maintain while zooming
* @param windowOffset where to maintain the frequency under the mouse
*/
public void setZoom(int zoom, long frequency, double windowOffset)
{
if(zoom < NO_ZOOM)
{
zoom = NO_ZOOM;
}
else if(zoom > MAX_ZOOM)
{
zoom = MAX_ZOOM;
}
if(zoom != mZoom)
{
mZoom = zoom;
//Calculate the bin offset that would place the reference frequency
//at the left edge of the zoom window.
double binOffsetToFrequency = getBinOffset(frequency);
//Calculate the bin offset into the newly sized zoom window that
//would place the frequency in the same proportional window location
//that it was in the previous zoom size
double windowBinOffset = (double)getZoomWindowSizeInBins() * windowOffset;
//Set the overall offset to place the reference frequency in the
//same location in the newly zoomed window
double offset = binOffsetToFrequency - windowBinOffset;
mSpectrumPanel.setZoom(mZoom);
mOverlayPanel.setZoom(mZoom);
mWaterfallPanel.setZoom(mZoom);
setZoomWindowOffset(offset);
}
}
/**
* Sets the offset (in DFT bins) to the first bin that the zoom window displays
*/
public void setZoomWindowOffset(double offset)
{
if(offset < 0)
{
offset = 0;
}
if(offset > (mDFTSize.getSize() - getZoomWindowSizeInBins()))
{
offset = mDFTSize.getSize() - getZoomWindowSizeInBins();
}
mDFTZoomWindowOffset = (int)offset;
mSpectrumPanel.setZoomWindowOffset(mDFTZoomWindowOffset);
mOverlayPanel.setZoomWindowOffset(mDFTZoomWindowOffset);
mWaterfallPanel.setZoomWindowOffset(mDFTZoomWindowOffset);
}
/**
* Calculates the size of the current zoom window in DFT bins
*/
private int getZoomWindowSizeInBins()
{
return mDFTSize.getSize() / getZoomMultiplier();
}
public int getZoomMultiplier()
{
return (int)FastMath.pow(2.0, mZoom);
}
/**
* Calculates the overall offset of the frequency from the current minimum
* frequency in terms of total FFT width
*
* @param frequency
* @return
*/
private double getBinOffset(long frequency)
{
double offset = 0.0;
if(mOverlayPanel.containsFrequency(frequency))
{
offset = (double)mDFTSize.getSize() * ((double)(frequency - mOverlayPanel.getMinFrequency())
/ (double)mOverlayPanel.getBandwidth());
}
return offset;
}
/**
* Overrides JComponent method to return false, since we have overlapping
* panels with the spectrum and channel panels
*/
public boolean isOptimizedDrawingEnabled()
{
return false;
}
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0", "[grow]", "[grow]"));
/**
* The layered pane holds the overlapping spectrum and channel panels
* and manages the sizing of each panel with the resize listener
*/
mLayeredPanel = new JLayeredPane();
mLayeredPanel.addComponentListener(new ResizeListener());
/**
* Create a mouse adapter to handle mouse events over the spectrum
* and waterfall panels
*/
MouseEventProcessor mouser = new MouseEventProcessor();
mOverlayPanel.addMouseListener(mouser);
mOverlayPanel.addMouseMotionListener(mouser);
mOverlayPanel.addMouseWheelListener(mouser);
//Add the spectrum and channel panels to the layered panel
mLayeredPanel.add(mSpectrumPanel, 0, 0);
mLayeredPanel.add(mOverlayPanel, 1, 0);
//Create the waterfall
mWaterfallPanel.addMouseListener(mouser);
mWaterfallPanel.addMouseMotionListener(mouser);
mWaterfallPanel.addMouseWheelListener(mouser);
/* Attempt to set a 50/50 split preferred size for the split pane */
double totalHeight =
mLayeredPanel.getPreferredSize().getHeight() + mWaterfallPanel.getPreferredSize().getHeight();
mLayeredPanel.setPreferredSize(
new Dimension((int)mLayeredPanel.getPreferredSize().getWidth(), (int)(totalHeight / 2.0d)));
mWaterfallPanel.setPreferredSize(
new Dimension((int)mWaterfallPanel.getPreferredSize().getWidth(), (int)(totalHeight / 2.0d)));
//Create the split pane to hold the layered pane and the waterfall
JideSplitPane splitPane = new JideSplitPane(JSplitPane.VERTICAL_SPLIT);
splitPane.setDividerSize(5);
splitPane.add(mLayeredPanel);
splitPane.add(mWaterfallPanel);
mScrollPane = new JScrollPane(splitPane);
add(mScrollPane, "grow");
/**
* Setup DFTProcessor to process samples and register the waterfall and
* spectrum panel to receive the processed dft results
*/
mComplexDftProcessor = new ComplexDftProcessor();
mDFTConverter = new ComplexDecibelConverter();
mComplexDftProcessor.addConverter(mDFTConverter);
mDFTConverter.addListener((DFTResultsListener)mSpectrumPanel);
mDFTConverter.addListener((DFTResultsListener)mWaterfallPanel);
}
/**
* Receives frequency change events -- primarily from tuner components.
*/
public void process(SourceEvent event)
{
mOverlayPanel.process(event);
}
/**
* Complex sample buffer receive method
*/
@Override public void receive(INativeBuffer nativeBuffer)
{
mComplexDftProcessor.receive(nativeBuffer);
}
/**
* Responds to tuner event by deregistering from the current
* complex sample buffer source and registering with the tuner argument.
*/
public void showTuner(Tuner tuner)
{
clearTuner();
mComplexDftProcessor.clearBuffer();
mComplexDftProcessor.start();
mTuner = tuner;
if(mTuner != null)
{
//Register the dft processor to receive samples from the tuner
mTuner.getTunerController().addBufferListener(mComplexDftProcessor);
//Verify that the tuner is still non-null, in case it encountered an error on starting sample stream
if(mTuner != null)
{
//Register to receive frequency change events
mTuner.getTunerController().addListener(this);
mSpectrumPanel.setSampleSize(mTuner.getSampleSize());
//Fire frequency and sample rate change events so that the spectrum
//and overlay panels can synchronize
process(SourceEvent.frequencyChange(null, mTuner.getTunerController().getFrequency()));
process(SourceEvent.sampleRateChange(mTuner.getTunerController().getSampleRate()));
}
}
}
/**
* Tuner de-selection cleanup method
*/
public void clearTuner()
{
if(mTuner != null)
{
//Deregister for frequency change events from the tuner
mTuner.getTunerController().removeListener(SpectralDisplayPanel.this);
//Deregister the dft processor from receiving samples
mTuner.getTunerController().removeBufferListener(mComplexDftProcessor);
mTuner = null;
}
mComplexDftProcessor.stop();
mComplexDftProcessor.clearBuffer();
mSpectrumPanel.clearSpectrum();
mWaterfallPanel.clearWaterfall();
}
/**
* Currently displayed tuner
*/
public Tuner getTuner()
{
return mTuner;
}
/**
* Monitors the sizing of the layered pane and resizes the spectrum and
* channel panels whenever the layered pane is resized
*/
public class ResizeListener implements ComponentListener
{
@Override public void componentResized(ComponentEvent e)
{
Component c = e.getComponent();
mSpectrumPanel.setBounds(0, 0, c.getWidth(), c.getHeight());
mOverlayPanel.setBounds(0, 0, c.getWidth(), c.getHeight());
}
@Override public void componentHidden(ComponentEvent arg0)
{
}
@Override public void componentMoved(ComponentEvent arg0)
{
}
@Override public void componentShown(ComponentEvent arg0)
{
}
}
/**
* Mouse event handler for the spectral display panel.
*/
public class MouseEventProcessor extends MouseInputAdapter
{
private int mDFTZoomWindowOffsetAtDragStart = 0;
private int mDragStartX = 0;
private double mPixelsPerBin;
public MouseEventProcessor()
{
}
@Override public void mouseWheelMoved(MouseWheelEvent e)
{
int zoom = mZoom - e.getWheelRotation();
long frequency = mOverlayPanel.getFrequencyFromAxis(e.getX());
double windowOffset = (double)e.getX() / (double)getWidth();
setZoom(zoom, frequency, windowOffset);
}
@Override public void mouseMoved(MouseEvent event)
{
update(event);
}
@Override public void mouseDragged(MouseEvent event)
{
update(event);
int dragDistance = mDragStartX - event.getX();
double binDistance = (double)dragDistance / mPixelsPerBin;
int offset = (int)(mDFTZoomWindowOffsetAtDragStart + binDistance);
if(offset < 0)
{
offset = 0;
}
int maxOffset = mDFTSize.getSize() - (mDFTSize.getSize() / getZoomMultiplier());
if(offset > maxOffset)
{
offset = maxOffset;
}
setZoomWindowOffset(offset);
}
@Override public void mousePressed(MouseEvent e)
{
mDragStartX = e.getX();
mDFTZoomWindowOffsetAtDragStart = mDFTZoomWindowOffset;
mPixelsPerBin = (double)getWidth() / ((double)(mDFTSize.getSize()) / (double)getZoomMultiplier());
}
/**
* Updates the cursor display while the mouse is performing actions
*/
private void update(MouseEvent event)
{
if(event.getComponent() == mOverlayPanel)
{
mOverlayPanel.setCursorLocation(event.getPoint());
}
else
{
mWaterfallPanel.setCursorLocation(event.getPoint());
mWaterfallPanel.setCursorFrequency(mOverlayPanel.getFrequencyFromAxis(event.getPoint().x));
}
}
@Override public void mouseEntered(MouseEvent e)
{
if(e.getComponent() == mOverlayPanel)
{
mOverlayPanel.setCursorVisible(true);
}
else
{
mWaterfallPanel.setCursorVisible(true);
}
}
@Override public void mouseExited(MouseEvent e)
{
mOverlayPanel.setCursorVisible(false);
mWaterfallPanel.setCursorVisible(false);
}
/**
* Displays the context menu.
*/
@Override public void mouseClicked(MouseEvent event)
{
if(SwingUtilities.isRightMouseButton(event))
{
JPopupMenu contextMenu = new JPopupMenu();
if(event.getComponent() == mWaterfallPanel)
{
contextMenu.add(new PauseItem(mWaterfallPanel, "Pause"));
contextMenu.add(new JSeparator());
}
long frequency = mOverlayPanel.getFrequencyFromAxis(event.getX());
if(event.getComponent() == mOverlayPanel)
{
ArrayList