mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
614 lines
19 KiB
Java
614 lines
19 KiB
Java
/*
|
|
* *****************************************************************************
|
|
* Copyright (C) 2014-2023 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 io.github.dsheirer.spectrum;
|
|
|
|
import io.github.dsheirer.settings.ColorSetting;
|
|
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
|
|
import io.github.dsheirer.settings.Setting;
|
|
import io.github.dsheirer.settings.SettingChangeListener;
|
|
import io.github.dsheirer.settings.SettingsManager;
|
|
import io.github.dsheirer.source.ISourceEventProcessor;
|
|
import io.github.dsheirer.source.SourceEvent;
|
|
import java.awt.Color;
|
|
import java.awt.FontMetrics;
|
|
import java.awt.Graphics;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Point;
|
|
import java.awt.RenderingHints;
|
|
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 org.apache.commons.math3.util.FastMath;
|
|
|
|
import javax.swing.JPanel;
|
|
|
|
/**
|
|
* Frequency overlay panel.
|
|
*/
|
|
public class FrequencyOverlayPanel extends JPanel implements ISourceEventProcessor, SettingChangeListener
|
|
{
|
|
private static final long serialVersionUID = 1L;
|
|
private final DecimalFormat PPM_FORMATTER = new DecimalFormat( "#.0" );
|
|
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 static DecimalFormat CURSOR_FORMAT = new DecimalFormat("000.00000");
|
|
private long mFrequency = 0;
|
|
private int mBandwidth = 0;
|
|
private int mChannelBandwidth = 0;
|
|
|
|
private long mPllTrackingFrequency = 0;
|
|
private Point mCursorLocation = new Point(0, 0);
|
|
private boolean mCursorVisible = false;
|
|
|
|
private DFTSize mDFTSize = DFTSize.FFT04096;
|
|
|
|
/**
|
|
* Colors used by this component
|
|
*/
|
|
private Color mColorSpectrumBackground;
|
|
private Color mColorSpectrumCursor;
|
|
private Color mColorSpectrumLine;
|
|
|
|
//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;
|
|
|
|
/**
|
|
* Translucent overlay panel for displaying channel configurations,
|
|
* processing channels, selected channels, frequency labels and lines, and
|
|
* a cursor with a frequency readout.
|
|
*/
|
|
public FrequencyOverlayPanel(SettingsManager settingsManager)
|
|
{
|
|
mSettingsManager = settingsManager;
|
|
|
|
if(mSettingsManager != null)
|
|
{
|
|
mSettingsManager.addListener(this);
|
|
}
|
|
|
|
addComponentListener(mLabelSizeMonitor);
|
|
|
|
//Set the background transparent, so the spectrum display can be seen
|
|
setOpaque(false);
|
|
|
|
//Fetch color settings from settings manager
|
|
setColors();
|
|
}
|
|
|
|
/**
|
|
* Sets the tracking frequency of the Phase Locked Loop (PLL)
|
|
* @param pllTrackingFrequency current setting. Set to 0 to reset.
|
|
*/
|
|
public void setPllTrackingFrequency(long pllTrackingFrequency)
|
|
{
|
|
mPllTrackingFrequency = pllTrackingFrequency;
|
|
}
|
|
|
|
/**
|
|
* Sets the bandwidth of the channel.
|
|
* @param channelBandwidth in Hertz
|
|
*/
|
|
public void setChannelBandwidth(int channelBandwidth)
|
|
{
|
|
mChannelBandwidth = channelBandwidth;
|
|
}
|
|
|
|
public void dispose()
|
|
{
|
|
if(mSettingsManager != null)
|
|
{
|
|
mSettingsManager.removeListener(this);
|
|
}
|
|
|
|
mSettingsManager = null;
|
|
}
|
|
|
|
/**
|
|
* Sets/changes the DFT bin size
|
|
*/
|
|
public void setDFTSize(DFTSize size)
|
|
{
|
|
mDFTSize = size;
|
|
}
|
|
|
|
public void setCursorLocation(Point point)
|
|
{
|
|
mCursorLocation = point;
|
|
|
|
repaint();
|
|
}
|
|
|
|
public void setCursorVisible(boolean visible)
|
|
{
|
|
mCursorVisible = visible;
|
|
repaint();
|
|
}
|
|
|
|
/**
|
|
* Fetches the color settings from the settings manager
|
|
*/
|
|
private void setColors()
|
|
{
|
|
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 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);
|
|
drawCursor(graphics);
|
|
drawPLL(graphics);
|
|
drawChannelBandwidth(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);
|
|
FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont());
|
|
Rectangle2D rect = fontMetrics.getStringBounds(frequency, graphics);
|
|
|
|
if(mCursorLocation.y > rect.getHeight())
|
|
{
|
|
graphics.drawString(frequency, mCursorLocation.x + 5, mCursorLocation.y);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws the frequency lines and labels every 10kHz
|
|
*/
|
|
private void drawFrequencies(Graphics2D graphics)
|
|
{
|
|
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;
|
|
}
|
|
if(label == 0)
|
|
{
|
|
label = 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 the Phase Locked Loop tracking frequency, offset from channel center
|
|
*/
|
|
private void drawPLL(Graphics2D graphics)
|
|
{
|
|
if(mPllTrackingFrequency == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long frequency = mFrequency - mPllTrackingFrequency;
|
|
double xAxis = getAxisFromFrequency(frequency);
|
|
|
|
double height = getSize().getHeight() - mSpectrumInset;
|
|
double verticalAxisTop = height * 0.02d;
|
|
double verticalAxisBottom = height * 0.98d;
|
|
|
|
graphics.setColor(Color.YELLOW);
|
|
|
|
//Vertical band edge lines
|
|
graphics.draw(new Line2D.Double(xAxis, verticalAxisTop, xAxis, verticalAxisBottom));
|
|
}
|
|
|
|
/**
|
|
* Draws the Phase Locked Loop tracking frequency, offset from channel center
|
|
*/
|
|
private void drawChannelBandwidth(Graphics2D graphics)
|
|
{
|
|
if(mChannelBandwidth == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long minFrequency = mFrequency - (mChannelBandwidth / 2);
|
|
long maxFrequency = mFrequency + (mChannelBandwidth / 2);
|
|
|
|
double height = getSize().getHeight() - mSpectrumInset;
|
|
double verticalAxisTop = height * 0.8d;
|
|
double verticalAxisBottom = height * 0.98d;
|
|
|
|
double minXAxis = getAxisFromFrequency(minFrequency);
|
|
double maxXAxis = getAxisFromFrequency(maxFrequency);
|
|
|
|
graphics.setColor(Color.GREEN);
|
|
|
|
graphics.draw(new Line2D.Double(minXAxis, verticalAxisTop, minXAxis, verticalAxisBottom));
|
|
graphics.draw(new Line2D.Double(maxXAxis, verticalAxisTop, maxXAxis, verticalAxisBottom));
|
|
}
|
|
|
|
/**
|
|
* 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() + FastMath.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));
|
|
}
|
|
|
|
/**
|
|
* Frequency change event handler
|
|
*/
|
|
@Override
|
|
public void process(SourceEvent 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;
|
|
}
|
|
}
|
|
|
|
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 FastMath.abs(mFrequency - frequency) <= (mBandwidth / 2);
|
|
}
|
|
|
|
private long getMinDisplayFrequency()
|
|
{
|
|
double bandwidthPerBin = (double)mBandwidth / (double)(mDFTSize.getSize());
|
|
|
|
return getMinFrequency();
|
|
// return getMinFrequency() + (int)((mDFTZoomWindowOffset) * bandwidthPerBin);
|
|
}
|
|
|
|
private long getMaxDisplayFrequency()
|
|
{
|
|
return getMinDisplayFrequency() + getDisplayBandwidth();
|
|
}
|
|
|
|
private int getDisplayBandwidth()
|
|
{
|
|
return mBandwidth;
|
|
}
|
|
|
|
@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(FrequencyOverlayPanel.this.getFont());
|
|
|
|
int maxLabelWidth = fontMetrics.stringWidth(format(getMaxDisplayFrequency()));
|
|
|
|
double maxLabels = ((double) FrequencyOverlayPanel.this.getWidth() * LABEL_FILL_THRESHOLD) / (double)maxLabelWidth;
|
|
|
|
//Calculate the next smallest base 10 value for the major increment
|
|
int power = (int)FastMath.log10((double)getDisplayBandwidth() / maxLabels);
|
|
|
|
//Set the number of decimal places to display in frequency labels
|
|
int precision = 5 - power;
|
|
|
|
int start = (int)FastMath.pow(10.0, power + 1);
|
|
|
|
int minimum = (int)FastMath.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) {}
|
|
}
|
|
}
|