Files
sdrtrunk/src/spectrum/OverlayPanel.java

944 lines
26 KiB
Java

/*******************************************************************************
* 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.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
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;
import source.tuner.TunerChannel;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.IFrequencyChangeProcessor;
import controller.channel.Channel;
import controller.channel.Channel.ChannelType;
import controller.channel.ChannelEvent;
import controller.channel.ChannelEventListener;
import controller.channel.ChannelModel;
public class OverlayPanel extends JPanel
implements ChannelEventListener,
IFrequencyChangeProcessor,
SettingChangeListener
{
private static final long serialVersionUID = 1L;
private final static Logger mLog =
LoggerFactory.getLogger( OverlayPanel.class );
private final static RenderingHints RENDERING_HINTS =
new RenderingHints( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON );
static
{
RENDERING_HINTS.put( RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY );
}
private final static BasicStroke DASHED_STROKE = new BasicStroke(0.8f,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 5.0f, new float[] {2.0f, 4.0f}, 0.0f);
private static DecimalFormat CURSOR_FORMAT = new DecimalFormat( "000.00000" );
private long mFrequency = 0;
private int mBandwidth = 0;
private Point mCursorLocation = new Point( 0, 0 );
private boolean mCursorVisible = false;
private DFTSize mDFTSize = DFTSize.FFT04096;
private int mZoom = 0;
private int mDFTZoomWindowOffset = 0;
/**
* Colors used by this component
*/
private Color mColorChannelConfig;
private Color mColorChannelConfigProcessing;
private Color mColorChannelConfigSelected;
private Color mColorSpectrumBackground;
private Color mColorSpectrumCursor;
private Color mColorSpectrumLine;
//Currently visible/displayable channels
private CopyOnWriteArrayList<Channel> mVisibleChannels = new CopyOnWriteArrayList<Channel>();
private ChannelDisplay mChannelDisplay = ChannelDisplay.ALL;
//Defines the offset at the bottom of the spectral display to account for
//the frequency labels
private double mSpectrumInset = 20.0d;
private LabelSizeManager mLabelSizeMonitor = new LabelSizeManager();
private SettingsManager mSettingsManager;
private ChannelModel mChannelModel;
/**
* Translucent overlay panel for displaying channel configurations,
* processing channels, selected channels, frequency labels and lines, and
* a cursor with a frequency readout.
*/
public OverlayPanel( SettingsManager settingsManager,
ChannelModel channelModel )
{
mSettingsManager = settingsManager;
if( mSettingsManager != null )
{
mSettingsManager.addListener( this );
}
mChannelModel = channelModel;
if( mChannelModel != null )
{
mChannelModel.addListener( this );
}
addComponentListener( mLabelSizeMonitor );
//Set the background transparent, so the spectrum display can be seen
setOpaque( false );
//Fetch color settings from settings manager
setColors();
}
public void dispose()
{
if( mChannelModel != null )
{
mChannelModel.removeListener( this );
}
mChannelModel = null;
mVisibleChannels.clear();
if( mSettingsManager != null )
{
mSettingsManager.removeListener( this );
}
mSettingsManager = null;
}
/**
* Sets/changes the DFT bin size
*/
public void setDFTSize( DFTSize size )
{
mDFTSize = size;
}
public ChannelDisplay getChannelDisplay()
{
return mChannelDisplay;
}
public void setChannelDisplay( ChannelDisplay display )
{
mChannelDisplay = display;
}
public void setCursorLocation( Point point )
{
mCursorLocation = point;
repaint();
}
public void setCursorVisible( boolean visible )
{
mCursorVisible = visible;
repaint();
}
/**
* 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 FFT bins for start zoom window
*/
public void setZoom( int zoom )
{
mZoom = zoom;
mLabelSizeMonitor.update();
}
public void setZoomWindowOffset( int offset )
{
mDFTZoomWindowOffset = offset;
}
/**
* Fetches the color settings from the settings manager
*/
private void setColors()
{
mColorChannelConfig = getColor( ColorSettingName.CHANNEL_CONFIG );
mColorChannelConfigProcessing =
getColor( ColorSettingName.CHANNEL_CONFIG_PROCESSING );
mColorChannelConfigSelected =
getColor( ColorSettingName.CHANNEL_CONFIG_SELECTED );
mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR );
mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE );
mColorSpectrumBackground =
getColor( ColorSettingName.SPECTRUM_BACKGROUND );
}
/**
* 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;
switch( colorSetting.getColorSettingName() )
{
case CHANNEL_CONFIG:
mColorChannelConfig = colorSetting.getColor();
break;
case CHANNEL_CONFIG_PROCESSING:
mColorChannelConfigProcessing = colorSetting.getColor();
break;
case CHANNEL_CONFIG_SELECTED:
mColorChannelConfigSelected = colorSetting.getColor();
break;
case SPECTRUM_BACKGROUND:
mColorSpectrumBackground = colorSetting.getColor();
break;
case SPECTRUM_CURSOR:
mColorSpectrumCursor = colorSetting.getColor();
break;
case SPECTRUM_LINE:
mColorSpectrumLine = colorSetting.getColor();
break;
default:
break;
}
}
}
/**
* Renders the channel configs, lines, labels, and cursor
*/
@Override
public void paintComponent( Graphics g )
{
super.paintComponent( g );
Graphics2D graphics = (Graphics2D) g;
graphics.setBackground( mColorSpectrumBackground );
graphics.setRenderingHints( RENDERING_HINTS );
drawFrequencies( graphics );
drawChannels( graphics );
drawCursor( graphics );
}
/**
* Draws a cursor on the panel, whenever the mouse is hovering over the
* panel
*/
private void drawCursor( Graphics2D graphics )
{
if( mCursorVisible )
{
drawFrequencyLine( graphics,
mCursorLocation.x,
mColorSpectrumCursor );
String frequency = CURSOR_FORMAT.format(
getFrequencyFromAxis( mCursorLocation.getX() ) / 1E6D );
graphics.drawString( frequency ,
mCursorLocation.x + 5,
mCursorLocation.y );
if( mZoom != 0 )
{
graphics.drawString( "Zoom: " + (int)Math.pow( 2.0, mZoom ) + "x",
mCursorLocation.x + 17, mCursorLocation.y + 11 );
}
}
}
/**
* Draws the frequency lines and labels every 10kHz
*/
private void drawFrequencies( Graphics2D graphics )
{
Stroke currentStroke = graphics.getStroke();
// graphics.setStroke( DASHED_STROKE );
long minFrequency = getMinDisplayFrequency();
long maxFrequency = getMaxDisplayFrequency();
//Frequency increments for label and tick spacing
int label = mLabelSizeMonitor.getLabelIncrement( graphics );
int major = mLabelSizeMonitor.getMajorTickIncrement( graphics );
int minor = mLabelSizeMonitor.getMinorTickIncrement( graphics );
//Avoid divide by zero error
if( minor == 0 )
{
minor = 1;
}
//Adjust the start frequency to a multiple of the minor tick spacing
long frequency = minFrequency - ( minFrequency % minor );
while( frequency < maxFrequency )
{
if( frequency % label == 0 )
{
drawFrequencyLineAndLabel( graphics, frequency );
}
else if( frequency % major == 0 )
{
drawTickLine( graphics, frequency, true );
}
else
{
drawTickLine( graphics, frequency, false );
}
frequency += minor;
}
// graphics.setStroke( currentStroke );
}
/**
* Draws a vertical line and a corresponding frequency label at the bottom
*/
private void drawFrequencyLineAndLabel( Graphics2D graphics, long frequency )
{
double xAxis = getAxisFromFrequency( frequency );
drawFrequencyLine( graphics, xAxis, mColorSpectrumLine );
drawTickLine( graphics, frequency, false );
graphics.setColor( mColorSpectrumLine );
drawFrequencyLabel( graphics, xAxis, frequency );
}
/**
* Draws a vertical line at the xaxis
*/
private void drawTickLine( Graphics2D graphics, long frequency, boolean major )
{
graphics.setColor( mColorSpectrumLine );
double xAxis = getAxisFromFrequency( frequency );
double start = getSize().getHeight() - mSpectrumInset;
double end = start + ( major ? 9.0d : 3.0d );
graphics.draw( new Line2D.Double( xAxis, start, xAxis, end ) );
}
/**
* Draws a vertical line at the xaxis
*/
private void drawFrequencyLine( Graphics2D graphics, double xaxis, Color color )
{
graphics.setColor( color );
graphics.draw( new Line2D.Double( xaxis, 0.0d,
xaxis, getSize().getHeight() - mSpectrumInset ) );
}
/**
* Draws a vertical line at the xaxis
*/
private void drawChannelCenterLine( Graphics2D graphics, double xaxis )
{
double height = getSize().getHeight() - mSpectrumInset;
graphics.setColor( Color.LIGHT_GRAY );
graphics.draw( new Line2D.Double( xaxis, height * 0.65d,
xaxis, height - 1.0d ) );
}
/**
* Draws a vertical line at the xaxis
*/
private void drawAFC( Graphics2D graphics, double frequencyAxis, double errorAxis )
{
double height = getSize().getHeight() - mSpectrumInset;
double lineHeight = height * 0.75d;
graphics.setColor( Color.YELLOW );
//Vertical frequency error line
graphics.draw( new Line2D.Double( errorAxis, lineHeight,
errorAxis, height - 1.0d ) );
//Horizontal line connecting frequency and error lines
graphics.draw( new Line2D.Double( frequencyAxis, lineHeight,
errorAxis, lineHeight ) );
}
/**
* Returns the x-axis value corresponding to the frequency
*/
private double getAxisFromFrequency( long frequency )
{
double screenWidth = (double)getSize().getWidth();
double pixelsPerBin = screenWidth / (double)mDFTSize.getSize();
double pixelOffsetToMinDisplayFrequency = pixelsPerBin * 2.0d;
//Calculate frequency offset from the min frequency
double frequencyOffset = (double)( frequency - getMinDisplayFrequency() );
//Determine ratio of frequency offset to overall bandwidth
double ratio = frequencyOffset / (double)getDisplayBandwidth();
//Apply the ratio to the screen width minus 1 bin width
double screenOffset = screenWidth * ratio;
return pixelOffsetToMinDisplayFrequency + screenOffset;
}
/**
* Returns the frequency corresponding to the x-axis value using the current
* zoom level.
*/
public long getFrequencyFromAxis( double xAxis )
{
double width = getSize().getWidth();
double offset = xAxis / width;
long frequency = getMinDisplayFrequency() +
Math.round( (double)getDisplayBandwidth() * offset );
if( frequency > ( getMaxFrequency() ) )
{
frequency = getMaxFrequency();
}
return frequency;
}
/**
* Draws a frequency label at the x-axis position, at the bottom of the panel
*/
private void drawFrequencyLabel( Graphics2D graphics,
double xaxis,
long frequency )
{
String label = mLabelSizeMonitor.format( frequency );
FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() );
Rectangle2D rect = fontMetrics.getStringBounds( label, graphics );
float xOffset = (float)rect.getWidth() / 2;
graphics.drawString( label, (float)( xaxis - xOffset ),
(float)( getSize().getHeight() - 2.0f ) );
}
/**
* Draws visible channel configs as translucent shaded frequency regions
*/
private void drawChannels( Graphics2D graphics )
{
for( Channel channel: mVisibleChannels )
{
if( mChannelDisplay == ChannelDisplay.ALL ||
( mChannelDisplay == ChannelDisplay.ENABLED &&
channel.getEnabled() ) )
{
//Choose the correct background color to use
if( channel.isSelected() )
{
graphics.setColor( mColorChannelConfigSelected );
}
else if( channel.getEnabled() )
{
graphics.setColor( mColorChannelConfigProcessing );
}
else
{
graphics.setColor( mColorChannelConfig );
}
TunerChannel tunerChannel = channel.getTunerChannel();
if( tunerChannel != null )
{
double xAxis = getAxisFromFrequency( tunerChannel.getFrequency() );
double width = (double)( tunerChannel.getBandwidth() ) /
(double)getDisplayBandwidth() * getSize().getWidth();
Rectangle2D.Double box =
new Rectangle2D.Double( xAxis - ( width / 2.0d ),
0.0d, width, getSize().getHeight() - mSpectrumInset );
//Fill the box with the correct color
graphics.fill( box );
graphics.draw( box );
//Change to the line color to render the channel name, etc.
graphics.setColor( mColorSpectrumLine );
//Draw the labels starting at yAxis position 0
double yAxis = 0;
//Draw the system label and adjust the y-axis position
String system = channel.hasSystem() ? channel.getSystem() : " ";
yAxis += drawLabel( graphics,
system,
this.getFont(),
xAxis,
yAxis,
width );
//Draw the site label and adjust the y-axis position
String site = channel.hasSite() ? channel.getSite() : " ";
yAxis += drawLabel( graphics,
site,
this.getFont(),
xAxis,
yAxis,
width );
//Draw the channel label and adjust the y-axis position
yAxis += drawLabel( graphics,
channel.getName(),
this.getFont(),
xAxis,
yAxis,
width );
//Draw the decoder label
drawLabel( graphics, channel.getDecodeConfiguration().getDecoderType().getShortDisplayString(),
this.getFont(), xAxis, yAxis, width );
long frequency = tunerChannel.getFrequency();
double frequencyAxis = getAxisFromFrequency( frequency );
drawChannelCenterLine( graphics, frequencyAxis );
/* Draw Automatic Frequency Control line */
int correction = channel.getChannelFrequencyCorrection();
if( correction != 0 )
{
long error = frequency + correction;
drawAFC( graphics, frequencyAxis, getAxisFromFrequency( error ) );
}
}
}
}
}
/**
* Draws a textual label at the x/y position, clipping the end of the text
* to fit within the maxwidth value.
*
* @return height of the drawn label
*/
private double drawLabel( Graphics2D graphics, String text, Font font,
double x, double baseY, double maxWidth )
{
FontMetrics fontMetrics = graphics.getFontMetrics( font );
Rectangle2D label = fontMetrics.getStringBounds( text, graphics );
double offset = label.getWidth() / 2.0d;
double y = baseY + label.getHeight();
/**
* If the label is wider than the max width, left justify the text and
* clip the end of it
*/
if( offset > ( maxWidth / 2.0d ) )
{
label.setRect( x - ( maxWidth / 2.0d ),
y - label.getHeight(),
maxWidth,
label.getHeight() );
graphics.setClip( label );
graphics.drawString( text,
(float)( x - ( maxWidth / 2.0d ) ), (float)y );
graphics.setClip( null );
}
else
{
graphics.drawString( text, (float)( x - offset ), (float)y );
}
return label.getHeight();
}
/**
* Frequency change event handler
*/
@Override
public void frequencyChanged( FrequencyChangeEvent event )
{
switch( event.getEvent() )
{
case NOTIFICATION_SAMPLE_RATE_CHANGE:
mBandwidth = event.getValue().intValue();
mLabelSizeMonitor.update();
break;
case NOTIFICATION_FREQUENCY_CHANGE:
mFrequency = event.getValue().longValue();
mLabelSizeMonitor.update();
break;
default:
break;
}
/**
* Reset the visible channels list
*/
mVisibleChannels.clear();
mVisibleChannels.addAll(
mChannelModel.getChannelsInFrequencyRange( getMinFrequency(),
getMaxFrequency() ) );
}
/**
* Channel change event handler
*/
@Override
@SuppressWarnings( "incomplete-switch" )
public void channelChanged( ChannelEvent event )
{
Channel channel = event.getChannel();
switch( event.getEvent() )
{
case NOTIFICATION_ADD:
case NOTIFICATION_PROCESSING_START:
if( !mVisibleChannels.contains( channel ) &&
channel.isWithin( getMinFrequency(), getMaxFrequency() ) )
{
mVisibleChannels.add( channel );
}
break;
case NOTIFICATION_DELETE:
mVisibleChannels.remove( channel );
break;
case NOTIFICATION_PROCESSING_STOP:
if( channel.getChannelType() == ChannelType.TRAFFIC )
{
mVisibleChannels.remove( channel );
}
break;
case NOTIFICATION_CONFIGURATION_CHANGE:
if( mVisibleChannels.contains( channel ) &&
!channel.isWithin( getMinFrequency(), getMaxFrequency() ) )
{
mVisibleChannels.remove( channel );
}
if( !mVisibleChannels.contains( channel ) &&
channel.isWithin( getMinFrequency(), getMaxFrequency() ) )
{
mVisibleChannels.add( channel );
}
break;
default:
break;
}
repaint();
}
public int getBandwidth()
{
return mBandwidth;
}
/**
* Currently displayed minimum frequency
*/
public long getMinFrequency()
{
return mFrequency - ( mBandwidth / 2 );
}
/**
* Currently displayed maximum frequency
*/
public long getMaxFrequency()
{
return mFrequency + ( mBandwidth / 2 );
}
public boolean containsFrequency( long frequency )
{
return Math.abs( mFrequency - frequency ) <= ( mBandwidth / 2 );
}
private long getMinDisplayFrequency()
{
double bandwidthPerBin = (double)mBandwidth / (double)( mDFTSize.getSize() );
return getMinFrequency() + (int)( ( mDFTZoomWindowOffset ) * bandwidthPerBin );
}
private long getMaxDisplayFrequency()
{
return getMinDisplayFrequency() + getDisplayBandwidth();
}
private int getDisplayBandwidth()
{
if( mZoom != 0 )
{
return mBandwidth / (int)Math.pow( 2.0, mZoom );
}
return mBandwidth;
}
/**
* Returns a list of channel configs that contain the frequency within their
* min/max frequency settings.
*/
public ArrayList<Channel> getChannelsAtFrequency( long frequency )
{
ArrayList<Channel> configs = new ArrayList<Channel>();
for( Channel config: mVisibleChannels )
{
TunerChannel channel = config.getTunerChannel();
if( channel != null &&
channel.getMinFrequency() <= frequency &&
channel.getMaxFrequency() >= frequency )
{
configs.add( config );
}
}
return configs;
}
@Override
public void settingDeleted( Setting setting ) { /* not implemented */ }
/**
* Calculates correct spacing and format for frequency labels and major/minor
* tick lines based on current frequency, bandwidth, zoom and screen size.
*/
public class LabelSizeManager implements ComponentListener
{
private static final double LABEL_FILL_THRESHOLD = 0.5d;
private DecimalFormat mFrequencyFormat = new DecimalFormat( "0.0" );
private boolean mUpdateRequired = true;
private int mLabelIncrement = 1;
private int mMajorTickIncrement = 1;
private int mMinorTickIncrement = 1;
public String format( long frequency )
{
return mFrequencyFormat.format( (double)frequency / 1E6D );
}
private void setPrecision( int precision )
{
if( precision < 1 )
{
precision = 1;
}
if( precision > 5 )
{
precision = 5;
}
mFrequencyFormat.setMinimumFractionDigits( precision );
mFrequencyFormat.setMaximumFractionDigits( precision );
}
private void update( Graphics2D graphics )
{
if( mUpdateRequired )
{
//Set maximum precision as a starting point
setPrecision( 5 );
FontMetrics fontMetrics =
graphics.getFontMetrics( OverlayPanel.this.getFont() );
int maxLabelWidth = fontMetrics.stringWidth( format( getMaxDisplayFrequency() ) );
double maxLabels = ( (double)OverlayPanel.this.getWidth() *
LABEL_FILL_THRESHOLD ) /
(double)maxLabelWidth;
//Calculate the next smallest base 10 value for the major increment
int power = (int)Math.log10( (double)getDisplayBandwidth() / maxLabels );
//Set the number of decimal places to display in frequency labels
int precision = 5 - power;
int start = (int)Math.pow( 10.0, power + 1 );
int minimum = (int)Math.pow( 10.0, power );
int labelIncrement = start;
while( ( (double)getDisplayBandwidth() /
(double)labelIncrement ) < maxLabels &&
labelIncrement >= minimum )
{
labelIncrement /= 2;
precision++;
}
if( labelIncrement == minimum )
{
precision = 5 - power;
}
setPrecision( precision );
mLabelIncrement = labelIncrement;
mMajorTickIncrement = labelIncrement / 2;
mMinorTickIncrement = labelIncrement / 10;
mUpdateRequired = false;
}
}
/**
* Forces the display to update the label and frequency display
* calculations
*/
public void update()
{
mUpdateRequired = true;
}
public int getMajorTickIncrement( Graphics2D graphics )
{
//Check to see if a calculation update is scheduled
update( graphics );
return mMajorTickIncrement;
}
public int getMinorTickIncrement( Graphics2D graphics )
{
return mMinorTickIncrement;
}
public int getLabelIncrement( Graphics2D graphics )
{
return mLabelIncrement;
}
@Override
public void componentResized( ComponentEvent arg0 )
{
update();
}
public void componentHidden( ComponentEvent arg0 ) {}
public void componentMoved( ComponentEvent arg0 ) {}
public void componentShown( ComponentEvent arg0 ) {}
}
public enum ChannelDisplay
{
ALL, ENABLED, NONE;
}
}